diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 8fc836d3..dfddf6dd 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -192,67 +192,66 @@ static crc32_t CRC32wchar(const unsigned char *data, size_t data_len, crc32_t cr
 }
 
 
+// TODO: have UNEXPECTED_VALUE set a flag in the importer/machine that this data set is unusual
+#define UNEXPECTED_VALUE(SRC, VALS) { qWarning() << this->sessionid << QString("%1: %2 = %3 != %4").arg(__func__).arg(#SRC).arg(SRC).arg(VALS); }
+#define CHECK_VALUE(SRC, VAL) if ((SRC) != (VAL)) UNEXPECTED_VALUE(SRC, VAL)
+#define CHECK_VALUES(SRC, VAL1, VAL2) if ((SRC) != (VAL1) && (SRC) != (VAL2)) UNEXPECTED_VALUE(SRC, #VAL1 " or " #VAL2)
+// for more than 2 values, just write the test manually and use UNEXPECTED_VALUE if it fails
+
+
 enum FlexMode { FLEX_None, FLEX_CFlex, FLEX_CFlexPlus, FLEX_AFlex, FLEX_RiseTime, FLEX_BiFlex, FLEX_Unknown  };
 
 ChannelID PRS1_TimedBreath = 0, PRS1_HeatedTubing = 0;
 
-#if 0  // Apparently unused
-PRS1::PRS1(Profile *profile, MachineID id): CPAP(profile, id)
-{
-}
-PRS1::~PRS1()
-{
-
-}
-#endif
-
 struct PRS1TestedModel
 {
     QString model;
     int family;
     int familyVersion;
+    const char* name;
 };
 
 static const PRS1TestedModel s_PRS1TestedModels[] = {
-    { "251P", 0, 2 },  // "REMstar Plus (Philips Respironics)" (brick)
-    { "450P", 0, 3 },  // "REMstar Pro (Philips Respironics)"
-    { "451P", 0, 3 },  // "REMstar Pro (Philips Respironics)"
-    { "550P", 0, 2 },  // "REMstar Auto (Philips Respironics)"
-    { "550P", 0, 3 },  // "REMstar Auto (Philips Respironics)"
-    { "551P", 0, 2 },  // "REMstar Auto (Philips Respironics)"
-    { "750P", 0, 2 },  // "BiPAP Auto (Philips Respironics)"
+    // This first set says "(Philips Respironics)" intead of "(System One)" on official reports.
+    { "251P", 0, 2, "REMstar Plus (System One)" },  // (brick)
+    { "450P", 0, 3, "REMstar Pro (System One)" },
+    { "451P", 0, 3, "REMstar Pro (System One)" },
+    { "550P", 0, 2, "REMstar Auto (System One)" },
+    { "550P", 0, 3, "REMstar Auto (System One)" },
+    { "551P", 0, 2, "REMstar Auto (System One)" },
+    { "750P", 0, 2, "BiPAP Auto (System One)" },
 
-    { "460P",   0, 4 },
-    { "461P",   0, 4 },
-    { "560P",   0, 4 },
-    { "560PBT", 0, 4 },
-    { "561P",   0, 4 },
-    { "660P",   0, 4 },
-    { "760P",   0, 4 },
+    { "460P",   0, 4, "REMstar Pro (System One 60 Series)" },
+    { "461P",   0, 4, "REMstar Pro (System One 60 Series)" },
+    { "560P",   0, 4, "REMstar Auto (System One 60 Series)" },
+    { "560PBT", 0, 4, "REMstar Auto (System One 60 Series)" },
+    { "561P",   0, 4, "REMstar Auto (System One 60 Series)" },
+    { "660P",   0, 4, "BiPAP Pro (System One 60 Series)" },
+    { "760P",   0, 4, "BiPAP Auto (System One 60 Series)" },
     
-    { "200X110", 0, 6 },  // "DreamStation CPAP" (brick)
-    { "400G110", 0, 6 },  // "DreamStation Go"
-    { "400X110", 0, 6 },  // "DreamStation CPAP Pro"
-    { "400X150", 0, 6 },
-    { "500X110", 0, 6 },  // "DreamStation Auto CPAP"
-    { "500X150", 0, 6 },
-    { "502G150", 0, 6 },  // "DreamStation Go Auto"
-    { "600X110", 0, 6 },  // "DreamStation BiPAP Pro"
-    { "700X110", 0, 6 },  // "DreamStation Auto BiPAP"
+    { "200X110", 0, 6, "DreamStation CPAP" },  // (brick)
+    { "400G110", 0, 6, "DreamStation Go" },
+    { "400X110", 0, 6, "DreamStation CPAP Pro" },
+    { "400X150", 0, 6, "DreamStation CPAP Pro" },
+    { "500X110", 0, 6, "DreamStation Auto CPAP" },
+    { "500X150", 0, 6, "DreamStation Auto CPAP" },
+    { "502G150", 0, 6, "DreamStation Go Auto" },
+    { "600X110", 0, 6, "DreamStation BiPAP Pro" },
+    { "700X110", 0, 6, "DreamStation Auto BiPAP" },
     
-    { "950P", 5, 0 },
-    { "960P", 5, 1 },
-    { "961P", 5, 1 },
-    { "960T", 5, 2 },
-    { "900X110", 5, 3 },
-    { "900X120", 5, 3 },
+    { "950P",    5, 0, "BiPAP AutoSV Advanced System One" },
+    { "960P",    5, 1, "BiPAP autoSV Advanced (System One 60 Series)" },
+    { "961P",    5, 1, "BiPAP autoSV Advanced (System One 60 Series)" },
+    { "960T",    5, 2, "BiPAP autoSV Advanced 30 (System One 60 Series)" },  // omits "(System One 60 Series)" on official reports
+    { "900X110", 5, 3, "DreamStation BiPAP autoSV" },
+    { "900X120", 5, 3, "DreamStation BiPAP autoSV" },
     
-    { "1061T", 3, 3 },
-    { "1160P", 3, 3 },
-    { "1030X110", 3, 6 },
-    { "1130X110", 3, 6 },
+    { "1061T",    3, 3, "BiPAP S/T 30 (System One 60 Series)" },
+    { "1160P",    3, 3, "BiPAP AVAPS 30 (System One 60 Series)" },
+    { "1030X110", 3, 6, "DreamStation BiPAP S/T 30" },
+    { "1130X110", 3, 6, "DreamStation BiPAP AVAPS 30" },
     
-    { "", 0, 0 },
+    { "", 0, 0, "" },
 };
 PRS1ModelInfo s_PRS1ModelInfo;
 
@@ -261,7 +260,11 @@ PRS1ModelInfo::PRS1ModelInfo()
     for (int i = 0; !s_PRS1TestedModels[i].model.isEmpty(); i++) {
         const PRS1TestedModel & model = s_PRS1TestedModels[i];
         m_testedModels[model.family][model.familyVersion].append(model.model);
+        
+        m_modelNames[model.model] = model.name;
     }
+    
+    m_bricks = { "251P", "200X110" };
 }
 
 bool PRS1ModelInfo::IsSupported(int family, int familyVersion) const
@@ -314,24 +317,45 @@ bool PRS1ModelInfo::IsTested(const QHash<QString,QString> & props) const
     return ok;
 };
 
-// TODO: add brick list, IsBrick() test
-// TODO: add model name, Name() function
+bool PRS1ModelInfo::IsBrick(const QString & model) const
+{
+    bool is_brick;
+    
+    if (m_modelNames.contains(model)) {
+        is_brick = m_bricks.contains(model);
+    } else {
+        // If we haven't seen it before, assume any 2xx is a brick.
+        is_brick = (model.at(0) == QChar('2'));
+    }
+    
+    return is_brick;
+};
 
