diff --git a/oscar/SleepLib/common.cpp b/oscar/SleepLib/common.cpp index 0fe56dc2..457744e9 100644 --- a/oscar/SleepLib/common.cpp +++ b/oscar/SleepLib/common.cpp @@ -394,6 +394,12 @@ void copyPath(QString src, QString dst) if (!QFile::exists(destFile)) { QFile::copy(srcFile, destFile); + // TODO: Since copyPath is only used by loaders, it should + // build the list of files first, and then update the progress bar + // while copying. + // TODO: copyPath should also either hide the abort button + // or respond to it. + QCoreApplication::processEvents(); } } } diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index a8ddff75..88d21958 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -238,6 +238,7 @@ static const PRS1TestedModel s_PRS1TestedModels[] = { // This first set says "(Philips Respironics)" intead of "(System One)" on official reports. { "251P", 0, 2, "REMstar Plus (System One)" }, // (brick) { "450P", 0, 3, "REMstar Pro (System One)" }, + { "451P", 0, 2, "REMstar Pro (System One)" }, { "451P", 0, 3, "REMstar Pro (System One)" }, { "550P", 0, 2, "REMstar Auto (System One)" }, { "550P", 0, 3, "REMstar Auto (System One)" }, @@ -402,74 +403,89 @@ bool isdigit(QChar c) return false; } -const QString PR_STR_PSeries = "P-Series"; - // Tests path to see if it has (what looks like) a valid PRS1 folder structure -bool PRS1Loader::Detect(const QString & path) +// This is used both to detect newly inserted media and to decide which loader +// to use after the user selects a folder. +// +// TODO: Ideally there should be a way to handle the two scenarios slightly +// differently. In the latter case, it should clean up the selection and +// return the canonical path if it detects one, allowing us to remove the +// notification about selecting the root of the card. That kind of cleanup +// wouldn't be appropriate when scanning devices. +bool PRS1Loader::Detect(const QString & selectedPath) { - QString newpath = checkDir(path); + QString path = selectedPath; + if (GetPSeriesPath(path).isEmpty()) { + // Try up one level in case the user selected the P-Series folder within the SD card. + path = QFileInfo(path).canonicalPath(); + } - return !newpath.isEmpty(); + QStringList machines = FindMachinesOnCard(path); + return !machines.isEmpty(); } - -QString PRS1Loader::checkDir(const QString & path) +QString PRS1Loader::GetPSeriesPath(const QString & path) { - QString newpath = path; - - newpath.replace("\\", "/"); - - if (!newpath.endsWith("/" + PR_STR_PSeries)) { - newpath = path + "/" + PR_STR_PSeries; + QString outpath = ""; + QDir root(path); + QStringList dirs = root.entryList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Hidden | QDir::NoSymLinks); + for (auto & dir : dirs) { + // We've seen P-Series, P-SERIES, and p-series, so we need to search for the directory + // in a way that won't break on a case-sensitive filesystem. + if (dir.toUpper() == "P-SERIES") { + outpath = path + QDir::separator() + dir; + break; + } } + return outpath; +} - QDir dir(newpath); +QStringList PRS1Loader::FindMachinesOnCard(const QString & cardPath) +{ + QStringList machinePaths; - if ((!dir.exists() || !dir.isReadable())) { - return QString(); - } - qDebug() << "PRS1Loader::Detect path=" << newpath; + QString pseriesPath = this->GetPSeriesPath(cardPath); + QDir pseries(pseriesPath); - QFile lastfile(newpath+"/last.txt"); + // If it contains a P-Series folder, it's a PRS1 SD card + if (!pseriesPath.isEmpty() && pseries.exists()) { + pseries.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks); + pseries.setSorting(QDir::Name); + QFileInfoList plist = pseries.entryInfoList(); - bool exists = true; - if (!lastfile.exists()) { - lastfile.setFileName(newpath+"/LAST.TXT"); - if (!lastfile.exists()) - exists = false; - } - - QString machpath; - if (exists) { - if (!lastfile.open(QIODevice::ReadOnly)) { - qDebug() << "PRS1Loader: last.txt exists but I couldn't open it!"; - } else { - QTextStream ts(&lastfile); - QString serial = ts.readLine(64).trimmed(); - lastfile.close(); - - machpath = newpath+"/"+serial; - - if (!QDir(machpath).exists()) { - machpath = QString(); + // Look for machine directories (containing a PROP.TXT or properties.txt) + QFileInfoList propertyfiles; + for (auto & pfi : plist) { + if (pfi.isDir()) { + QString machinePath = pfi.canonicalFilePath(); + QDir machineDir(machinePath); + QFileInfoList mlist = machineDir.entryInfoList(); + for (auto & mfi : mlist) { + if (QDir::match("PROP*.TXT", mfi.fileName())) { + // Found a properties file, this is a machine folder + propertyfiles.append(mfi); + } + } } } - } - if (machpath.isEmpty()) { - QDir dir(newpath); - QStringList dirs = dir.entryList(QDir::NoDotAndDotDot | QDir::Dirs); - if (dirs.size() > 0) { - machpath = dir.cleanPath(newpath+"/"+dirs[0]); + // Sort machines from oldest to newest. + std::sort(propertyfiles.begin(), propertyfiles.end(), + [](const QFileInfo & a, const QFileInfo & b) + { + return a.lastModified() < b.lastModified(); + }); + for (auto & propertyfile : propertyfiles) { + machinePaths.append(propertyfile.canonicalPath()); } } - - return machpath; + return machinePaths; } + void parseModel(MachineInfo & info, const QString & modelnum) { info.modelnumber = modelnum; @@ -583,89 +599,44 @@ bool PRS1Loader::PeekProperties(MachineInfo & info, const QString & filename, Ma MachineInfo PRS1Loader::PeekInfo(const QString & path) { - QString newpath = checkDir(path); - if (newpath.isEmpty()) + QStringList machines = FindMachinesOnCard(path); + if (machines.isEmpty()) { return MachineInfo(); + } + // Present information about the newest machine on the card. + QString newpath = machines.last(); + MachineInfo info = newInfo(); - info.serial = newpath.section("/", -1); - if (!PeekProperties(info, newpath+"/properties.txt")) { - PeekProperties(info, newpath+"/PROP.TXT"); + if (!PeekProperties(info, newpath+"/PROP.TXT")) { + qWarning() << "No properties file found in" << newpath; + } } return info; } -int PRS1Loader::Open(const QString & dirpath) +int PRS1Loader::Open(const QString & selectedPath) { - QString newpath; - QString path(dirpath); - path = path.replace("\\", "/"); - - if (path.endsWith("/" + PR_STR_PSeries)) { - newpath = path; - } else { - newpath = path + "/" + PR_STR_PSeries; + QString path = selectedPath; + if (GetPSeriesPath(path).isEmpty()) { + // Try up one level in case the user selected the P-Series folder within the SD card. + path = QFileInfo(path).canonicalPath(); } - qDebug() << "PRS1Loader::Open path=" << newpath; - - QDir dir(newpath); - - if ((!dir.exists() || !dir.isReadable())) { + QStringList machines = FindMachinesOnCard(path); + // Return an error if no machines were found. + if (machines.isEmpty()) { + qDebug() << "No PRS1 machines found at" << path; return -1; } - dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks); - dir.setSorting(QDir::Name); - QFileInfoList flist = dir.entryInfoList(); - - QStringList SerialNumbers; - QStringList::iterator sn; - - for (int i = 0; i < flist.size(); i++) { - QFileInfo fi = flist.at(i); - QString filename = fi.fileName(); - - if (fi.isDir() && (filename.size() > 4) && (isdigit(filename[1])) && (isdigit(filename[2]))) { - SerialNumbers.push_back(filename); - } else if (filename.toLower() == "last.txt") { // last.txt points to the current serial number - QString file = fi.canonicalFilePath(); - QFile f(file); - - if (!fi.isReadable()) { - qDebug() << "PRS1Loader: last.txt exists but I couldn't read it!"; - continue; - } - - if (!f.open(QIODevice::ReadOnly)) { - qDebug() << "PRS1Loader: last.txt exists but I couldn't open it!"; - continue; - } - - last = f.readLine(64); - last = last.trimmed(); - f.close(); - } - } - - if (SerialNumbers.empty()) { return -1; } - + // Import each machine, from oldest to newest. int c = 0; - - for (sn = SerialNumbers.begin(); sn != SerialNumbers.end(); sn++) { - if ((*sn)[0].isLetter()) { - c += OpenMachine(newpath + "/" + *sn); - } + for (auto & machinePath : machines) { + c += OpenMachine(machinePath); } - // Serial numbers that don't start with a letter. - for (sn = SerialNumbers.begin(); sn != SerialNumbers.end(); sn++) { - if (!(*sn)[0].isLetter()) { - c += OpenMachine(newpath + "/" + *sn); - } - } - return c; } @@ -700,6 +671,9 @@ int PRS1Loader::OpenMachine(const QString & path) return -1; } + emit updateMessage(QObject::tr("Backing Up Files...")); + QCoreApplication::processEvents(); + QString backupPath = m->getBackupPath() + path.section("/", -2); if (QDir::cleanPath(path).compare(QDir::cleanPath(backupPath)) != 0) { @@ -909,6 +883,9 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin // Scan for individual session files for (int i = 0; i < flist.size(); i++) { +#ifndef UNITTEST_MODE + QCoreApplication::processEvents(); +#endif if (isAborted()) { qDebug() << "received abort signal"; break; @@ -2713,6 +2690,9 @@ void PRS1Import::CreateEventChannels(const PRS1DataChunk* chunk) EventList* PRS1Import::GetImportChannel(ChannelID channel) { + if (!channel) { + qCritical() << this->sessionid << "channel in import table has not been added to schema!"; + } EventList* C = m_importChannels[channel]; if (C == nullptr) { C = session->AddEventList(channel, EVL_Event); @@ -5386,8 +5366,9 @@ bool PRS1DataChunk::ParseSummaryF3V6(void) qWarning() << this->sessionid << "summary data too short:" << chunk_size; return false; } - // We've once seen a short summary with no mask-on/off: just equipment-on, settings, 9, equipment-off - if (chunk_size < 75) UNEXPECTED_VALUE(chunk_size, ">= 75"); + // We've once seen a short summary with no mask-on/off: just equipment-on, settings, 2, equipment-off + // (And we've seen something similar in F5V3.) + if (chunk_size < 58) UNEXPECTED_VALUE(chunk_size, ">= 58"); bool ok = true; int pos = 0; @@ -7011,6 +6992,7 @@ bool PRS1DataChunk::ParseSummaryF5V3(void) return false; } // We've once seen a short summary with no mask-on/off: just equipment-on, settings, 9, equipment-off + // (And we've seen something similar in F3V6.) if (chunk_size < 75) UNEXPECTED_VALUE(chunk_size, ">= 75"); bool ok = true; diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index 6b634af7..39f0c885 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -387,9 +387,6 @@ class PRS1Loader : public CPAPLoader PRS1Loader(); virtual ~PRS1Loader(); - //! \brief Examine path and return it back if it contains what looks to be a valid PRS1 SD card structure - QString checkDir(const QString & path); - //! \brief Peek into PROP.TXT or properties.txt at given path, and return it as a normalized key/value hash bool PeekProperties(const QString & filename, QHash & props); @@ -444,6 +441,12 @@ class PRS1Loader : public CPAPLoader QString last; QHash PRS1List; + //! \brief Returns the path of the P-Series folder (whatever case) if present on the card + QString GetPSeriesPath(const QString & path); + + //! \brief Returns the path for each machine detected on an SD card, from oldest to newest + QStringList FindMachinesOnCard(const QString & cardPath); + //! \brief Opens the SD folder structure for this machine, scans for data files and imports any new sessions int OpenMachine(const QString & path); diff --git a/oscar/SleepLib/schema.cpp b/oscar/SleepLib/schema.cpp index e31cc165..69f5bc1e 100644 --- a/oscar/SleepLib/schema.cpp +++ b/oscar/SleepLib/schema.cpp @@ -338,8 +338,8 @@ void init() // // -// schema::channel.add(GRP_CPAP, ch=new Channel(CPAP_Test1 = 0x111e, DATA, MT_CPAP, SESSION, STR_GRAPH_TestChan1, QObject::tr("Debugging channel #1"), QObject::tr("Top secret internal stuff you're not supposed to see ;)"), QObject::tr("Test #1"), QString(), INTEGER, QColor("pink"))); -// schema::channel.add(GRP_CPAP, ch=new Channel(CPAP_Test2 = 0x111f, DATA, MT_CPAP, SESSION, STR_GRAPH_TestChan2, QObject::tr("Debugging channel #2"), QObject::tr("Top secret internal stuff you're not supposed to see ;)"), QObject::tr("Test #2"), QString(), INTEGER, Qt::blue)); + schema::channel.add(GRP_CPAP, ch=new Channel(CPAP_Test1 = 0x111e, DATA, MT_CPAP, SESSION, STR_GRAPH_TestChan1, QObject::tr("Debugging channel #1"), QObject::tr("Top secret internal stuff you're not supposed to see ;)"), QObject::tr("Test #1"), QString(), INTEGER, QColor("pink"))); + schema::channel.add(GRP_CPAP, ch=new Channel(CPAP_Test2 = 0x111f, DATA, MT_CPAP, SESSION, STR_GRAPH_TestChan2, QObject::tr("Debugging channel #2"), QObject::tr("Top secret internal stuff you're not supposed to see ;)"), QObject::tr("Test #2"), QString(), INTEGER, Qt::blue)); RMS9_E01 = schema::channel["RMS9_E01"].id(); RMS9_E02 = schema::channel["RMS9_E02"].id(); diff --git a/oscar/docs/about.html b/oscar/docs/about.html index cd47ab72..686480aa 100644 --- a/oscar/docs/about.html +++ b/oscar/docs/about.html @@ -29,7 +29,7 @@

OSCAR is free (as in freedom) software, released under the GNU Public License v3, and comes with no warranty, and without ANY claims to fitness for any purpose.

-

OSCAR is ©2019 The OSCAR Team: members of the apnea community as listed in the git log

+

OSCAR is ©2019-2020 The OSCAR Team: members of the apnea community as listed in the git log

OSCAR is a derivative of the SleepyHead program which is copyright ©2011-2018 Mark Watkins

diff --git a/oscar/docs/release_notes.html b/oscar/docs/release_notes.html index 5bc7d579..57f74253 100644 --- a/oscar/docs/release_notes.html +++ b/oscar/docs/release_notes.html @@ -13,6 +13,7 @@ Which was written and copyright 2011-2018 © Mark Watkins
  • [new] Add preliminary support for Viatom/Wellue pulse oximeters
  • [new] Ask where to save screenshots
  • [fix] Improved import of Philips Respironics flex and humidification settings
  • +
  • [new]Extensive re-organization of the ResMed loader to facilitate understanding and future improvements
  • diff --git a/oscar/tests/prs1tests.cpp b/oscar/tests/prs1tests.cpp index 6b80291b..806b16b0 100644 --- a/oscar/tests/prs1tests.cpp +++ b/oscar/tests/prs1tests.cpp @@ -395,32 +395,22 @@ void iterateTestCards(const QString & root, void (*action)(const QString &)) QFileInfoList flist = dir.entryInfoList(); // Look through each folder in the given root - for (int i = 0; i < flist.size(); i++) { - QFileInfo fi = flist.at(i); + for (auto & fi : flist) { if (fi.isDir()) { - // If it contains a P-Series folder, it's a PRS1 SD card - QDir pseries(fi.canonicalFilePath() + QDir::separator() + "P-Series"); - if (pseries.exists()) { - pseries.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks); - pseries.setSorting(QDir::Name); - QFileInfoList plist = pseries.entryInfoList(); + QStringList machinePaths = s_loader->FindMachinesOnCard(fi.canonicalFilePath()); - // Look for machine directories (containing a PROP.TXT or properties.txt) - for (int j = 0; j < plist.size(); j++) { - QFileInfo pfi = plist.at(j); - if (pfi.isDir()) { - QString machinePath = pfi.canonicalFilePath(); - QDir machineDir(machinePath); - QFileInfoList mlist = machineDir.entryInfoList(); - for (int k = 0; k < mlist.size(); k++) { - QFileInfo mfi = mlist.at(k); - if (QDir::match("PROP*.TXT", mfi.fileName())) { - // Found a properties file, this is a machine folder - action(machinePath); - } - } - } - } + // Tests should be run newest to oldest, since older sets tend to have more + // complete data. (These are usually previously cleared data in the Clear0/Cn + // directories.) The machines themselves will write out the summary data they + // remember when they see an empty folder, without event or waveform data. + // And since these tests (by design) overwrite existing output, we want the + // earlier (more complete) data to be what's written last. + // + // Since the loader itself keeps only the first set of data it sees for a session, + // we want to leave its earliest-to-latest ordering in place, and just reverse it + // here. + for (auto i = machinePaths.crbegin(); i != machinePaths.crend(); i++) { + action(*i); } } }