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/21] 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<PRS1DataChunk *> 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<PRS1DataChunk *> PRS1Loader::ParseFile(const QString & path)
 {
     QList<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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/21] 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<PRS1DataChunk *> PRS1Import::CoalesceWaveformChunks(QList<PRS1DataChunk *> & allchunks)
+{
+    QList<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> CoalesceWaveformChunks(QList<PRS1DataChunk *> & 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/21] 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/21] 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<PRS1DataChunk *> PRS1Import::CoalesceWaveformChunks(QList<PRS1DataChunk *>
 {
     QList<PRS1DataChunk *> 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<PRS1DataChunk *> PRS1Import::CoalesceWaveformChunks(QList<PRS1DataChunk *>
                 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<PRS1DataChunk *> PRS1Import::CoalesceWaveformChunks(QList<PRS1DataChunk *>
             // 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<PRS1DataChunk *> 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<PRS1DataChunk *> PRS1Loader::ParseFile(const QString & path)
     QList<PRS1Waveform> waveformInfo;
 
     do {
+        qint64 filepos = f.pos();
         headerBA = f.read(16);
         if (headerBA.size() != 16) {
             qDebug() << path << "file too short?";
@@ -3445,9 +3480,6 @@ QList<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> PRS1Loader::ParseFile(const QString & path)
                        break;
                    }
 
+                   cnt++;
                    continue;
                    // Corrupt header.. skip it.
             }
@@ -3522,6 +3551,10 @@ QList<PRS1DataChunk *> 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<PRS1DataChunk *> 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/21] 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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/21] 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<PRS1DataChunk *> 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<PRS1Waveform> 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;i<waveformInfo.size(); ++i) {
-                chunk->waveformInfo.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/21] 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<PRS1Waveform> waveformInfo;
     QMap<unsigned char, short> 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/21] 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; i<recs; i++) {
+                    chunk->hblock[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; i<recs; i++) {
-                chunk->hblock[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/21] 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<PRS1DataChunk *> 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<PRS1DataChunk *> 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; i<recs; i++) {
-                    chunk->hblock[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<PRS1DataChunk *> 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/21] 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/21] 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/21] 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; i<recs; i++) {
-                    this->hblock[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; i<recs; i++) {
+            this->hblock[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/21] 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<int, QString> 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<unsigned char, short> 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/21] 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 <guy.oscar@moxis.com>
Date: Thu, 16 May 2019 22:33:05 -0700
Subject: [PATCH 15/21] 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!><S@U4N{@*Mr&@L*#2*gZ4%)DJxfMwE20PzNKz5oCK

delta 313
zcmZ3!S$O$o;SF|dj1M;3vt40ge7{+Q^8yp&&&f}@UHK#z-~A5*egcy%cog^-88RRN
zmC5NmCfsv`p+a8QCkF<La{XHk;agss{E$hU<KJ{JcPmg%lKo#gkQ4X{C?Pdjj#rVz
zoWbTfkR#3V?<M;YpbA;$e=*;H0#%bgFl7U=1k=C8lN<RI7@q(IWmx`IGgJW?vdjz&
zydVMALqOve0a?;)AWO7v0htmU|7L<X481^(_~dAQJ0Rwqyog_%v2OA%ep&eiU|D2B
pa>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
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
Hu0mjf%f2gv

delta 4366
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
IM6N<$f?F0}S^xk5

diff --git a/Building/Icons/logo.ico b/Building/Icons/logo.ico
index 47e20f78e443a084253bc0f74801a1de086167a6..2d46429433964f2fe78d4a806236a1dcf2452618 100644
GIT binary patch
delta 555
zcmYk4KS&#47{)zwrx#;l67M8;8vhYg>JY(p&_Y|MQZ04RK|~0}K~M=5LBvp7QI`%v
z`_Lf{Md@O<LKX!P94b_hgo21e#7(r+!68UU@9XnL13mchz3=b&-tX?X+f0^kCd<En
z_Gy|B;yv~~5gd4%k@0ou6zyaibv2#%;ccOeNZCfVuvYW9D9_c9lv5;!D*yAM+)%60
z*jtHJ{|oGSJFuau#a;=_E4x?-B(y&6KSe$;fn$F_Ws}BSFrk>Z3C<d#?Ea$PBI4}Z
zDBcFc*sIj>rFKZaE!^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<tWpYLwIQDp0&V4E+rIl`k{K8^=Op`kf@KFs_vxt)u;-I2os2H6vYL-!y
z?2rOJGz_@q)KAjHIO1o~L#D~ybTIr>%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><Pnf$^>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!><S@U4N{@*Mr&@L*#2*gZ4%)DJxfMwE20PzNKz5oCK

delta 313
zcmZ3!S$O$o;SF|dj1M;3vt40ge7{+Q^8yp&&&f}@UHK#z-~A5*egcy%cog^-88RRN
zmC5NmCfsv`p+a8QCkF<La{XHk;agss{E$hU<KJ{JcPmg%lKo#gkQ4X{C?Pdjj#rVz
zoWbTfkR#3V?<M;YpbA;$e=*;H0#%bgFl7U=1k=C8lN<RI7@q(IWmx`IGgJW?vdjz&
zydVMALqOve0a?;)AWO7v0htmU|7L<X481^(_~dAQJ0Rwqyog_%v2OA%ep&eiU|D2B
pa>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&#47{)zwrx#;l67M8;8vhYg>JY(p&_Y|MQZ04RK|~0}K~M=5LBvp7QI`%v
z`_Lf{Md@O<LKX!P94b_hgo21e#7(r+!68UU@9XnL13mchz3=b&-tX?X+f0^kCd<En
z_Gy|B;yv~~5gd4%k@0ou6zyaibv2#%;ccOeNZCfVuvYW9D9_c9lv5;!D*yAM+)%60
z*jtHJ{|oGSJFuau#a;=_E4x?-B(y&6KSe$;fn$F_Ws}BSFrk>Z3C<d#?Ea$PBI4}Z
zDBcFc*sIj>rFKZaE!^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<tWpYLwIQDp0&V4E+rIl`k{K8^=Op`kf@KFs_vxt)u;-I2os2H6vYL-!y
z?2rOJGz_@q)KAjHIO1o~L#D~ybTIr>%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><Pnf$^>yWf|m%93_&iOU1z
WtLI3GKf_I>m#R??FDI|xNuC363L7i{


From cb1a6f97bcbd6da5c5e04833b52f7d10944cbf1e Mon Sep 17 00:00:00 2001
From: Seeker4 <guy.oscar@moxis.com>
Date: Thu, 16 May 2019 23:03:40 -0700
Subject: [PATCH 16/21] 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<|+<Y<1*$&2_sC$Hz1m>lQKKAB5^i&3LlRG?i{fDwq9fS7r^
Jr~u2PlK}fEBzyn>

delta 166
zcmZ3oS$O$oVUEn?ykZ8Ho69$H%w}e+-MoZ(78B$D$@XjtEIj}J|K6O$wt-3Y|Njn#
zhW`vxI;)r)_Wb|f#nAYlY3bzZrYe^9x!3;x?_&Te{kz$Wa}8YSwazol=bk~8-o19V
z=?u%ooA04Y|7~XDnZOLRWjcQ#<G;yk`Nby*2r~X}77}O|5?};kCLm_sE+oJ*=_CNJ
C0!)Db

diff --git a/Building/Icons/Wave-32.png b/Building/Icons/Wave-32.png
index ccb74ae3a7d9b44ca064daa3be91a6b2fa174bf4..3a8a6950b74caab747bd941e021e317e88865224 100644
GIT binary patch
delta 4367
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
JPDHLkV1i<?NfiJ9

delta 1609
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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*<i{7;~ov3GKnU^S~2kUw|wFTrZYMU%^f
zsu_7Fe-f(a0GoJkWz^(aA=%0M*f}Q86AtEh^5@5={})=tCO1kcPL>k63{?A0q@Ho{
z<~&hmW{z)ve|`IZb+Z2CLM_qBeG<tWM}Q{(dwYH{P`T)2KFN5_7@&X+5O11XCRr@<
z0?1+qVn!e?1>*m&PtBa1XeTjQQq6R-mz2DqIRg{`<pP0t`{XLAY{|tiQ5X%B*$Tuz
zCjXO4mrRF=!f2q3B@j=UTqm8(IDPU&770+Gc>(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<v3142(Z@R&5|l0A%>)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{b<W3NxH5t;XJ1j7kOof
z+iG)Z_{g#L^Q}S6AD61QJ3u&pRvM-HM7DiaOk>5~fwR17>{=`+Y083d%!yM39U-ZW
zT@Oe)LL!D4XGpm5;arpt=<1J${g`)pah4@xr=VN@N_~Ck@wGxMtL0E#w?e_YYf;3b
z0e41*to<ZE^*i`6S>s07Gb}&z3hgbn&vrL#%Aj?X#sGQ3j56g)QsodqqC|haieRYO
z1Es0~?P{r`C+&YRn<Fm>s-sH6bj`}-k!aA=5oObK&B~;Y++d+-SX^Mk;xWD~K<<f#
z%;H{gPrJ;p{p5tCuwL9HT~d`Ys<cxyk}>jk%IR&965LJT+*WVI6~uU6oTRYr^;@s0
bVx)sDt>&1FIKlb(q)sZ;xI7vtsSEr9MOp!M


From c6c11fd4f904cf10ae50e48b1f031d376e335039 Mon Sep 17 00:00:00 2001
From: Seeker4 <guy.oscar@moxis.com>
Date: Fri, 17 May 2019 11:23:39 -0700
Subject: [PATCH 17/21] 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 <norman.heino@gmail.com>
Date: Sun, 5 May 2019 13:40:15 +0200
Subject: [PATCH 18/21] 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/21] 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<PRS1Waveform> waveformInfo;
+    
+    // V3 normal/non-waveform fields
     QMap<unsigned char, short> 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/21] 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<unsigned char, short> 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<PRS1DataChunk *> 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/21] 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);