+const char* PRS1ModelInfo::Name(const QString & model) const
+{
+    const char* name;
+    if (m_modelNames.contains(model)) {
+        name = m_modelNames[model];
+    } else {
+        name = "Unknown Model";
+    }
+    return name;
+};
+
+QMap<const char*,const char*> s_PRS1Series = {
+    { "System One 60 Series", ":/icons/prs1_60s.png" },  // needs to come before following substring
+    { "System One",           ":/icons/prs1.png" },
+    { "DreamStation",         ":/icons/dreamstation.png" },
+};
 
 PRS1Loader::PRS1Loader()
 {
 #ifndef UNITTEST_MODE  // no QPixmap without a QGuiApplication
-    const QString PRS1_ICON = ":/icons/prs1.png";
-    const QString PRS1_60_ICON = ":/icons/prs1_60s.png";
-    const QString DREAMSTATION_ICON = ":/icons/dreamstation.png";
-
-   // QString s = newInfo().series;
-    m_pixmap_paths["System One"] = PRS1_ICON;
-    m_pixmaps["System One"] = QPixmap(PRS1_ICON);
-    m_pixmap_paths["System One (60 Series)"] = PRS1_60_ICON;
-    m_pixmaps["System One (60 Series)"] = QPixmap(PRS1_60_ICON);
-    m_pixmap_paths["DreamStation"] = DREAMSTATION_ICON;
-    m_pixmaps["DreamStation"] = QPixmap(DREAMSTATION_ICON);
+    for (auto & series : s_PRS1Series.keys()) {
+        QString path = s_PRS1Series[series];
+        m_pixmap_paths[series] = path;
+        m_pixmaps[series] = QPixmap(path);
+    }
 #endif
 
     m_type = MT_CPAP;
@@ -433,11 +457,13 @@ void parseModel(MachineInfo & info, const QString & modelnum)
     bool ok;
     int num = modelstr.toInt(&ok);
 
-    int series = ((num / 10) % 10);
     int type = (num / 100);
-    int country = num % 10;
 
 
+    // TODO: Replace the below with s_PRS1ModelInfo.Name(modelnum), but
+    // first sort out the display of manufacturer/series/model in the
+    // various views, reports, and menus. Those displays should include
+    // the model number as well.
     switch (type) {
     case 1: // cpap
     case 2: // cpap
@@ -466,29 +492,19 @@ void parseModel(MachineInfo & info, const QString & modelnum)
         info.model = QObject::tr("Unknown Model");
     }
 
-    switch (series) {
-    case 5:
-        info.series = QObject::tr("System One");
-        break;
-    case 6:
-        info.series = QObject::tr("System One (60 Series)");
-        break;
-    case 7:
-        info.series = QObject::tr("DreamStation");
-        break;
-    default:
-        info.series = QObject::tr("unknown");
-        break;
-
+    const char* name = s_PRS1ModelInfo.Name(modelnum);
+    const char* series = nullptr;
+    for (auto & s : s_PRS1Series.keys()) {
+        if (QString(name).contains(s)) {
+            series = s;
+            break;
+        }
     }
-    switch (country) {
-    case '0':
-        break;
-    case '1':
-        break;
-    default:
-        break;
+    if (series == nullptr) {
+        qWarning() << "unknown series for" << name << modelnum;
+        series = "unknown";
     }
+    info.series = QObject::tr(series);
 }
 
 bool PRS1Loader::PeekProperties(const QString & filename, QHash<QString,QString> & props)
@@ -548,7 +564,6 @@ bool PRS1Loader::PeekProperties(MachineInfo & info, const QString & filename, Ma
     }
     QString modelnum;
     int ptype=0;
-    int dfv=0;
     bool ok;
     for (auto & key : props.keys()) {
         bool skip = false;
@@ -566,11 +581,6 @@ bool PRS1Loader::PeekProperties(MachineInfo & info, const QString & filename, Ma
             if (!ok) qWarning() << "ProductType" << props[key];
             skip = true;
         }
-        if (key == "DataFormatVersion") {
-            dfv = props[key].toInt(&ok, 10);
-            if (!ok) qWarning() << "DataFormatVersion" << props[key];
-            skip = true;
-        }
         if (!mach || skip) continue;
 
         mach->properties[key] = props[key];
@@ -580,19 +590,17 @@ bool PRS1Loader::PeekProperties(MachineInfo & info, const QString & filename, Ma
     
     if (!modelnum.isEmpty()) {
         parseModel(info, modelnum);
+    } else {
+        qWarning() << "missing model number" << filename;
     }
 
+    // TODO: Replace this with PRS1ModelInfo.
     if (ptype > 0) {
         if (ModelMap.contains(ptype)) {
             info.model = ModelMap[ptype];
         }
     }
 
-    if (dfv == 3) {
-        info.series = QObject::tr("DreamStation");
-    }
-
-
     return true;
 }
 
@@ -685,64 +693,6 @@ int PRS1Loader::Open(const QString & dirpath)
     return c;
 }
 
-/*bool PRS1Loader::ParseProperties(Machine *m, QString filename)
-{
-    QFile f(filename);
-
-    if (!f.open(QIODevice::ReadOnly)) {
-        return false;
-    }
-
-    QString line;
-    QHash<QString, QString> prop;
-
-    QString s = f.readLine();
-    QChar sep = '=';
-    QString key, value;
-
-    MachineInfo info = newInfo();
-    bool ok;
-
-    while (!f.atEnd()) {
-        key = s.section(sep, 0, 0);
-
-        if (key == s) { continue; }
-
-        value = s.section(sep, 1).trimmed();
-
-        if (value == s) { continue; }
-
-        if (key.contains("serialnumber",Qt::CaseInsensitive)) {
-            info.serial = value;
-        } else if (key.contains("modelnumber",Qt::CaseInsensitive)) {
-            parseModel(info, value);
-        } else {
-            if (key.contains("producttype", Qt::CaseInsensitive)) {
-                int i = value.toInt(&ok, 16);
-
-                if (ok) {
-                    if (ModelMap.find(i) != ModelMap.end()) {
-                        info.model = ModelMap[i];
-                    }
-                }
-            }
-            prop[key] = value;
-        }
-        s = f.readLine();
-    }
-
-    if (info.serial != m->serial()) {
-        qDebug() << "Serial Number in PRS1 properties.txt doesn't match machine record";
-    }
-    m->setInfo(info);
-
-    for (QHash<QString, QString>::iterator i = prop.begin(); i != prop.end(); i++) {
-        m->properties[i.key()] = i.value();
-    }
-
-    f.close();
-    return true;
-}*/
 
 int PRS1Loader::OpenMachine(const QString & path)
 {
@@ -855,25 +805,9 @@ Machine* PRS1Loader::CreateMachineFromProperties(QString propertyfile)
     MachineInfo info = newInfo();
     // Have a peek first to get the model number.
     PeekProperties(info, propertyfile);
-    
-    QString modelstr;
-    bool fnd = false;
-    for (int i=0; i<info.modelnumber.size(); i++) {
-        QChar c = info.modelnumber.at(i);
-        if (c.isDigit()) {
-            modelstr += c;
-            fnd = true;
-        } else if (fnd) break;
-    }
 
-    bool ok;
-    int model = modelstr.toInt(&ok);
-    if (ok) {
-        int series = ((model / 10) % 10);
-        int type = (model / 100);
-
-        // Assumption is made here all PRS1 machines less than 450P are not data capable.. this could be wrong one day.
-        if ((type < 4) && p_profile->cpap->brickWarning()) {
+    if (true) {
+        if (s_PRS1ModelInfo.IsBrick(info.modelnumber) && p_profile->cpap->brickWarning()) {
 #ifndef UNITTEST_MODE
             QApplication::processEvents();
             QMessageBox::information(QApplication::activeWindow(),
@@ -886,9 +820,8 @@ Machine* PRS1Loader::CreateMachineFromProperties(QString propertyfile)
 
         }
 
-        // A bit of protection against future annoyances..
-        if (!s_PRS1ModelInfo.IsSupported(props) || ((series != 5) && (series != 6) && (series != 0) && (series != 3))) { // || (type >= 10)) {
-            qDebug() << model << type << series << info.modelnumber << "unsupported";
+        if (!s_PRS1ModelInfo.IsSupported(props)) {
+            qWarning() << info.modelnumber << "unsupported";
 #ifndef UNITTEST_MODE
             QMessageBox::information(QApplication::activeWindow(),
                                      QObject::tr("Machine Unsupported"),
@@ -899,17 +832,8 @@ Machine* PRS1Loader::CreateMachineFromProperties(QString propertyfile)
 #endif
             return nullptr;
         }
-    } else {
-        // model number didn't parse.. Meh... Silently ignore it
-//        QMessageBox::information(QApplication::activeWindow(),
-//                                 QObject::tr("Machine Unsupported"),
-//                                 QObject::tr("OSCAR could not parse the model number, this machine can not be imported..") +"\n\n"+
-//                                 QObject::tr("The developers needs a .zip copy of this machines' SD card and matching Encore .pdf reports to make it work with OSCAR.")
-//                                ,QMessageBox::Ok);
-        return nullptr;
     }
 
-
     // Which is needed to get the right machine record..
     Machine *m = p_profile->CreateMachine(info);
 
@@ -929,7 +853,7 @@ Machine* PRS1Loader::CreateMachineFromProperties(QString propertyfile)
 #endif
     }
 
-    // TODO: Replace much of the above logic with PRS1ModelInfo logic.
+    // Mark the machine in the profile as unsupported.
     if (!s_PRS1ModelInfo.IsSupported(props)) {
         if (!m->unsupported()) {
             unsupported(m);
@@ -1324,10 +1248,10 @@ public:
     static constexpr float GAIN = 0.1;
     static const PRS1ParsedEventUnit UNIT = PRS1_UNIT_CMH2O;
     
-    PRS1PressureEvent(PRS1ParsedEventType type, int start, int value) 
+    PRS1PressureEvent(PRS1ParsedEventType type, int start, int value, float gain=GAIN)
         : PRS1ParsedValueEvent(type, start, value) 
     { 
-        m_gain = GAIN;
+        m_gain = gain;
         m_unit = UNIT;
     }
 };
@@ -1371,10 +1295,10 @@ public:
     static constexpr float GAIN = PRS1PressureEvent::GAIN;
     static const PRS1ParsedEventUnit UNIT = PRS1PressureEvent::UNIT;
     
-    PRS1PressureSettingEvent(PRS1ParsedSettingType setting, int value) 
+    PRS1PressureSettingEvent(PRS1ParsedSettingType setting, int value, float gain=GAIN)
         : PRS1ParsedSettingEvent(setting, value) 
     { 
-        m_gain = GAIN;
+        m_gain = gain;
         m_unit = UNIT;
     }
 };
@@ -1413,7 +1337,14 @@ public: \
 const PRS1ParsedEventType T::TYPE
 #define PRS1_DURATION_EVENT(T, E) _PRS1_EVENT(T, E, PRS1ParsedDurationEvent, duration)
 #define PRS1_VALUE_EVENT(T, E)    _PRS1_EVENT(T, E, PRS1ParsedValueEvent, value)
-#define PRS1_PRESSURE_EVENT(T, E) _PRS1_EVENT(T, E, PRS1PressureEvent, value)
+#define PRS1_PRESSURE_EVENT(T, E) \
+class T : public PRS1PressureEvent \
+{ \
+public: \
+    static const PRS1ParsedEventType TYPE = E; \
+    T(int start, int value, float gain=PRS1PressureEvent::GAIN) : PRS1PressureEvent(TYPE, start, value, gain) {} \
+}; \
+const PRS1ParsedEventType T::TYPE
 
 PRS1_DURATION_EVENT(PRS1TimedBreathEvent, EV_PRS1_TB);
 PRS1_DURATION_EVENT(PRS1ObstructiveApneaEvent, EV_PRS1_OA);
@@ -1576,8 +1507,11 @@ void PRS1DataChunk::AddEvent(PRS1ParsedEvent* const event)
     m_parsedData.push_back(event);
 }
 
-bool PRS1Import::ParseF5EventsFV3()
+bool PRS1Import::ParseEventsF5V3()
 {
+    // F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
+    static const float GAIN = 0.125F;  // TODO: parameterize this somewhere better
+    
     // Required channels
     EventList *OA = session->AddEventList(CPAP_Obstructive, EVL_Event);
     EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event);
@@ -1591,28 +1525,23 @@ bool PRS1Import::ParseF5EventsFV3()
     EventList *MV = session->AddEventList(CPAP_MinuteVent, EVL_Event);
     EventList *PB = session->AddEventList(CPAP_PB, EVL_Event);
     EventList *PTB = session->AddEventList(CPAP_PTB, EVL_Event);
-    EventList *TB = session->AddEventList(PRS1_TimedBreath, EVL_Event);
-    EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, 0.1F);
-    EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, 0.1F);
-    EventList *PS = session->AddEventList(CPAP_PS, EVL_Event, 0.1F);
-    EventList *IPAPLo = session->AddEventList(CPAP_IPAPLo, EVL_Event, 0.1F);
-    EventList *IPAPHi = session->AddEventList(CPAP_IPAPHi, EVL_Event, 0.1F);
+    EventList *TB = session->AddEventList(PRS1_TimedBreath, EVL_Event, 0.1F);  // TODO: a gain of 0.1 should affect display, but it doesn't
+    EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, GAIN);
+    EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, GAIN);
+    EventList *PS = session->AddEventList(CPAP_PS, EVL_Event, GAIN);
+    EventList *IPAPLo = session->AddEventList(CPAP_IPAPLo, EVL_Event, GAIN);
+    EventList *IPAPHi = session->AddEventList(CPAP_IPAPHi, EVL_Event, GAIN);
     EventList *FL = session->AddEventList(CPAP_FlowLimit, EVL_Event);
     EventList *SNORE = session->AddEventList(CPAP_Snore, EVL_Event);
     EventList *VS = session->AddEventList(CPAP_VSnore, EVL_Event);
+    EventList *VS2 = session->AddEventList(CPAP_VSnore2, EVL_Event);
 
+    // On-demand channels
+    EventList *PP = nullptr;
 
-    // Unintentional leak calculation, see zMaskProfile:calcLeak in calcs.cpp for explanation
-    EventDataType currentPressure=0, leak;
-
-    bool calcLeaks = p_profile->cpap->calculateUnintentionalLeaks();
-    EventDataType lpm4 = p_profile->cpap->custom4cmH2OLeaks();
-    EventDataType lpm20 = p_profile->cpap->custom20cmH2OLeaks();
-
-    EventDataType lpm = lpm20 - lpm4;
-    EventDataType ppm = lpm / 16.0;
-
+    EventDataType currentPressure=0;
 
+    qint64 duration;
     qint64 t = qint64(event->timestamp) * 1000L;
     session->updateFirst(t);
 
@@ -1642,7 +1571,12 @@ bool PRS1Import::ParseF5EventsFV3()
                 PS->AddEvent(t, currentPressure - e->m_value);           // Pressure Support
                 break;
             case PRS1TimedBreathEvent::TYPE:
-                TB->AddEvent(t, e->m_duration);
+                // The duration appears to correspond to the length of the timed breath in seconds when multiplied by 0.1 (100ms)!
+                // TODO: consider changing parsers to use milliseconds for time, since it turns out there's at least one way
+                // they can express durations less than 1 second.
+                // TODO: consider allowing OSCAR to record millisecond durations so that the display will say "2.1" instead of "21" or "2".
+                duration = e->m_duration * 100L;  // for now do this here rather than in parser, since parser events don't use milliseconds
+                TB->AddEvent(t - duration, e->m_duration * 0.1F);  // TODO: a gain of 0.1 should render this unnecessary, but gain doesn't seem to work currently
                 break;
             case PRS1ObstructiveApneaEvent::TYPE:
                 OA->AddEvent(t, e->m_duration);
@@ -1657,26 +1591,40 @@ bool PRS1Import::ParseF5EventsFV3()
                 FL->AddEvent(t, e->m_duration);
                 break;
             case PRS1PeriodicBreathingEvent::TYPE:
-                PB->AddEvent(t, e->m_duration);
+                // TODO: The graphs silently treat the timestamp of a span as an end time rather than start (see gFlagsLine::paint).
+                // Decide whether to preserve that behavior or change it universally and update either this code or comment.
+                duration = e->m_duration * 1000L;
+                PB->AddEvent(t + duration, e->m_duration);
                 break;
             case PRS1LargeLeakEvent::TYPE:
-                LL->AddEvent(t, e->m_duration);
+                // TODO: see PB comment above.
+                duration = e->m_duration * 1000L;
+                LL->AddEvent(t + duration, e->m_duration);
                 break;
             case PRS1TotalLeakEvent::TYPE:
                 TOTLEAK->AddEvent(t, e->m_value);
-                leak = e->m_value;
-                if (calcLeaks) { // Much Quicker doing this here than the recalc method.
-                    leak -= (((currentPressure/10.0f) - 4.0) * ppm + lpm4);
-                    if (leak < 0) leak = 0;
-                    LEAK->AddEvent(t, leak);
-                }
                 break;
-            case PRS1SnoreEvent::TYPE:
+            case PRS1LeakEvent::TYPE:
+                LEAK->AddEvent(t, e->m_value);
+                break;
+            case PRS1SnoreEvent::TYPE:  // snore count that shows up in flags but not waveform
+                // TODO: The numeric snore graph is the right way to present this information,
+                // but it needs to be shifted left 2 minutes, since it's not a starting value
+                // but a past statistic.
                 SNORE->AddEvent(t, e->m_value);
                 if (e->m_value > 0) {
-                    VS->AddEvent(t, 0); //data2); // VSnore
+                    // TODO: currently these get drawn on our waveforms, but they probably shouldn't,
+                    // since they don't have a precise timestamp. They should continue to be drawn
+                    // on the flags overview.
+                    VS2->AddEvent(t, 0);
                 }
                 break;
+            case PRS1VibratorySnoreEvent::TYPE:  // real VS marker on waveform
+                // TODO: These don't need to be drawn separately on the flag overview, since
+                // they're presumably included in the overall snore count statistic. They should
+                // continue to be drawn on the waveform, due to their precise timestamp.
+                VS->AddEvent(t, 0);
+                break;
             case PRS1RespiratoryRateEvent::TYPE:
                 RR->AddEvent(t, e->m_value);
                 break;
@@ -1689,6 +1637,16 @@ bool PRS1Import::ParseF5EventsFV3()
             case PRS1TidalVolumeEvent::TYPE:
                 TV->AddEvent(t, e->m_value);
                 break;
+            case PRS1PressurePulseEvent::TYPE:
+                if (!PP) {
+                    if (!(PP = session->AddEventList(CPAP_PressurePulse, EVL_Event))) { return false; }
+                }
+                PP->AddEvent(t, e->m_value);
+                break;
+            case PRS1UnknownDataEvent::TYPE:
+                // These will show up in chunk YAML and any user alerts will be driven
+                // by the parser.
+                break;
             default:
                 qWarning() << "Unknown PRS1 event type" << (int) e->m_type;
                 break;
@@ -1707,151 +1665,165 @@ bool PRS1Import::ParseF5EventsFV3()
 }
 
 
-// 900X series
+// Outer loop based on ParseSummaryF5V3 along with hint as to event codes from old ParseEventsF5V3,
+// except this actually does something with the data.
 bool PRS1DataChunk::ParseEventsF5V3(void)
 {
     if (this->family != 5 || this->familyVersion != 3) {
         qWarning() << "ParseEventsF5V3 called with family" << this->family << "familyVersion" << this->familyVersion;
         return false;
     }
-    
-    EventDataType data0, data1, data2, data3, data4, data5;
-    Q_UNUSED(data3)
-
-    int t = 0;
-    int pos = 0;
-    //int cnt = 0;
-    short delta;//,duration;
-    //bool badcode = false;
-    unsigned char lastcode3 = 0, lastcode2 = 0, lastcode = 0, code = 0;
-    int lastpos = 0, startpos = 0, lastpos2 = 0, lastpos3 = 0;
-
-    int size = this->m_data.size();
-    unsigned char * buffer = (unsigned char *)this->m_data.data();
-
-    while (pos < size) {
-        lastcode3 = lastcode2;
-        lastcode2 = lastcode;
-        lastcode = code;
-        lastpos3 = lastpos2;
-        lastpos2 = lastpos;
-        lastpos = startpos;
-        startpos = pos;
-        code = buffer[pos++];
-
-        if (code >= 0x12) {
-            qDebug() << "Illegal PRS1 code " << hex << int(code) << " appeared at " << hex << startpos << "in" << this->sessionid;;
-            qDebug() << "1: (" << int(lastcode) << hex << lastpos << ")";
-            qDebug() << "2: (" << int(lastcode2) << hex << lastpos2 << ")";
-            qDebug() << "3: (" << int(lastcode3) << hex << lastpos3 << ")";
-            this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
-            return false;
-        }
-        delta = buffer[pos];
-        //delta=buffer[pos+1] << 8 | buffer[pos];
-        pos += 2;
-        t += delta;
-
-        switch(code) {
-        case 0x01: // Leak ???
-            data0 = buffer[pos++];
-            //tt -= qint64(data0) * 1000L; // Subtract Time Offset
-            break;
-        case 0x02: // Meh??? Timed Breath??
-            data0 = buffer[pos++];
-            this->AddEvent(new PRS1TimedBreathEvent(t - data0, data0));
-            break;
-        case 0x03: // Graph Data
-            data0 = buffer[pos++];
-            this->AddEvent(new PRS1IPAPEvent(t, data0));                             // 00=IAP
-            data4 = buffer[pos++];
-            this->AddEvent(new PRS1IPAPLowEvent(t, data4));                          // 01=IAP Low
-            data5 = buffer[pos++];
-            this->AddEvent(new PRS1IPAPHighEvent(t, data5));                         // 02=IAP High
-            this->AddEvent(new PRS1TotalLeakEvent(t, buffer[pos++]));                // 03=LEAK
-
-
-            this->AddEvent(new PRS1RespiratoryRateEvent(t, buffer[pos++]));          // 04=Breaths Per Minute
-            this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, buffer[pos++]));  // 05=Patient Triggered Breaths
-            this->AddEvent(new PRS1MinuteVentilationEvent(t, buffer[pos++]));        // 06=Minute Ventilation
-            //tmp=buffer[pos++] * 10.0;
-            this->AddEvent(new PRS1TidalVolumeEvent(t, buffer[pos++]));              // 07=Tidal Volume
-            this->AddEvent(new PRS1SnoreEvent(t, buffer[pos++]));                    // 08=Snore
-            this->AddEvent(new PRS1EPAPEvent(t, buffer[pos++]));                     // 09=EPAP
-            data0 = buffer[pos++];
-
-
-            break;
-        case 0x05:
-            data0 = buffer[pos++];
-            this->AddEvent(new PRS1ObstructiveApneaEvent(t - data0, data0));
-
-//            PS->AddEvent(tt, data0);
-            break;
-        case 0x06: // Clear Airway
-            data0 = buffer[pos++];
-            this->AddEvent(new PRS1ClearAirwayEvent(t - data0, data0));
-
-//            PTB->AddEvent(tt, data0);
-            break;
-        case 0x07:
-            data0 = buffer[pos++];
-            data1 = buffer[pos++];
-            //tt -= qint64(data0) * 1000L; // Subtract Time Offset
-
-
-            break;
-        case 0x08: // Flow Limitation
-            data0 = buffer[pos++];
-            this->AddEvent(new PRS1FlowLimitationEvent(t - data0, data0));
-            break;
-        case 0x09:
-            data0 = buffer[pos++];
-            data1 = buffer[pos++];
-            data2 = buffer[pos++];
-            data3 = buffer[pos++];
-            //tt -= qint64(data0) * 1000L; // Subtract Time Offset
-
-
-          //  TB->AddEvent(tt, data0);
-            break;
-        case 0x0a: // Periodic Breathing?
-            data0 = (buffer[pos + 1] << 8 | buffer[pos]);
-            data0 *= 2;
-            pos += 2;
-            data1 = buffer[pos++];
-            this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0));
-
-            break;
-        case 0x0b: // Large Leak
-            data0 = (buffer[pos + 1] << 8 | buffer[pos]);
-            data0 *= 2;
-            pos += 2;
-            data1 = buffer[pos++];
-            this->AddEvent(new PRS1LargeLeakEvent(t - data1, data0));
-
-            break;
-        case 0x0d: // flag ??
-            data0 = buffer[pos++];
-            this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
-
-
-            break;
-        case 0x0e:
-            data0 = buffer[pos++];
-            this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
-
-            break;
-        default:
-            qDebug() << "Unknown code:" << hex << code << "in" << this->sessionid << "at" << startpos;
-            this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
-
-
-        }
+    const unsigned char * data = (unsigned char *)this->m_data.constData();
+    int chunk_size = this->m_data.size();
+    static const int minimum_sizes[] = { 2, 3, 3, 0xd, 3, 3, 3, 4, 3, 2, 5, 5, 3, 3, 3, 3 };
+    static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
 
