From f19ad331c982eade44c12f9c066085a0575e0c2a Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Mon, 13 May 2019 12:11:04 -0400 Subject: [PATCH 01/22] Add debugging output to all error handling in PRS1 loader. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 97 ++++++++++++++++--- 1 file changed, 83 insertions(+), 14 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 5065bcff..712337ca 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -112,6 +112,7 @@ PRS1::~PRS1() } +#if 0 // TODO: Remove: unused, superseded by PRS1Waveform /*! \struct WaveHeaderList \brief Used in PRS1 Waveform Parsing */ struct WaveHeaderList { @@ -119,6 +120,7 @@ struct WaveHeaderList { quint8 sample_format; WaveHeaderList(quint16 i, quint8 f) { interleave = i; sample_format = f; } }; +#endif PRS1Loader::PRS1Loader() @@ -701,20 +703,28 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin for (int p=0; p < size; ++p) { dir.setPath(paths.at(p)); - if (!dir.exists() || !dir.isReadable()) { continue; } + if (!dir.exists() || !dir.isReadable()) { + qWarning() << dir.canonicalPath() << "can't read directory"; + continue; + } QFileInfoList flist = dir.entryInfoList(); // Scan for individual session files for (int i = 0; i < flist.size(); i++) { - if (isAborted()) break; + if (isAborted()) { + qDebug() << "received abort signal"; + break; + } QFileInfo fi = flist.at(i); + QString path = fi.canonicalFilePath(); bool ok; QString ext_s = fi.fileName().section(".", -1); ext = ext_s.toInt(&ok); if (!ok) { // not a numerical extension + qWarning() << path << "unexpected filename"; continue; } @@ -722,6 +732,7 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin sid = session_s.toInt(&ok, sessionid_base); if (!ok) { // not a numerical session ID + qWarning() << path << "unexpected filename"; continue; } @@ -737,6 +748,7 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin if (m->SessionExists(sid)) { // Skip already imported session + qDebug() << path << "session already exists, skipping" << sid; continue; } @@ -766,10 +778,16 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin } // Parse the data chunks and read the files.. + if (fi.canonicalFilePath().isEmpty()) { + qWarning() << fi; + } QList Chunks = ParseFile(fi.canonicalFilePath()); for (int i=0; i < Chunks.size(); ++i) { - if (isAborted()) break; + if (isAborted()) { + qDebug() << "received abort signal 2"; + break; + } PRS1DataChunk * chunk = Chunks.at(i); @@ -777,16 +795,18 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin const unsigned char * data = (unsigned char *)chunk->m_data.constData(); if (data[0x00] != 0) { + qWarning() << path << "data doesn't start with 0, skipping:" << data[0x00] << chunk->m_data.size(); delete chunk; continue; } } SessionID chunk_sid = chunk->sessionid; - if (chunk_sid != sid && chunk_sid > 2000) { // log any really weird session IDs + if (i > 0 || chunk_sid != sid) { // log multiple chunks in non-waveform files and session ID mismatches qDebug() << fi.canonicalFilePath() << chunk_sid; } - if (m->SessionExists(sid)) { + if (m->SessionExists(sid)) { // BUG: this should presumably be chunk_sid, but any change needs to be tested. + qDebug() << path << "session already exists, skipping" << sid << chunk_sid; delete chunk; continue; } @@ -804,23 +824,36 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin } switch (ext) { case 0: - if (task->compliance) continue; // (skipping to avoid duplicates) + if (task->compliance) { + qWarning() << path << "duplicate compliance?"; + continue; // (skipping to avoid duplicates) + } task->compliance = chunk; break; case 1: - if (task->summary) continue; + if (task->summary) { + qWarning() << path << "duplicate summary?"; + continue; + } task->summary = chunk; break; case 2: - if (task->event) continue; + if (task->event) { + qWarning() << path << "duplicate events?"; + continue; + } task->event = chunk; break; default: + qWarning() << path << "unexpected file"; break; } } } - if (isAborted()) break; + if (isAborted()) { + qDebug() << "received abort signal 3"; + break; + } } } @@ -3072,6 +3105,7 @@ bool PRS1Import::ParseOximetery() int size = oxi->m_data.size(); if (size == 0) { + qDebug() << oxi->sessionid << oxi->timestamp << "empty?"; continue; } quint64 ti = quint64(oxi->timestamp) * 1000L; @@ -3121,12 +3155,16 @@ bool PRS1Import::ParseWaveforms() int size = waveform->m_data.size(); if (size == 0) { + qDebug() << waveform->sessionid << waveform->timestamp << "empty?"; continue; } quint64 ti = quint64(waveform->timestamp) * 1000L; quint64 dur = qint64(waveform->duration) * 1000L; quint64 diff = ti - lastti; + if ((lastti != 0) && diff > 0) { + qDebug() << waveform->sessionid << waveform->timestamp << "BND?" << (diff / 1000L) << "=" << waveform->timestamp << "-" << (lastti / 1000L); + } if ((diff > 500) && (lastti != 0)) { if (!bnd) { bnd = session->AddEventList(PRS1_BND, EVL_Event); @@ -3245,16 +3283,21 @@ QList PRS1Loader::ParseFile(const QString & path) { QList CHUNKS; - if (path.isEmpty()) + if (path.isEmpty()) { + // ParseSession passes empty filepaths for waveforms if none exist. + //qWarning() << path << "ParseFile given empty path"; return CHUNKS; + } QFile f(path); if (!f.exists()) { + qWarning() << path << "missing"; return CHUNKS; } if (!f.open(QIODevice::ReadOnly)) { + qWarning() << path << "can't open"; return CHUNKS; } @@ -3285,6 +3328,7 @@ QList PRS1Loader::ParseFile(const QString & path) do { headerBA = f.read(16); if (headerBA.size() != 16) { + qDebug() << path << "file too short?"; break; } @@ -3299,8 +3343,10 @@ QList PRS1Loader::ParseFile(const QString & path) sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7]; timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11]; - if (blocksize == 0) + if (blocksize == 0) { + qDebug() << path << "blocksize 0?"; break; + } if (fileVersion < 2) { qDebug() << "Never seen PRS1 header version < 2 before"; @@ -3328,6 +3374,7 @@ QList PRS1Loader::ParseFile(const QString & path) headerB2 = f.read(hdb_size+1); // add extra byte for checksum if (headerB2.size() != hdb_size+1) { + qWarning() << path << "read error in extended header"; break; } @@ -3338,8 +3385,18 @@ QList PRS1Loader::ParseFile(const QString & path) } else headerB2 = QByteArray(); } else { // Waveform Chunk + QFileInfo fi(path); + bool ok; + int sessionid_base = (fileVersion == 2 ? 10 : 16); + QString session_s = fi.fileName().section(".", 0, -2); + quint32 sid = session_s.toInt(&ok, sessionid_base); + if (!ok || sid != sessionid) { + qDebug() << path << sessionid; // log mismatched waveforum session IDs + } + extra = f.read(4); if (extra.size() != 4) { + qWarning() << path << "read error in waveform header"; break; } header_size += 4; @@ -3349,25 +3406,32 @@ QList PRS1Loader::ParseFile(const QString & path) duration = header[0x0f] | header[0x10] << 8; wvfm_signals = header[0x12] | header[0x13] << 8; + if (wvfm_signals > 2) { + qDebug() << path << wvfm_signals << "channels"; + } int ws_size = (fileVersion == 3) ? 4 : 3; int sbsize = wvfm_signals * ws_size + 1; extra = f.read(sbsize); if (extra.size() != sbsize) { + qWarning() << path << "read error in waveform header 2"; break; } headerBA.append(extra); header = (unsigned char *)headerBA.data(); header_size += sbsize; - // Read the waveform information in reverse. + // Read the waveform information in reverse. // TODO: Double-check this, always seems to be flow then pressure. int pos = 0x14 + (wvfm_signals - 1) * ws_size; for (int i = 0; i < wvfm_signals; ++i) { quint16 interleave = header[pos] | header[pos + 1] << 8; // samples per block (Usually 05 00) + if (interleave != 5) { + qDebug() << path << "interleave?" << interleave; + } if (fileVersion == 2) { - quint8 sample_format = header[pos + 2]; + quint8 sample_format = header[pos + 2]; // TODO: sample_format seems to be unused anywhere else in the loader. waveformInfo.push_back(PRS1Waveform(interleave, sample_format)); pos -= 3; } else if (fileVersion == 3) { @@ -3387,6 +3451,7 @@ QList PRS1Loader::ParseFile(const QString & path) for (int i=0; i < (header_size-1); i++) achk += header[i]; if (achk != header[header_size-1]) { // Header checksum mismatch? + qWarning() << path << "header checksum calc" << achk << "!= stored" << header[header_size-1]; break; } @@ -3399,6 +3464,7 @@ QList PRS1Loader::ParseFile(const QString & path) || (lastchunk->family != family) || (lastchunk->familyVersion != familyVersion) || (lastchunk->htype != htype)) { + qWarning() << path << "unexpected header data, skipping"; QByteArray junk = f.read(lastblocksize - header_size); Q_UNUSED(junk) @@ -3408,8 +3474,10 @@ QList PRS1Loader::ParseFile(const QString & path) } ++cruft; // quit after 3 attempts - if (cruft > 3) + if (cruft > 3) { + qWarning() << path << "too many unexpected headers, bailing"; break; + } continue; // Corrupt header.. skip it. @@ -3456,6 +3524,7 @@ QList PRS1Loader::ParseFile(const QString & path) chunk->m_data = f.read(blocksize); if (chunk->m_data.size() < blocksize) { + qWarning() << "less data in file than specified in header"; delete chunk; break; } From 9b3aaad4b06b06af5c6d8392a079deda6eb8b41d Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Mon, 13 May 2019 21:20:11 -0400 Subject: [PATCH 02/22] Move PRS1 waveform chunk coalescing out of parsing and into importing. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 70 +++++++++++-------- oscar/SleepLib/loader_plugins/prs1_loader.h | 3 + 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 712337ca..fcc187ff 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -795,6 +795,7 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin const unsigned char * data = (unsigned char *)chunk->m_data.constData(); if (data[0x00] != 0) { + // 5 length 5, 6 length 1, 7 length 3, 8 length 3 seen on 960P qWarning() << path << "data doesn't start with 0, skipping:" << data[0x00] << chunk->m_data.size(); delete chunk; continue; @@ -3095,6 +3096,45 @@ bool PRS1Import::ParseEvents() return res; } + +QList PRS1Import::CoalesceWaveformChunks(QList & allchunks) +{ + QList coalesced; + PRS1DataChunk *chunk = nullptr, *lastchunk = nullptr; + + for (int i=0; i < allchunks.size(); ++i) { + chunk = allchunks.at(i); + + if (lastchunk != nullptr) { + if (lastchunk->sessionid != chunk->sessionid) { + qWarning() << "lastchunk->sessionid != chunk->sessionid in PRS1Loader::CoalesceWaveformChunks()"; + // Free any remaining chunks + for (int j=i; j < allchunks.size(); ++j) { + chunk = allchunks.at(j); + delete chunk; + } + break; + } + + qint64 diff = (chunk->timestamp - lastchunk->timestamp) - lastchunk->duration; + if (diff == 0) { + // In sync, so append waveform data to previous chunk + lastchunk->m_data.append(chunk->m_data); + lastchunk->duration += chunk->duration; + delete chunk; + continue; + } + // else start a new chunk to resync + } + + coalesced.append(chunk); + lastchunk = chunk; + } + + return coalesced; +} + + bool PRS1Import::ParseOximetery() { int size = oximetry.size(); @@ -3232,6 +3272,7 @@ bool PRS1Import::ParseSession(void) // Parse .005 Waveform file waveforms = loader->ParseFile(wavefile); + waveforms = CoalesceWaveformChunks(waveforms); if (session->eventlist.contains(CPAP_FlowRate)) { if (waveforms.size() > 0) { // Delete anything called "Flow rate" picked up in the events file if real data is present @@ -3242,6 +3283,7 @@ bool PRS1Import::ParseSession(void) // Parse .006 Waveform file oximetry = loader->ParseFile(oxifile); + oximetry = CoalesceWaveformChunks(oximetry); ParseOximetery(); if (session->first() > 0) { @@ -3308,7 +3350,6 @@ QList PRS1Loader::ParseFile(const QString & path) quint16 wvfm_signals=0; unsigned char * header; - int cnt = 0; //int lastheadersize = 0; int lastblocksize = 0; @@ -3355,8 +3396,6 @@ QList PRS1Loader::ParseFile(const QString & path) header_size = 16; // most common header size, newer familyVersion 3 models are larger. - int diff = 0; - waveformInfo.clear(); bool hasHeaderDataBlock = (fileVersion == 3); @@ -3441,9 +3480,6 @@ QList PRS1Loader::ParseFile(const QString & path) pos -= 4; } } - if (lastchunk != nullptr) { - diff = (timestamp - lastchunk->timestamp) - lastchunk->duration; - } } // Calculate 8bit additive header checksum @@ -3548,31 +3584,9 @@ QList PRS1Loader::ParseFile(const QString & path) #endif } - if ((chunk->ext == 5) || (chunk->ext == 6)) { // if Flow/MaskPressure Waveform or OXI Waveform file - if (lastchunk != nullptr) { - if (lastchunk->sessionid != chunk->sessionid) { - qWarning() << "lastchunk->sessionid != chunk->sessionid in PRS1Loader::ParseFile2()"; - break; - } - - if (diff == 0) { - // In sync, so append waveform data to previous chunk - lastchunk->m_data.append(chunk->m_data); - lastchunk->duration += chunk->duration; - delete chunk; - cnt++; - chunk = lastchunk; - continue; - } - // else start a new chunk to resync - } - } - CHUNKS.append(chunk); lastchunk = chunk; - cnt++; - } while (!f.atEnd()); return CHUNKS; diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index fd4bf620..d5447a42 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -138,6 +138,9 @@ public: //! \brief Figures out which Event Parser to call, based on machine family/version and calls it. bool ParseEvents(); + //! \brief Coalesce contiguous .005 or .006 waveform chunks from the file into larger chunks for import. + QList CoalesceWaveformChunks(QList & allchunks); + //! \brief Takes the parsed list of Flow/MaskPressure waveform chunks and adds them to the database bool ParseWaveforms(); From 6e12cfea612e24ad460137bea587ef57398130ed Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Tue, 14 May 2019 16:17:23 -0400 Subject: [PATCH 03/22] Remove spurious warning about weird PRS1 session IDs --- oscar/tests/sessiontests.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/oscar/tests/sessiontests.cpp b/oscar/tests/sessiontests.cpp index ac216a24..f5491d85 100644 --- a/oscar/tests/sessiontests.cpp +++ b/oscar/tests/sessiontests.cpp @@ -162,10 +162,6 @@ void SessionToYaml(QString filepath, Session* session) } QTextStream out(&file); - // TODO: We sometimes see invalid session IDs. Either memory is getting trampled or the file - // header has the wrong ID (or isn't getting parsed right). Track this down once we can test parsing. - if (session->session() > 2000) qDebug() << "memory trampled? session ID" << session->session(); - out << "session:" << endl; out << " id: " << session->session() << endl; out << " start: " << ts(session->first()) << endl; From 76053b0469f568dd46b141971aca1d1ded847d3a Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Tue, 14 May 2019 16:20:32 -0400 Subject: [PATCH 04/22] Check for format change before coalescing PRS1 chunks, move data warnings out of parsing. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 52 +++++++++++++++---- oscar/SleepLib/loader_plugins/prs1_loader.h | 4 ++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index fcc187ff..6d9acdd5 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3101,11 +3101,13 @@ QList PRS1Import::CoalesceWaveformChunks(QList { QList coalesced; PRS1DataChunk *chunk = nullptr, *lastchunk = nullptr; + int num; for (int i=0; i < allchunks.size(); ++i) { chunk = allchunks.at(i); if (lastchunk != nullptr) { + // Waveform files shouldn't contain multiple sessions if (lastchunk->sessionid != chunk->sessionid) { qWarning() << "lastchunk->sessionid != chunk->sessionid in PRS1Loader::CoalesceWaveformChunks()"; // Free any remaining chunks @@ -3116,9 +3118,28 @@ QList PRS1Import::CoalesceWaveformChunks(QList break; } + // Check whether the data format is the same between the two chunks + bool same_format = (lastchunk->waveformInfo.size() == chunk->waveformInfo.size()); + if (same_format) { + num = chunk->waveformInfo.size(); + for (int n=0; n < num; n++) { + const PRS1Waveform &a = lastchunk->waveformInfo.at(n); + const PRS1Waveform &b = chunk->waveformInfo.at(n); + if (a.interleave != b.interleave) { + // We've never seen this before + qWarning() << chunk->m_path << "format change?" << a.interleave << b.interleave; + same_format = false; + break; + } + } + } else { + // We've never seen this before + qWarning() << chunk->m_path << "channels change?" << lastchunk->waveformInfo.size() << chunk->waveformInfo.size(); + } + qint64 diff = (chunk->timestamp - lastchunk->timestamp) - lastchunk->duration; - if (diff == 0) { - // In sync, so append waveform data to previous chunk + if (same_format && diff == 0) { + // Same format and in sync, so append waveform data to previous chunk lastchunk->m_data.append(chunk->m_data); lastchunk->duration += chunk->duration; delete chunk; @@ -3127,6 +3148,18 @@ QList PRS1Import::CoalesceWaveformChunks(QList // else start a new chunk to resync } + // Report any formats we haven't seen before + num = chunk->waveformInfo.size(); + if (num > 2) { + qDebug() << chunk->m_path << num << "channels"; + } + for (int n=0; n < num; n++) { + int interleave = chunk->waveformInfo.at(n).interleave; + if (interleave != 5) { + qDebug() << chunk->m_path << "interleave?" << interleave; + } + } + coalesced.append(chunk); lastchunk = chunk; } @@ -3350,6 +3383,7 @@ QList PRS1Loader::ParseFile(const QString & path) quint16 wvfm_signals=0; unsigned char * header; + int cnt = 0; //int lastheadersize = 0; int lastblocksize = 0; @@ -3367,6 +3401,7 @@ QList PRS1Loader::ParseFile(const QString & path) QList waveformInfo; do { + qint64 filepos = f.pos(); headerBA = f.read(16); if (headerBA.size() != 16) { qDebug() << path << "file too short?"; @@ -3445,9 +3480,6 @@ QList PRS1Loader::ParseFile(const QString & path) duration = header[0x0f] | header[0x10] << 8; wvfm_signals = header[0x12] | header[0x13] << 8; - if (wvfm_signals > 2) { - qDebug() << path << wvfm_signals << "channels"; - } int ws_size = (fileVersion == 3) ? 4 : 3; int sbsize = wvfm_signals * ws_size + 1; @@ -3465,10 +3497,6 @@ QList PRS1Loader::ParseFile(const QString & path) int pos = 0x14 + (wvfm_signals - 1) * ws_size; for (int i = 0; i < wvfm_signals; ++i) { quint16 interleave = header[pos] | header[pos + 1] << 8; // samples per block (Usually 05 00) - if (interleave != 5) { - qDebug() << path << "interleave?" << interleave; - } - if (fileVersion == 2) { quint8 sample_format = header[pos + 2]; // TODO: sample_format seems to be unused anywhere else in the loader. waveformInfo.push_back(PRS1Waveform(interleave, sample_format)); @@ -3515,6 +3543,7 @@ QList PRS1Loader::ParseFile(const QString & path) break; } + cnt++; continue; // Corrupt header.. skip it. } @@ -3522,6 +3551,10 @@ QList PRS1Loader::ParseFile(const QString & path) chunk = new PRS1DataChunk(); + chunk->m_path = path; + chunk->m_filepos = filepos; + chunk->m_index = cnt; + chunk->sessionid = sessionid; if (!firstsession) { @@ -3587,6 +3620,7 @@ QList PRS1Loader::ParseFile(const QString & path) CHUNKS.append(chunk); lastchunk = chunk; + cnt++; } while (!f.atEnd()); return CHUNKS; diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index d5447a42..d0c5a55f 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -78,6 +78,10 @@ public: QByteArray m_data; QByteArray m_headerblock; + QString m_path; + qint64 m_filepos; // file offset + int m_index; // nth chunk in file + SessionID sessionid; quint8 fileVersion; From 74863e538ad737d00393fb540573b8ce3968c629 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Tue, 14 May 2019 18:57:04 -0400 Subject: [PATCH 05/22] Split PRS1Loader::ParseFile in to ParseFile/ParseChunk. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 119 +++++++++++------- oscar/SleepLib/loader_plugins/prs1_loader.h | 5 + 2 files changed, 76 insertions(+), 48 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 6d9acdd5..5a673b73 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3378,18 +3378,75 @@ QList PRS1Loader::ParseFile(const QString & path) PRS1DataChunk *chunk = nullptr, *lastchunk = nullptr; + int cnt = 0; + + int cruft = 0; + int firstsession = 0; + + do { + chunk = ParseChunk(f, cnt); + if (chunk == nullptr) { + break; + } + + if (lastchunk != nullptr) { + // If there's any mismatch between header information, try and skip the block + // This probably isn't the best approach for dealing with block corruption :/ + if ((lastchunk->fileVersion != chunk->fileVersion) + || (lastchunk->ext != chunk->ext) + || (lastchunk->family != chunk->family) + || (lastchunk->familyVersion != chunk->familyVersion) + || (lastchunk->htype != chunk->htype)) { + qWarning() << path << "unexpected header data, skipping"; + + // TODO: Find a sample of this problem to see if the below approach has any + // value, or whether we should just drop the chunk. + QByteArray junk = f.read(lastchunk->blockSize - chunk->m_header.size()); + + Q_UNUSED(junk) + if (lastchunk->ext == 5) { + // The data is random crap + // lastchunk->m_data.append(junk.mid(lastheadersize-16)); + } + ++cruft; + // quit after 3 attempts + if (cruft > 3) { + qWarning() << path << "too many unexpected headers, bailing"; + break; + } + + cnt++; + delete chunk; + continue; + // Corrupt header.. skip it. + } + } + + if (!firstsession) { + firstsession = chunk->sessionid; + } + + CHUNKS.append(chunk); + + lastchunk = chunk; + cnt++; + } while (!f.atEnd()); + + return CHUNKS; +} + + +PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) +{ + QString path = QFileInfo(f).canonicalFilePath(); + PRS1DataChunk* chunk = nullptr; + PRS1DataChunk* out_chunk = nullptr; + quint8 fileVersion; quint16 blocksize; quint16 wvfm_signals=0; unsigned char * header; - int cnt = 0; - - //int lastheadersize = 0; - int lastblocksize = 0; - - int cruft = 0; - int firstsession = 0; int htype,family,familyVersion,ext,header_size = 0; quint8 achk=0; quint32 sessionid=0, timestamp=0; @@ -3519,36 +3576,6 @@ QList PRS1Loader::ParseFile(const QString & path) break; } - - if (lastchunk != nullptr) { - // If there's any mismatch between header information, try and skip the block - // This probably isn't the best approach for dealing with block corruption :/ - if ((lastchunk->fileVersion != fileVersion) - || (lastchunk->ext != ext) - || (lastchunk->family != family) - || (lastchunk->familyVersion != familyVersion) - || (lastchunk->htype != htype)) { - qWarning() << path << "unexpected header data, skipping"; - QByteArray junk = f.read(lastblocksize - header_size); - - Q_UNUSED(junk) - if (lastchunk->ext == 5) { - // The data is random crap - // lastchunk->m_data.append(junk.mid(lastheadersize-16)); - } - ++cruft; - // quit after 3 attempts - if (cruft > 3) { - qWarning() << path << "too many unexpected headers, bailing"; - break; - } - - cnt++; - continue; - // Corrupt header.. skip it. - } - } - chunk = new PRS1DataChunk(); chunk->m_path = path; @@ -3557,9 +3584,6 @@ QList PRS1Loader::ParseFile(const QString & path) chunk->sessionid = sessionid; - if (!firstsession) { - firstsession = chunk->sessionid; - } chunk->fileVersion = fileVersion; chunk->htype = htype; chunk->family = family; @@ -3576,8 +3600,9 @@ QList PRS1Loader::ParseFile(const QString & path) } } chunk->m_headerblock = headerB2; + chunk->m_header = headerBA; + chunk->blockSize = blocksize; - lastblocksize = blocksize; blocksize -= header_size; if (ext >= 5) { @@ -3616,14 +3641,12 @@ QList PRS1Loader::ParseFile(const QString & path) } #endif } + + // Only return the chunk if it has passed all tests above. + out_chunk = chunk; + } while (false); - CHUNKS.append(chunk); - - lastchunk = chunk; - cnt++; - } while (!f.atEnd()); - - return CHUNKS; + return out_chunk; } void InitModelMap() diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index d0c5a55f..fabc6c00 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -75,6 +75,7 @@ public: } inline int size() const { return m_data.size(); } + QByteArray m_header; QByteArray m_data; QByteArray m_headerblock; @@ -85,6 +86,7 @@ public: SessionID sessionid; quint8 fileVersion; + quint16 blockSize; quint8 ext; quint8 htype; quint8 family; @@ -230,6 +232,9 @@ class PRS1Loader : public CPAPLoader //! \brief Parse a PRS1 summary/event/waveform file and break into invidivual session or waveform chunks QList ParseFile(const QString & path); + //! \brief Parse and return the next chunk from a PRS1 file + PRS1DataChunk* ParseChunk(class QFile & f, int index=0); + //! \brief Register this Module to the list of Loaders, so it knows to search for PRS1 data. static void Register(); From d216e677e156a52992e328d9df5093849b8fd190 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Tue, 14 May 2019 19:57:01 -0400 Subject: [PATCH 06/22] Simplify PRS1Loader::ParseChunk by using a chunk instead of local variables. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 141 +++++++----------- 1 file changed, 52 insertions(+), 89 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 5a673b73..64313e5b 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3438,60 +3438,50 @@ QList PRS1Loader::ParseFile(const QString & path) PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) { - QString path = QFileInfo(f).canonicalFilePath(); - PRS1DataChunk* chunk = nullptr; PRS1DataChunk* out_chunk = nullptr; + + PRS1DataChunk* chunk = new PRS1DataChunk(); + chunk->m_path = QFileInfo(f).canonicalFilePath(); + chunk->m_filepos = f.pos(); + chunk->m_index = cnt; - quint8 fileVersion; - quint16 blocksize; quint16 wvfm_signals=0; unsigned char * header; - int htype,family,familyVersion,ext,header_size = 0; quint8 achk=0; - quint32 sessionid=0, timestamp=0; - - int duration=0; QByteArray headerBA, headerB2, extra; - QList waveformInfo; - do { - qint64 filepos = f.pos(); - headerBA = f.read(16); - if (headerBA.size() != 16) { - qDebug() << path << "file too short?"; + chunk->m_header = f.read(16); + if (chunk->m_header.size() != 16) { + qWarning() << chunk->m_path << "file too short?"; break; } - header = (unsigned char *)headerBA.data(); + header = (unsigned char *)chunk->m_header.data(); - fileVersion = header[0]; // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed - blocksize = (header[2] << 8) | header[1]; - htype = header[3]; // 00 = normal, 01=waveform - family = header[4]; - familyVersion = header[5]; - ext = header[6]; - sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7]; - timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11]; + chunk->fileVersion = header[0]; // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed + chunk->blockSize = (header[2] << 8) | header[1]; + chunk->htype = header[3]; // 00 = normal, 01=waveform + chunk->family = header[4]; + chunk->familyVersion = header[5]; + chunk->ext = header[6]; + chunk->sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7]; + chunk->timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11]; - if (blocksize == 0) { - qDebug() << path << "blocksize 0?"; + if (chunk->blockSize == 0) { + qWarning() << chunk->m_path << "blocksize 0?"; break; } - if (fileVersion < 2) { - qDebug() << "Never seen PRS1 header version < 2 before"; + if (chunk->fileVersion < 2 || chunk->fileVersion > 3) { + qWarning() << chunk->m_path << "Never seen PRS1 header version < 2 or > 3 before"; break; } - header_size = 16; // most common header size, newer familyVersion 3 models are larger. - - waveformInfo.clear(); - - bool hasHeaderDataBlock = (fileVersion == 3); - if (ext < 5) { // Not a waveform chunk + bool hasHeaderDataBlock = (chunk->fileVersion == 3); + if (chunk->ext < 5) { // Not a waveform chunk // Check if this is a newer machine with a header data block @@ -3505,63 +3495,60 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) headerB2 = f.read(hdb_size+1); // add extra byte for checksum if (headerB2.size() != hdb_size+1) { - qWarning() << path << "read error in extended header"; + qWarning() << chunk->m_path << "read error in extended header"; break; } - headerBA.append(headerB2); - header = (unsigned char *)headerBA.data(); // important because it's memory location could move - - header_size += hdb_size+1; - } else headerB2 = QByteArray(); + chunk->m_header.append(headerB2); + } else { + headerB2 = QByteArray(); + } } else { // Waveform Chunk - QFileInfo fi(path); + QFileInfo fi(f); bool ok; - int sessionid_base = (fileVersion == 2 ? 10 : 16); + int sessionid_base = (chunk->fileVersion == 2 ? 10 : 16); QString session_s = fi.fileName().section(".", 0, -2); quint32 sid = session_s.toInt(&ok, sessionid_base); - if (!ok || sid != sessionid) { - qDebug() << path << sessionid; // log mismatched waveforum session IDs + if (!ok || sid != chunk->sessionid) { + qDebug() << chunk->m_path << chunk->sessionid; // log mismatched waveforum session IDs } extra = f.read(4); if (extra.size() != 4) { - qWarning() << path << "read error in waveform header"; + qWarning() << chunk->m_path << "read error in waveform header"; break; } - header_size += 4; - headerBA.append(extra); + chunk->m_header.append(extra); // Get the header address again to be safe - header = (unsigned char *)headerBA.data(); + header = (unsigned char *)chunk->m_header.data(); - duration = header[0x0f] | header[0x10] << 8; + chunk->duration = header[0x0f] | header[0x10] << 8; wvfm_signals = header[0x12] | header[0x13] << 8; - int ws_size = (fileVersion == 3) ? 4 : 3; + int ws_size = (chunk->fileVersion == 3) ? 4 : 3; int sbsize = wvfm_signals * ws_size + 1; extra = f.read(sbsize); if (extra.size() != sbsize) { - qWarning() << path << "read error in waveform header 2"; + qWarning() << chunk->m_path << "read error in waveform header 2"; break; } - headerBA.append(extra); - header = (unsigned char *)headerBA.data(); - header_size += sbsize; + chunk->m_header.append(extra); + header = (unsigned char *)chunk->m_header.data(); // Read the waveform information in reverse. // TODO: Double-check this, always seems to be flow then pressure. int pos = 0x14 + (wvfm_signals - 1) * ws_size; for (int i = 0; i < wvfm_signals; ++i) { quint16 interleave = header[pos] | header[pos + 1] << 8; // samples per block (Usually 05 00) - if (fileVersion == 2) { + if (chunk->fileVersion == 2) { quint8 sample_format = header[pos + 2]; // TODO: sample_format seems to be unused anywhere else in the loader. - waveformInfo.push_back(PRS1Waveform(interleave, sample_format)); + chunk->waveformInfo.push_back(PRS1Waveform(interleave, sample_format)); pos -= 3; - } else if (fileVersion == 3) { + } else if (chunk->fileVersion == 3) { //quint16 sample_size = header[pos + 2] | header[pos + 3] << 8; // size in bits?? (08 00) // Possibly this is size in bits, and sign bit for the other byte? - waveformInfo.push_back(PRS1Waveform(interleave, 0)); + chunk->waveformInfo.push_back(PRS1Waveform(interleave, 0)); pos -= 4; } } @@ -3569,27 +3556,15 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) // Calculate 8bit additive header checksum achk=0; + header = (unsigned char *)chunk->m_header.data(); // important because its memory location could move + int header_size = chunk->m_header.size(); for (int i=0; i < (header_size-1); i++) achk += header[i]; if (achk != header[header_size-1]) { // Header checksum mismatch? - qWarning() << path << "header checksum calc" << achk << "!= stored" << header[header_size-1]; + qWarning() << chunk->m_path << "header checksum calc" << achk << "!= stored" << header[header_size-1]; break; } - chunk = new PRS1DataChunk(); - - chunk->m_path = path; - chunk->m_filepos = filepos; - chunk->m_index = cnt; - - chunk->sessionid = sessionid; - - chunk->fileVersion = fileVersion; - chunk->htype = htype; - chunk->family = family; - chunk->familyVersion = familyVersion; - chunk->ext = ext; - chunk->timestamp = timestamp; if (hasHeaderDataBlock) { const unsigned char * hd = (unsigned char *)headerB2.constData(); int pos = 0; @@ -3600,26 +3575,13 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) } } chunk->m_headerblock = headerB2; - chunk->m_header = headerBA; - chunk->blockSize = blocksize; - - blocksize -= header_size; - - if (ext >= 5) { - chunk->duration = duration; - - // I don't trust deep copy, just being safe... - for (int i=0;iwaveformInfo.push_back(waveformInfo.at(i)); - } - } + int data_size = chunk->blockSize - chunk->m_header.size(); // Read data block - chunk->m_data = f.read(blocksize); + chunk->m_data = f.read(data_size); - if (chunk->m_data.size() < blocksize) { - qWarning() << "less data in file than specified in header"; - delete chunk; + if (chunk->m_data.size() < data_size) { + qWarning() << chunk->m_path << "less data in file than specified in header"; break; } @@ -3646,6 +3608,7 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) out_chunk = chunk; } while (false); + if (out_chunk == nullptr) delete chunk; return out_chunk; } From d7cd22c918d3cc3f9c349bd62896e48c9de18403 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Tue, 14 May 2019 20:47:00 -0400 Subject: [PATCH 07/22] Separate checksum reading in PRS1Loader::ParseChunk instead of burying it with other data. While slightly more verbose, this makes the code more clearly correct. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 62 +++++++++++++------ oscar/SleepLib/loader_plugins/prs1_loader.h | 3 + 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 64313e5b..5f6127d1 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3448,13 +3448,12 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) quint16 wvfm_signals=0; unsigned char * header; - quint8 achk=0; QByteArray headerBA, headerB2, extra; do { - chunk->m_header = f.read(16); - if (chunk->m_header.size() != 16) { + chunk->m_header = f.read(15); + if (chunk->m_header.size() != 15) { qWarning() << chunk->m_path << "file too short?"; break; } @@ -3489,16 +3488,23 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) // This is a new machine, byte 15 is header data block length // followed by variable, data byte pairs // then the 8bit Checksum + QByteArray extra = f.read(1); + if (extra.size() < 1) { + qWarning() << chunk->m_path << "read error extended header"; + break; + } + chunk->m_header.append(extra); + header = (unsigned char *)chunk->m_header.data(); int hdb_len = header[15]; int hdb_size = hdb_len * 2; - headerB2 = f.read(hdb_size+1); // add extra byte for checksum - if (headerB2.size() != hdb_size+1) { + headerB2 = f.read(hdb_size); + if (headerB2.size() != hdb_size) { qWarning() << chunk->m_path << "read error in extended header"; break; } - + chunk->m_header.append(headerB2); } else { headerB2 = QByteArray(); @@ -3514,8 +3520,8 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) qDebug() << chunk->m_path << chunk->sessionid; // log mismatched waveforum session IDs } - extra = f.read(4); - if (extra.size() != 4) { + extra = f.read(5); + if (extra.size() != 5) { qWarning() << chunk->m_path << "read error in waveform header"; break; } @@ -3524,10 +3530,15 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) header = (unsigned char *)chunk->m_header.data(); chunk->duration = header[0x0f] | header[0x10] << 8; + int always_1 = header[0x11]; + if (always_1 != 1) { + qWarning() << chunk->m_path << always_1 << "!= 1"; + //break; // don't break to avoid changing behavior (for now) + } wvfm_signals = header[0x12] | header[0x13] << 8; int ws_size = (chunk->fileVersion == 3) ? 4 : 3; - int sbsize = wvfm_signals * ws_size + 1; + int sbsize = wvfm_signals * ws_size; extra = f.read(sbsize); if (extra.size() != sbsize) { @@ -3552,20 +3563,33 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) pos -= 4; } } - } + } - // Calculate 8bit additive header checksum - achk=0; - header = (unsigned char *)chunk->m_header.data(); // important because its memory location could move - int header_size = chunk->m_header.size(); - for (int i=0; i < (header_size-1); i++) achk += header[i]; + // Calculate 8bit additive header checksum + QByteArray checksum = f.read(1); + if (checksum.size() < 1) { + qWarning() << chunk->m_path << "read error header checksum"; + break; + } + chunk->storedChecksum = checksum.data()[0]; - if (achk != header[header_size-1]) { // Header checksum mismatch? - qWarning() << chunk->m_path << "header checksum calc" << achk << "!= stored" << header[header_size-1]; - break; - } + header = (unsigned char *)chunk->m_header.data(); // important because its memory location could move + int header_size = chunk->m_header.size(); + quint8 achk=0; + for (int i=0; i < header_size; i++) { + achk += header[i]; + } + chunk->calcChecksum = achk; + + chunk->m_header.append(checksum); + + if (chunk->calcChecksum != chunk->storedChecksum) { // Header checksum mismatch? + qWarning() << chunk->m_path << "header checksum calc" << chunk->calcChecksum << "!= stored" << chunk->storedChecksum; + break; + } if (hasHeaderDataBlock) { + header = (unsigned char *)chunk->m_header.data(); const unsigned char * hd = (unsigned char *)headerB2.constData(); int pos = 0; int recs = header[15]; diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index fabc6c00..70394067 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -97,6 +97,9 @@ public: QList waveformInfo; QMap hblock; + + quint8 storedChecksum; // header checksum stored in file, last byte of m_header + quint8 calcChecksum; // header checksum as calculated when parsing }; class PRS1Loader; From 7103650023f24de7acef58ab91917a5afa56b972 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Tue, 14 May 2019 21:49:43 -0400 Subject: [PATCH 08/22] Move PRS1Loader::ParseChunk variable declarations and V3 header unpacking to the appropriate scope. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 44 ++++++++----------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 5f6127d1..8b019d0c 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3445,20 +3445,13 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) chunk->m_filepos = f.pos(); chunk->m_index = cnt; - quint16 wvfm_signals=0; - - unsigned char * header; - - QByteArray headerBA, headerB2, extra; - do { chunk->m_header = f.read(15); if (chunk->m_header.size() != 15) { qWarning() << chunk->m_path << "file too short?"; break; } - - header = (unsigned char *)chunk->m_header.data(); + unsigned char * header = (unsigned char *)chunk->m_header.data(); chunk->fileVersion = header[0]; // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed chunk->blockSize = (header[2] << 8) | header[1]; @@ -3481,13 +3474,13 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) bool hasHeaderDataBlock = (chunk->fileVersion == 3); if (chunk->ext < 5) { // Not a waveform chunk + QByteArray headerB2; // Check if this is a newer machine with a header data block if (hasHeaderDataBlock) { // This is a new machine, byte 15 is header data block length // followed by variable, data byte pairs - // then the 8bit Checksum QByteArray extra = f.read(1); if (extra.size() < 1) { qWarning() << chunk->m_path << "read error extended header"; @@ -3506,9 +3499,18 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) } chunk->m_header.append(headerB2); + header = (unsigned char *)chunk->m_header.data(); + const unsigned char * hd = (unsigned char *)headerB2.constData(); + int pos = 0; + int recs = header[15]; + for (int i=0; ihblock[hd[pos]] = hd[pos+1]; + pos += 2; + } } else { headerB2 = QByteArray(); } + chunk->m_headerblock = headerB2; } else { // Waveform Chunk QFileInfo fi(f); @@ -3520,7 +3522,7 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) qDebug() << chunk->m_path << chunk->sessionid; // log mismatched waveforum session IDs } - extra = f.read(5); + QByteArray extra = f.read(5); if (extra.size() != 5) { qWarning() << chunk->m_path << "read error in waveform header"; break; @@ -3535,7 +3537,7 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) qWarning() << chunk->m_path << always_1 << "!= 1"; //break; // don't break to avoid changing behavior (for now) } - wvfm_signals = header[0x12] | header[0x13] << 8; + quint16 wvfm_signals = header[0x12] | header[0x13] << 8; int ws_size = (chunk->fileVersion == 3) ? 4 : 3; int sbsize = wvfm_signals * ws_size; @@ -3565,7 +3567,7 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) } } - // Calculate 8bit additive header checksum + // The 8bit checksum comes at the end. QByteArray checksum = f.read(1); if (checksum.size() < 1) { qWarning() << chunk->m_path << "read error header checksum"; @@ -3573,6 +3575,7 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) } chunk->storedChecksum = checksum.data()[0]; + // Calculate 8bit additive header checksum. header = (unsigned char *)chunk->m_header.data(); // important because its memory location could move int header_size = chunk->m_header.size(); quint8 achk=0; @@ -3581,29 +3584,18 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) } chunk->calcChecksum = achk; + // Append the stored checksum to the raw data *after* calculating the checksum on the preceding data. chunk->m_header.append(checksum); + // Make sure the calculated checksum matches the stored checksum. if (chunk->calcChecksum != chunk->storedChecksum) { // Header checksum mismatch? qWarning() << chunk->m_path << "header checksum calc" << chunk->calcChecksum << "!= stored" << chunk->storedChecksum; break; } - if (hasHeaderDataBlock) { - header = (unsigned char *)chunk->m_header.data(); - const unsigned char * hd = (unsigned char *)headerB2.constData(); - int pos = 0; - int recs = header[15]; - for (int i=0; ihblock[hd[pos]] = hd[pos+1]; - pos += 2; - } - } - chunk->m_headerblock = headerB2; - int data_size = chunk->blockSize - chunk->m_header.size(); - // Read data block + int data_size = chunk->blockSize - chunk->m_header.size(); chunk->m_data = f.read(data_size); - if (chunk->m_data.size() < data_size) { qWarning() << chunk->m_path << "less data in file than specified in header"; break; From 451963de25dbf29ef8b66085be788cddceebe6de Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Tue, 14 May 2019 22:49:41 -0400 Subject: [PATCH 09/22] Move chunk parsing into PRS1DataChunk class. The diff looks messy, but it's mostly chunk -> this search-and-replace. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 167 ++++++++++-------- oscar/SleepLib/loader_plugins/prs1_loader.h | 21 ++- 2 files changed, 107 insertions(+), 81 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 8b019d0c..318254ea 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3384,10 +3384,11 @@ QList PRS1Loader::ParseFile(const QString & path) int firstsession = 0; do { - chunk = ParseChunk(f, cnt); + chunk = PRS1DataChunk::ParseNext(f); if (chunk == nullptr) { break; } + chunk->SetIndex(cnt); // for logging/debugging purposes if (lastchunk != nullptr) { // If there's any mismatch between header information, try and skip the block @@ -3436,44 +3437,63 @@ QList PRS1Loader::ParseFile(const QString & path) } -PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) +PRS1DataChunk::PRS1DataChunk(QFile & f) +{ + m_path = QFileInfo(f).canonicalFilePath(); +} + + +PRS1DataChunk* PRS1DataChunk::ParseNext(QFile & f) { PRS1DataChunk* out_chunk = nullptr; + PRS1DataChunk* chunk = new PRS1DataChunk(f); - PRS1DataChunk* chunk = new PRS1DataChunk(); - chunk->m_path = QFileInfo(f).canonicalFilePath(); - chunk->m_filepos = f.pos(); - chunk->m_index = cnt; - do { - chunk->m_header = f.read(15); - if (chunk->m_header.size() != 15) { - qWarning() << chunk->m_path << "file too short?"; + bool ok = chunk->ReadHeader(f); + if (!ok) break; + + // Only return the chunk if it has passed all tests above. + out_chunk = chunk; + } while (false); + + if (out_chunk == nullptr) delete chunk; + return out_chunk; +} + + +bool PRS1DataChunk::ReadHeader(QFile & f) +{ + bool ok = false; + do { + this->m_filepos = f.pos(); + this->m_header = f.read(15); + if (this->m_header.size() != 15) { + qWarning() << this->m_path << "file too short?"; break; } - unsigned char * header = (unsigned char *)chunk->m_header.data(); + unsigned char * header = (unsigned char *)this->m_header.data(); - chunk->fileVersion = header[0]; // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed - chunk->blockSize = (header[2] << 8) | header[1]; - chunk->htype = header[3]; // 00 = normal, 01=waveform - chunk->family = header[4]; - chunk->familyVersion = header[5]; - chunk->ext = header[6]; - chunk->sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7]; - chunk->timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11]; + this->fileVersion = header[0]; // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed + this->blockSize = (header[2] << 8) | header[1]; + this->htype = header[3]; // 00 = normal, 01=waveform + this->family = header[4]; + this->familyVersion = header[5]; + this->ext = header[6]; + this->sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7]; + this->timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11]; - if (chunk->blockSize == 0) { - qWarning() << chunk->m_path << "blocksize 0?"; + if (this->blockSize == 0) { + qWarning() << this->m_path << "blocksize 0?"; break; } - if (chunk->fileVersion < 2 || chunk->fileVersion > 3) { - qWarning() << chunk->m_path << "Never seen PRS1 header version < 2 or > 3 before"; + if (this->fileVersion < 2 || this->fileVersion > 3) { + qWarning() << this->m_path << "Never seen PRS1 header version < 2 or > 3 before"; break; } - bool hasHeaderDataBlock = (chunk->fileVersion == 3); - if (chunk->ext < 5) { // Not a waveform chunk + bool hasHeaderDataBlock = (this->fileVersion == 3); + if (this->ext < 5) { // Not a waveform chunk QByteArray headerB2; // Check if this is a newer machine with a header data block @@ -3483,85 +3503,85 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) // followed by variable, data byte pairs QByteArray extra = f.read(1); if (extra.size() < 1) { - qWarning() << chunk->m_path << "read error extended header"; + qWarning() << this->m_path << "read error extended header"; break; } - chunk->m_header.append(extra); - header = (unsigned char *)chunk->m_header.data(); + this->m_header.append(extra); + header = (unsigned char *)this->m_header.data(); int hdb_len = header[15]; int hdb_size = hdb_len * 2; headerB2 = f.read(hdb_size); if (headerB2.size() != hdb_size) { - qWarning() << chunk->m_path << "read error in extended header"; + qWarning() << this->m_path << "read error in extended header"; break; } - chunk->m_header.append(headerB2); - header = (unsigned char *)chunk->m_header.data(); + this->m_header.append(headerB2); + header = (unsigned char *)this->m_header.data(); const unsigned char * hd = (unsigned char *)headerB2.constData(); int pos = 0; int recs = header[15]; for (int i=0; ihblock[hd[pos]] = hd[pos+1]; + this->hblock[hd[pos]] = hd[pos+1]; pos += 2; } } else { headerB2 = QByteArray(); } - chunk->m_headerblock = headerB2; + this->m_headerblock = headerB2; } else { // Waveform Chunk QFileInfo fi(f); bool ok; - int sessionid_base = (chunk->fileVersion == 2 ? 10 : 16); + int sessionid_base = (this->fileVersion == 2 ? 10 : 16); QString session_s = fi.fileName().section(".", 0, -2); quint32 sid = session_s.toInt(&ok, sessionid_base); - if (!ok || sid != chunk->sessionid) { - qDebug() << chunk->m_path << chunk->sessionid; // log mismatched waveforum session IDs + if (!ok || sid != this->sessionid) { + qDebug() << this->m_path << this->sessionid; // log mismatched waveforum session IDs } QByteArray extra = f.read(5); if (extra.size() != 5) { - qWarning() << chunk->m_path << "read error in waveform header"; + qWarning() << this->m_path << "read error in waveform header"; break; } - chunk->m_header.append(extra); + this->m_header.append(extra); // Get the header address again to be safe - header = (unsigned char *)chunk->m_header.data(); + header = (unsigned char *)this->m_header.data(); - chunk->duration = header[0x0f] | header[0x10] << 8; + this->duration = header[0x0f] | header[0x10] << 8; int always_1 = header[0x11]; if (always_1 != 1) { - qWarning() << chunk->m_path << always_1 << "!= 1"; + qWarning() << this->m_path << always_1 << "!= 1"; //break; // don't break to avoid changing behavior (for now) } quint16 wvfm_signals = header[0x12] | header[0x13] << 8; - int ws_size = (chunk->fileVersion == 3) ? 4 : 3; + int ws_size = (this->fileVersion == 3) ? 4 : 3; int sbsize = wvfm_signals * ws_size; extra = f.read(sbsize); if (extra.size() != sbsize) { - qWarning() << chunk->m_path << "read error in waveform header 2"; + qWarning() << this->m_path << "read error in waveform header 2"; break; } - chunk->m_header.append(extra); - header = (unsigned char *)chunk->m_header.data(); + this->m_header.append(extra); + header = (unsigned char *)this->m_header.data(); // Read the waveform information in reverse. // TODO: Double-check this, always seems to be flow then pressure. int pos = 0x14 + (wvfm_signals - 1) * ws_size; for (int i = 0; i < wvfm_signals; ++i) { quint16 interleave = header[pos] | header[pos + 1] << 8; // samples per block (Usually 05 00) - if (chunk->fileVersion == 2) { + if (this->fileVersion == 2) { quint8 sample_format = header[pos + 2]; // TODO: sample_format seems to be unused anywhere else in the loader. - chunk->waveformInfo.push_back(PRS1Waveform(interleave, sample_format)); + this->waveformInfo.push_back(PRS1Waveform(interleave, sample_format)); pos -= 3; - } else if (chunk->fileVersion == 3) { + } else if (this->fileVersion == 3) { //quint16 sample_size = header[pos + 2] | header[pos + 3] << 8; // size in bits?? (08 00) // Possibly this is size in bits, and sign bit for the other byte? - chunk->waveformInfo.push_back(PRS1Waveform(interleave, 0)); + this->waveformInfo.push_back(PRS1Waveform(interleave, 0)); pos -= 4; } } @@ -3570,63 +3590,62 @@ PRS1DataChunk* PRS1Loader::ParseChunk(QFile & f, int cnt) // The 8bit checksum comes at the end. QByteArray checksum = f.read(1); if (checksum.size() < 1) { - qWarning() << chunk->m_path << "read error header checksum"; + qWarning() << this->m_path << "read error header checksum"; break; } - chunk->storedChecksum = checksum.data()[0]; + this->storedChecksum = checksum.data()[0]; // Calculate 8bit additive header checksum. - header = (unsigned char *)chunk->m_header.data(); // important because its memory location could move - int header_size = chunk->m_header.size(); + header = (unsigned char *)this->m_header.data(); // important because its memory location could move + int header_size = this->m_header.size(); quint8 achk=0; for (int i=0; i < header_size; i++) { achk += header[i]; } - chunk->calcChecksum = achk; + this->calcChecksum = achk; // Append the stored checksum to the raw data *after* calculating the checksum on the preceding data. - chunk->m_header.append(checksum); + this->m_header.append(checksum); // Make sure the calculated checksum matches the stored checksum. - if (chunk->calcChecksum != chunk->storedChecksum) { // Header checksum mismatch? - qWarning() << chunk->m_path << "header checksum calc" << chunk->calcChecksum << "!= stored" << chunk->storedChecksum; + if (this->calcChecksum != this->storedChecksum) { // Header checksum mismatch? + qWarning() << this->m_path << "header checksum calc" << this->calcChecksum << "!= stored" << this->storedChecksum; break; } // Read data block - int data_size = chunk->blockSize - chunk->m_header.size(); - chunk->m_data = f.read(data_size); - if (chunk->m_data.size() < data_size) { - qWarning() << chunk->m_path << "less data in file than specified in header"; + int data_size = this->blockSize - this->m_header.size(); + this->m_data = f.read(data_size); + if (this->m_data.size() < data_size) { + qWarning() << this->m_path << "less data in file than specified in header"; break; } - if (chunk->fileVersion==3) { - //int ds = chunk->m_data.size(); - //quint32 crc16 = chunk->m_data.at(ds-2) | chunk->m_data.at(ds-1) << 8; - chunk->m_data.chop(4); + if (this->fileVersion==3) { + //int ds = this->m_data.size(); + //quint32 crc16 = this->m_data.at(ds-2) | this->m_data.at(ds-1) << 8; + this->m_data.chop(4); } else { // last two bytes contain crc16 checksum. - int ds = chunk->m_data.size(); - quint16 crc16 = chunk->m_data.at(ds-2) | chunk->m_data.at(ds-1) << 8; - chunk->m_data.chop(2); + int ds = this->m_data.size(); + quint16 crc16 = this->m_data.at(ds-2) | this->m_data.at(ds-1) << 8; + this->m_data.chop(2); #ifdef PRS1_CRC_CHECK // This fails.. it needs to include the header! - quint16 calc16 = CRC16((unsigned char *)chunk->m_data.data(), chunk->m_data.size()); + quint16 calc16 = CRC16((unsigned char *)this->m_data.data(), this->m_data.size()); if (calc16 != crc16) { // corrupt data block.. bleh.. - // qDebug() << "CRC16 doesn't match for chunk" << chunk->sessionid << "for" << path; + // qDebug() << "CRC16 doesn't match for chunk" << this->sessionid << "for" << path; } #endif } - // Only return the chunk if it has passed all tests above. - out_chunk = chunk; + ok = true; } while (false); - if (out_chunk == nullptr) delete chunk; - return out_chunk; + return ok; } + void InitModelMap() { diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index 70394067..1db47ffb 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -62,15 +62,19 @@ class PRS1DataChunk friend class PRS1DataGroup; public: PRS1DataChunk() { - timestamp = 0; + fileVersion = 0; + blockSize = 0; ext = 255; - sessionid = 0; htype = 0; family = 0; familyVersion = 0; + timestamp = 0; + sessionid = 0; + duration = 0; } + PRS1DataChunk(class QFile & f); ~PRS1DataChunk() { } inline int size() const { return m_data.size(); } @@ -82,8 +86,7 @@ public: QString m_path; qint64 m_filepos; // file offset int m_index; // nth chunk in file - - SessionID sessionid; + inline void SetIndex(int index) { m_index = index; } quint8 fileVersion; quint16 blockSize; @@ -92,6 +95,7 @@ public: quint8 family; quint8 familyVersion; quint32 timestamp; + SessionID sessionid; quint16 duration; @@ -100,6 +104,12 @@ public: quint8 storedChecksum; // header checksum stored in file, last byte of m_header quint8 calcChecksum; // header checksum as calculated when parsing + + //! \brief Parse and return the next chunk from a PRS1 file + static PRS1DataChunk* ParseNext(class QFile & f); + + //! \brief Read and parse the next chunk header from a PRS1 file + bool ReadHeader(class QFile & f); }; class PRS1Loader; @@ -235,9 +245,6 @@ class PRS1Loader : public CPAPLoader //! \brief Parse a PRS1 summary/event/waveform file and break into invidivual session or waveform chunks QList ParseFile(const QString & path); - //! \brief Parse and return the next chunk from a PRS1 file - PRS1DataChunk* ParseChunk(class QFile & f, int index=0); - //! \brief Register this Module to the list of Loaders, so it knows to search for PRS1 data. static void Register(); From e07c4ce63c2eb56f31a40a06080ec767a01b7042 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Wed, 15 May 2019 08:36:31 -0400 Subject: [PATCH 10/22] Split PRS1DataChunk::ReadHeader into ReadHeader/ReadData. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 55 ++++++++++++++----- oscar/SleepLib/loader_plugins/prs1_loader.h | 5 ++ 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 318254ea..535782ca 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3449,8 +3449,37 @@ PRS1DataChunk* PRS1DataChunk::ParseNext(QFile & f) PRS1DataChunk* chunk = new PRS1DataChunk(f); do { + // Parse the header and calculate its checksum. bool ok = chunk->ReadHeader(f); - if (!ok) break; + if (!ok) { + break; + } + + // Make sure the calculated checksum matches the stored checksum. + if (chunk->calcChecksum != chunk->storedChecksum) { + qWarning() << chunk->m_path << "header checksum calc" << chunk->calcChecksum << "!= stored" << chunk->storedChecksum; + break; + } + + // Log mismatched waveform session IDs + if (chunk->ext >= 5) { + QFileInfo fi(f); + bool numeric; + int sessionid_base = (chunk->fileVersion == 2 ? 10 : 16); + QString session_s = fi.fileName().section(".", 0, -2); + quint32 sid = session_s.toInt(&numeric, sessionid_base); + if (!numeric || sid != chunk->sessionid) { + qDebug() << chunk->m_path << chunk->sessionid; + } + } + + // Read the block's data and calculate the block CRC. + ok = chunk->ReadData(f); + if (!ok) { + break; + } + + // TODO: move block CRC comparison here // Only return the chunk if it has passed all tests above. out_chunk = chunk; @@ -3533,15 +3562,6 @@ bool PRS1DataChunk::ReadHeader(QFile & f) this->m_headerblock = headerB2; } else { // Waveform Chunk - QFileInfo fi(f); - bool ok; - int sessionid_base = (this->fileVersion == 2 ? 10 : 16); - QString session_s = fi.fileName().section(".", 0, -2); - quint32 sid = session_s.toInt(&ok, sessionid_base); - if (!ok || sid != this->sessionid) { - qDebug() << this->m_path << this->sessionid; // log mismatched waveforum session IDs - } - QByteArray extra = f.read(5); if (extra.size() != 5) { qWarning() << this->m_path << "read error in waveform header"; @@ -3607,12 +3627,17 @@ bool PRS1DataChunk::ReadHeader(QFile & f) // Append the stored checksum to the raw data *after* calculating the checksum on the preceding data. this->m_header.append(checksum); - // Make sure the calculated checksum matches the stored checksum. - if (this->calcChecksum != this->storedChecksum) { // Header checksum mismatch? - qWarning() << this->m_path << "header checksum calc" << this->calcChecksum << "!= stored" << this->storedChecksum; - break; - } + ok = true; + } while (false); + return ok; +} + + +bool PRS1DataChunk::ReadData(QFile & f) +{ + bool ok = false; + do { // Read data block int data_size = this->blockSize - this->m_header.size(); this->m_data = f.read(data_size); diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index 1db47ffb..41e081f0 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -73,6 +73,8 @@ public: duration = 0; + m_filepos = -1; + m_index = -1; } PRS1DataChunk(class QFile & f); ~PRS1DataChunk() { @@ -110,6 +112,9 @@ public: //! \brief Read and parse the next chunk header from a PRS1 file bool ReadHeader(class QFile & f); + + //! \brief Read the chunk's data from a PRS1 file and calculate its CRC, must be called after ReadHeader + bool ReadData(class QFile & f); }; class PRS1Loader; From d3c6d6445bf32af062f459955b714e357aa9227d Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Wed, 15 May 2019 10:26:31 -0400 Subject: [PATCH 11/22] Split PRS1DataChunk::ReadHeader into ReadHeader/ReadWaveformHeader. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 101 ++++++++++-------- oscar/SleepLib/loader_plugins/prs1_loader.h | 4 + 2 files changed, 63 insertions(+), 42 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 535782ca..8a78f7f0 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3561,50 +3561,11 @@ bool PRS1DataChunk::ReadHeader(QFile & f) } this->m_headerblock = headerB2; - } else { // Waveform Chunk - QByteArray extra = f.read(5); - if (extra.size() != 5) { - qWarning() << this->m_path << "read error in waveform header"; + } else { // Waveform Chunk + bool hdr_ok = ReadWaveformHeader(f); + if (!hdr_ok) { break; } - this->m_header.append(extra); - // Get the header address again to be safe - header = (unsigned char *)this->m_header.data(); - - this->duration = header[0x0f] | header[0x10] << 8; - int always_1 = header[0x11]; - if (always_1 != 1) { - qWarning() << this->m_path << always_1 << "!= 1"; - //break; // don't break to avoid changing behavior (for now) - } - quint16 wvfm_signals = header[0x12] | header[0x13] << 8; - - int ws_size = (this->fileVersion == 3) ? 4 : 3; - int sbsize = wvfm_signals * ws_size; - - extra = f.read(sbsize); - if (extra.size() != sbsize) { - qWarning() << this->m_path << "read error in waveform header 2"; - break; - } - this->m_header.append(extra); - header = (unsigned char *)this->m_header.data(); - - // Read the waveform information in reverse. // TODO: Double-check this, always seems to be flow then pressure. - int pos = 0x14 + (wvfm_signals - 1) * ws_size; - for (int i = 0; i < wvfm_signals; ++i) { - quint16 interleave = header[pos] | header[pos + 1] << 8; // samples per block (Usually 05 00) - if (this->fileVersion == 2) { - quint8 sample_format = header[pos + 2]; // TODO: sample_format seems to be unused anywhere else in the loader. - this->waveformInfo.push_back(PRS1Waveform(interleave, sample_format)); - pos -= 3; - } else if (this->fileVersion == 3) { - //quint16 sample_size = header[pos + 2] | header[pos + 3] << 8; // size in bits?? (08 00) - // Possibly this is size in bits, and sign bit for the other byte? - this->waveformInfo.push_back(PRS1Waveform(interleave, 0)); - pos -= 4; - } - } } // The 8bit checksum comes at the end. @@ -3634,6 +3595,62 @@ bool PRS1DataChunk::ReadHeader(QFile & f) } +bool PRS1DataChunk::ReadWaveformHeader(QFile & f) +{ + bool ok = false; + unsigned char * header; + do { + QByteArray extra = f.read(5); + if (extra.size() != 5) { + qWarning() << this->m_path << "read error in waveform header"; + break; + } + this->m_header.append(extra); + // Get the header address again to be safe + header = (unsigned char *)this->m_header.data(); + + this->duration = header[0x0f] | header[0x10] << 8; + int always_1 = header[0x11]; + if (always_1 != 1) { + qWarning() << this->m_path << always_1 << "!= 1"; + //break; // don't break to avoid changing behavior (for now) + } + quint16 wvfm_signals = header[0x12] | header[0x13] << 8; + + int ws_size = (this->fileVersion == 3) ? 4 : 3; + int sbsize = wvfm_signals * ws_size; + + extra = f.read(sbsize); + if (extra.size() != sbsize) { + qWarning() << this->m_path << "read error in waveform header 2"; + break; + } + this->m_header.append(extra); + header = (unsigned char *)this->m_header.data(); + + // Read the waveform information in reverse. // TODO: Double-check this, always seems to be flow then pressure. + int pos = 0x14 + (wvfm_signals - 1) * ws_size; + for (int i = 0; i < wvfm_signals; ++i) { + quint16 interleave = header[pos] | header[pos + 1] << 8; // samples per block (Usually 05 00) + if (this->fileVersion == 2) { + quint8 sample_format = header[pos + 2]; // TODO: sample_format seems to be unused anywhere else in the loader. + this->waveformInfo.push_back(PRS1Waveform(interleave, sample_format)); + pos -= 3; + } else if (this->fileVersion == 3) { + //quint16 sample_size = header[pos + 2] | header[pos + 3] << 8; // size in bits?? (08 00) + // Possibly this is size in bits, and sign bit for the other byte? + this->waveformInfo.push_back(PRS1Waveform(interleave, 0)); + pos -= 4; + } + } + + ok = true; + } while (false); + + return ok; +} + + bool PRS1DataChunk::ReadData(QFile & f) { bool ok = false; diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index 41e081f0..cef45c42 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -115,6 +115,10 @@ public: //! \brief Read the chunk's data from a PRS1 file and calculate its CRC, must be called after ReadHeader bool ReadData(class QFile & f); + +protected: + //! \brief Read and parse the waveform-specific header data from a PRS1 file + bool ReadWaveformHeader(class QFile & f); }; class PRS1Loader; From c8cd66992a4ed79235f468c297aecaeab5124002 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Wed, 15 May 2019 12:32:39 -0400 Subject: [PATCH 12/22] Split PRS1DataChunk::ReadHeader into ReadHeader/ReadNormalHeaderV2/ReadNormalHeaderV3. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 104 +++++++++++------- oscar/SleepLib/loader_plugins/prs1_loader.h | 6 + 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 8a78f7f0..eedba61e 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3521,51 +3521,24 @@ bool PRS1DataChunk::ReadHeader(QFile & f) break; } - bool hasHeaderDataBlock = (this->fileVersion == 3); + bool hdr_ok = false; if (this->ext < 5) { // Not a waveform chunk - QByteArray headerB2; - - // Check if this is a newer machine with a header data block - - if (hasHeaderDataBlock) { - // This is a new machine, byte 15 is header data block length - // followed by variable, data byte pairs - QByteArray extra = f.read(1); - if (extra.size() < 1) { - qWarning() << this->m_path << "read error extended header"; + switch (this->fileVersion) { + case 2: + hdr_ok = ReadNormalHeaderV2(f); break; - } - this->m_header.append(extra); - header = (unsigned char *)this->m_header.data(); - - int hdb_len = header[15]; - int hdb_size = hdb_len * 2; - - headerB2 = f.read(hdb_size); - if (headerB2.size() != hdb_size) { - qWarning() << this->m_path << "read error in extended header"; + case 3: + hdr_ok = ReadNormalHeaderV3(f); + break; + default: + //hdr_ok remains false, warning is above break; - } - - this->m_header.append(headerB2); - header = (unsigned char *)this->m_header.data(); - const unsigned char * hd = (unsigned char *)headerB2.constData(); - int pos = 0; - int recs = header[15]; - for (int i=0; ihblock[hd[pos]] = hd[pos+1]; - pos += 2; - } - } else { - headerB2 = QByteArray(); } - this->m_headerblock = headerB2; - } else { // Waveform Chunk - bool hdr_ok = ReadWaveformHeader(f); - if (!hdr_ok) { - break; - } + hdr_ok = ReadWaveformHeader(f); + } + if (!hdr_ok) { + break; } // The 8bit checksum comes at the end. @@ -3595,6 +3568,57 @@ bool PRS1DataChunk::ReadHeader(QFile & f) } +bool PRS1DataChunk::ReadNormalHeaderV2(QFile & /*f*/) +{ + this->m_headerblock = QByteArray(); + return true; // always OK +} + + +bool PRS1DataChunk::ReadNormalHeaderV3(QFile & f) +{ + bool ok = false; + unsigned char * header; + QByteArray headerB2; + + // This is a new machine, byte 15 is header data block length + // followed by variable, data byte pairs + do { + QByteArray extra = f.read(1); + if (extra.size() < 1) { + qWarning() << this->m_path << "read error extended header"; + break; + } + this->m_header.append(extra); + header = (unsigned char *)this->m_header.data(); + + int hdb_len = header[15]; + int hdb_size = hdb_len * 2; + + headerB2 = f.read(hdb_size); + if (headerB2.size() != hdb_size) { + qWarning() << this->m_path << "read error in extended header"; + break; + } + this->m_headerblock = headerB2; + + this->m_header.append(headerB2); + header = (unsigned char *)this->m_header.data(); + const unsigned char * hd = (unsigned char *)headerB2.constData(); + int pos = 0; + int recs = header[15]; + for (int i=0; ihblock[hd[pos]] = hd[pos+1]; + pos += 2; + } + + ok = true; + } while (false); + + return ok; +} + + bool PRS1DataChunk::ReadWaveformHeader(QFile & f) { bool ok = false; diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index cef45c42..fd676c84 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -117,6 +117,12 @@ public: bool ReadData(class QFile & f); protected: + //! \brief Read and parse the non-waveform header data from a V2 PRS1 file + bool ReadNormalHeaderV2(class QFile & f); + + //! \brief Read and parse the non-waveform header data from a V3 PRS1 file + bool ReadNormalHeaderV3(class QFile & f); + //! \brief Read and parse the waveform-specific header data from a PRS1 file bool ReadWaveformHeader(class QFile & f); }; From 1c564fb2967acc00fe3fcf10c2ab1dab055349f4 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Wed, 15 May 2019 15:16:14 -0400 Subject: [PATCH 13/22] Calculate and check PRS1 CRC16 on V2 files. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 157 ++++++++++-------- oscar/SleepLib/loader_plugins/prs1_loader.h | 7 +- 2 files changed, 97 insertions(+), 67 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index eedba61e..62ab8f0d 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -43,61 +43,55 @@ QHash ModelMap; -#define PRS1_CRC_CHECK -#ifdef PRS1_CRC_CHECK -typedef quint16 crc_t; +// CRC-16/KERMIT, polynomial: 0x11021, bit reverse algorithm +// Table generated by crcmod (crc-kermit) -static const crc_t crc_table[256] = { - 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, - 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, - 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, - 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, - 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, - 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, - 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, - 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, - 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, - 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, - 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, - 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, - 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, - 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, - 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, - 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, - 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, - 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, - 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, - 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, - 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, - 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, - 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, - 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, - 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, - 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, - 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, - 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, - 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, - 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, - 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, - 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 -}; - -crc_t CRC16(const unsigned char *data, size_t data_len) +typedef quint16 crc16_t; +static crc16_t CRC16(unsigned char * data, size_t data_len, crc16_t crc=0) { - crc_t crc = 0; - unsigned int tbl_idx; - - while (data_len--) { - tbl_idx = (crc ^ *data) & 0xff; - crc = (crc_table[tbl_idx] ^ (crc >> 8)) & 0xffff; + static const crc16_t table[256] = { + 0x0000U, 0x1189U, 0x2312U, 0x329bU, 0x4624U, 0x57adU, 0x6536U, 0x74bfU, + 0x8c48U, 0x9dc1U, 0xaf5aU, 0xbed3U, 0xca6cU, 0xdbe5U, 0xe97eU, 0xf8f7U, + 0x1081U, 0x0108U, 0x3393U, 0x221aU, 0x56a5U, 0x472cU, 0x75b7U, 0x643eU, + 0x9cc9U, 0x8d40U, 0xbfdbU, 0xae52U, 0xdaedU, 0xcb64U, 0xf9ffU, 0xe876U, + 0x2102U, 0x308bU, 0x0210U, 0x1399U, 0x6726U, 0x76afU, 0x4434U, 0x55bdU, + 0xad4aU, 0xbcc3U, 0x8e58U, 0x9fd1U, 0xeb6eU, 0xfae7U, 0xc87cU, 0xd9f5U, + 0x3183U, 0x200aU, 0x1291U, 0x0318U, 0x77a7U, 0x662eU, 0x54b5U, 0x453cU, + 0xbdcbU, 0xac42U, 0x9ed9U, 0x8f50U, 0xfbefU, 0xea66U, 0xd8fdU, 0xc974U, + 0x4204U, 0x538dU, 0x6116U, 0x709fU, 0x0420U, 0x15a9U, 0x2732U, 0x36bbU, + 0xce4cU, 0xdfc5U, 0xed5eU, 0xfcd7U, 0x8868U, 0x99e1U, 0xab7aU, 0xbaf3U, + 0x5285U, 0x430cU, 0x7197U, 0x601eU, 0x14a1U, 0x0528U, 0x37b3U, 0x263aU, + 0xdecdU, 0xcf44U, 0xfddfU, 0xec56U, 0x98e9U, 0x8960U, 0xbbfbU, 0xaa72U, + 0x6306U, 0x728fU, 0x4014U, 0x519dU, 0x2522U, 0x34abU, 0x0630U, 0x17b9U, + 0xef4eU, 0xfec7U, 0xcc5cU, 0xddd5U, 0xa96aU, 0xb8e3U, 0x8a78U, 0x9bf1U, + 0x7387U, 0x620eU, 0x5095U, 0x411cU, 0x35a3U, 0x242aU, 0x16b1U, 0x0738U, + 0xffcfU, 0xee46U, 0xdcddU, 0xcd54U, 0xb9ebU, 0xa862U, 0x9af9U, 0x8b70U, + 0x8408U, 0x9581U, 0xa71aU, 0xb693U, 0xc22cU, 0xd3a5U, 0xe13eU, 0xf0b7U, + 0x0840U, 0x19c9U, 0x2b52U, 0x3adbU, 0x4e64U, 0x5fedU, 0x6d76U, 0x7cffU, + 0x9489U, 0x8500U, 0xb79bU, 0xa612U, 0xd2adU, 0xc324U, 0xf1bfU, 0xe036U, + 0x18c1U, 0x0948U, 0x3bd3U, 0x2a5aU, 0x5ee5U, 0x4f6cU, 0x7df7U, 0x6c7eU, + 0xa50aU, 0xb483U, 0x8618U, 0x9791U, 0xe32eU, 0xf2a7U, 0xc03cU, 0xd1b5U, + 0x2942U, 0x38cbU, 0x0a50U, 0x1bd9U, 0x6f66U, 0x7eefU, 0x4c74U, 0x5dfdU, + 0xb58bU, 0xa402U, 0x9699U, 0x8710U, 0xf3afU, 0xe226U, 0xd0bdU, 0xc134U, + 0x39c3U, 0x284aU, 0x1ad1U, 0x0b58U, 0x7fe7U, 0x6e6eU, 0x5cf5U, 0x4d7cU, + 0xc60cU, 0xd785U, 0xe51eU, 0xf497U, 0x8028U, 0x91a1U, 0xa33aU, 0xb2b3U, + 0x4a44U, 0x5bcdU, 0x6956U, 0x78dfU, 0x0c60U, 0x1de9U, 0x2f72U, 0x3efbU, + 0xd68dU, 0xc704U, 0xf59fU, 0xe416U, 0x90a9U, 0x8120U, 0xb3bbU, 0xa232U, + 0x5ac5U, 0x4b4cU, 0x79d7U, 0x685eU, 0x1ce1U, 0x0d68U, 0x3ff3U, 0x2e7aU, + 0xe70eU, 0xf687U, 0xc41cU, 0xd595U, 0xa12aU, 0xb0a3U, 0x8238U, 0x93b1U, + 0x6b46U, 0x7acfU, 0x4854U, 0x59ddU, 0x2d62U, 0x3cebU, 0x0e70U, 0x1ff9U, + 0xf78fU, 0xe606U, 0xd49dU, 0xc514U, 0xb1abU, 0xa022U, 0x92b9U, 0x8330U, + 0x7bc7U, 0x6a4eU, 0x58d5U, 0x495cU, 0x3de3U, 0x2c6aU, 0x1ef1U, 0x0f78U, + }; + for (size_t i=0; i < data_len; i++) { + crc = table[(*data ^ (unsigned char)crc) & 0xFF] ^ (crc >> 8); data++; } - - return crc & 0xffff; + return crc; } -#endif + enum FlexMode { FLEX_None, FLEX_CFlex, FLEX_CFlexPlus, FLEX_AFlex, FLEX_RiseTime, FLEX_BiFlex, FLEX_Unknown }; @@ -3479,7 +3473,12 @@ PRS1DataChunk* PRS1DataChunk::ParseNext(QFile & f) break; } - // TODO: move block CRC comparison here + // Make sure the calculated CRC over the entire chunk (header and data) matches the stored CRC. + if (chunk->calcCrc != chunk->storedCrc) { + // corrupt data block.. bleh.. + qDebug() << chunk->m_path << "@" << chunk->m_filepos << "block CRC calc" << hex << chunk->calcCrc << "!= stored" << hex << chunk->storedCrc; + //break; // don't break to avoid changing behavior (for now) + } // Only return the chunk if it has passed all tests above. out_chunk = chunk; @@ -3494,14 +3493,15 @@ bool PRS1DataChunk::ReadHeader(QFile & f) { bool ok = false; do { + // Read common header fields. this->m_filepos = f.pos(); this->m_header = f.read(15); if (this->m_header.size() != 15) { qWarning() << this->m_path << "file too short?"; break; } + unsigned char * header = (unsigned char *)this->m_header.data(); - this->fileVersion = header[0]; // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed this->blockSize = (header[2] << 8) | header[1]; this->htype = header[3]; // 00 = normal, 01=waveform @@ -3511,16 +3511,17 @@ bool PRS1DataChunk::ReadHeader(QFile & f) this->sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7]; this->timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11]; + // Do a few early sanity checks before any variable-length header data. if (this->blockSize == 0) { qWarning() << this->m_path << "blocksize 0?"; break; } - if (this->fileVersion < 2 || this->fileVersion > 3) { qWarning() << this->m_path << "Never seen PRS1 header version < 2 or > 3 before"; break; } + // Read format-specific variable-length header data. bool hdr_ok = false; if (this->ext < 5) { // Not a waveform chunk switch (this->fileVersion) { @@ -3681,29 +3682,29 @@ bool PRS1DataChunk::ReadData(QFile & f) do { // Read data block int data_size = this->blockSize - this->m_header.size(); + if (data_size < 0) { + qWarning() << this->m_path << "chunk size smaller than header"; + break; + } this->m_data = f.read(data_size); if (this->m_data.size() < data_size) { qWarning() << this->m_path << "less data in file than specified in header"; break; } + // Extract the stored CRC from the data buffer and calculate the current CRC. if (this->fileVersion==3) { - //int ds = this->m_data.size(); - //quint32 crc16 = this->m_data.at(ds-2) | this->m_data.at(ds-1) << 8; - this->m_data.chop(4); - } else { - // last two bytes contain crc16 checksum. - int ds = this->m_data.size(); - quint16 crc16 = this->m_data.at(ds-2) | this->m_data.at(ds-1) << 8; - this->m_data.chop(2); -#ifdef PRS1_CRC_CHECK - // This fails.. it needs to include the header! - quint16 calc16 = CRC16((unsigned char *)this->m_data.data(), this->m_data.size()); - if (calc16 != crc16) { - // corrupt data block.. bleh.. - // qDebug() << "CRC16 doesn't match for chunk" << this->sessionid << "for" << path; + // The last 4 bytes contain a CRC32 checksum of the data. + if (!ExtractStoredCrc(4)) { + break; } -#endif + this->calcCrc = this->storedCrc; // TODO + } else { + // The last 2 bytes contain a CRC16 checksum of the data. + if (!ExtractStoredCrc(2)) { + break; + } + this->calcCrc = CRC16((unsigned char *)this->m_data.data(), this->m_data.size()); } ok = true; @@ -3711,6 +3712,30 @@ bool PRS1DataChunk::ReadData(QFile & f) return ok; } + + +bool PRS1DataChunk::ExtractStoredCrc(int size) +{ + // Make sure there's enough data for the CRC. + int offset = this->m_data.size() - size; + if (offset < 0) { + qWarning() << this->m_path << "chunk truncated"; + return false; + } + + // Read the last 16- or 32-bit little-endian integer. + quint32 storedCrc = 0; + unsigned char* data = (unsigned char*)this->m_data.data(); + for (int i=0; i < size; i++) { + storedCrc |= data[offset+i] << (8*i); + } + this->storedCrc = storedCrc; + + // Drop the CRC from the data. + this->m_data.chop(size); + + return true; +} void InitModelMap() diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index fd676c84..cb7a59aa 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -105,7 +105,9 @@ public: QMap hblock; quint8 storedChecksum; // header checksum stored in file, last byte of m_header - quint8 calcChecksum; // header checksum as calculated when parsing + quint8 calcChecksum; // header checksum as calculated when parsing + quint32 storedCrc; // header + data CRC stored in file, last 2-4 bytes of chunk + quint32 calcCrc; // header + data CRC as calculated when parsing //! \brief Parse and return the next chunk from a PRS1 file static PRS1DataChunk* ParseNext(class QFile & f); @@ -125,6 +127,9 @@ protected: //! \brief Read and parse the waveform-specific header data from a PRS1 file bool ReadWaveformHeader(class QFile & f); + + //! \brief Extract the stored CRC from the end of the data of a PRS1 chunk + bool ExtractStoredCrc(int size); }; class PRS1Loader; From ccafa1f16ee58678d29f097b6aa80501fc6a40a3 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Wed, 15 May 2019 17:41:37 -0400 Subject: [PATCH 14/22] Calculate and check PRS1 CRC32 on V3 files, fix memory leak. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 103 +++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 62ab8f0d..8dfaca59 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -93,6 +93,102 @@ static crc16_t CRC16(unsigned char * data, size_t data_len, crc16_t crc=0) } +// CRC-32/MPEG-2, polynomial: 0x104C11DB7 +// Table generated by crcmod (crc-32-mpeg) + +typedef quint32 crc32_t; +static crc32_t CRC32(const unsigned char *data, size_t data_len, crc32_t crc=0xffffffffU) +{ + static const crc32_t table[256] = { + 0x00000000U, 0x04c11db7U, 0x09823b6eU, 0x0d4326d9U, + 0x130476dcU, 0x17c56b6bU, 0x1a864db2U, 0x1e475005U, + 0x2608edb8U, 0x22c9f00fU, 0x2f8ad6d6U, 0x2b4bcb61U, + 0x350c9b64U, 0x31cd86d3U, 0x3c8ea00aU, 0x384fbdbdU, + 0x4c11db70U, 0x48d0c6c7U, 0x4593e01eU, 0x4152fda9U, + 0x5f15adacU, 0x5bd4b01bU, 0x569796c2U, 0x52568b75U, + 0x6a1936c8U, 0x6ed82b7fU, 0x639b0da6U, 0x675a1011U, + 0x791d4014U, 0x7ddc5da3U, 0x709f7b7aU, 0x745e66cdU, + 0x9823b6e0U, 0x9ce2ab57U, 0x91a18d8eU, 0x95609039U, + 0x8b27c03cU, 0x8fe6dd8bU, 0x82a5fb52U, 0x8664e6e5U, + 0xbe2b5b58U, 0xbaea46efU, 0xb7a96036U, 0xb3687d81U, + 0xad2f2d84U, 0xa9ee3033U, 0xa4ad16eaU, 0xa06c0b5dU, + 0xd4326d90U, 0xd0f37027U, 0xddb056feU, 0xd9714b49U, + 0xc7361b4cU, 0xc3f706fbU, 0xceb42022U, 0xca753d95U, + 0xf23a8028U, 0xf6fb9d9fU, 0xfbb8bb46U, 0xff79a6f1U, + 0xe13ef6f4U, 0xe5ffeb43U, 0xe8bccd9aU, 0xec7dd02dU, + 0x34867077U, 0x30476dc0U, 0x3d044b19U, 0x39c556aeU, + 0x278206abU, 0x23431b1cU, 0x2e003dc5U, 0x2ac12072U, + 0x128e9dcfU, 0x164f8078U, 0x1b0ca6a1U, 0x1fcdbb16U, + 0x018aeb13U, 0x054bf6a4U, 0x0808d07dU, 0x0cc9cdcaU, + 0x7897ab07U, 0x7c56b6b0U, 0x71159069U, 0x75d48ddeU, + 0x6b93dddbU, 0x6f52c06cU, 0x6211e6b5U, 0x66d0fb02U, + 0x5e9f46bfU, 0x5a5e5b08U, 0x571d7dd1U, 0x53dc6066U, + 0x4d9b3063U, 0x495a2dd4U, 0x44190b0dU, 0x40d816baU, + 0xaca5c697U, 0xa864db20U, 0xa527fdf9U, 0xa1e6e04eU, + 0xbfa1b04bU, 0xbb60adfcU, 0xb6238b25U, 0xb2e29692U, + 0x8aad2b2fU, 0x8e6c3698U, 0x832f1041U, 0x87ee0df6U, + 0x99a95df3U, 0x9d684044U, 0x902b669dU, 0x94ea7b2aU, + 0xe0b41de7U, 0xe4750050U, 0xe9362689U, 0xedf73b3eU, + 0xf3b06b3bU, 0xf771768cU, 0xfa325055U, 0xfef34de2U, + 0xc6bcf05fU, 0xc27dede8U, 0xcf3ecb31U, 0xcbffd686U, + 0xd5b88683U, 0xd1799b34U, 0xdc3abdedU, 0xd8fba05aU, + 0x690ce0eeU, 0x6dcdfd59U, 0x608edb80U, 0x644fc637U, + 0x7a089632U, 0x7ec98b85U, 0x738aad5cU, 0x774bb0ebU, + 0x4f040d56U, 0x4bc510e1U, 0x46863638U, 0x42472b8fU, + 0x5c007b8aU, 0x58c1663dU, 0x558240e4U, 0x51435d53U, + 0x251d3b9eU, 0x21dc2629U, 0x2c9f00f0U, 0x285e1d47U, + 0x36194d42U, 0x32d850f5U, 0x3f9b762cU, 0x3b5a6b9bU, + 0x0315d626U, 0x07d4cb91U, 0x0a97ed48U, 0x0e56f0ffU, + 0x1011a0faU, 0x14d0bd4dU, 0x19939b94U, 0x1d528623U, + 0xf12f560eU, 0xf5ee4bb9U, 0xf8ad6d60U, 0xfc6c70d7U, + 0xe22b20d2U, 0xe6ea3d65U, 0xeba91bbcU, 0xef68060bU, + 0xd727bbb6U, 0xd3e6a601U, 0xdea580d8U, 0xda649d6fU, + 0xc423cd6aU, 0xc0e2d0ddU, 0xcda1f604U, 0xc960ebb3U, + 0xbd3e8d7eU, 0xb9ff90c9U, 0xb4bcb610U, 0xb07daba7U, + 0xae3afba2U, 0xaafbe615U, 0xa7b8c0ccU, 0xa379dd7bU, + 0x9b3660c6U, 0x9ff77d71U, 0x92b45ba8U, 0x9675461fU, + 0x8832161aU, 0x8cf30badU, 0x81b02d74U, 0x857130c3U, + 0x5d8a9099U, 0x594b8d2eU, 0x5408abf7U, 0x50c9b640U, + 0x4e8ee645U, 0x4a4ffbf2U, 0x470cdd2bU, 0x43cdc09cU, + 0x7b827d21U, 0x7f436096U, 0x7200464fU, 0x76c15bf8U, + 0x68860bfdU, 0x6c47164aU, 0x61043093U, 0x65c52d24U, + 0x119b4be9U, 0x155a565eU, 0x18197087U, 0x1cd86d30U, + 0x029f3d35U, 0x065e2082U, 0x0b1d065bU, 0x0fdc1becU, + 0x3793a651U, 0x3352bbe6U, 0x3e119d3fU, 0x3ad08088U, + 0x2497d08dU, 0x2056cd3aU, 0x2d15ebe3U, 0x29d4f654U, + 0xc5a92679U, 0xc1683bceU, 0xcc2b1d17U, 0xc8ea00a0U, + 0xd6ad50a5U, 0xd26c4d12U, 0xdf2f6bcbU, 0xdbee767cU, + 0xe3a1cbc1U, 0xe760d676U, 0xea23f0afU, 0xeee2ed18U, + 0xf0a5bd1dU, 0xf464a0aaU, 0xf9278673U, 0xfde69bc4U, + 0x89b8fd09U, 0x8d79e0beU, 0x803ac667U, 0x84fbdbd0U, + 0x9abc8bd5U, 0x9e7d9662U, 0x933eb0bbU, 0x97ffad0cU, + 0xafb010b1U, 0xab710d06U, 0xa6322bdfU, 0xa2f33668U, + 0xbcb4666dU, 0xb8757bdaU, 0xb5365d03U, 0xb1f740b4U, + }; + + for (size_t i=0; i < data_len; i++) { + crc = table[(*data ^ (unsigned char)(crc >> 24)) & 0xFF] ^ (crc << 8); + data++; + } + return crc; +} + + +// Strangely, the PRS1 CRC32 appears to consider every byte a 32-bit wchar_t. +// Nothing like trying a bunch of encodings and CRC32 variants on PROP.TXT files +// until you find a winner. + +static crc32_t CRC32wchar(const unsigned char *data, size_t data_len, crc32_t crc=0xffffffffU) +{ + for (size_t i=0; i < data_len; i++) { + static unsigned char wch[4] = { 0, 0, 0, 0 }; + wch[3] = *data++; + crc = CRC32(wch, 4, crc); + } + return crc; +} + + enum FlexMode { FLEX_None, FLEX_CFlex, FLEX_CFlexPlus, FLEX_AFlex, FLEX_RiseTime, FLEX_BiFlex, FLEX_Unknown }; ChannelID PRS1_TimedBreath = 0, PRS1_HeatedTubing = 0; @@ -821,6 +917,7 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin case 0: if (task->compliance) { qWarning() << path << "duplicate compliance?"; + delete chunk; continue; // (skipping to avoid duplicates) } task->compliance = chunk; @@ -828,6 +925,7 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin case 1: if (task->summary) { qWarning() << path << "duplicate summary?"; + delete chunk; continue; } task->summary = chunk; @@ -835,6 +933,7 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin case 2: if (task->event) { qWarning() << path << "duplicate events?"; + delete chunk; continue; } task->event = chunk; @@ -3517,7 +3616,7 @@ bool PRS1DataChunk::ReadHeader(QFile & f) break; } if (this->fileVersion < 2 || this->fileVersion > 3) { - qWarning() << this->m_path << "Never seen PRS1 header version < 2 or > 3 before"; + qWarning() << this->m_path << "@" << hex << this->m_filepos << "Never seen PRS1 header version < 2 or > 3 before"; break; } @@ -3698,7 +3797,7 @@ bool PRS1DataChunk::ReadData(QFile & f) if (!ExtractStoredCrc(4)) { break; } - this->calcCrc = this->storedCrc; // TODO + this->calcCrc = CRC32wchar((unsigned char *)this->m_data.data(), this->m_data.size()); } else { // The last 2 bytes contain a CRC16 checksum of the data. if (!ExtractStoredCrc(2)) { From e033a5a4ef451346313402202ae4068b5338948a Mon Sep 17 00:00:00 2001 From: Seeker4 Date: Thu, 16 May 2019 22:33:05 -0700 Subject: [PATCH 15/22] Tweaking the white rim in the lower right corner of 32x32 icon for taskbar etc. --- Building/Icons/OSCAR.icns | Bin 317863 -> 317863 bytes Building/Icons/Wave-32.png | Bin 4381 -> 1646 bytes Building/Icons/logo.ico | Bin 36001 -> 36001 bytes oscar/icons/OSCAR.icns | Bin 317863 -> 317863 bytes oscar/icons/logo.ico | Bin 36001 -> 36001 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Building/Icons/OSCAR.icns b/Building/Icons/OSCAR.icns index ae6ad4108d78c34e8f09f4d146cd530df4ce2a94..947fc46d7afc7031385996c39779d91bceb72ce4 100644 GIT binary patch delta 310 zcmZ3!S$O$o;SF|djQ=;=vt3~VQX-refYjut+^&3zd++{-0YA~n7CZ|4d+ZsIfd1rk z9uw~El2D=8>yrb6MY;YRh44KuO@7EE&hc*(n0pQ=C&~V=9>__11(cAQEXS+J;>O_f z9LSMo`S+FcHc*8u^S|OhK!K{sADFU%Sc2)_-pP%83XGqCf-)@s{xeJlGGv(<7z9By z3(&MZK!!9M$PTkxK&Au-P!R~M0CL19NAueOeKU(+9!T!umz9UeffYar#U1~ko&?g9 g1D!>qZYKmYxYm>lRV!p#~1^SDN{s6e}@03#4H0WtG-Q2~}oCjpu;Z0i63 diff --git a/Building/Icons/Wave-32.png b/Building/Icons/Wave-32.png index b69b843ef14d909de168a1e80dd092d3c2014a33..ccb74ae3a7d9b44ca064daa3be91a6b2fa174bf4 100644 GIT binary patch delta 1609 zcmV-P2DbT~BJK>3IDZBpNklOSZK_rP8(_XhGGc(REC#NF_$wjY+G7)K05G2(_VU zS}3B$(3m6`;y4Y)b`r-v`@^xF7u)&4{p;O#?z`Xb`Mz_{Ie!sra&=eD~8PF7F-NaoLI!#qJ+G=fl+wEXhi3z2Y z^Ntk|5qZ~ti+|t$FUWsC7zMkT6=hbYl0qUS`5#QDU?v5nW^5LXXVzD+r=u2^&HN?s zQ{a82)KpOdBGRCg8p?qG;LR^NdUg~?G*O1)Qz zz;~5W$Kr{k)c1TsIGW5_x3!jY_{k>9ZHudLB|Js<$zhJ4AJ4PB*4f05H&k2(9#=|T z%n}fhCZ*J%h{%RNUF6(wY(Dd4tKZ`b(tUC`@BOu3t)tPYXC2o>L}kEV{V-$z@AA~# z5%@yiK7Vo4kB9_Bqyj(#D$^>!Fa90KE_9ZdvK#pQeuBZ^f`Excf`NenCMPEi>b2L| z4eLG{iE-g(ybkz{h?q3saS@s99SAcs=V*05SVOtZgx~MS>-DmI`*xzyXi)+k9Ubi4 zxs#0>HyVG%qxE@f_n*39N+iV#G;6?P@kC1Be}CHO;kMRt0K8r=y}iBc-Mg1fn>G~$ z9~&D3z~}R270ZlAT2}InTB8c@T$13`LU5`u~jFZtAJo_vF51%*zCr)H-_UE26to8Ku;oGnQo_`*I zbs59#_W)1y^}z!#=c!P%8h$mI#M9TuJAZGz1r7(qV&HVbLl1G|>8Gh)vj&gH1Hjv_ zzfQ8U5>qM#CKDrp0QB|2OD{3x_wzlkm$!R+sr>Uz1BOF4e7hm4A+-(a{`%YdHcYrRaXJ#-dbOa}D?uVE>~Hd3C)| zO+S42Fb5AFEJ`4gFZS)*htKC@Jf7rlAB|=0-t~8-j7^hL31D;WN(-Lm6^6|U)pLOM z_V%I#91h1kB}bWFo7qy;}?%prWE85B!bo&58No1>15Fk!Sun$ZLJZAZ#huIkv;i`g;4K1de?= z&JT}YWjZNnN^x}Cs_7kVE`RHM@S^tu5s@BWkYAr1W`6ho&4yC8tS;lX57!!xT|CJv zXKwP>b0Go~>Dp53urSj7b!JsocXTmn|KmmaM`FwB2g|G)yS`PEc%h@#vJm)^?uMDm z=GHf^a5+51`Cy!+nvRicsT@YN~w4vnL-*rdoAkxBp7G# zt7*n=CrBj)r_DrtnU&R5w&dp3Wu~P9FMEiHd#P9R<&6IW*oekjMZUmB00000NkvXX Hu0mjf%f2gv delta 4366 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 W{N6Io@<(XbUa_*};i-6Icl;l^_bR6V delta 559 zcmZ9KPe>F|9LIg`PR?fTpK(TZMyFU66O|+~kfPls6Cni!>5vx@4?+tIvL>-hyHs@P zu%TZ&Y*Df7sav=QZ-OU52)%ff4tWv{B*L|I$L~W1!hsL(`}=<1@An?ODHiIBh5GLw z{EEkMoESD5JA?soV?t#aB5AY)dc`el_#a72Ckf;O_r()hgVK6Hc5pQ~B8G8W>GQH2 z!t3CexAOaE4bedNg;NV=L;~qhMzOi$GfqQu#?!h>ACoK=wT$rLSWD?CH>N*DoV{;T zc&XV~Z&2~2c~af1o*X+nZ(H!nionvxMGVjMH=+qgdQRlZNyFn&_s>rMI=kq{e2;;A zcvk(&LAAr}5}OS8&1v658a-qci)KpyL8TBuo)hO_tALqk7-rX9JhobmfAqTkyV*U~ zYe-uOv*fz&%H%ycLCu=AD^#Oy;;!_%$qKTq(~6wDfF*DE8pQzlM5?&bHYuvui*!oc zBpI@gcWt9S+3|ujv8yvkA>$~KoqwR&F{MNm=VJ}XSR>yWf|m%93_&iOU1z WtLI3GKf_I>m#R??FDI|xNuC363L7i{ diff --git a/oscar/icons/OSCAR.icns b/oscar/icons/OSCAR.icns index ae6ad4108d78c34e8f09f4d146cd530df4ce2a94..947fc46d7afc7031385996c39779d91bceb72ce4 100644 GIT binary patch delta 310 zcmZ3!S$O$o;SF|djQ=;=vt3~VQX-refYjut+^&3zd++{-0YA~n7CZ|4d+ZsIfd1rk z9uw~El2D=8>yrb6MY;YRh44KuO@7EE&hc*(n0pQ=C&~V=9>__11(cAQEXS+J;>O_f z9LSMo`S+FcHc*8u^S|OhK!K{sADFU%Sc2)_-pP%83XGqCf-)@s{xeJlGGv(<7z9By z3(&MZK!!9M$PTkxK&Au-P!R~M0CL19NAueOeKU(+9!T!umz9UeffYar#U1~ko&?g9 g1D!>qZYKmYxYm>lRV!p#~1^SDN{s6e}@03#4H0WtG-Q2~}oCjpu;Z0i63 diff --git a/oscar/icons/logo.ico b/oscar/icons/logo.ico index 47e20f78e443a084253bc0f74801a1de086167a6..2d46429433964f2fe78d4a806236a1dcf2452618 100644 GIT binary patch delta 555 zcmYk4KS/{)zwrx#;l67M8;8vhYg>JY(p&_Y|MQZ04RK|~0}K~M=5LBvp7QI`%v z`_Lf{Md@OZ3CrFKZaE!^IS%xWE$>=xL@pdv77EGrhSjdw~W6*pZj-T9l(iDN`D^T5Pf zXk5SKLC&A_R}Pu*Suytt#TdE3oHc9YS$T*;QbhbdOyc#E5UjRG=(p=lhi>@4C!1$~ zg#~-c%9d%d&feecwYFw-Wiv8B)M{IO;xPl_oE_<(a&J W{N6Io@<(XbUa_*};i-6Icl;l^_bR6V delta 559 zcmZ9KPe>F|9LIg`PR?fTpK(TZMyFU66O|+~kfPls6Cni!>5vx@4?+tIvL>-hyHs@P zu%TZ&Y*Df7sav=QZ-OU52)%ff4tWv{B*L|I$L~W1!hsL(`}=<1@An?ODHiIBh5GLw z{EEkMoESD5JA?soV?t#aB5AY)dc`el_#a72Ckf;O_r()hgVK6Hc5pQ~B8G8W>GQH2 z!t3CexAOaE4bedNg;NV=L;~qhMzOi$GfqQu#?!h>ACoK=wT$rLSWD?CH>N*DoV{;T zc&XV~Z&2~2c~af1o*X+nZ(H!nionvxMGVjMH=+qgdQRlZNyFn&_s>rMI=kq{e2;;A zcvk(&LAAr}5}OS8&1v658a-qci)KpyL8TBuo)hO_tALqk7-rX9JhobmfAqTkyV*U~ zYe-uOv*fz&%H%ycLCu=AD^#Oy;;!_%$qKTq(~6wDfF*DE8pQzlM5?&bHYuvui*!oc zBpI@gcWt9S+3|ujv8yvkA>$~KoqwR&F{MNm=VJ}XSR>yWf|m%93_&iOU1z WtLI3GKf_I>m#R??FDI|xNuC363L7i{ From cb1a6f97bcbd6da5c5e04833b52f7d10944cbf1e Mon Sep 17 00:00:00 2001 From: Seeker4 Date: Thu, 16 May 2019 23:03:40 -0700 Subject: [PATCH 16/22] Pixel editing to 32x32 icon (Wave-32.png) to make white border more even. --- Building/Icons/OSCAR.icns | Bin 317863 -> 317860 bytes Building/Icons/Wave-32.png | Bin 1646 -> 4382 bytes Building/Icons/logo.ico | Bin 36001 -> 36001 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Building/Icons/OSCAR.icns b/Building/Icons/OSCAR.icns index 947fc46d7afc7031385996c39779d91bceb72ce4..551d01cc01a707a8c38c2fc7d9bfa85519b82337 100644 GIT binary patch delta 114 zcmZ3!S$N52VUEn?ykZ8Hn@cuw%w}e++Ps8$78B!x$@Xjt%nkoJHz%`gU}AhfS%ee9 zwcuRC#Q1aaQ*H>ClV<|+lQKKAB5^i&3LlRG?i{fDwq9fS7r^ Jr~u2PlK}fEBzyn> delta 166 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 Hu0mjf(g!Pq diff --git a/Building/Icons/logo.ico b/Building/Icons/logo.ico index 2d46429433964f2fe78d4a806236a1dcf2452618..583fa8ef0870e481a05db6988a6c0248f4ba7d10 100644 GIT binary patch delta 482 zcmZ2DlWE~hrVUg1SegI-{~s~=Ah$B3=wwFu03IM$5{RpS`2S=n{(M#uAR~KnAAdQk zEs%9#GM7L(qx0k@0Uws1?;re|Y{##`0u*k63{?A0q@Ho{ z<~&hmW{z)ve|`IZb+Z2CLM_qBeG*m&PtBa1XeTjQQq6R-mz2DqIRg{`(ckApSF%O(vXiHBeXq7M9gO;ir?k zWc*p@09nF7DHQ=ALj{QKfEc9f%VZ&0d)CE3mc!(Od?p}iAt3e#;z>aKWb!Ooea=20 gYY`9|0+l$x3)JUQb$00BAs#{d8T delta 482 zcmXw$O(;ZB9K~Jc@y5rP88e#q7+=ZAW)v1Id~78a6iG=I7Bq;^Og>{{F~!MZEJQZ! z)NCjz8%1d*k}Sqnh=qkT#(U0-ck9>x+;jf-*1i9|NcvtR{b5~fwR17>{=`+Y083d%!yM39U-ZW zT@Oe)LL!D4XGpm5;arpt=<1J${g`)pah4@xr=VN@N_~Ck@wGxMtL0E#w?e_YYf;3b z0e41*tos07Gb}&z3hgbn&vrL#%Aj?X#sGQ3j56g)QsodqqC|haieRYO z1Es0~?P{r`C+&YRns-sH6bj`}-k!aA=5oObK&B~;Y++d+-SX^Mk;xWD~K<jk%IR&965LJT+*WVI6~uU6oTRYr^;@s0 bVx)sDt>&1FIKlb(q)sZ;xI7vtsSEr9MOp!M From c6c11fd4f904cf10ae50e48b1f031d376e335039 Mon Sep 17 00:00:00 2001 From: Seeker4 Date: Fri, 17 May 2019 11:23:39 -0700 Subject: [PATCH 17/22] Fix crash when taking screenshot before a profile is open. --- oscar/mainwindow.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/oscar/mainwindow.cpp b/oscar/mainwindow.cpp index 82800b70..a95eb658 100644 --- a/oscar/mainwindow.cpp +++ b/oscar/mainwindow.cpp @@ -1345,7 +1345,8 @@ void MainWindow::on_actionCheck_for_Updates_triggered() bool toolbox_visible = false; void MainWindow::on_action_Screenshot_triggered() { - daily->hideSpaceHogs(); + if (daily) + daily->hideSpaceHogs(); toolbox_visible = ui->toolBox->isVisible(); ui->toolBox->hide(); QTimer::singleShot(250, this, SLOT(DelayedScreenshot())); @@ -1393,7 +1394,8 @@ void MainWindow::DelayedScreenshot() } else { Notify(tr("Screenshot saved to file \"%1\"").arg(QDir::toNativeSeparators(a))); } - daily->showSpaceHogs(); + if (daily) + daily->showSpaceHogs(); ui->toolBox->setVisible(toolbox_visible); } From d0f0aed29d458dbb8cad7d2054ac2b8f308e476d Mon Sep 17 00:00:00 2001 From: Norman Heino Date: Sun, 5 May 2019 13:40:15 +0200 Subject: [PATCH 18/22] Improve screenshot feature Limit screenshots to OSCAR's application window under macOS. Use main window geometry as basis for screen capture rectangle, removing the need for resizeing hacks. Tested on: * macOS 10.14 * Ubuntu 18.04 * Windows 10 --- oscar/mainwindow.cpp | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/oscar/mainwindow.cpp b/oscar/mainwindow.cpp index a95eb658..5171873f 100644 --- a/oscar/mainwindow.cpp +++ b/oscar/mainwindow.cpp @@ -1351,32 +1351,19 @@ void MainWindow::on_action_Screenshot_triggered() ui->toolBox->hide(); QTimer::singleShot(250, this, SLOT(DelayedScreenshot())); } + void MainWindow::DelayedScreenshot() { // Make sure to scale for high resolution displays (like Retina) // qreal pr = devicePixelRatio(); - - QScreen * screen = QApplication::primaryScreen(); - - - int titleBarHeight = -QApplication::style()->pixelMetric(QStyle::PM_TitleBarHeight); -#ifdef Q_OS_WIN - titleBarHeight += 6; -#endif - - QPixmap pixmap = screen->grabWindow(winId(),0,titleBarHeight); - -/*#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_HAIKU) - // grab the whole screen - grab() - QPixmap desktop = QPixmap::grabWindow(QApplication::desktop()->winId()); - - QPixmap pixmap = desktop.copy(x() * pr, y() * pr, (width()+6) * pr, (height()+22) * pr); - -#elif defined(Q_OS_MAC) - QPixmap pixmap = QPixmap::grabWindow(this->winId(), x(), y(), width() / pr, (height() / pr) + 10); -#endif */ + auto screenshotRect = geometry(); + auto titleBarHeight = QApplication::style()->pixelMetric(QStyle::PM_TitleBarHeight); + auto pixmap = QApplication::primaryScreen()->grabWindow(QDesktopWidget().winId(), + screenshotRect.left(), + screenshotRect.top() - titleBarHeight, + screenshotRect.width(), + screenshotRect.height() + titleBarHeight); QString a = p_pref->Get("{home}/Screenshots"); QDir dir(a); From 21adfb79870890f02c1c405f79bc92a3442eb2e5 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Sat, 18 May 2019 19:17:55 -0400 Subject: [PATCH 19/22] Fix header parsing for 1160P event files, fix misconceptions in ReadWaveformHeader. Now that we check header checksums, it uncovered a problem parsing 1160P event headers. It turns out that the 1160P uses a "waveform" header for its .002 events files. So we can't use the file extension to decide which header to parse, but there's a flag in the standard header that seems to reliably indicate a waveform header. The 1160P events are listed at fixed intervals, as are waveforms, so the flag has been named "interval" rather than "waveform." The 1160P event headers have more than 2 signals in the header and an interval longer than 1sec. This clarified the meaning of multiple waveform header fields that were previously being parsed incorrectly. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 77 +++++++++++++------ oscar/SleepLib/loader_plugins/prs1_loader.h | 18 +++-- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 8dfaca59..7db293db 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -33,6 +33,9 @@ //const int PRS1_EVENT_FILE=2; //const int PRS1_WAVEFORM_FILE=5; +const int PRS1_HTYPE_NORMAL=0; +const int PRS1_HTYPE_INTERVAL=1; + //******************************************************************************************** /// IMPORTANT!!! @@ -1783,6 +1786,12 @@ bool PRS1Import::ParseF3Events() int hy, oa, ca; qint64 div = 0; + // TODO: make sure the assumptions here agree with the header: + // size == number of intervals + // interval seconds = 120 + // interleave for each channel = 1 + // also warn on any remainder of data size % record size (but don't fail) + const qint64 block_duration = 120000; for (int x=0; x < size; x++) { @@ -3555,10 +3564,11 @@ PRS1DataChunk* PRS1DataChunk::ParseNext(QFile & f) } // Log mismatched waveform session IDs - if (chunk->ext >= 5) { + if (chunk->htype == PRS1_HTYPE_INTERVAL) { QFileInfo fi(f); bool numeric; int sessionid_base = (chunk->fileVersion == 2 ? 10 : 16); + if (chunk->family == 3 && chunk->familyVersion >= 3) sessionid_base = 16; QString session_s = fi.fileName().section(".", 0, -2); quint32 sid = session_s.toInt(&numeric, sessionid_base); if (!numeric || sid != chunk->sessionid) { @@ -3619,10 +3629,15 @@ bool PRS1DataChunk::ReadHeader(QFile & f) qWarning() << this->m_path << "@" << hex << this->m_filepos << "Never seen PRS1 header version < 2 or > 3 before"; break; } + if (this->htype != PRS1_HTYPE_NORMAL && this->htype != PRS1_HTYPE_INTERVAL) { + qWarning() << this->m_path << "unexpected htype:" << this->htype; + //break; // don't break to avoid changing behavior (for now) + } // Read format-specific variable-length header data. bool hdr_ok = false; - if (this->ext < 5) { // Not a waveform chunk + if (this->htype != PRS1_HTYPE_INTERVAL) { // Not just waveforms: the 1160P uses this for its .002 events file. + // Not a waveform/interval chunk switch (this->fileVersion) { case 2: hdr_ok = ReadNormalHeaderV2(f); @@ -3634,7 +3649,8 @@ bool PRS1DataChunk::ReadHeader(QFile & f) //hdr_ok remains false, warning is above break; } - } else { // Waveform Chunk + } else { + // Waveform/interval chunk hdr_ok = ReadWaveformHeader(f); } if (!hdr_ok) { @@ -3724,25 +3740,24 @@ bool PRS1DataChunk::ReadWaveformHeader(QFile & f) bool ok = false; unsigned char * header; do { - QByteArray extra = f.read(5); - if (extra.size() != 5) { + // Read the fixed-length waveform header. + QByteArray extra = f.read(4); + if (extra.size() != 4) { qWarning() << this->m_path << "read error in waveform header"; break; } this->m_header.append(extra); - // Get the header address again to be safe header = (unsigned char *)this->m_header.data(); - this->duration = header[0x0f] | header[0x10] << 8; - int always_1 = header[0x11]; - if (always_1 != 1) { - qWarning() << this->m_path << always_1 << "!= 1"; - //break; // don't break to avoid changing behavior (for now) - } - quint16 wvfm_signals = header[0x12] | header[0x13] << 8; + // Parse the fixed-length portion. + this->interval_count = header[0x0f] | header[0x10] << 8; + this->interval_seconds = header[0x11]; // not always 1 after all + this->duration = this->interval_count * this->interval_seconds; // ??? the last entry doesn't always seem to be a full interval? + quint8 wvfm_signals = header[0x12]; + // Read the variable-length data + trailing byte. int ws_size = (this->fileVersion == 3) ? 4 : 3; - int sbsize = wvfm_signals * ws_size; + int sbsize = wvfm_signals * ws_size + 1; extra = f.read(sbsize); if (extra.size() != sbsize) { @@ -3752,22 +3767,36 @@ bool PRS1DataChunk::ReadWaveformHeader(QFile & f) this->m_header.append(extra); header = (unsigned char *)this->m_header.data(); - // Read the waveform information in reverse. // TODO: Double-check this, always seems to be flow then pressure. - int pos = 0x14 + (wvfm_signals - 1) * ws_size; + // Parse the variable-length waveform information. + int pos = 0x13; for (int i = 0; i < wvfm_signals; ++i) { - quint16 interleave = header[pos] | header[pos + 1] << 8; // samples per block (Usually 05 00) + quint8 kind = header[pos]; + if (kind != i) { // always seems to range from 0...wvfm_signals-1, alert if not + qWarning() << this->m_path << kind << "!=" << i << "waveform kind"; + //break; // don't break to avoid changing behavior (for now) + } + quint16 interleave = header[pos + 1] | header[pos + 2] << 8; // samples per interval if (this->fileVersion == 2) { - quint8 sample_format = header[pos + 2]; // TODO: sample_format seems to be unused anywhere else in the loader. - this->waveformInfo.push_back(PRS1Waveform(interleave, sample_format)); - pos -= 3; + this->waveformInfo.push_back(PRS1Waveform(interleave, kind)); + pos += 3; } else if (this->fileVersion == 3) { - //quint16 sample_size = header[pos + 2] | header[pos + 3] << 8; // size in bits?? (08 00) - // Possibly this is size in bits, and sign bit for the other byte? - this->waveformInfo.push_back(PRS1Waveform(interleave, 0)); - pos -= 4; + int always_8 = header[pos + 3]; // sample size in bits? + if (always_8 != 8) { + qWarning() << this->m_path << always_8 << "!= 8 in waveform header"; + //break; // don't break to avoid changing behavior (for now) + } + this->waveformInfo.push_back(PRS1Waveform(interleave, kind)); + pos += 4; } } + // And the trailing byte, whatever it is. + int always_0 = header[pos]; + if (always_0 != 0) { + qWarning() << this->m_path << always_0 << "!= 0 in waveform header"; + //break; // don't break to avoid changing behavior (for now) + } + ok = true; } while (false); diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index cb7a59aa..17772769 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -64,12 +64,12 @@ public: PRS1DataChunk() { fileVersion = 0; blockSize = 0; - ext = 255; htype = 0; family = 0; familyVersion = 0; - timestamp = 0; + ext = 255; sessionid = 0; + timestamp = 0; duration = 0; @@ -90,20 +90,26 @@ public: int m_index; // nth chunk in file inline void SetIndex(int index) { m_index = index; } + // Common fields quint8 fileVersion; quint16 blockSize; - quint8 ext; quint8 htype; quint8 family; quint8 familyVersion; - quint32 timestamp; + quint8 ext; SessionID sessionid; + quint32 timestamp; - quint16 duration; - + // Waveform-specific fields + quint16 interval_count; + quint8 interval_seconds; + int duration; QList waveformInfo; + + // V3 normal/non-waveform fields QMap hblock; + // Trailing common fields quint8 storedChecksum; // header checksum stored in file, last byte of m_header quint8 calcChecksum; // header checksum as calculated when parsing quint32 storedCrc; // header + data CRC stored in file, last 2-4 bytes of chunk From 4511ee36770819e55ae1769610230981199f8c75 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Sat, 18 May 2019 19:20:36 -0400 Subject: [PATCH 20/22] PRS1 parsing regression test: generate YAML for each parsed chunk. Each input file's chunks get emitted into a single output YAML file. As parsing gets separated from conversion/import, this will allow for testing and examination of the parsed input files before they are transformed into sessions. --- oscar/tests/prs1tests.cpp | 160 +++++++++++++++++++++++++++++++++++++- oscar/tests/prs1tests.h | 1 + 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/oscar/tests/prs1tests.cpp b/oscar/tests/prs1tests.cpp index f1cfd470..8b282de4 100644 --- a/oscar/tests/prs1tests.cpp +++ b/oscar/tests/prs1tests.cpp @@ -13,6 +13,7 @@ static PRS1Loader* s_loader = nullptr; static void iterateTestCards(const QString & root, void (*action)(const QString &)); +static QString prs1OutputPath(const QString & inpath, const QString & serial, const QString & basename, const QString & suffix); static QString prs1OutputPath(const QString & inpath, const QString & serial, int session, const QString & suffix); void PRS1Tests::initTestCase(void) @@ -74,9 +75,166 @@ void PRS1Tests::testSessionsToYaml() } +// ==================================================================================================== + +static QString ts(qint64 msecs) +{ + return QDateTime::fromMSecsSinceEpoch(msecs).toString(Qt::ISODate); +} + +static QString byteList(QByteArray data) +{ + QStringList l; + for (int i = 0; i < data.size(); i++) { + l.push_back(QString( "%1" ).arg((int) data[i] & 0xFF, 2, 16, QChar('0') ).toUpper()); + } + QString s = l.join(""); + return s; +} + +void ChunkToYaml(QFile & file, PRS1DataChunk* chunk) +{ + QTextStream out(&file); + + // chunk header + out << "chunk:" << endl; + out << " at: " << hex << chunk->m_filepos << endl; + out << " version: " << dec << chunk->fileVersion << endl; + out << " size: " << chunk->blockSize << endl; + out << " htype: " << chunk->htype << endl; + out << " family: " << chunk->family << endl; + out << " familyVersion: " << chunk->familyVersion << endl; + out << " ext: " << chunk->ext << endl; + out << " session: " << chunk->sessionid << endl; + out << " start: " << ts(chunk->timestamp * 1000L) << endl; + + // hblock for V3 non-waveform chunks + if (chunk->fileVersion == 3 && chunk->htype == 0) { + out << " hblock:" << endl; + QMapIterator i(chunk->hblock); + while (i.hasNext()) { + i.next(); + out << " " << (int) i.key() << ": " << i.value() << endl; + } + } + + // waveform chunks + if (chunk->htype == 1) { + out << " intervals: " << chunk->interval_count << endl; + out << " intervalSeconds: " << (int) chunk->interval_seconds << endl; + out << " interleave:" << endl; + for (int i=0; i < chunk->waveformInfo.size(); i++) { + const PRS1Waveform & w = chunk->waveformInfo.at(i); + out << " " << i << ": " << w.interleave << endl; + } + out << " end: " << ts((chunk->timestamp + chunk->duration) * 1000L) << endl; + } + + // header checksum + out << " checksum: " << hex << chunk->storedChecksum << endl; + if (chunk->storedChecksum != chunk->calcChecksum) { + out << " calcChecksum: " << hex << chunk->calcChecksum << endl; + } + + // data + out << " data: " << byteList(chunk->m_data) << endl; + + // data CRC + out << " crc: " << hex << chunk->storedCrc << endl; + if (chunk->storedCrc != chunk->calcCrc) { + out << " calcCrc: " << hex << chunk->calcCrc << endl; + } + out << endl; +} + +void parseAndEmitChunkYaml(const QString & path) +{ + qDebug() << path; + + QStringList paths; + QString propertyfile; + int sessionid_base; + sessionid_base = s_loader->FindSessionDirsAndProperties(path, paths, propertyfile); + + Machine *m = s_loader->CreateMachineFromProperties(propertyfile); + Q_ASSERT(m != nullptr); + + // This mirrors the functional bits of PRS1Loader::ScanFiles. + + QDir dir; + dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks); + dir.setSorting(QDir::Name); + + int size = paths.size(); + + // for each p0/p1/p2/etc... folder + for (int p=0; p < size; ++p) { + dir.setPath(paths.at(p)); + if (!dir.exists() || !dir.isReadable()) { + qWarning() << dir.canonicalPath() << "can't read directory"; + continue; + } + QFileInfoList flist = dir.entryInfoList(); + + // Scan for individual .00X files + for (int i = 0; i < flist.size(); i++) { + QFileInfo fi = flist.at(i); + QString inpath = fi.canonicalFilePath(); + bool ok; + + QString ext_s = fi.fileName().section(".", -1); + ext_s.toInt(&ok); + if (!ok) { + // not a numerical extension + qWarning() << inpath << "unexpected filename"; + continue; + } + + QString session_s = fi.fileName().section(".", 0, -2); + session_s.toInt(&ok, sessionid_base); + if (!ok) { + // not a numerical session ID + qWarning() << inpath << "unexpected filename"; + continue; + } + + // Create the YAML file. + QString outpath = prs1OutputPath(path, m->serial(), fi.fileName(), "-chunks.yml"); + QFile file(outpath); + if (!file.open(QFile::WriteOnly | QFile::Truncate)) { + qDebug() << outpath; + Q_ASSERT(false); + } + + // Parse the chunks in the file. + QList chunks = s_loader->ParseFile(inpath); + for (int i=0; i < chunks.size(); i++) { + // Emit the YAML. + PRS1DataChunk * chunk = chunks.at(i); + ChunkToYaml(file, chunk); + delete chunk; + } + + file.close(); + } + } +} + +void PRS1Tests::testChunksToYaml() +{ + iterateTestCards(TESTDATA_PATH "prs1/input/", parseAndEmitChunkYaml); +} + + // ==================================================================================================== QString prs1OutputPath(const QString & inpath, const QString & serial, int session, const QString & suffix) +{ + QString basename = QString("%1").arg(session, 8, 10, QChar('0')); + return prs1OutputPath(inpath, serial, basename, suffix); +} + +QString prs1OutputPath(const QString & inpath, const QString & serial, const QString & basename, const QString & suffix) { // Output to prs1/output/FOLDER/SERIAL-000000(-session.yml, etc.) QDir path(inpath); @@ -90,7 +248,7 @@ QString prs1OutputPath(const QString & inpath, const QString & serial, int sessi QString filename = QString("%1-%2%3") .arg(serial) - .arg(session, 6, 10, QChar('0')) + .arg(basename) .arg(suffix); return outdir.path() + QDir::separator() + filename; } diff --git a/oscar/tests/prs1tests.h b/oscar/tests/prs1tests.h index 592d7225..5c8f2992 100644 --- a/oscar/tests/prs1tests.h +++ b/oscar/tests/prs1tests.h @@ -18,6 +18,7 @@ class PRS1Tests : public QObject private slots: void initTestCase(); + void testChunksToYaml(); void testSessionsToYaml(); // void test2(); void cleanupTestCase(); From f8e4ff754b728580d836500c8c0aff7052ce5d95 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Sat, 18 May 2019 19:46:24 -0400 Subject: [PATCH 21/22] Add OSCAR version number to PRS1 unit test logs. --- oscar/SleepLib/common.cpp | 8 ++++++-- oscar/tests/prs1tests.cpp | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/oscar/SleepLib/common.cpp b/oscar/SleepLib/common.cpp index 0f287401..be4e249f 100644 --- a/oscar/SleepLib/common.cpp +++ b/oscar/SleepLib/common.cpp @@ -181,8 +181,12 @@ QString getBranchVersion() if (GIT_BRANCH != "master") { version += GIT_BRANCH+"-"; } - version += GIT_REVISION +" "; - version += getGraphicsEngine()+"]"; + version += GIT_REVISION; +#ifndef UNITTEST_MODE + // There is no graphics engine on the console. + version += QString(" ") + getGraphicsEngine(); +#endif + version += "]"; return version; } diff --git a/oscar/tests/prs1tests.cpp b/oscar/tests/prs1tests.cpp index 8b282de4..16ffb412 100644 --- a/oscar/tests/prs1tests.cpp +++ b/oscar/tests/prs1tests.cpp @@ -18,6 +18,8 @@ static QString prs1OutputPath(const QString & inpath, const QString & serial, in void PRS1Tests::initTestCase(void) { + initializeStrings(); + qDebug() << STR_TR_OSCAR + " " + getBranchVersion(); QString profile_path = TESTDATA_PATH "profile/"; Profiles::Create("test", &profile_path); From ff2bdf3d01c7865f08f395695e52b6d9e7bd65ac Mon Sep 17 00:00:00 2001 From: Seeker4 Date: Sun, 19 May 2019 17:44:10 -0700 Subject: [PATCH 22/22] Session bar now shows "no sessions present" if there are not sessions, instead of showing last days's sessions. --- oscar/daily.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oscar/daily.cpp b/oscar/daily.cpp index 0866b908..17bcabf1 100644 --- a/oscar/daily.cpp +++ b/oscar/daily.cpp @@ -1557,10 +1557,11 @@ void Daily::Load(QDate date) const int maxcolors=sizeof(cols)/sizeof(QColor); QList::iterator i; + sessionbar->clear(); // clear sessionbar as some days don't have sessions + if (cpap) { int c=0; - sessionbar->clear(); for (i=day->begin();i!=day->end();++i) { Session * s=*i; if ((*s).type() == MT_CPAP)