+    if (chunk_size < 1) {
+        // This does occasionally happen.
+        qDebug() << this->sessionid << "Empty event data";
+        return false;
     }
 
-    return true;
+    // F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
+    static const float GAIN = 0.125;  // TODO: this should be parameterized somewhere more logical
+    bool ok = true;
+    int pos = 0, startpos;
+    int code, size;
+    int t = 0;
+    int elapsed, duration;
+    do {
+        code = data[pos++];
+        if (!this->hblock.contains(code)) {
+            qWarning() << this->sessionid << "missing hblock entry for event" << code;
+            ok = false;
+            break;
+        }
+        size = this->hblock[code];
+        if (code < ncodes) {
+            // make sure the handlers below don't go past the end of the buffer
+            if (size < minimum_sizes[code]) {
+                qWarning() << this->sessionid << "event" << code << "too small" << size << "<" << minimum_sizes[code];
+                ok = false;
+                break;
+            }
+        } // else if it's past ncodes, we'll log its information below (rather than handle it)
+        if (pos + size > chunk_size) {
+            qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk";
+            ok = false;
+            break;
+        }
+        startpos = pos;
+        t += data[pos] | (data[pos+1] << 8);
+        pos += 2;
+
+        switch (code) {
+            case 1:  // Pressure adjustment
+                // TODO: Have OSCAR treat EPAP adjustment events differently than (average?) stats below.
+                //this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN));
+                this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
+                break;
+            case 2:  // Timed Breath
+                // TB events have a duration in 0.1s, based on the review of pressure waveforms.
+                // TODO: Ideally the starting time here would be adjusted here, but PRS1ParsedEvents
+                // currently assume integer seconds rather than ms, so that's done at import.
+                duration = data[pos++];
+                this->AddEvent(new PRS1TimedBreathEvent(t, duration));
+                break;
+            case 3:  // Statistics
+                // These appear every 2 minutes, so presumably summarize the preceding period.
+                this->AddEvent(new PRS1IPAPEvent(t, data[pos++], GAIN));               // 00=IPAP (average?)
+                this->AddEvent(new PRS1IPAPLowEvent(t, data[pos++], GAIN));            // 01=IAP Low
+                this->AddEvent(new PRS1IPAPHighEvent(t, data[pos++], GAIN));           // 02=IAP High
+                this->AddEvent(new PRS1TotalLeakEvent(t, data[pos++]));                // 03=Total leak (average?)
+                this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos++]));          // 04=Breaths Per Minute (average?)
+                this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos++]));  // 05=Patient Triggered Breaths (average?)
+                this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos++]));        // 06=Minute Ventilation (average?)
+                this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos++]));              // 07=Tidal Volume (average?)
+                this->AddEvent(new PRS1SnoreEvent(t, data[pos++]));                    // 08=Snore count  // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
+                this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN));               // 09=EPAP (average? see event 1 above)
+                this->AddEvent(new PRS1LeakEvent(t, data[pos++]));                     // 0A=Leak (average?)
+                break;
+            case 0x04:  // Pressure Pulse
+                duration = data[pos++];  // TODO: is this a duration?
+                this->AddEvent(new PRS1PressurePulseEvent(t, duration));
+                break;
+            case 0x05:  // Obstructive Apnea
+                // OA events are instantaneous flags with no duration: reviewing waveforms
+                // shows that the time elapsed between the flag and reporting often includes
+                // non-apnea breathing.
+                elapsed = data[pos++];
+                this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0));
+                break;
+            case 0x06:  // Clear Airway Apnea
+                // CA events are instantaneous flags with no duration: reviewing waveforms
+                // shows that the time elapsed between the flag and reporting often includes
+                // non-apnea breathing.
+                elapsed = data[pos++];
+                this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0));
+                break;
+            case 0x07:  // Hypopnea
+                // TODO: How is this hypopnea different from events 0xd and 0xe?
+                // TODO: What is the first byte?
+                pos++;  // unknown first byte?
+                elapsed = data[pos++];  // based on sample waveform, the hypopnea is over after this
+                this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
+                break;
+            case 0x08:  // Flow Limitation
+                // TODO: We should revisit whether this is elapsed or duration once (if)
+                // we start calculating flow limitations ourselves. Flow limitations aren't
+                // as obvious as OA/CA when looking at a waveform.
+                elapsed = data[pos++];
+                this->AddEvent(new PRS1FlowLimitationEvent(t - elapsed, 0));
+                break;
+            case 0x09:  // Vibratory Snore
+                // VS events are instantaneous flags with no duration, drawn on the official waveform.
+                // The current thinking is that these are the snores that cause a change in auto-titrating
+                // pressure. The snoring statistic above seems to be a total count. It's unclear whether
+                // the trigger for pressure change is severity or count or something else.
+                // no data bytes
+                this->AddEvent(new PRS1VibratorySnoreEvent(t, 0));
+                break;
+            case 0x0a:  // Periodic Breathing
+                // PB events are reported some time after they conclude, and they do have a reported duration.
+                duration = 2 * (data[pos] | (data[pos+1] << 8));
+                pos += 2;
+                elapsed = data[pos++];
+                this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
+                break;
+            case 0x0b:  // Large Leak
+                // LL events are reported some time after they conclude, and they do have a reported duration.
+                duration = 2 * (data[pos] | (data[pos+1] << 8));
+                pos += 2;
+                elapsed = data[pos++];
+                this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
+                break;
+            case 0x0d:  // Hypopnea
+                // TODO: Why does this hypopnea have a different event code?
+                // fall through
+            case 0x0e:  // Hypopnea
+                // TODO: We should revisit whether this is elapsed or duration once (if)
+                // we start calculating hypopneas ourselves. Their official definition
+                // is 40% reduction in flow lasting at least 10s.
+                duration = data[pos++];
+                this->AddEvent(new PRS1HypopneaEvent(t - duration, 0));
+                break;
+            case 0x0f:
+                // TODO: some other pressure adjustment?
+                // Appears near the beginning and end of a session when Opti-Start is on, at least once in middle
+                //CHECK_VALUES(data[pos], 0x20, 0x28);
+                this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
+                break;
+            default:
+                qWarning() << "Unknown event:" << code << "in" << this->sessionid << "at" << startpos-1;
+                this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
+                break;
+        }
+        pos = startpos + size;
+    } while (ok && pos < chunk_size);
+
+    this->duration = t;
+
+    return ok;
 }
 
 
@@ -2019,7 +1991,7 @@ bool PRS1Import::ParseF5Events()
 // 950P is F5V0, 960P and 961P are F5V1, 960T is F5V2
 bool PRS1DataChunk::ParseEventsF5V012(void)
 {
-    EventDataType data0, data1, data2, data4, data5;
+    EventDataType data0, data1, data4, data5;
 
     int t = 0;
     int pos = 0;
@@ -2077,7 +2049,8 @@ bool PRS1DataChunk::ParseEventsF5V012(void)
             }
 
             if (!buffer[pos - 1]) {
-                data2 = buffer[pos++];
+                //data2 = buffer[pos++];
+                pos++;
                 fc++;
             }
 
@@ -2282,7 +2255,7 @@ bool PRS1DataChunk::ParseEventsF5V012(void)
             qDebug() << "0x12 Observed in ASV data!!????";
             data0 = buffer[pos++];
             data1 = buffer[pos++];
-            data2 = buffer[pos + 1] << 8 | buffer[pos];
+            //data2 = buffer[pos + 1] << 8 | buffer[pos];
             pos += 2;
             //session->AddEvent(new Event(t,cpapcode, 0, data,3));
             break;
@@ -3086,6 +3059,8 @@ bool PRS1DataChunk::ParseEventsF0(CPAPMode mode)
             pos += 2;
             data1 = buffer[pos++];
             if (this->familyVersion == 2 || this->familyVersion == 3) {
+                // TODO: this fixed some timing errors on parsing/import, but may have broken drawing, since OSCAR
+                // apparently does treat a span's timestamp as an endpoint (at least when drawing, see gFlagsLine::paint)!
                 this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1 - data0, data0));  // PB event appears data1 seconds after conclusion
             } else {
                 this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0));  // TODO: this should probably be the same as F0V23, but it hasn't been tested
@@ -3224,12 +3199,6 @@ bool PRS1Import::ImportCompliance()
 }
 
 
-// TODO: have UNEXPECTED_VALUE set a flag in the importer/machine that this data set is unusual
-#define UNEXPECTED_VALUE(SRC, VALS) { qWarning() << this->sessionid << QString("%1: %2 = %3 != %4").arg(__func__).arg(#SRC).arg(SRC).arg(VALS); }
-#define CHECK_VALUE(SRC, VAL) if ((SRC) != (VAL)) UNEXPECTED_VALUE(SRC, VAL)
-#define CHECK_VALUES(SRC, VAL1, VAL2) if ((SRC) != (VAL1) && (SRC) != (VAL2)) UNEXPECTED_VALUE(SRC, #VAL1 " or " #VAL2)
-// for more than 2 values, just write the test manually and use UNEXPECTED_VALUE if it fails
-
 bool PRS1DataChunk::ParseCompliance(void)
 {
     switch (this->family) {
@@ -3723,38 +3692,6 @@ void PRS1DataChunk::ParseHumidifierSettingV2(int humid, bool supportsHeatedTubin
 }
 
 
-bool PRS1DataChunk::ParseSummaryF5V3(void)
-{
-    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) MODE_ASV_VARIABLE_EPAP));
-
-    unsigned char * pressureBlock = (unsigned char *)mainblock[0x0a].data();
-
-    int epapHi = pressureBlock[0];
-    int epapRange = pressureBlock[2];
-    int epapLo = epapHi - epapRange;
-
-    int minps = pressureBlock[3] ;
-    int maxps = pressureBlock[4]+epapLo;
-
-    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, epapHi));
-    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, epapLo));
-    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, minps));
-    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, maxps));
-    
-    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, epapLo + minps));
-    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, qMin(250, epapHi + maxps)));  // 25.0 cmH2O max
-
-    if (hbdata[4].size() < 2) {
-        qDebug() << "summary missing duration section:" << this->sessionid;
-        return false;
-    }
-    unsigned char * durBlock = (unsigned char *)hbdata[4].data();
-    this->duration = durBlock[0] | durBlock[1] << 8;
-
-    return true;
-}
-
-
 // The below is based on fixing the fileVersion == 3 parsing in ParseSummary() based
 // on our understanding of slices from F0V23. The switch values come from sample files.
 bool PRS1DataChunk::ParseComplianceF0V6(void)
@@ -3818,7 +3755,7 @@ bool PRS1DataChunk::ParseComplianceF0V6(void)
             case 3:  // Mask On
                 tt += data[pos] | (data[pos+1] << 8);
                 this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
-                this->ParseHumidifierSettingF0V6(data[pos+2], data[pos+3]);
+                this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
                 break;
             case 4:  // Mask Off
                 tt += data[pos] | (data[pos+1] << 8);
@@ -3848,7 +3785,7 @@ bool PRS1DataChunk::ParseComplianceF0V6(void)
                 break;
             case 6:  // Humidier setting change
                 tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
-                this->ParseHumidifierSettingF0V6(data[pos+2], data[pos+3]);
+                this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
                 break;
             default:
                 UNEXPECTED_VALUE(code, "known slice code");
@@ -3863,7 +3800,8 @@ bool PRS1DataChunk::ParseComplianceF0V6(void)
 }
 
 
-void PRS1DataChunk::ParseHumidifierSettingF0V6(unsigned char byte1, unsigned char byte2, bool add_setting)
+// It turns out this is used by F5V3 in addition to F0V6, so it's likely common to all fileVersion 3 machines.
+void PRS1DataChunk::ParseHumidifierSettingV3(unsigned char byte1, unsigned char byte2, bool add_setting)
 {
     // Byte 1: 0x90 (no humidifier data), 0x50 (15ht, tube 4/5, humid 4), 0x54 (15ht, tube 5, humid 5) 0x4c (15ht, tube temp 3, humidifier 3)
     // 0x0c (15, tube 3, humid 3, fixed)
@@ -4061,7 +3999,7 @@ bool PRS1DataChunk::ParseSettingsF0V6(const unsigned char* data, int size)
                 this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[pos]));
                 break;
             case 0x35:  // Humidifier setting
-                this->ParseHumidifierSettingF0V6(data[pos], data[pos+1], true);
+                this->ParseHumidifierSettingV3(data[pos], data[pos+1], true);
                 break;
             case 0x36:
                 CHECK_VALUE(data[pos], 0);
@@ -4176,7 +4114,7 @@ bool PRS1DataChunk::ParseSummaryF0V6(void)
             case 3:  // Mask On
                 tt += data[pos] | (data[pos+1] << 8);
                 this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
-                this->ParseHumidifierSettingF0V6(data[pos+2], data[pos+3]);
+                this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
                 break;
             case 4:  // Mask Off
                 tt += data[pos] | (data[pos+1] << 8);
@@ -4238,7 +4176,7 @@ bool PRS1DataChunk::ParseSummaryF0V6(void)
                 break;
             case 0x0a:  // Humidier setting change
                 tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
-                this->ParseHumidifierSettingF0V6(data[pos+2], data[pos+3]);
+                this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
                 break;
             case 0x0e:
                 // only seen once on 400G?
@@ -4295,6 +4233,323 @@ bool PRS1DataChunk::ParseSummaryF0V6(void)
 }
 
 
+// Originally based on ParseSummaryF0V6, with changes observed in ASV sample data
+// based on size, slices 0-5 look similar, and it looks like F0V6 slides 8-B are equivalent to 6-9
+//
+// TODO: surely there will be a way to merge these loops and abstract the machine-specific
+// encodings into another function or class, but that's probably worth pursuing only after
+// the details have been figured out.
+bool PRS1DataChunk::ParseSummaryF5V3(void)
+{
+    if (this->family != 5 || this->familyVersion != 3) {
+        qWarning() << "ParseSummaryF5V3 called with family" << this->family << "familyVersion" << this->familyVersion;
+        return false;
+    }
+    const unsigned char * data = (unsigned char *)this->m_data.constData();
+    int chunk_size = this->m_data.size();
+    static const int minimum_sizes[] = { 1, 0x35, 9, 4, 2, 4, 0x1e, 2, 4, 9 };
+    static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
+    // NOTE: The sizes contained in hblock can vary, even within a single machine, as can the length of hblock itself!
+
+    // TODO: hardcoding this is ugly, think of a better approach
+    if (chunk_size < minimum_sizes[0] + minimum_sizes[1] + minimum_sizes[2]) {
+        qWarning() << this->sessionid << "summary data too short:" << chunk_size;
+        return false;
+    }
+    // We've once seen a short summary with no mask-on/off: just equipment-on, settings, 9, equipment-off
+    if (chunk_size < 75) UNEXPECTED_VALUE(chunk_size, ">= 75");
+
+    bool ok = true;
+    int pos = 0;
+    int code, size;
+    int tt = 0;
+    do {
+        code = data[pos++];
+        if (!this->hblock.contains(code)) {
+            qWarning() << this->sessionid << "missing hblock entry for" << code;
+            ok = false;
+            break;
+        }
+        size = this->hblock[code];
+        if (code < ncodes) {
+            // make sure the handlers below don't go past the end of the buffer
+            if (size < minimum_sizes[code]) {
+                qWarning() << this->sessionid << "slice" << code << "too small" << size << "<" << minimum_sizes[code];
+                ok = false;
+                break;
+            }
+        } // else if it's past ncodes, we'll log its information below (rather than handle it)
+        if (pos + size > chunk_size) {
+            qWarning() << this->sessionid << "slice" << code << "@" << pos << "longer than remaining chunk";
+            ok = false;
+            break;
+        }
+
+        switch (code) {
+            case 0:  // Equipment On
+                CHECK_VALUE(pos, 1);  // Always first?
+                //CHECK_VALUES(data[pos], 1, 7);  // or 3, or 0?
+                CHECK_VALUE(size, 1);
+                break;
+            case 1:  // Settings
+                ok = this->ParseSettingsF5V3(data + pos, size);
+                break;
+            case 9:  // new to F5V3 vs. F0V6, comes right after settings, before mask on?
+                CHECK_VALUE(data[pos], 0);
+                CHECK_VALUE(data[pos+1], 1);
+                CHECK_VALUE(data[pos+2], 0);
+                CHECK_VALUE(data[pos+3], 1);
+                CHECK_VALUE(data[pos+4], 1);
+                CHECK_VALUE(data[pos+5], 0);
+                CHECK_VALUE(data[pos+6], 2);
+                CHECK_VALUE(data[pos+7], 1);
+                CHECK_VALUE(data[pos+8], 0);
+                break;
+            case 3:  // Mask On
+                tt += data[pos] | (data[pos+1] << 8);
+                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
+                this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
+                break;
+            case 4:  // Mask Off
+                tt += data[pos] | (data[pos+1] << 8);
+                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
+                break;
+            case 5:  // ASV pressure stats per mask-on slice
+                //CHECK_VALUE(data[pos], 0x28);  // 90% EPAP
+                //CHECK_VALUE(data[pos+1], 0x23);  // average EPAP
+                //CHECK_VALUE(data[pos+2], 0x24);  // 90% PS
+                //CHECK_VALUE(data[pos+3], 0x17);  // average PS
+                break;
+            case 6:  // Patient statistics per mask-on slice
+                // These get averaged on a time-weighted basis in the final report.
+                // Where is H count?
+                //CHECK_VALUE(data[pos], 0x00);  // probably 16-bit value
+                CHECK_VALUE(data[pos+1], 0x00);
+                //CHECK_VALUE(data[pos+2], 0x00);  // 16-bit OA count
+                //CHECK_VALUE(data[pos+3], 0x00);
+                //CHECK_VALUE(data[pos+4], 0x00);  // probably 16-bit value
+                CHECK_VALUE(data[pos+5], 0x00);
+                //CHECK_VALUE(data[pos+6], 0x00);  // 16-bit CA count
+                //CHECK_VALUE(data[pos+7], 0x00);
+                //CHECK_VALUE(data[pos+8], 0x00);  // 16-bit minutes in LL
+                //CHECK_VALUE(data[pos+9], 0x00);
+                //CHECK_VALUE(data[pos+0xa], 0x0f);  // 16-bit minutes in PB
+                //CHECK_VALUE(data[pos+0xb], 0x00);
+                //CHECK_VALUE(data[pos+0xc], 0x14);  // 16-bit VS count
+                //CHECK_VALUE(data[pos+0xd], 0x00);
+                //CHECK_VALUE(data[pos+0xe], 0x05);  // 16-bit H count for type 0xd
+                //CHECK_VALUE(data[pos+0xf], 0x00);
+                //CHECK_VALUE(data[pos+0x10], 0x00);  // 16-bit H count for type 7
+                //CHECK_VALUE(data[pos+0x11], 0x00);
+                //CHECK_VALUE(data[pos+0x12], 0x02);  // 16-bit FL count
+                //CHECK_VALUE(data[pos+0x13], 0x00);
+                //CHECK_VALUE(data[pos+0x14], 0x28);  // 0x69 (105)
+                //CHECK_VALUE(data[pos+0x15], 0x17);  // average total leak
+                //CHECK_VALUE(data[pos+0x16], 0x5b);  // 0x7d (125)
+                //CHECK_VALUE(data[pos+0x17], 0x09);  // 16-bit H count for type 0xe
+                //CHECK_VALUE(data[pos+0x18], 0x00);
+                //CHECK_VALUE(data[pos+0x19], 0x10);  // average breath rate
+                //CHECK_VALUE(data[pos+0x1a], 0x2d);  // average TV / 10
+                //CHECK_VALUE(data[pos+0x1b], 0x63);  // average % PTB
+                //CHECK_VALUE(data[pos+0x1c], 0x07);  // average minute vent
+                //CHECK_VALUE(data[pos+0x1d], 0x06);  // average leak
+                break;
+            case 2:  // Equipment Off
+                tt += data[pos] | (data[pos+1] << 8);
+                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
+                //CHECK_VALUE(data[pos+2], 0x01);  // 0x08
+                //CHECK_VALUE(data[pos+3], 0x17);  // 0x16, 0x18
+                //CHECK_VALUE(data[pos+4], 0x00);
+                //CHECK_VALUE(data[pos+5], 0x29);  // 0x2a, 0x28, 0x26, 0x36
+                //CHECK_VALUE(data[pos+6], 0x01);  // 0x00
+                CHECK_VALUE(data[pos+7], 0x00);
+                CHECK_VALUE(data[pos+8], 0x00);
+                break;
+            case 8:  // Humidier setting change
+                tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
+                this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
+                break;
+            default:
+                UNEXPECTED_VALUE(code, "known slice code");
+                break;
+        }
+        pos += size;
+    } while (ok && pos < chunk_size);
+
+    this->duration = tt;
+
+    return ok;
+}
+
+
+// Based initially on ParseSettingsF0V6. Many of the codes look the same, like always starting with 0, 0x35 looking like
+// a humidifier setting, etc., but the contents are sometimes a bit different, such as mode values and pressure settings.
+//
+// new settings to find: breath rate, tubing lock, alarms,
+bool PRS1DataChunk::ParseSettingsF5V3(const unsigned char* data, int size)
+{
+    static const QMap<int,int> expected_lengths = { {0x0a,5}, /*{0x0c,3}, {0x0d,2}, {0x0e,2}, {0x0f,4}, {0x10,3},*/ {0x14,3}, {0x2e,2}, {0x35,2} };
+    bool ok = true;
+
+    CPAPMode cpapmode = MODE_UNKNOWN;
+
+    // F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
+    static const float GAIN = 0.125;  // TODO: parameterize this somewhere better
+
+    int max_pressure = 0;
+    int min_ps   = 0;
+    int max_ps   = 0;
+    int min_epap = 0;
+    int max_epap = 0;
+
+    // Parse the nested data structure which contains settings
+    int pos = 0;
+    do {
+        int code = data[pos++];
+        int len = data[pos++];
+
+        int expected_len = 1;
+        if (expected_lengths.contains(code)) {
+            expected_len = expected_lengths[code];
+        }
+        //CHECK_VALUE(len, expected_len);
+        if (len < expected_len) {
+            qWarning() << this->sessionid << "setting" << code << "too small" << len << "<" << expected_len;
+            ok = false;
+            break;
+        }
+        if (pos + len > size) {
+            qWarning() << this->sessionid << "setting" << code << "@" << pos << "longer than remaining slice";
+            ok = false;
+            break;
+        }
+
+        switch (code) {
+            case 0: // Device Mode
+                CHECK_VALUE(pos, 2);  // always first?
+                switch (data[pos]) {
+                case 0: cpapmode = MODE_ASV_VARIABLE_EPAP; break;
+                default:
+                    UNEXPECTED_VALUE(data[pos], "known device mode");
+                    break;
+                }
+                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
+                break;
+            case 1: // ???
+                CHECK_VALUES(data[pos], 0, 1);  // 1 when when Opti-Start is on? 0 when off?
+                /*
+                if (data[pos] != 0 && data[pos] != 3) {
+                    CHECK_VALUES(data[pos], 1, 2);  // 1 when EZ-Start is enabled? 2 when Auto-Trial? 3 when Auto-Trial is off or Opti-Start isn't off?
+                }
+                */
+                break;
+            case 0x0a:  // ASV with variable EPAP pressure setting
+                CHECK_VALUE(cpapmode, MODE_ASV_VARIABLE_EPAP);
+                max_pressure = data[pos];
+                min_epap = data[pos+1];
+                max_epap = data[pos+2];
+                min_ps = data[pos+3];
+                max_ps = data[pos+4];
+                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, min_epap, GAIN));
+                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, max_epap, GAIN));
+                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_epap + min_ps, GAIN));
+                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, qMin(max_pressure, max_epap + max_ps), GAIN));
+                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ps, GAIN));
+                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, max_ps, GAIN));
+                break;
+            case 0x14:  // new to ASV, ???
+                CHECK_VALUE(data[pos], 1);
+                CHECK_VALUE(data[pos+1], 0);
+                CHECK_VALUE(data[pos+2], 0);
+                break;
+            /*
+            case 0x2a:  // EZ-Start
+                CHECK_VALUE(data[pos], 0x80);  // EZ-Start enabled
+                break;
+            */
+            case 0x2b:  // Ramp Type
+                CHECK_VALUES(data[pos], 0, 0x80);  // 0 == "Linear", 0x80 = "SmartRamp"
+                break;
+            case 0x2c:  // Ramp Time
+                if (data[pos] != 0) {  // 0 == ramp off, and ramp pressure setting doesn't appear
+                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, data[pos]));
+                }
+                break;
+            case 0x2d:  // Ramp Pressure (with ASV pressure encoding)
+                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, data[pos], GAIN));
+                break;
+            case 0x2e:
+                CHECK_VALUE(data[pos], 0);
+                //CHECK_VALUES(data[pos+1], 2, 3);  // Bi-Flex level
+                /*
+                if (data[pos] != 0) {
+                    CHECK_VALUES(data[pos], 0x80, 0x90);  // maybe flex related? 0x80 when c-flex? 0x90 when c-flex+ or A-flex?, 0x00 when no flex
+                }
+                */
+                break;
+            case 0x2f:  // Flex lock? (was on F0V6, 0x80 for locked)
+                CHECK_VALUE(data[pos], 0);
+                break;
+            /*
+            case 0x30:  // Flex level
+                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[pos]));
+                break;
+            */
+            case 0x35:  // Humidifier setting
+                this->ParseHumidifierSettingV3(data[pos], data[pos+1], true);
+                break;
+            case 0x36:  // Mask Resistance Lock
+                CHECK_VALUES(data[pos], 0, 0x80);  // 0x80 = locked
+                break;
+            case 0x38:  // Mask Resistance
+                if (data[pos] != 0) {  // 0 == mask resistance off
+                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SYSTEMONE_RESIST_SETTING, data[pos]));
+                }
+                break;
+            case 0x39:
+                CHECK_VALUE(data[pos], 0);  // 0x80 maybe auto-trial in F0V6?
+                break;
+            case 0x3b:  // Tubing Type
+                if (data[pos] != 0) {
+                    CHECK_VALUES(data[pos], 2, 1);  // 15HT = 2, 15 = 1, 22 = 0, though report only says "15" for 15HT
+                }
+                break;
+            case 0x3c:
+                CHECK_VALUES(data[pos], 0, 0x80);  // 0x80 maybe show AHI?
+                break;
+            case 0x3d:  // new to ASV
+                //CHECK_VALUES(data[pos], 0, 0x80);  // 0x80 maybe auto-on?
+                break;
+            /*
+            case 0x3e:
+                CHECK_VALUES(data[pos], 0, 0x80);  // 0x80 maybe auto-on?
+                break;
+            case 0x3f:
+                CHECK_VALUES(data[pos], 0, 0x80);  // 0x80 maybe auto-off?
+                break;
+            case 0x43:  // new to 502G, sessions 3-8, Auto-Trial is off, Opti-Start is missing
+                CHECK_VALUE(data[pos], 0x3C);
+                break;
+            case 0x44:  // new to 502G, sessions 3-8, Auto-Trial is off, Opti-Start is missing
+                CHECK_VALUE(data[pos], 0xFF);
+                break;
+            case 0x45:  // new to 400G, only in last session?
+                CHECK_VALUE(data[pos], 1);
+                break;
+            */
+            default:
+                qDebug() << "Unknown setting:" << hex << code << "in" << this->sessionid << "at" << pos;
+                this->AddEvent(new PRS1UnknownDataEvent(QByteArray((const char*) data, size), pos, len));
+                break;
+        }
+
+        pos += len;
+    } while (ok && pos + 2 <= size);
+
+    return ok;
+}
+
+
 bool PRS1Import::ImportSummary()
 {
     if (!summary) {
@@ -4632,7 +4887,7 @@ bool PRS1Import::ParseEvents()
         break;
     case 5:
         if (event->fileVersion==3) {
-            res = ParseF5EventsFV3();
+            res = ParseEventsF5V3();
         } else {
             res = ParseF5Events();
         }
@@ -4875,6 +5130,12 @@ bool PRS1Import::ParseWaveforms()
         }
 
         if (num > 1) {
+            float pressure_gain = 0.1F;  // standard pressure gain
+            if (waveform->family == 5 && waveform->familyVersion == 3) {
+                // F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
+                pressure_gain = 0.125F;  // TODO: this should be parameterized somewhere better, once we have a clear idea of which machines use this
+            }
+            
             // Process interleaved samples
             QVector<QByteArray> data;
             data.resize(num);
@@ -4897,7 +5158,7 @@ bool PRS1Import::ParseWaveforms()
             }
 
             if (s2 > 0) {
-                EventList * pres = session->AddEventList(CPAP_MaskPressureHi, EVL_Waveform, 0.1f, 0.0f, 0.0f, 0.0f, double(dur) / double(s2));
+                EventList * pres = session->AddEventList(CPAP_MaskPressureHi, EVL_Waveform, pressure_gain, 0.0f, 0.0f, 0.0f, double(dur) / double(s2));
                 pres->AddWaveform(ti, (unsigned char *)data[1].data(), data[1].size(), dur);
             }
 
@@ -5000,8 +5261,11 @@ bool PRS1Import::ParseSession(void)
                     qWarning() << sessionid << "compliance didn't set session end?";
                 }
 
-                // Events and waveforms use updateLast() to set the session's last timestamp,
+                // Events and  use updateLast() to set the session's last timestamp,
                 // so they should only reach this point if there was a problem parsing them.
+
+                // TODO: It turns out waveforms *don't* update the timestamp, so this
+                // is depending entirely on events. See TODO below.
                 if (event != nullptr || !wavefile.isEmpty() || !oxifile.isEmpty()) {
                     qWarning() << sessionid << "Downgrading session to summary only";
                 }
@@ -5009,7 +5273,10 @@ bool PRS1Import::ParseSession(void)
 
                 // Only use the summary's duration if the session's duration couldn't be
                 // derived from events or waveforms.
-                // TODO: Revisit this once summary parsing is reliable.
+                
+                // TODO: Change this once summary parsing is reliable: event duration is less
+                // accurate than either waveforms or correctly-parsed summaries, since there
+                // won't necessarily be events at the very end of a session.
                 session->really_set_last(session->first()+(qint64(summary_duration) * 1000L));
             }
             save = true;
@@ -5716,7 +5983,7 @@ void PRS1Loader::initChannels()
         QObject::tr("Timed Breath"),
         QObject::tr("Machine Initiated Breath"),
         QObject::tr("TB"),
-        STR_UNIT_Unknown,
+        STR_UNIT_Seconds,
         DEFAULT,    QColor("black")));
 }
 
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h
index 0cfd1a4c..daeeb0dd 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.h
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.h
@@ -164,8 +164,8 @@ public:
     //! \brief Parse an humidifier setting byte from a .000 or .001 containing compliance/summary data for fileversion 2 machines: F0V234, F5V012, and maybe others
     void ParseHumidifierSettingV2(int humid, bool supportsHeatedTubing=true);
 
-    //! \brief Parse an humidifier setting byte from a .000 or .001 containing compliance/summary data for family 0 CPAP/APAP family version 6 machines
-    void ParseHumidifierSettingF0V6(unsigned char byte1, unsigned char byte2, bool add_setting=false);
+    //! \brief Parse humidifier setting bytes from a .000 or .001 containing compliance/summary data for fileversion 3 machines
+    void ParseHumidifierSettingV3(unsigned char byte1, unsigned char byte2, bool add_setting=false);
 
     //! \brief Figures out which Event Parser to call, based on machine family/version and calls it.
     bool ParseEvents(CPAPMode mode);
@@ -201,8 +201,11 @@ protected:
     //! \brief Extract the stored CRC from the end of the data of a PRS1 chunk
     bool ExtractStoredCrc(int size);
 
-    //! \brief Parse a settings slice from a .000 (and maybe .001) file
+    //! \brief Parse a settings slice from a .000 and .001 file
     bool ParseSettingsF0V6(const unsigned char* data, int size);
+
+    //! \brief Parse a settings slice from a .000 and .001 file
+    bool ParseSettingsF5V3(const unsigned char* data, int size);
 };
 
 
@@ -273,7 +276,7 @@ public:
     //! \brief Parse a single data chunk from a .002 file containing event data for a family 5 ASV machine (which has a different format)
     bool ParseF5Events();
     //! \brief Parse a single data chunk from a .002 file containing event data for a family 5 ASV file version 3 machine (which has a different format again)
-    bool ParseF5EventsFV3();
+    bool ParseEventsF5V3();
 
 
 protected:
@@ -400,6 +403,8 @@ class PRS1ModelInfo
 {
 protected:
     QHash<int, QHash<int, QStringList>> m_testedModels;
+    QHash<QString,const char*> m_modelNames;
+    QSet<QString> m_bricks;
     
 public:
     PRS1ModelInfo();
@@ -407,6 +412,8 @@ public:
     bool IsSupported(int family, int familyVersion) const;
     bool IsTested(const QHash<QString,QString> & properties) const;
     bool IsTested(const QString & modelNumber, int family, int familyVersion) const;
+    bool IsBrick(const QString & model) const;
+    const char* Name(const QString & model) const;
 };
 
 
diff --git a/oscar/daily.cpp b/oscar/daily.cpp
index dec0dbea..8d6f7b4b 100644
--- a/oscar/daily.cpp
+++ b/oscar/daily.cpp
@@ -1457,6 +1457,9 @@ void Daily::Load(QDate date)
                     val = day->count(code) / hours;
                     data = QString("%1").arg(val,0,'f',2);
                 }
+                // TODO: percentage would be another useful option here for things like
+                // percentage of patient-triggered breaths, which is much more useful
+                // than the duration of timed breaths per hour.
                 values[code] = val;
                 QColor altcolor = (brightness(chan.defaultColor()) < 0.3) ? Qt::white : Qt::black; // pick a contrasting color
                 html+=QString("<tr><td align='left' bgcolor='%1'><b><font color='%2'><a href='event=%5' style='text-decoration:none;color:%2'>%3</a></font></b></td><td width=20% bgcolor='%1'><b><font color='%2'>%4</font></b></td></tr>\n")