/* PRS1 Parsing for CPAP and BIPAP (Family 0)
 *
 * Copyright (c) 2019-2021 The OSCAR Team
 * Portions copyright (c) 2011-2018 Mark Watkins <mark@jedimark.net>
 *
 * This file is subject to the terms and conditions of the GNU General Public
 * License. See the file COPYING in the main directory of the source code
 * for more details. */

#include "prs1_parser.h"
#include "prs1_loader.h"

bool PRS1DataChunk::ParseComplianceF0V23(void)
{
    if (this->family != 0 || (this->familyVersion != 2 && this->familyVersion != 3)) {
        qWarning() << "ParseComplianceF0V23 called with family" << this->family << "familyVersion" << this->familyVersion;
        return false;
    }
    // All sample machines with FamilyVersion 3 in the properties.txt file have familyVersion 2 in their .001/.002/.005 files!
    // We should flag an actual familyVersion 3 file if we ever encounter one!
    CHECK_VALUE(this->familyVersion, 2);
    
    const unsigned char * data = (unsigned char *)this->m_data.constData();
    int chunk_size = this->m_data.size();
    static const int minimum_sizes[] = { 0xd, 5, 2, 2 };
    static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
    // NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.

    bool ok = true;
    int pos = 0;
    int code, size, delta;
    int tt = 0;
    while (ok && pos < chunk_size) {
        code = data[pos++];
        // There is no hblock prior to F0V6.
        size = 0;
        if (code < ncodes) {
            // make sure the handlers below don't go past the end of the buffer
            size = minimum_sizes[code];
        } // 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, 0);  // usually 1, occasionally 0, no visible difference in report
            // F0V23 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data.
                ok = ParseSettingsF0V23(data, 0x0e);
                // Compliance doesn't have pressure set events following settings like summary does.
                break;
            case 2:  // Mask On
                delta = data[pos] | (data[pos+1] << 8);
                if (tt == 0) {
                    CHECK_VALUE(delta, 0);  // we've never seen the initial MaskOn have any delta
                } else {
                    if (delta % 60) UNEXPECTED_VALUE(delta, "even minutes");  // mask-off events seem to be whole minutes?
                }
                tt += delta;
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
                // no per-slice humidifer settings as in F0V6
                break;
            case 3:  // Mask Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
                // Compliance doesn't record any stats after mask-off like summary does.
                break;
            case 1:  // Equipment Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));

                // also seems to be a trailing 01 00 81 after the slices?
                CHECK_VALUES(data[pos+2], 1, 0);  // usually 1, occasionally 0, no visible difference in report
                //CHECK_VALUE(data[pos+3], 0);  // sometimes 1, 2, or 5, no visible difference in report, maybe ramp?
                ParseHumidifierSetting50Series(data[pos+4]);
                break;
            default:
                UNEXPECTED_VALUE(code, "known slice code");
                ok = false;  // unlike F0V6, we don't know the size of unknown slices, so we can't recover
                break;
        }
        pos += size;
    }

    if (ok && pos != chunk_size) {
        qWarning() << this->sessionid << (this->size() - pos) << "trailing bytes";
    }

    this->duration = tt;

    return ok;
}


bool PRS1DataChunk::ParseSummaryF0V23()
{
    if (this->family != 0 || (this->familyVersion != 2 && this->familyVersion != 3)) {
        qWarning() << "ParseSummaryF0V23 called with family" << this->family << "familyVersion" << this->familyVersion;
        return false;
    }
    // All sample machines with FamilyVersion 3 in the properties.txt file have familyVersion 2 in their .001/.002/.005 files!
    // We should flag an actual familyVersion 3 file if we ever encounter one!
    CHECK_VALUE(this->familyVersion, 2);
    
    const unsigned char * data = (unsigned char *)this->m_data.constData();
    int chunk_size = this->m_data.size();
    static const int minimum_sizes[] = { 0xf, 5, 2, 0x21, 0, 4 };
    static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
    // NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.
    
    bool ok = true;
    int pos = 0;
    int code, size, delta;
    int tt = 0;
    while (ok && pos < chunk_size) {
        code = data[pos++];
        // There is no hblock prior to F0V6.
        size = 0;
        if (code < ncodes) {
            // make sure the handlers below don't go past the end of the buffer
            size = minimum_sizes[code];
        } // 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] & 0xF0, 0x60, 0x70);  // TODO: what are these?
                switch (data[pos] & 0x0F) {
                    case 0:  // TODO: What is this? It seems to be related to errors.
                    case 1:  // This is the most frequent value.
                    case 3:  // TODO: What is this?
                    case 4:  // This seems to be related to an automatic transition from CPAP to AutoCPAP.
                        break;
                    default:
                        UNEXPECTED_VALUE(data[pos] & 0x0F, "[0,1,3,4]");
                }
            // F0V23 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data.
                ok = ParseSettingsF0V23(data, 0x0e);
                // TODO: register these as pressure set events
                //CHECK_VALUES(data[0x0e], ramp_pressure, min_pressure);  // initial CPAP/EPAP, can be minimum pressure or ramp, or whatever auto decides to use
                //if (cpapmode == PRS1_MODE_BILEVEL) {  // initial IPAP for bilevel modes
                //    CHECK_VALUE(data[0x0f], max_pressure);
                //} else if (cpapmode == PRS1_MODE_AUTOBILEVEL) {
                //    CHECK_VALUE(data[0x0f], min_pressure + 20);
                //}
                break;
            case 2:  // Mask On
                delta = data[pos] | (data[pos+1] << 8);
                if (tt == 0) {
                    if (delta) {
                        CHECK_VALUES(delta, 1, 59);  // we've seen the 550P start its first mask-on at these time deltas
                    }
                } else {
                    if (delta % 60) {
                        if (this->familyVersion == 2 && ((delta + 1) % 60) == 0) {
                            // For some reason F0V2 frequently is frequently 1 second less than whole minute intervals.
                        } else {
                            UNEXPECTED_VALUE(delta, "even minutes");  // mask-off events seem to be whole minutes?
                        }
                    }
                }
                tt += delta;
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
                // no per-slice humidifer settings as in F0V6
                break;
            case 3:  // Mask Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
            // F0V23 doesn't have a separate stats record like F0V6 does, the stats just follow the MaskOff data.
                // These are 0x22 bytes in a summary vs. 3 bytes in compliance data
                // TODO: What are these values?
                break;
            case 1:  // Equipment Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));

                switch (data[pos+2]) {
                    case 0:  // TODO: What is this? It seems to be related to errors.
                    case 1:  // This is the usual value.
                    case 3:  // TODO: What is this? This has been seen after 90 sec large leak before turning off.
                    case 4:  // TODO: What is this? We've seen it once.
                    case 5:  // This seems to be related to an automatic transition from CPAP to AutoCPAP.
                        break;
                    default:
                        UNEXPECTED_VALUE(data[pos+2], "[0,1,3,4,5]");
                }
                //CHECK_VALUES(data[pos+3], 0, 1);  // TODO: may be related to ramp? 1-5 seems to have a ramp start or two
                ParseHumidifierSetting50Series(data[pos+4]);
                break;
            case 5:  // Clock adjustment? See ParseSummaryF0V4.
                CHECK_VALUE(pos, 1);  // Always first
                CHECK_VALUE(chunk_size, 5);  // and the only record in the session.
                if (false) {
                    long value = data[pos] | data[pos+1]<<8 | data[pos+2]<<16 | data[pos+3]<<24;
                    qDebug() << this->sessionid << "clock changing from" << ts(value * 1000L)
                                                << "to" << ts(this->timestamp * 1000L)
                                                << "delta:" << (this->timestamp - value);
                }
                break;
            case 6:  // Cleared?
                // Appears in the very first session when that session number is > 1.
                // Presumably previous sessions were cleared out.
                // TODO: add an internal event for this.
                CHECK_VALUE(pos, 1);  // Always first
                CHECK_VALUE(chunk_size, 1);  // and the only record in the session.
                if (this->sessionid == 1) UNEXPECTED_VALUE(this->sessionid, ">1");
                break;
            default:
                UNEXPECTED_VALUE(code, "known slice code");
                ok = false;  // unlike F0V6, we don't know the size of unknown slices, so we can't recover
                break;
        }
        pos += size;
    }

    if (ok && pos != chunk_size) {
        qWarning() << this->sessionid << (this->size() - pos) << "trailing bytes";
    }

    this->duration = tt;

    return ok;
}


bool PRS1DataChunk::ParseSettingsF0V23(const unsigned char* data, int /*size*/)
{
    PRS1Mode cpapmode = PRS1_MODE_UNKNOWN;

    switch (data[0x02]) {  // PRS1 mode   // 0 = CPAP, 2 = APAP
    case 0x00:
        cpapmode = PRS1_MODE_CPAP;
        break;
    case 0x01:
        cpapmode = PRS1_MODE_BILEVEL;
        break;
    case 0x02:
        cpapmode = PRS1_MODE_AUTOCPAP;
        break;
    case 0x03:
        cpapmode = PRS1_MODE_AUTOBILEVEL;
        break;
    default:
        UNEXPECTED_VALUE(data[0x02], "known device mode");
        break;
    }

    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));

    int min_pressure = data[0x03];
    int max_pressure = data[0x04];
    int ps  = data[0x05];  // max pressure support (for variable), seems to be zero otherwise

    if (cpapmode == PRS1_MODE_CPAP) {
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, min_pressure));
        //CHECK_VALUE(max_pressure, 0);  // occasionally nonzero, usually seems to be when the next session is AutoCPAP with this max
        CHECK_VALUE(ps, 0);
    } else if (cpapmode == PRS1_MODE_AUTOCPAP) {
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, min_pressure));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, max_pressure));
        CHECK_VALUE(ps, 0);
    } else if (cpapmode == PRS1_MODE_BILEVEL) {
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, min_pressure));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP, max_pressure));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS, max_pressure - min_pressure));
        CHECK_VALUE(ps, 0);  // this seems to be unused on fixed bilevel
    } else if (cpapmode == PRS1_MODE_AUTOBILEVEL) {
        int min_ps = 20;  // 2.0 cmH2O
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, min_pressure));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, max_pressure - min_ps));  // TODO: not yet confirmed
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_pressure + min_ps));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, max_pressure));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ps));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, ps));
    }

    int ramp_time = data[0x06];
    int ramp_pressure = data[0x07];
    if (ramp_time > 0) {
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, ramp_time));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, ramp_pressure));
    }

    quint8 flex = data[0x08];
    this->ParseFlexSettingF0V2345(flex, cpapmode);

    int humid = data[0x09];
    this->ParseHumidifierSetting50Series(humid, true);
    
    // Tubing lock has no setting byte

    // Menu Options
    bool mask_resist_on = ((data[0x0a] & 0x40) != 0);  // System One Resistance Status bit
    int mask_resist_setting = data[0x0a] & 7;  // System One Resistance setting value
    CHECK_VALUE(mask_resist_on, mask_resist_setting > 0);  // Confirm that we can ignore the status bit.
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_LOCK, (data[0x0a] & 0x80) != 0)); // System One Resistance Lock Setting, only seen on bricks
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HOSE_DIAMETER, (data[0x0a] & 0x08) ? 15 : 22));  // TODO: unconfirmed
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_SETTING, mask_resist_setting));
    CHECK_VALUE(data[0x0a] & (0x20 | 0x10), 0);

    CHECK_VALUE(data[0x0b], 1);
    
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_ON, (data[0x0c] & 0x40) != 0));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_OFF, (data[0x0c] & 0x10) != 0));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_ALERT, (data[0x0c] & 0x04) != 0));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SHOW_AHI, (data[0x0c] & 0x02) != 0));
    CHECK_VALUE(data[0x0c] & (0xA0 | 0x09), 0);

    CHECK_VALUE(data[0x0d], 0);

    return true;
}


// Flex F0V2 confirmed
// 0x00 = None
// 0x81 = C-Flex 1, lock off (AutoCPAP mode)
// 0x82 = Bi-Flex 2 (Bi-Level mode)
// 0x89 = A-Flex 1 (AutoCPAP mode)
// 0x8A = A-Flex 2, lock off (AutoCPAP mode)
// 0x8B = C-Flex+ 3, lock off (CPAP mode)
// 0x93 = Rise Time 3 (AutoBiLevel mode)

// Flex F0V4 confirmed
// 0x00 = None
// 0x81 = Bi-Flex 1 (AutoBiLevel mode)
// 0x81 = C-Flex 1 (AutoCPAP mode)
// 0x82 = C-Flex 2 (CPAP mode)
// 0x82 = C-Flex 2 (CPAP-Check mode)
// 0x82 = C-Flex 2 (Auto-Trial mode)
// 0x83 = Bi-Flex 3 (Bi-Level mode)
// 0x89 = A-Flex 1 (AutoCPAP mode)
// 0x8A = C-Flex+ 2 (CPAP mode)
// 0x8A = C-Flex+ 2, lock off (CPAP-Check mode)
// 0x8A = A-Flex 2, lock off (Auto-Trial mode)
// 0xCB = C-Flex+ 3 (CPAP-Check mode), C-Flex+ Lock on
//
// 0x8A = A-Flex 1 (AutoCPAP mode)
// 0x8B = C-Flex+ 3 (CPAP mode)
// 0x8B = A-Flex 3 (AutoCPAP mode)

// Flex F0V5 confirmed
// 0xE1 = Flex (AutoCPAP mode)
// 0xA1 = Flex (AutoCPAP mode)
// 0xA2 = Flex (AutoCPAP mode)

//   8  = enabled
//   4  = lock
//   2  = Flex (only seen on Dorma series)
//   1  = rise time
//    8 = C-Flex+ / A-Flex (depending on mode)
//    3 = level

void PRS1DataChunk::ParseFlexSettingF0V2345(quint8 flex, int cpapmode)
{
    FlexMode flexmode = FLEX_None;
    bool enabled  = (flex & 0x80) != 0;
    bool lock     = (flex & 0x40) != 0;
    bool plain_flex = (flex & 0x20) != 0;  // "Flex", seen on Dorma series
    bool risetime = (flex & 0x10) != 0;
    bool plusmode = (flex & 0x08) != 0;
    int flexlevel = flex & 0x03;
    if (flex & 0x04) UNEXPECTED_VALUE(flex, "known bits");
    if (this->familyVersion == 2) {
        //CHECK_VALUE(lock, false);  // We've seen this set on F0V2, but it doesn't appear on the reports.
    }

    if (enabled) {
        if (flexlevel < 1) UNEXPECTED_VALUE(flexlevel, "!= 0");
        if (risetime) {
            flexmode = FLEX_RiseTime;
            CHECK_VALUES(cpapmode, PRS1_MODE_BILEVEL, PRS1_MODE_AUTOBILEVEL);
            CHECK_VALUE(plusmode, 0);
        } else if (plusmode) {
            switch (cpapmode) {
                case PRS1_MODE_CPAP:
                case PRS1_MODE_CPAPCHECK:
                    flexmode = FLEX_CFlexPlus;
                    break;
                case PRS1_MODE_AUTOCPAP:
                case PRS1_MODE_AUTOTRIAL:
                    flexmode = FLEX_AFlex;
                    break;
                default:
                    HEX(flex);
                    UNEXPECTED_VALUE(cpapmode, "expected C-Flex+/A-Flex mode");
                    break;
            }
        } else if (plain_flex) {
            CHECK_VALUE(this->familyVersion, 5);  // so far only seen with F0V5
            switch (cpapmode) {
                case PRS1_MODE_AUTOCPAP:
                    flexmode = FLEX_Flex;  // unknown whether this is equivalent to C-Flex, C-Flex+, or A-Flex
                    break;
                default:
                    UNEXPECTED_VALUE(cpapmode, "expected mode");
                    flexmode = FLEX_Flex;  // probably the same for CPAP mode as well, but we haven't tested that yet
                    break;
            }
        } else {
            switch (cpapmode) {
                case PRS1_MODE_CPAP:
                case PRS1_MODE_CPAPCHECK:
                case PRS1_MODE_AUTOCPAP:
                case PRS1_MODE_AUTOTRIAL:
                    flexmode = FLEX_CFlex;
                    break;
                case PRS1_MODE_BILEVEL:
                case PRS1_MODE_AUTOBILEVEL:
                    flexmode = FLEX_BiFlex;
                    break;
                default:
                    HEX(flex);
                    UNEXPECTED_VALUE(cpapmode, "expected mode");
                    break;
            }
        }
    }

    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, (int) flexmode));
    if (flexmode != FLEX_None) {
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, flexlevel));
    }
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, lock));
}


const QVector<PRS1ParsedEventType> ParsedEventsF0V23 = {
    PRS1PressureSetEvent::TYPE,
    PRS1IPAPSetEvent::TYPE,
    PRS1EPAPSetEvent::TYPE,
    PRS1PressurePulseEvent::TYPE,
    PRS1RERAEvent::TYPE,
    PRS1ObstructiveApneaEvent::TYPE,
    PRS1ClearAirwayEvent::TYPE,
    PRS1HypopneaEvent::TYPE,
    PRS1FlowLimitationEvent::TYPE,
    PRS1VibratorySnoreEvent::TYPE,
    PRS1VariableBreathingEvent::TYPE,
    PRS1PeriodicBreathingEvent::TYPE,
    PRS1LargeLeakEvent::TYPE,
    PRS1TotalLeakEvent::TYPE,
    PRS1SnoreEvent::TYPE,
    PRS1SnoresAtPressureEvent::TYPE,
};

// 750P is F0V2; 550P is F0V2/F0V3 (properties.txt sometimes says F0V3, data files always say F0V2); 450P is F0V3
bool PRS1DataChunk::ParseEventsF0V23()
{
    if (this->family != 0 || this->familyVersion < 2 || this->familyVersion > 3) {
        qWarning() << "ParseEventsF0V23 called with family" << this->family << "familyVersion" << this->familyVersion;
        return false;
    }
    // All sample machines with FamilyVersion 3 in the properties.txt file have familyVersion 2 in their .001/.002/.005 files!
    // We should flag an actual familyVersion 3 file if we ever encounter one!
    CHECK_VALUE(this->familyVersion, 2);
    
    const unsigned char * data = (unsigned char *)this->m_data.constData();
    int chunk_size = this->m_data.size();
    static const QMap<int,int> event_sizes = { {1,2}, {3,4}, {0xb,4}, {0xd,2}, {0xe,5}, {0xf,5}, {0x10,5}, {0x11,4}, {0x12,4} };
   
    if (chunk_size < 1) {
        // This does occasionally happen in F0V6.
        qDebug() << this->sessionid << "Empty event data";
        return false;
    }

    bool ok = true;
    int pos = 0, startpos;
    int code, size;
    int t = 0;
    int elapsed, duration, value;
    do {
        code = data[pos++];

        size = 3;  // default size = 2 bytes time delta + 1 byte data
        if (event_sizes.contains(code)) {
            size = event_sizes[code];
        }
        if (pos + size > chunk_size) {
            qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk";
            ok = false;
            break;
        }
        startpos = pos;
        if (code != 0x12 && code != 0x01) {  // This one event has no timestamp in F0V6
            elapsed = data[pos] | (data[pos+1] << 8);
            if (elapsed > 0x7FFF) UNEXPECTED_VALUE(elapsed, "<32768s");  // check whether this is generally unsigned, since 0x01 isn't
            t += elapsed;
            pos += 2;
        }

        switch (code) {
            case 0x00:  // Humidifier setting change (logged in summary in 60 series)
                ParseHumidifierSetting50Series(data[pos]);
                if (this->familyVersion == 3) DUMP_EVENT();
                break;
            case 0x01:  // Time elapsed?
                // Only seen twice, on a 550P and 650P.
                // It looks almost like a time-elapsed event 4 found in F0V4 summaries, but
                // 0xFFCC looks like it represents a time adjustment of -52 seconds,
                // since the subsequent 0x11 statistics event has a time offset of 172 seconds,
                // and counting this as -52 seconds results in a total session time that
                // matches the summary and waveform data. Very weird.
                //
                // Similarly 0xFFDC looks like it represents a time adjustment of -36 seconds.
                CHECK_VALUES(data[pos], 0xDC, 0xCC);
                CHECK_VALUE(data[pos+1], 0xFF);
                elapsed = data[pos] | (data[pos+1] << 8);
                if (elapsed & 0x8000) {
                    elapsed = (~0xFFFF | elapsed);  // sign extend 16-bit number to native int
                }
                t += elapsed;
                break;
            case 0x02:  // Pressure adjustment
                // See notes in ParseEventsF0V6.
                this->AddEvent(new PRS1PressureSetEvent(t, data[pos]));
                break;
            case 0x03:  // Pressure adjustment (bi-level)
                // See notes in ParseEventsF0V6.
                this->AddEvent(new PRS1IPAPSetEvent(t, data[pos+1]));
                this->AddEvent(new PRS1EPAPSetEvent(t, data[pos]));  // EPAP needs to be added second to calculate PS
                break;
            case 0x04:  // Pressure Pulse
                duration = data[pos];  // TODO: is this a duration?
                this->AddEvent(new PRS1PressurePulseEvent(t, duration));
                break;
            case 0x05:  // RERA
                elapsed = data[pos++];
                this->AddEvent(new PRS1RERAEvent(t - elapsed, 0));
                break;
            case 0x06:  // 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 0x07:  // 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 0x08:  // never seen
            //case 0x09:  // never seen
            case 0x0a:  // Hypopnea
                // TODO: How is this hypopnea different from events 0xb, [0x14 and 0x15 on F0V6]?
                elapsed = data[pos++];
                this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
                break;
            case 0x0b:  // Hypopnea
                // TODO: How is this hypopnea different from events 0xa, [0x14 and 0x15 on F0V6]?
                // TODO: What is the first byte?
                //data[pos+0];  // unknown first byte?
                elapsed = data[pos+1];  // based on sample waveform, the hypopnea is over after this
                this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
                break;
            case 0x0c:  // 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 0x0d:  // 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 statistics below seem 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 0x0e:  // Variable Breathing?
                // TODO: does duration double like F0V4?
                duration = (data[pos] | (data[pos+1] << 8));
                elapsed = data[pos+2];  // this is always 60 seconds unless it's at the end, so it seems like elapsed
                CHECK_VALUES(elapsed, 60, 0);
                this->AddEvent(new PRS1VariableBreathingEvent(t - elapsed - duration, duration));
                break;
            case 0x0f:  // Periodic Breathing
                // PB events are reported some time after they conclude, and they do have a reported duration.
                // NOTE: F0V2 does NOT double this like F0V6 does
                if (this->familyVersion == 3)  // double-check whether there's doubling on F0V3
                    DUMP_EVENT();
                duration = (data[pos] | (data[pos+1] << 8));
                elapsed = data[pos+2];
                this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
                break;
            case 0x10:  // Large Leak
                // LL events are reported some time after they conclude, and they do have a reported duration.
                // NOTE: F0V2 does NOT double this like F0V4 and F0V6 does
                if (this->familyVersion == 3)  // double-check whether there's doubling on F0V3
                    DUMP_EVENT();
                duration = (data[pos] | (data[pos+1] << 8));
                elapsed = data[pos+2];
                this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
                break;
            case 0x11:  // Statistics
                this->AddEvent(new PRS1TotalLeakEvent(t, data[pos]));
                this->AddEvent(new PRS1SnoreEvent(t, data[pos+1]));
                this->AddEvent(new PRS1IntervalBoundaryEvent(t));
                break;
            case 0x12:  // Snore count per pressure
                // Some sessions (with lots of ramps) have multiple of these, each with a
                // different pressure. The total snore count across all of them matches the
                // total found in the stats event.
                if (data[pos] != 0) {
                    CHECK_VALUES(data[pos], 1, 2);  // 0 = CPAP pressure, 1 = bi-level EPAP, 2 = bi-level IPAP
                }
                //CHECK_VALUE(data[pos+1], 0x78);  // pressure
                //CHECK_VALUE(data[pos+2], 1);  // 16-bit snore count
                //CHECK_VALUE(data[pos+3], 0);
                value = (data[pos+2] | (data[pos+3] << 8));
                this->AddEvent(new PRS1SnoresAtPressureEvent(t, data[pos], data[pos+1], value));
                break;
            default:
                DUMP_EVENT();
                UNEXPECTED_VALUE(code, "known event code");
                this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
                ok = false;  // unlike F0V6, we don't know the size of unknown events, so we can't recover
                break;
        }
        pos = startpos + size;
    } while (ok && pos < chunk_size);

    if (ok && pos != chunk_size) {
        qWarning() << this->sessionid << (this->size() - pos) << "trailing event bytes";
    }

    this->duration = t;

    return ok;
}


bool PRS1DataChunk::ParseComplianceF0V4(void)
{
    if (this->family != 0 || (this->familyVersion != 4)) {
        qWarning() << "ParseComplianceF0V4 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[] = { 0x18, 7, 4, 2, 0, 0, 0, 4, 0 };
    static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
    // NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.
    
    bool ok = true;
    int pos = 0;
    int code, size;
    int tt = 0;
    while (ok && pos < chunk_size) {
        code = data[pos++];
        // There is no hblock prior to F0V6.
        size = 0;
        if (code < ncodes) {
            // make sure the handlers below don't go past the end of the buffer
            size = minimum_sizes[code];
        } // 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, 3);
            // F0V4 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data.
                ok = ParseSettingsF0V45(data, 0x11);
                CHECK_VALUE(data[pos+0x11], 0);
                CHECK_VALUE(data[pos+0x12], 0);
                CHECK_VALUE(data[pos+0x13], 0);
                CHECK_VALUE(data[pos+0x14], 0);
                CHECK_VALUE(data[pos+0x15], 0);
                CHECK_VALUE(data[pos+0x16], 0);
                CHECK_VALUE(data[pos+0x17], 0);
                break;
            case 2:  // Mask On
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
                this->ParseHumidifierSetting60Series(data[pos+2], data[pos+3]);
                break;
            case 3:  // Mask Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
                // Compliance doesn't have any MaskOff stats like summary does
                break;
            case 1:  // Equipment Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
                // TODO: check values
                CHECK_VALUES(data[pos+2], 1, 3);
                //CHECK_VALUE(data[pos+2] & ~(0x40|8|4|2|1), 0);  // ???, seen various bit combinations
                //CHECK_VALUE(data[pos+3], 0x19);  // 0x17, 0x16
                CHECK_VALUES(data[pos+4], 0, 1);
                //CHECK_VALUES(data[pos+4], 0, 1);  // or 2
                //CHECK_VALUE(data[pos+5], 0x35);  // 0x36, 0x36
                if (data[pos+6] != 1) {
                    CHECK_VALUE(data[pos+6] & ~(4|2|1), 0);  // On F0V23 0 seems to be related to errors, 3 seen after 90 sec large leak before turning off?
                }
                // pos+4 == 2, pos+6 == 10 on the session that had a time-elapsed event, maybe it shut itself off
                // when approaching 24h of continuous use?
                break;
            /*
            case 4:  // Time Elapsed
                // For example: mask-on 5:18:49 in a session of 23:41:20 total leaves mask-off time of 18:22:31.
                // That's represented by a mask-off event 19129 seconds after the mask-on, then a time-elapsed
                // event after 65535 seconds, then an equipment off event after another 616 seconds.
                tt += data[pos] | (data[pos+1] << 8);
                // TODO: see if this event exists in earlier versions
                break;
            case 5:  // Clock adjustment?
                CHECK_VALUE(pos, 1);  // Always first
                CHECK_VALUE(chunk_size, 5);  // and the only record in the session.
                // This looks like it's minor adjustments to the clock, but 560PBT-3917 sessions 1-2 are weird:
                // session 1 starts at 2015-12-23T00:01:20 and contains this event with timestamp 2015-12-23T00:05:14.
                // session 2 starts at 2015-12-23T00:01:29, which suggests the event didn't change the clock.
                //
                // It looks like this happens when there are discontinuities in timestamps, for example 560P-4727:
                // session 58 ends at 2015-05-26T09:53:17.
                // session 59 starts at 2015-05-26T09:53:15 with an event 5 timestamp of 2015-05-26T09:53:18.
                //
                // So the session/chunk timestamp has gone backwards. Whenever this happens, it seems to be in
                // a session with an event-5 event having a timestamp that hasn't gone backwards. So maybe
                // this timestamp is the old clock before adjustment? This would explain the 560PBT-3917 sessions above.
                //
                // This doesn't seem particularly associated with discontinuities in the waveform data: there are
                // often clock adjustments without corresponding discontinuities in the waveform, and vice versa.
                // It's possible internal clock inaccuracy causes both independently.
                //
                // TODO: why do some machines have lots of these and others none? Maybe cellular modems make daily tweaks?
                if (false) {
                    long value = data[pos] | data[pos+1]<<8 | data[pos+2]<<16 | data[pos+3]<<24;
                    qDebug() << this->sessionid << "clock changing from" << ts(value * 1000L)
                                                << "to" << ts(this->timestamp * 1000L)
                                                << "delta:" << (this->timestamp - value);
                }
                break;
            */
            case 6:  // Cleared?
                // Appears in the very first session when that session number is > 1.
                // Presumably previous sessions were cleared out.
                // TODO: add an internal event for this.
                CHECK_VALUE(pos, 1);  // Always first
                CHECK_VALUE(chunk_size, 1);  // and the only record in the session.
                if (this->sessionid == 1) UNEXPECTED_VALUE(this->sessionid, ">1");
                break;
            case 7:  // Humidifier setting change (logged in events in 50 series)
                tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
                this->ParseHumidifierSetting60Series(data[pos+2], data[pos+3]);
                break;
            /*
            case 8:  // CPAP-Check related, follows Mask On in CPAP-Check mode
                tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
                //CHECK_VALUES(data[pos+2], 0, 79);  // probably 16-bit value, sometimes matches OA + H + FL + VS + RE?
                CHECK_VALUE(data[pos+3], 0);
                //CHECK_VALUES(data[pos+4], 0, 10);  // probably 16-bit value
                CHECK_VALUE(data[pos+5], 0);
                //CHECK_VALUES(data[pos+6], 0, 79);  // probably 16-bit value, usually the same as +2, but not always?
                CHECK_VALUE(data[pos+7], 0);
                //CHECK_VALUES(data[pos+8], 0, 10);  // probably 16-bit value
                CHECK_VALUE(data[pos+9], 0);
                //CHECK_VALUES(data[pos+0xa], 0, 4);  // or 0? 44 when changed pressure mid-session?
                break;
            */
            default:
                UNEXPECTED_VALUE(code, "known slice code");
                ok = false;  // unlike F0V6, we don't know the size of unknown slices, so we can't recover
                break;
        }
        pos += size;
    }

    if (ok && pos != chunk_size) {
        qWarning() << this->sessionid << (this->size() - pos) << "trailing bytes";
    }

    this->duration = tt;

    return ok;
}


bool PRS1DataChunk::ParseSummaryF0V4(void)
{
    if (this->family != 0 || (this->familyVersion != 4)) {
        qWarning() << "ParseSummaryF0V4 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[] = { 0x18, 7, 7, 0x24, 2, 4, 0, 4, 0xb };
    static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
    // NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.
    
    bool ok = true;
    int pos = 0;
    int code, size;
    int tt = 0;
    while (ok && pos < chunk_size) {
        code = data[pos++];
        // There is no hblock prior to F0V6.
        size = 0;
        if (code < ncodes) {
            // make sure the handlers below don't go past the end of the buffer
            size = minimum_sizes[code];
        } // 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] & 0xF0, 0x80, 0xC0);  // TODO: what are these?
                if ((data[pos] & 0x0F) != 1) {  // This is the most frequent value.
                    //CHECK_VALUES(data[pos] & 0x0F, 3, 5);  // TODO: what are these? 0 seems to be related to errors.
                }
            // F0V4 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data.
                ok = ParseSettingsF0V45(data, 0x11);
                CHECK_VALUE(data[pos+0x11], 0);
                CHECK_VALUE(data[pos+0x12], 0);
                CHECK_VALUE(data[pos+0x13], 0);
                CHECK_VALUE(data[pos+0x14], 0);
                CHECK_VALUE(data[pos+0x15], 0);
                CHECK_VALUE(data[pos+0x16], 0);
                CHECK_VALUE(data[pos+0x17], 0);
                break;
            case 2:  // Mask On
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
                //CHECK_VALUES(data[pos+2], 120, 110);  // probably initial pressure
                //CHECK_VALUE(data[pos+3], 0);  // initial IPAP on bilevel?
                //CHECK_VALUES(data[pos+4], 0, 130);  // minimum pressure in auto-cpap
                this->ParseHumidifierSetting60Series(data[pos+5], data[pos+6]);
                break;
            case 3:  // Mask Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
            // F0V4 doesn't have a separate stats record like F0V6 does, the stats just follow the MaskOff data.
                //CHECK_VALUES(data[pos+2], 130);  // probably ending pressure
                //CHECK_VALUE(data[pos+3], 0);  // ending IPAP for bilevel? average?
                //CHECK_VALUES(data[pos+4], 0, 130);  // 130 pressure in auto-cpap: min pressure? 90% IPAP in bilevel?
                //CHECK_VALUES(data[pos+5], 0, 130);  // 130 pressure in auto-cpap, 90% EPAP in bilevel?
                //CHECK_VALUE(data[pos+6], 0);  // 145 maybe max pressure in Auto-CPAP?
                //CHECK_VALUE(data[pos+7], 0);  // Average 90% Pressure (Auto-CPAP)
                //CHECK_VALUE(data[pos+8], 0);  // Average CPAP (Auto-CPAP)
                //CHECK_VALUES(data[pos+9], 0, 4);  // or 1; PB count? LL count? minutes of something?
                CHECK_VALUE(data[pos+0xa], 0);
                //CHECK_VALUE(data[pos+0xb], 0);  // OA count, probably 16-bit
                CHECK_VALUE(data[pos+0xc], 0);
                //CHECK_VALUE(data[pos+0xd], 0);
                CHECK_VALUE(data[pos+0xe], 0);
                //CHECK_VALUE(data[pos+0xf], 0);  // CA count, probably 16-bit
                CHECK_VALUE(data[pos+0x10], 0);
                //CHECK_VALUE(data[pos+0x11], 40);  // 16-bit something: 0x88, 0x26, etc. ???
                //CHECK_VALUE(data[pos+0x12], 0);
                //CHECK_VALUE(data[pos+0x13], 0);  // 16-bit minutes in LL
                //CHECK_VALUE(data[pos+0x14], 0);
                //CHECK_VALUE(data[pos+0x15], 0);  // minutes in PB, probably 16-bit
                CHECK_VALUE(data[pos+0x16], 0);
                //CHECK_VALUE(data[pos+0x17], 0);  // 16-bit VS count
                //CHECK_VALUE(data[pos+0x18], 0);
                //CHECK_VALUE(data[pos+0x19], 0);  // H count, probably 16-bit
                CHECK_VALUE(data[pos+0x1a], 0);
                //CHECK_VALUE(data[pos+0x1b], 0);  // 0 when no PB or LL?
                CHECK_VALUE(data[pos+0x1c], 0);
                //CHECK_VALUE(data[pos+0x1d], 9);  // RE count, probably 16-bit
                CHECK_VALUE(data[pos+0x1e], 0);
                //CHECK_VALUE(data[pos+0x1f], 0);  // FL count, probably 16-bit
                CHECK_VALUE(data[pos+0x20], 0);
                //CHECK_VALUE(data[pos+0x21], 0x32);  // 0x55, 0x19  // ???
                //CHECK_VALUE(data[pos+0x22], 0x23);  // 0x3f, 0x14  // Average total leak
                //CHECK_VALUE(data[pos+0x23], 0x40);  // 0x7d, 0x3d  // ???
                break;
            case 1:  // Equipment Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
                CHECK_VALUE(data[pos+2] & ~(0x40|8|4|2|1), 0);  // ???, seen various bit combinations
                //CHECK_VALUE(data[pos+3], 0x19);  // 0x17, 0x16
                //CHECK_VALUES(data[pos+4], 0, 1);  // or 2
                //CHECK_VALUE(data[pos+5], 0x35);  // 0x36, 0x36
                if (data[pos+6] != 1) {  // This is the usual value.
                    CHECK_VALUE(data[pos+6] & ~(8|4|2|1), 0);  // On F0V23 0 seems to be related to errors, 3 seen after 90 sec large leak before turning off?
                }
                // pos+4 == 2, pos+6 == 10 on the session that had a time-elapsed event, maybe it shut itself off
                // when approaching 24h of continuous use?
                break;
            case 4:  // Time Elapsed
                // For example: mask-on 5:18:49 in a session of 23:41:20 total leaves mask-off time of 18:22:31.
                // That's represented by a mask-off event 19129 seconds after the mask-on, then a time-elapsed
                // event after 65535 seconds, then an equipment off event after another 616 seconds.
                tt += data[pos] | (data[pos+1] << 8);
                // TODO: see if this event exists in earlier versions
                break;
            case 5:  // Clock adjustment?
                CHECK_VALUE(pos, 1);  // Always first
                CHECK_VALUE(chunk_size, 5);  // and the only record in the session.
                // This looks like it's minor adjustments to the clock, but 560PBT-3917 sessions 1-2 are weird:
                // session 1 starts at 2015-12-23T00:01:20 and contains this event with timestamp 2015-12-23T00:05:14.
                // session 2 starts at 2015-12-23T00:01:29, which suggests the event didn't change the clock.
                //
                // It looks like this happens when there are discontinuities in timestamps, for example 560P-4727:
                // session 58 ends at 2015-05-26T09:53:17.
                // session 59 starts at 2015-05-26T09:53:15 with an event 5 timestamp of 2015-05-26T09:53:18.
                //
                // So the session/chunk timestamp has gone backwards. Whenever this happens, it seems to be in
                // a session with an event-5 event having a timestamp that hasn't gone backwards. So maybe
                // this timestamp is the old clock before adjustment? This would explain the 560PBT-3917 sessions above.
                //
                // This doesn't seem particularly associated with discontinuities in the waveform data: there are
                // often clock adjustments without corresponding discontinuities in the waveform, and vice versa.
                // It's possible internal clock inaccuracy causes both independently.
                //
                // TODO: why do some machines have lots of these and others none? Maybe cellular modems make daily tweaks?
                if (false) {
                    long value = data[pos] | data[pos+1]<<8 | data[pos+2]<<16 | data[pos+3]<<24;
                    qDebug() << this->sessionid << "clock changing from" << ts(value * 1000L)
                                                << "to" << ts(this->timestamp * 1000L)
                                                << "delta:" << (this->timestamp - value);
                }
                break;
            case 6:  // Cleared?
                // Appears in the very first session when that session number is > 1.
                // Presumably previous sessions were cleared out.
                // TODO: add an internal event for this.
                CHECK_VALUE(pos, 1);  // Always first
                CHECK_VALUE(chunk_size, 1);  // and the only record in the session.
                if (this->sessionid == 1) UNEXPECTED_VALUE(this->sessionid, ">1");
                break;
            case 7:  // Humidifier setting change (logged in events in 50 series)
                tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
                this->ParseHumidifierSetting60Series(data[pos+2], data[pos+3]);
                break;
            case 8:  // CPAP-Check related, follows Mask On in CPAP-Check mode
                tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
                //CHECK_VALUES(data[pos+2], 0, 79);  // probably 16-bit value, sometimes matches OA + H + FL + VS + RE?
                CHECK_VALUE(data[pos+3], 0);
                //CHECK_VALUES(data[pos+4], 0, 10);  // probably 16-bit value
                CHECK_VALUE(data[pos+5], 0);
                //CHECK_VALUES(data[pos+6], 0, 79);  // probably 16-bit value, usually the same as +2, but not always?
                CHECK_VALUE(data[pos+7], 0);
                //CHECK_VALUES(data[pos+8], 0, 10);  // probably 16-bit value
                CHECK_VALUE(data[pos+9], 0);
                //CHECK_VALUES(data[pos+0xa], 0, 4);  // or 0? 44 when changed pressure mid-session?
                break;
            default:
                UNEXPECTED_VALUE(code, "known slice code");
                ok = false;  // unlike F0V6, we don't know the size of unknown slices, so we can't recover
                break;
        }
        pos += size;
    }

    if (ok && pos != chunk_size) {
        qWarning() << this->sessionid << (this->size() - pos) << "trailing bytes";
    }

    this->duration = tt;

    return ok;
}


bool PRS1DataChunk::ParseSettingsF0V45(const unsigned char* data, int size)
{
    if (size < 0xd) {
        qWarning() << "invalid size passed to ParseSettingsF0V45";
        return false;
    }
    PRS1Mode cpapmode = PRS1_MODE_UNKNOWN;

    switch (data[0x02]) {  // PRS1 mode
    case 0x00:
        cpapmode = PRS1_MODE_CPAP;
        break;
    case 0x20:
        cpapmode = PRS1_MODE_BILEVEL;
        break;
    case 0x40:
        cpapmode = PRS1_MODE_AUTOCPAP;
        break;
    case 0x60:
        cpapmode = PRS1_MODE_AUTOBILEVEL;
        break;
    case 0x80:
        cpapmode = PRS1_MODE_AUTOTRIAL;  // Auto-Trial TODO: where is duration?
        break;
    case 0xA0:
        cpapmode = PRS1_MODE_CPAPCHECK;
        break;
    default:
        UNEXPECTED_VALUE(data[0x02], "known device mode");
        break;
    }

    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));

    int min_pressure = data[0x03];
    int max_pressure = data[0x04];
    int min_ps  = data[0x05]; // pressure support
    int max_ps  = data[0x06]; // pressure support

    if (cpapmode == PRS1_MODE_CPAP) {
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, min_pressure));
        CHECK_VALUE(max_pressure, 0);
        CHECK_VALUE(min_ps, 0);
        CHECK_VALUE(max_ps, 0);
    } else if (cpapmode == PRS1_MODE_AUTOCPAP || cpapmode == PRS1_MODE_AUTOTRIAL) {
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, min_pressure));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, max_pressure));
        CHECK_VALUE(min_ps, 0);
        CHECK_VALUE(max_ps, 0);
    } else if (cpapmode == PRS1_MODE_CPAPCHECK) {
        // Sometimes the CPAP pressure is stored in max_ps instead of min_ps, not sure why.
        if (min_ps == 0) {
            if (max_ps == 0) UNEXPECTED_VALUE(max_ps, "nonzero");
            min_ps = max_ps;
        } else {
            CHECK_VALUE(max_ps, 0);
        }
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, min_ps));
        // TODO: Once OSCAR can handle more modes, we can include these settings; right now including
        // these settings makes it think this is AutoCPAP.
        //this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, min_pressure));
        //this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, max_pressure));
    } else if (cpapmode == PRS1_MODE_BILEVEL) {
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, min_pressure));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP, max_pressure));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS, max_pressure - min_pressure));
        CHECK_VALUE(min_ps, 0);  // this seems to be unused on fixed bilevel
        CHECK_VALUE(max_ps, 0);  // this seems to be unused on fixed bilevel
    } else if (cpapmode == PRS1_MODE_AUTOBILEVEL) {
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, min_pressure));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, max_pressure - min_ps));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_pressure + min_ps));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, max_pressure));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ps));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, max_ps));
    }

    CHECK_VALUES(data[0x07], 0, 0x20);  // 0x20 seems to be Opti-Start

    int ramp_time = data[0x08];
    int ramp_pressure = data[0x09];
    if (ramp_time > 0) {
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, ramp_time));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, ramp_pressure));
    }

    quint8 flex = data[0x0a];
    if (this->familyVersion == 5) { if (flex != 0xE1) CHECK_VALUES(flex, 0xA1, 0xA2); }
    this->ParseFlexSettingF0V2345(flex, cpapmode);

    if (this->familyVersion == 5) {
        CHECK_VALUES(data[0x0c], 0x60, 0x70);
    }
    this->ParseHumidifierSetting60Series(data[0x0b], data[0x0c], true);

    if (size <= 0xd) {
        return true;
    }

    int resist_level = (data[0x0d] >> 3) & 7;  // 0x18 resist=3, 0x11 resist=2, 0x28 resist=5
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_LOCK, (data[0x0d] & 0x40) != 0));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_SETTING, resist_level));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HOSE_DIAMETER, (data[0x0d] & 0x01) ? 15 : 22));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TUBING_LOCK, (data[0x0d] & 0x02) != 0));
    CHECK_VALUE(data[0x0d] & (0x80|0x04), 0);

    CHECK_VALUE(data[0x0e], 1);

    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_ON, (data[0x0f] & 0x40) != 0));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_OFF, (data[0x0f] & 0x10) != 0));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_ALERT, (data[0x0f] & 0x04) != 0));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SHOW_AHI, (data[0x0f] & 0x02) != 0));
    CHECK_VALUE(data[0x0f] & (0xA0 | 0x08), 0);
    //CHECK_VALUE(data[0x0f] & 0x01, 0);  // TODO: What is bit 1? It's sometimes set.
    // TODO: Where is altitude compensation set? We've seen it on 261CA.

    CHECK_VALUE(data[0x10], 0);
    if (cpapmode == PRS1_MODE_AUTOTRIAL) {
        CHECK_VALUE(data[0x11], 7);  // 7-day duration?
    } else {
        CHECK_VALUE(data[0x11], 0);
    }

    return true;
}


const QVector<PRS1ParsedEventType> ParsedEventsF0V4 = {
    PRS1PressureSetEvent::TYPE,
    PRS1IPAPSetEvent::TYPE,
    PRS1EPAPSetEvent::TYPE,
    PRS1AutoPressureSetEvent::TYPE,
    PRS1PressurePulseEvent::TYPE,
    PRS1RERAEvent::TYPE,
    PRS1ObstructiveApneaEvent::TYPE,
    PRS1ClearAirwayEvent::TYPE,
    PRS1HypopneaEvent::TYPE,
    PRS1FlowLimitationEvent::TYPE,
    PRS1VibratorySnoreEvent::TYPE,
    PRS1VariableBreathingEvent::TYPE,
    PRS1PeriodicBreathingEvent::TYPE,
    PRS1LargeLeakEvent::TYPE,
    PRS1TotalLeakEvent::TYPE,
    PRS1SnoreEvent::TYPE,
    PRS1PressureAverageEvent::TYPE,
    PRS1FlexPressureAverageEvent::TYPE,
    PRS1SnoresAtPressureEvent::TYPE,
};

// 460P, 560P[BT], 660P, 760P are F0V4
bool PRS1DataChunk::ParseEventsF0V4()
{
    if (this->family != 0 || this->familyVersion != 4) {
        qWarning() << "ParseEventsF0V4 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 QMap<int,int> event_sizes = { {0,4}, {2,4}, {3,3}, {0xb,4}, {0xd,2}, {0xe,5}, {0xf,5}, {0x10,5}, {0x11,5}, {0x12,4} };
   
    if (chunk_size < 1) {
        // This does occasionally happen in F0V6.
        qDebug() << this->sessionid << "Empty event data";
        return false;
    }

    bool ok = true;
    int pos = 0, startpos;
    int code, size;
    int t = 0;
    int elapsed, duration, value;
    bool is_bilevel = false;
    do {
        code = data[pos++];

        size = 3;  // default size = 2 bytes time delta + 1 byte data
        if (event_sizes.contains(code)) {
            size = event_sizes[code];
        }
        if (pos + size > chunk_size) {
            qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk";
            ok = false;
            break;
        }
        startpos = pos;
        if (code != 0x12) {  // This one event has no timestamp in F0V6
            t += data[pos] | (data[pos+1] << 8);
            pos += 2;
        }

        switch (code) {
            //case 0x00:  // never seen
                // NOTE: the original code thought 0x00 had 2 data bytes, unlike the 1 in F0V23.
                // We don't have any sample data with this event, so it's left out here.
            case 0x01:  // Pressure adjustment: note this was 0x02 in F0V23 and is 0x01 in F0V6
                // See notes in ParseEventsF0V6.
                this->AddEvent(new PRS1PressureSetEvent(t, data[pos]));
                break;
            case 0x02:  // Pressure adjustment (bi-level): note that this was 0x03 in F0V23 and is 0x02 in F0V6
                // See notes above on interpolation.
                this->AddEvent(new PRS1IPAPSetEvent(t, data[pos+1]));
                this->AddEvent(new PRS1EPAPSetEvent(t, data[pos]));  // EPAP needs to be added second to calculate PS
                is_bilevel = true;
                break;
            case 0x03:  // Adjust Opti-Start pressure
                // On F0V4 this occasionally shows up in the middle of a session.
                // In that cases, the new pressure corresponds to the next night's Opti-Start
                // pressure. It does not appear to have any effect on the current night's pressure,
                // though presumaby it could if there's a long gap between sessions.
                // See F0V6 event 3 for comparison.
                // TODO: Does this occur in bi-level mode?
                this->AddEvent(new PRS1AutoPressureSetEvent(t, data[pos]));
                break;
            case 0x04:  // Pressure Pulse
                duration = data[pos];  // TODO: is this a duration?
                this->AddEvent(new PRS1PressurePulseEvent(t, duration));
                break;
            case 0x05:  // RERA
                elapsed = data[pos];
                this->AddEvent(new PRS1RERAEvent(t - elapsed, 0));
                break;
            case 0x06:  // 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 0x07:  // 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 0x08:  // never seen
            //case 0x09:  // never seen
            case 0x0a:  // Hypopnea
                // TODO: How is this hypopnea different from events 0xb, [0x14 and 0x15 on F0V6]?
                elapsed = data[pos++];
                this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
                break;
            case 0x0b:  // Hypopnea
                // TODO: How is this hypopnea different from events 0xa, 0x14 and 0x15?
                // TODO: What is the first byte?
                //data[pos+0];  // unknown first byte?
                elapsed = data[pos+1];  // based on sample waveform, the hypopnea is over after this
                this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
                break;
            case 0x0c:  // 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 0x0d:  // 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 statistics below seem 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 0x0e:  // Variable Breathing?
                // TODO: does duration double like it does for PB/LL?
                duration = 2 * (data[pos] | (data[pos+1] << 8));
                elapsed = data[pos+2];  // this is always 60 seconds unless it's at the end, so it seems like elapsed
                CHECK_VALUES(elapsed, 60, 0);
                this->AddEvent(new PRS1VariableBreathingEvent(t - elapsed - duration, duration));
                break;
            case 0x0f:  // Periodic Breathing
                // PB events are reported some time after they conclude, and they do have a reported duration.
                // NOTE: This (and F0V6) doubles the duration, unlike F0V23.
                duration = 2 * (data[pos] | (data[pos+1] << 8));
                elapsed = data[pos+2];
                this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
                break;
            case 0x10:  // Large Leak
                // LL events are reported some time after they conclude, and they do have a reported duration.
                // NOTE: This (and F0V6) doubles the duration, unlike F0V23.
                duration = 2 * (data[pos] | (data[pos+1] << 8));
                elapsed = data[pos+2];
                this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
                break;
            case 0x11:  // Statistics
                this->AddEvent(new PRS1TotalLeakEvent(t, data[pos]));
                this->AddEvent(new PRS1SnoreEvent(t, data[pos+1]));

                value = data[pos+2];
                if (is_bilevel) {
                    // For bi-level modes, this appears to be the time-weighted average of EPAP and IPAP actually provided.
                    this->AddEvent(new PRS1PressureAverageEvent(t, value));
                } else {
                    // For single-pressure modes, this appears to be the average effective "EPAP" provided by Flex.
                    //
                    // Sample data shows this value around 10.3 cmH2O for a prescribed pressure of 12.0 (C-Flex+ 3).
                    // That's too low for an average pressure over time, but could easily be an average commanded EPAP.
                    // When flex mode is off, this is exactly the current CPAP set point.
                    this->AddEvent(new PRS1FlexPressureAverageEvent(t, value));
                }
                this->AddEvent(new PRS1IntervalBoundaryEvent(t));
                break;
            case 0x12:  // Snore count per pressure
                // Some sessions (with lots of ramps) have multiple of these, each with a
                // different pressure. The total snore count across all of them matches the
                // total found in the stats event.
                if (data[pos] != 0) {
                    CHECK_VALUES(data[pos], 1, 2);  // 0 = CPAP pressure, 1 = bi-level EPAP, 2 = bi-level IPAP
                }
                //CHECK_VALUE(data[pos+1], 0x78);  // pressure
                //CHECK_VALUE(data[pos+2], 1);  // 16-bit snore count
                //CHECK_VALUE(data[pos+3], 0);
                value = (data[pos+2] | (data[pos+3] << 8));
                this->AddEvent(new PRS1SnoresAtPressureEvent(t, data[pos], data[pos+1], value));
                break;
            default:
                DUMP_EVENT();
                UNEXPECTED_VALUE(code, "known event code");
                this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
                ok = false;  // unlike F0V6, we don't know the size of unknown events, so we can't recover
                break;
        }
        pos = startpos + size;
    } while (ok && pos < chunk_size);

    if (ok && pos != chunk_size) {
        qWarning() << this->sessionid << (this->size() - pos) << "trailing event bytes";
    }

    this->duration = t;

    return ok;
}


// Based on ParseComplianceF0V4, but this has shorter settings and stats following equipment off.
bool PRS1DataChunk::ParseComplianceF0V5(void)
{
    if (this->family != 0 || (this->familyVersion != 5)) {
        qWarning() << "ParseComplianceF0V5 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[] = { 0xf, 7, 4, 0xf, 0, 4, 0, 4 };
    static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
    // NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.
    
    bool ok = true;
    int pos = 0;
    int code, size;
    int tt = 0;
    while (ok && pos < chunk_size) {
        code = data[pos++];
        // There is no hblock prior to F0V6.
        size = 0;
        if (code < ncodes) {
            // make sure the handlers below don't go past the end of the buffer
            size = minimum_sizes[code];
        } // 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], 0x73, 0x31);  // 0x71
            // F0V5 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data.
                ok = ParseSettingsF0V45(data, 0x0d);
                CHECK_VALUE(data[pos+0xd], 0);
                CHECK_VALUE(data[pos+0xe], 0);
                CHECK_VALUES(data[pos+0xf], 0, 2);
                break;
            case 2:  // Mask On
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
                CHECK_VALUES(data[pos+3], 0x60, 0x70);
                this->ParseHumidifierSetting60Series(data[pos+2], data[pos+3]);
                break;
            case 3:  // Mask Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
                // F0V5 compliance has MaskOff stats unlike all other compliance.
                // This is presumably because the 501V is an Auto-CPAP, so it needs to record titration data.
                //CHECK_VALUES(data[pos+2], 40, 50);   // min pressure
                //CHECK_VALUES(data[pos+3], 40, 150);  // max pressure
                //CHECK_VALUES(data[pos+4], 40, 150);  // Average Device Pressure <= 90% of Time (report is time-weighted per slice, for all sessions)
                //CHECK_VALUES(data[pos+5], 40, 108);  // Auto CPAP Mean Pressure (report is time-weighted per slice, for all sessions)
                                                       // Peak Average Pressure is the maximum "mean pressure" reported in any session.
                //CHECK_VALUES(data[pos+6], 0, 5);     // Apnea or Hypopnea count (probably 16-bit), contributes to AHI
                CHECK_VALUE(data[pos+7],  0);
                //CHECK_VALUES(data[pos+8], 0, 6);     // Apnea or Hypopnea count (probably 16-bit), contributes to AHI
                CHECK_VALUE(data[pos+9],  0);
                //CHECK_VALUES(data[pos+10], 0, 2);    // Average Large Leak minutes (probably 16-bit, report show sum of all slices)
                CHECK_VALUE(data[pos+11], 0);
                //CHECK_VALUES(data[pos+12], 179, 50);  // Average 90% Leak (report is time-weighted per slice)
                //CHECK_VALUES(data[pos+13], 178, 32);  // Average Total Leak (report is time-weighted per slice)
                //CHECK_VALUES(data[pos+14], 180, 36);  // Max leak (report shows max for all slices)
                break;
            case 1:  // Equipment Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
                CHECK_VALUE(data[pos+2] & ~(0x40|0x02|0x01), 0);
                //CHECK_VALUES(data[pos+3], 0x16, 0x13);  // 22, 19
                if (data[pos+4] > 3) UNEXPECTED_VALUE(data[pos+4], "0-3");
                //CHECK_VALUES(data[pos+5], 0x2F, 0x26);  // 47, 38
                if (data[pos+6] > 7) UNEXPECTED_VALUE(data[pos+6], "0-7");
                break;
            //case 4:  // Time Elapsed?  See ParseComplianceF0V4 if we encounter this.
            case 5:  // Clock adjustment?
                CHECK_VALUE(pos, 1);  // Always first
                CHECK_VALUE(chunk_size, 5);  // and the only record in the session.
                // This looks like it's minor adjustments to the clock, see ParseComplianceF0V4 for details.
                break;
            //case 6:  // Cleared?  See ParseComplianceF0V4 if we encounter this.
            case 7:  // Humidifier setting change (logged in events in 50 series)
                tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
                this->ParseHumidifierSetting60Series(data[pos+2], data[pos+3]);
                break;
            default:
                UNEXPECTED_VALUE(code, "known slice code");
                ok = false;  // unlike F0V6, we don't know the size of unknown slices, so we can't recover
                break;
        }
        pos += size;
    }

    if (ok && pos != chunk_size) {
        qWarning() << this->sessionid << (this->size() - pos) << "trailing bytes";
    }

    this->duration = tt;

    return ok;
}


// 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)
{
    if (this->family != 0 || this->familyVersion != 6) {
        qWarning() << "ParseComplianceF0V6 called with family" << this->family << "familyVersion" << this->familyVersion;
        return false;
    }
    // TODO: hardcoding this is ugly, think of a better approach
    if (this->m_data.size() < 82) {
        qWarning() << this->sessionid << "compliance data too short:" << this->m_data.size();
        return false;
    }
    const unsigned char * data = (unsigned char *)this->m_data.constData();
    int chunk_size = this->m_data.size();
    static const int expected_sizes[] = { 1, 0x34, 9, 4, 2, 2, 4, 8 };
    static const int ncodes = sizeof(expected_sizes) / sizeof(int);
    for (int i = 0; i < ncodes; i++) {
        if (this->hblock.contains(i)) {
            CHECK_VALUE(this->hblock[i], expected_sizes[i]);
        } else {
            UNEXPECTED_VALUE(this->hblock.contains(i), true);
        }
    }

    bool ok = true;
    int pos = 0;
    int code, size;
    int tt = 0;
    while (ok && pos < chunk_size) {
        code = data[pos++];
        if (!this->hblock.contains(code)) {
            qWarning() << this->sessionid << "missing hblock entry for" << code;
            ok = false;
            break;
        }
        size = this->hblock[code];
        if (size < expected_sizes[code]) {
            UNEXPECTED_VALUE(size, expected_sizes[code]);
            qWarning() << this->sessionid << "slice" << code << "too small" << size << "<" << expected_sizes[code];
            if (code != 1) {  // Settings are variable-length, so shorter settings slices aren't fatal.
                ok = false;
                break;
            }
        }
        if (pos + size > chunk_size) {
            qWarning() << this->sessionid << "slice" << code << "@" << pos << "longer than remaining chunk";
            ok = false;
            break;
        }

        switch (code) {
            case 0:
                // always first? Maybe equipmenton? Maybe 0 was always equipmenton, even in F0V23?
                CHECK_VALUE(pos, 1);
                //CHECK_VALUES(data[pos], 1, 3);  // sometimes 7?
                break;
            case 1:  // Settings
                // This is where ParseSummaryF0V6 started (after "3 bytes that don't follow the pattern")
                // Both compliance and summary files seem to have the same length for this slice, so maybe the
                // settings are the same?
                ok = this->ParseSettingsF0V6(data + pos, size);
                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 7:
                // Always follows mask off?
                //CHECK_VALUES(data[pos], 0x01, 0x00);  // sometimes 32, 4
                CHECK_VALUE(data[pos+1], 0x00);
                //CHECK_VALUES(data[pos+2], 0x00, 0x01);  // sometimes 11, 3, 15
                CHECK_VALUE(data[pos+3], 0x00);
                //CHECK_VALUE(data[pos+4], 0x05, 0x0A);  // 00
                CHECK_VALUE(data[pos+5], 0x00);
                //CHECK_VALUE(data[pos+6], 0x64, 0x69);  // 6E, 6D, 6E, 6E, 80
                //CHECK_VALUE(data[pos+7], 0x3d, 0x5c);  // 6A, 6A, 6B, 6C, 80
                break;
            case 2:  // Equipment Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
                //CHECK_VALUE(data[pos+2], 0x08);  // 0x01
                //CHECK_VALUE(data[pos+3], 0x14);  // 0x12
                //CHECK_VALUE(data[pos+4], 0x01);  // 0x00
                //CHECK_VALUE(data[pos+5], 0x22);  // 0x28
                //CHECK_VALUE(data[pos+6], 0x02);  // sometimes 1, 0
                CHECK_VALUE(data[pos+7], 0x00);  // 0x00
                CHECK_VALUE(data[pos+8], 0x00);  // 0x00
                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->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
                break;
            default:
                UNEXPECTED_VALUE(code, "known slice code");
                break;
        }
        pos += size;
    }

    this->duration = tt;

    return ok;
}


bool PRS1DataChunk::ParseSummaryF0V6(void)
{
    if (this->family != 0 || this->familyVersion != 6) {
        qWarning() << "ParseSummaryF0V6 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, 0x2b, 9, 4, 2, 4, 1, 4, 0x1b, 2, 4, 0x0b, 1, 2, 6 };
    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;
    }
    if (chunk_size < 59) UNEXPECTED_VALUE(chunk_size, ">= 59");

    bool ok = true;
    int pos = 0;
    int code, size;
    int tt = 0;
    while (ok && pos < chunk_size) {
        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]) {
                UNEXPECTED_VALUE(size, minimum_sizes[code]);
                qWarning() << this->sessionid << "slice" << code << "too small" << size << "<" << minimum_sizes[code];
                if (code != 1) {  // Settings are variable-length, so shorter settings slices aren't fatal.
                    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?
                if (size == 4) {  // 400G has 3 more bytes?
                    //CHECK_VALUE(data[pos+1], 0);  // or 2, 14, 4, etc.
                    //CHECK_VALUES(data[pos+2], 8, 65);  // or 1
                    //CHECK_VALUES(data[pos+3], 0, 20);  // or 21, 22, etc.
                }
                break;
            case 1:  // Settings
                ok = this->ParseSettingsF0V6(data + pos, size);
                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 8:  // vs. 7 in compliance, always follows mask off (except when there's a 5, see below), also longer
                // Maybe statistics of some kind, given the pressure stats that seem to appear before it on AutoCPAP machines?
                //CHECK_VALUES(data[pos], 0x02, 0x01);  // probably 16-bit value
                CHECK_VALUE(data[pos+1], 0x00);
                //CHECK_VALUES(data[pos+2], 0x0d, 0x0a);  // probably 16-bit value, maybe OA count?
                CHECK_VALUE(data[pos+3], 0x00);
                //CHECK_VALUES(data[pos+4], 0x09, 0x0b);  // probably 16-bit value
                CHECK_VALUE(data[pos+5], 0x00);
                //CHECK_VALUES(data[pos+6], 0x1e, 0x35);  // probably 16-bit value
                CHECK_VALUE(data[pos+7], 0x00);
                //CHECK_VALUES(data[pos+8], 0x8c, 0x4c);  // 16-bit value, not sure what
                //CHECK_VALUE(data[pos+9], 0x00);
                //CHECK_VALUES(data[pos+0xa], 0xbb, 0x00);  // 16-bit minutes in large leak
                //CHECK_VALUE(data[pos+0xb], 0x00);
                //CHECK_VALUES(data[pos+0xc], 0x15, 0x02);  // 16-bit minutes in PB
                //CHECK_VALUE(data[pos+0xd], 0x00);
                //CHECK_VALUES(data[pos+0xe], 0x01, 0x00);  // 16-bit VS count
                //CHECK_VALUE(data[pos+0xf], 0x00);
                //CHECK_VALUES(data[pos+0x10], 0x21, 5);  // probably 16-bit value, maybe H count?
                CHECK_VALUE(data[pos+0x11], 0x00);
                //CHECK_VALUES(data[pos+0x12], 0x13, 0);  // 16-bit value, not sure what
                //CHECK_VALUE(data[pos+0x13], 0x00);
                //CHECK_VALUES(data[pos+0x14], 0x05, 0);  // probably 16-bit value, maybe RE count?
                CHECK_VALUE(data[pos+0x15], 0x00);
                //CHECK_VALUE(data[pos+0x16], 0x00, 4);  // probably a 16-bit value, PB or FL count?
                CHECK_VALUE(data[pos+0x17], 0x00);
                //CHECK_VALUES(data[pos+0x18], 0x69, 0x23);
                //CHECK_VALUES(data[pos+0x19], 0x44, 0x18);
                //CHECK_VALUES(data[pos+0x1a], 0x80, 0x49);
                if (size >= 0x1f) {  // 500X is only 0x1b long!
                    //CHECK_VALUES(data[pos+0x1b], 0x00, 6);
                    CHECK_VALUE(data[pos+0x1c], 0x00);
                    //CHECK_VALUES(data[pos+0x1d], 0x0c, 0x0d);
                    //CHECK_VALUES(data[pos+0x1e], 0x31, 0x3b);
                    // TODO: 400G and 500G has 8 more bytes?
                    // TODO: 400G sometimes has another 4 on top of that?
                }
                break;
            case 2:  // Equipment Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
                //CHECK_VALUE(data[pos+2], 0x08);  // 0x01
                //CHECK_VALUE(data[pos+3], 0x14);  // 0x12
                //CHECK_VALUE(data[pos+4], 0x01);  // 0x00
                //CHECK_VALUE(data[pos+5], 0x22);  // 0x28
                //CHECK_VALUE(data[pos+6], 0x02);  // sometimes 1, 0
                CHECK_VALUE(data[pos+7], 0x00);  // 0x00
                CHECK_VALUE(data[pos+8], 0x00);  // 0x00
                if (size == 0x0c) {  // 400G has 3 more bytes, seem to match Equipment On bytes
                    //CHECK_VALUE(data[pos+1], 0);
                    //CHECK_VALUES(data[pos+2], 8, 65);
                    //CHECK_VALUE(data[pos+3], 0);
                }
                break;
            case 0x09:  // Time Elapsed (event 4 in F0V4)
                tt += data[pos] | (data[pos+1] << 8);
                break;
            case 0x0a:  // Humidifier 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;
            case 0x0d:  // ???
                // seen on one 500G multiple times
                //CHECK_VALUE(data[pos], 0);  // 16-bit value
                //CHECK_VALUE(data[pos+1], 0);
                break;
            case 0x0e:
                // only seen once on 400G, many times on 500G
                //CHECK_VALUES(data[pos], 0, 6);  // 16-bit value
                //CHECK_VALUE(data[pos+1], 0);
                //CHECK_VALUES(data[pos+2], 7, 9);
                //CHECK_VALUES(data[pos+3], 7, 15);
                //CHECK_VALUES(data[pos+4], 7, 12);
                //CHECK_VALUES(data[pos+5], 0, 3);
                break;
            case 0x05:
                // AutoCPAP-related? First appeared on 500X, follows 4, before 8, look like pressure values
                //CHECK_VALUE(data[pos], 0x4b);    // maybe min pressure? (matches ramp pressure, see ramp on pressure graph)
                //CHECK_VALUE(data[pos+1], 0x5a);  // maybe max pressure? (close to max on pressure graph, time at pressure graph)
                //CHECK_VALUE(data[pos+2], 0x5a);  // seems to match Average 90% Pressure
                //CHECK_VALUE(data[pos+3], 0x58);  // seems to match Average CPAP
                break;
            case 0x07:
                // AutoBiLevel-related? First appeared on 700X, follows 4, before 8, looks like pressure values
                //CHECK_VALUE(data[pos], 0x50);    // maybe min IPAP or max titrated EPAP? (matches time at pressure graph, auto bi-level summary)
                //CHECK_VALUE(data[pos+1], 0x64);  // maybe max IPAP or max titrated IPAP? (matches time at pressure graph, auto bi-level summary)
                //CHECK_VALUE(data[pos+2], 0x4b);  // seems to match 90% EPAP
                //CHECK_VALUE(data[pos+3], 0x64);  // seems to match 90% IPAP
                break;
            case 0x0b:
                // CPAP-Check related, follows Mask On in CPAP-Check mode
                tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
                //CHECK_VALUE(data[pos+2], 0);  // probably 16-bit value
                CHECK_VALUE(data[pos+3], 0);
                //CHECK_VALUE(data[pos+4], 0);  // probably 16-bit value
                CHECK_VALUE(data[pos+5], 0);
                //CHECK_VALUE(data[pos+6], 0);  // probably 16-bit value
                CHECK_VALUE(data[pos+7], 0);
                //CHECK_VALUE(data[pos+8], 0);  // probably 16-bit value
                CHECK_VALUE(data[pos+9], 0);
                //CHECK_VALUES(data[pos+0xa], 20, 60);  // or 0? 44 when changed pressure mid-session?
                break;
            case 0x06:
                // Maybe starting pressure? follows 4, before 8, looks like a pressure value, seen with CPAP-Check and EZ-Start
                // Maybe ending pressure: matches ending CPAP-Check pressure if it changes mid-session.
                // TODO: The daily details will show when it changed, so maybe there's an event that indicates a pressure change.
                //CHECK_VALUES(data[pos], 90, 60);  // maybe CPAP-Check pressure, also matches EZ-Start Pressure
                break;
            case 0x0c:
                // EZ-Start pressure for Auto-CPAP, seen on 500X110 following 4, before 8
                // Appears to reflect the current session's EZ-Start pressure, though reported afterwards
                //CHECK_VALUE(data[pos], 70, 80);
                break;
            default:
                UNEXPECTED_VALUE(code, "known slice code");
                break;
        }
        pos += size;
    }

    this->duration = tt;

    return ok;
}


// The below is based on a combination of the old mainblock parsing for fileVersion == 3
// in ParseSummary() and the switch statements of ParseSummaryF0V6.
//
// Both compliance and summary files (at least for 200X and 400X machines) seem to have
// the same length for this slice, so maybe the settings are the same? At least 0x0a
// looks like a pressure in compliance files.
bool PRS1DataChunk::ParseSettingsF0V6(const unsigned char* data, int size)
{
    static const QMap<int,int> expected_lengths = { {0x0c,3}, {0x0d,2}, {0x0e,2}, {0x0f,4}, {0x10,3}, {0x35,2} };
    bool ok = true;

    PRS1Mode cpapmode = PRS1_MODE_UNKNOWN;
    FlexMode flexmode = FLEX_Unknown;

    int pressure = 0;
    int imin_ps   = 0;
    int imax_ps   = 0;
    int min_pressure = 0;
    int max_pressure = 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?
                CHECK_VALUE(len, 1);
                switch (data[pos]) {
                case 0: cpapmode = PRS1_MODE_CPAP; break;
                case 1: cpapmode = PRS1_MODE_BILEVEL; break;
                case 2: cpapmode = PRS1_MODE_AUTOCPAP; break;
                case 3: cpapmode = PRS1_MODE_AUTOBILEVEL; break;
                case 4: cpapmode = PRS1_MODE_CPAPCHECK; 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(len, 1, 2);
                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?
                }
                if (len == 2) {  // 400G, 500G has extra byte
                    switch (data[pos+1]) {
                        case 0x00:  // 0x00 seen with EZ-Start disabled, no auto-trial, with CPAP-Check on 400X110
                        case 0x10:  // 0x10 seen with EZ-Start enabled, Opti-Start off on 500X110
                        case 0x20:  // 0x20 seen with Opti-Start enabled
                        case 0x30:  // 0x30 seen with both Opti-Start and EZ-Start enabled on 500X110
                        case 0x40:  // 0x40 seen with Auto-Trial
                        case 0x80:  // 0x80 seen with EZ-Start and CPAP-Check+ on 500X150
                            break;
                        default:
                            UNEXPECTED_VALUE(data[pos+1], "[0,0x10,0x20,0x30,0x40,0x80]")
                    }
                }
                break;
            case 0x0a:  // CPAP pressure setting
                CHECK_VALUE(len, 1);
                CHECK_VALUE(cpapmode, PRS1_MODE_CPAP);
                pressure = data[pos];
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, pressure));
                break;
            case 0x0c:  // CPAP-Check pressure setting
                CHECK_VALUE(len, 3);
                CHECK_VALUE(cpapmode, PRS1_MODE_CPAPCHECK);
                min_pressure = data[pos];  // Min Setting on pressure graph
                max_pressure = data[pos+1];  // Max Setting on pressure graph
                pressure = data[pos+2];  // CPAP on pressure graph and CPAP-Check Pressure on settings detail
                // This seems to be the initial pressure. If the pressure changes mid-session, the pressure
                // graph will show either the changed pressure or the majority pressure, not sure which.
                // The time of change is most likely in the events file. See slice 6 for ending pressure.
                //CHECK_VALUE(pressure, 0x5a);
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, pressure));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, min_pressure));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, max_pressure));
                break;
            case 0x0d:  // AutoCPAP pressure setting
                CHECK_VALUE(len, 2);
                CHECK_VALUE(cpapmode, PRS1_MODE_AUTOCPAP);
                min_pressure = data[pos];
                max_pressure = data[pos+1];
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, min_pressure));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, max_pressure));
                break;
            case 0x0e:  // Bi-Level pressure setting
                CHECK_VALUE(len, 2);
                CHECK_VALUE(cpapmode, PRS1_MODE_BILEVEL);
                min_pressure = data[pos];
                max_pressure = data[pos+1];
                imin_ps = max_pressure - min_pressure;
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, min_pressure));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP, max_pressure));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS, imin_ps));
                break;
            case 0x0f:  // Auto Bi-Level pressure setting
                CHECK_VALUE(len, 4);
                CHECK_VALUE(cpapmode, PRS1_MODE_AUTOBILEVEL);
                min_pressure = data[pos];
                max_pressure = data[pos+1];
                imin_ps = data[pos+2];
                imax_ps = data[pos+3];
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, min_pressure));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, max_pressure));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, imin_ps));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, imax_ps));
                break;
            case 0x10: // Auto-Trial mode
                // This is not encoded as a separate mode as in F0V4, but instead as an auto-trial
                // duration on top of the CPAP or CPAP-Check mode. Reports show Auto-CPAP results,
                // but curiously report the use of C-Flex+, even though Auto-CPAP uses A-Flex.
                CHECK_VALUE(len, 3);
                CHECK_VALUES(cpapmode, PRS1_MODE_CPAP, PRS1_MODE_CPAPCHECK);
                if (data[pos] != 30 && data[pos] != 9) {
                    CHECK_VALUES(data[pos], 5, 25);  // Auto-Trial Duration
                }
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_TRIAL, data[pos]));
                // If we want C-Flex+ to be reported as A-Flex, we can set cpapmode = PRS1_MODE_AUTOTRIAL here.
                // (Note that the setting event has already been added above, which is why ImportSummary needs
                // to adjust it when it sees this setting.)
                min_pressure = data[pos+1];
                max_pressure = data[pos+2];
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, min_pressure));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, max_pressure));
                break;
            case 0x2a:  // EZ-Start
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0x00, 0x80);  // both seem to mean enabled
                // 0x80 is CPAP Mode - EZ-Start in pressure detail chart, 0x00 is just CPAP mode with no EZ-Start pressure
                // TODO: How to represent which one is active in practice? Should this always be "true" since
                // either value means that the setting is enabled?
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_EZ_START, data[pos] != 0));
                break;
            case 0x42:  // EZ-Start enabled for Auto-CPAP?
                // Seen on 500X110 before 0x2b when EZ-Start is enabled on Auto-CPAP
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0x00, 0x80);  // both seem to mean enabled, 0x00 appears when Opti-Start is used instead
                // TODO: How to represent which one is active in practice? Should this always be "true" since
                // either value means that the setting is enabled?
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_EZ_START, data[pos] != 0));
                break;
            case 0x2b:  // Ramp Type
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0, 0x80);  // 0 == "Linear", 0x80 = "SmartRamp"
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TYPE, data[pos] != 0));
                break;
            case 0x2c:  // Ramp Time
                CHECK_VALUE(len, 1);
                if (data[pos] != 0) {  // 0 == ramp off, and ramp pressure setting doesn't appear
                    if (data[pos] < 5 || data[pos] > 45) UNEXPECTED_VALUE(data[pos], "5-45");
                }
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, data[pos]));
                break;
            case 0x2d:  // Ramp Pressure
                CHECK_VALUE(len, 1);
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, data[pos]));
                break;
            case 0x2e:  // Flex mode
                CHECK_VALUE(len, 1);
                switch (data[pos]) {
                case 0:
                    flexmode = FLEX_None;
                    break;
                case 0x80:
                    switch (cpapmode) {
                        case PRS1_MODE_CPAP:
                        case PRS1_MODE_CPAPCHECK:
                        case PRS1_MODE_AUTOCPAP:
                        //case PRS1_MODE_AUTOTRIAL:
                            flexmode = FLEX_CFlex;
                            break;
                        case PRS1_MODE_BILEVEL:
                        case PRS1_MODE_AUTOBILEVEL:
                            flexmode = FLEX_BiFlex;
                            break;
                        default:
                            HEX(flexmode);
                            UNEXPECTED_VALUE(cpapmode, "untested mode");
                            break;
                    }
                    break;
                case 0x90:  // C-Flex+ or A-Flex, depending on machine mode
                    switch (cpapmode) {
                    case PRS1_MODE_CPAP:
                    case PRS1_MODE_CPAPCHECK:
                        flexmode = FLEX_CFlexPlus;
                        break;
                    case PRS1_MODE_AUTOCPAP:
                        flexmode = FLEX_AFlex;
                        break;
                    default:
                        UNEXPECTED_VALUE(cpapmode, "cpap or apap");
                        break;
                    }
                    break;
                case 0xA0:  // Rise Time
                    flexmode = FLEX_RiseTime;
                    switch (cpapmode) {
                        case PRS1_MODE_BILEVEL:
                        case PRS1_MODE_AUTOBILEVEL:
                            break;
                        default:
                            HEX(flexmode);
                            UNEXPECTED_VALUE(cpapmode, "autobilevel");
                            break;
                    }
                    break;
                case 0xB0:  // P-Flex
                    flexmode = FLEX_PFlex;
                    switch (cpapmode) {
                        case PRS1_MODE_AUTOCPAP:
                            break;
                        default:
                            HEX(flexmode);
                            UNEXPECTED_VALUE(cpapmode, "apap");
                            break;
                    }
                    break;
                default:
                    UNEXPECTED_VALUE(data[pos], "known flex mode");
                    break;
                }
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, flexmode));
                break;
            case 0x2f:  // Flex lock
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0, 0x80);
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, data[pos] != 0));
                break;
            case 0x30:  // Flex level
                CHECK_VALUE(len, 1);
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[pos]));
                if (flexmode == FLEX_PFlex) {
                    CHECK_VALUE(data[pos], 4);  // No number appears on reports.
                }
                if (flexmode == FLEX_RiseTime) {
                    if (data[pos] < 1 || data[pos] > 3) UNEXPECTED_VALUE(data[pos], "1-3");
                }
                break;
            case 0x35:  // Humidifier setting
                CHECK_VALUE(len, 2);
                this->ParseHumidifierSettingV3(data[pos], data[pos+1], true);
                break;
            case 0x36:  // Mask Resistance Lock
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0, 0x80);
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_LOCK, data[pos] != 0));
                break;
            case 0x38:  // Mask Resistance
                CHECK_VALUE(len, 1);
                if (data[pos] != 0) {  // 0 == mask resistance off
                    if (data[pos] < 1 || data[pos] > 5) UNEXPECTED_VALUE(data[pos], "1-5");
                }
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_SETTING, data[pos]));
                break;
            case 0x39:  // Tubing Type Lock
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0, 0x80);
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TUBING_LOCK, data[pos] != 0));
                break;
            case 0x3b:  // Tubing Type
                CHECK_VALUE(len, 1);
                if (data[pos] != 0) {
                    CHECK_VALUES(data[pos], 2, 1);  // 15HT = 2, 15 = 1, 22 = 0
                }
                this->ParseTubingTypeV3(data[pos]);
                break;
            case 0x40:  // new to 400G, also seen on 500X110, alternate tubing type? appears after 0x39 and before 0x3c
                CHECK_VALUE(len, 1);
                if (data[pos] > 3) UNEXPECTED_VALUE(data[pos], "0-3");  // 0 = 22mm, 1 = 15mm, 2 = 15HT, 3 = 12mm
                this->ParseTubingTypeV3(data[pos]);
                break;
            case 0x3c:  // View Optional Screens
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0, 0x80);
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SHOW_AHI, data[pos] != 0));
                break;
            case 0x3e:  // Auto On
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0, 0x80);
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_ON, data[pos] != 0));
                break;
            case 0x3f:  // Auto Off
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0, 0x80);
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_OFF, data[pos] != 0));
                break;
            case 0x43:  // new to 502G, sessions 3-8, Auto-Trial is off, Opti-Start is missing
                CHECK_VALUE(len, 1);
                CHECK_VALUE(data[pos], 0x3C);
                break;
            case 0x44:  // new to 502G, sessions 3-8, Auto-Trial is off, Opti-Start is missing
                CHECK_VALUE(len, 1);
                CHECK_VALUE(data[pos], 0xFF);
                break;
            case 0x45:  // Target Time, specific to DreamStation Go
                CHECK_VALUE(len, 1);
                // Included in the data, but not shown on reports when humidifier is in Fixed mode.
                // According to the FAQ, this setting is only available in Adaptive mode.
                if (data[pos] < 40 || data[pos] > 100) {  // 4.0 through 10.0 hours in 0.5-hour increments
                    CHECK_VALUES(data[pos], 0, 1);  // Off and Auto
                }
                this->AddEvent(new PRS1ScaledSettingEvent(PRS1_SETTING_HUMID_TARGET_TIME, data[pos], 0.1));
                break;
            default:
                UNEXPECTED_VALUE(code, "known setting");
                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;
}


const QVector<PRS1ParsedEventType> ParsedEventsF0V6 = {
    PRS1PressureSetEvent::TYPE,
    PRS1IPAPSetEvent::TYPE,
    PRS1EPAPSetEvent::TYPE,
    PRS1AutoPressureSetEvent::TYPE,
    PRS1PressurePulseEvent::TYPE,
    PRS1RERAEvent::TYPE,
    PRS1ObstructiveApneaEvent::TYPE,
    PRS1ClearAirwayEvent::TYPE,
    PRS1HypopneaEvent::TYPE,
    PRS1FlowLimitationEvent::TYPE,
    PRS1VibratorySnoreEvent::TYPE,
    PRS1VariableBreathingEvent::TYPE,
    PRS1PeriodicBreathingEvent::TYPE,
    PRS1LargeLeakEvent::TYPE,
    PRS1TotalLeakEvent::TYPE,
    PRS1SnoreEvent::TYPE,
    PRS1PressureAverageEvent::TYPE,
    PRS1FlexPressureAverageEvent::TYPE,
    PRS1SnoresAtPressureEvent::TYPE,
};

// DreamStation family 0 CPAP/APAP machines (400X-700X, 400G-502G)
// Originally derived from F5V3 parsing + (incomplete) F0V234 parsing + sample data
bool PRS1DataChunk::ParseEventsF0V6()
{
    if (this->family != 0 || this->familyVersion != 6) {
        qWarning() << "ParseEventsF0V6 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[] = { 2, 3, 4, 3, 3, 3, 3, 3, 3, 2, 3, 4, 3, 2, 5, 5, 5, 5, 4, 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;
    }

    bool ok = true;
    int pos = 0, startpos;
    int code, size;
    int t = 0;
    int elapsed, duration, value;
    bool is_bilevel = false;
    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;
        if (code != 0x12) {  // This one event has no timestamp
            t += data[pos] | (data[pos+1] << 8);
            pos += 2;
        }

        switch (code) {
            //case 0x00:  // never seen
            case 0x01:  // Pressure adjustment
                // Matches pressure setting, both initial and when ramp button pressed.
                // Based on waveform reports, it looks like the pressure graph is drawn by
                // interpolating between these pressure adjustments, by 0.5 cmH2O spaced evenly between
                // adjustments. E.g. 6 at 28:11 and 7.3 at 29:05 results in the following dots:
                // 6 at 28:11, 6.5 around 28:30, 7.0 around 28:50, 7(.3) at 29:05. That holds until
                // subsequent "adjustment" of 7.3 at 30:09 followed by 8.0 at 30:19.
                this->AddEvent(new PRS1PressureSetEvent(t, data[pos]));
                break;
            case 0x02:  // Pressure adjustment (bi-level)
                // See notes above on interpolation.
                this->AddEvent(new PRS1IPAPSetEvent(t, data[pos+1]));
                this->AddEvent(new PRS1EPAPSetEvent(t, data[pos]));  // EPAP needs to be added second to calculate PS
                is_bilevel = true;
                break;
            case 0x03:  // Auto-CPAP starting pressure
                // Most of the time this occurs, it's at the start and end of a session with
                // the same pressure at both. Occasionally an additional event shows up in the
                // middle of a session, and then the pressure at the end matches that.
                // In these cases, the new pressure corresponds to the next night's starting
                // pressure for auto-CPAP. It does not appear to have any effect on the current
                // night's pressure, unless there's a substantial gap between sessions, in
                // which case the next session may use the new starting pressure.
                //CHECK_VALUE(data[pos], 40);
                // TODO: What does this mean in bi-level mode?
                // See F0V4 event 3 for comparison. TODO: See if there's an Opti-Start label on F0V6 reports.
                this->AddEvent(new PRS1AutoPressureSetEvent(t, data[pos]));
                break;
            case 0x04:  // Pressure Pulse
                duration = data[pos];  // TODO: is this a duration?
                this->AddEvent(new PRS1PressurePulseEvent(t, duration));
                break;
            case 0x05:  // RERA
                elapsed = data[pos];  // based on sample waveform, the RERA is over after this
                this->AddEvent(new PRS1RERAEvent(t - elapsed, 0));
                break;
            case 0x06:  // 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 0x07:  // 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 0x08:  // never seen
            //case 0x09:  // never seen
            //case 0x0a:  // Hypopnea, see 0x15
            case 0x0b:  // Hypopnea
                // TODO: How is this hypopnea different from events 0xa, 0x14 and 0x15?
                // TODO: What is the first byte?
                //data[pos+0];  // unknown first byte?
                elapsed = data[pos+1];  // based on sample waveform, the hypopnea is over after this
                this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
                break;
            case 0x0c:  // 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 0x0d:  // 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 statistics below seem 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 0x0e:  // Variable Breathing?
                duration = 2 * (data[pos] | (data[pos+1] << 8));
                elapsed = data[pos+2];  // this is always 60 seconds unless it's at the end, so it seems like elapsed
                CHECK_VALUES(elapsed, 60, 0);
                this->AddEvent(new PRS1VariableBreathingEvent(t - elapsed - duration, duration));
                break;
            case 0x0f:  // 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));
                elapsed = data[pos+2];
                this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
                break;
            case 0x10:  // 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));
                elapsed = data[pos+2];
                this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
                break;
            case 0x11:  // Statistics
                this->AddEvent(new PRS1TotalLeakEvent(t, data[pos]));
                this->AddEvent(new PRS1SnoreEvent(t, data[pos+1]));

                value = data[pos+2];
                if (is_bilevel) {
                    // For bi-level modes, this appears to be the time-weighted average of EPAP and IPAP actually provided.
                    this->AddEvent(new PRS1PressureAverageEvent(t, value));
                } else {
                    // For single-pressure modes, this appears to be the average effective "EPAP" provided by Flex.
                    //
                    // Sample data shows this value around 10.3 cmH2O for a prescribed pressure of 12.0 (C-Flex+ 3).
                    // That's too low for an average pressure over time, but could easily be an average commanded EPAP.
                    // When flex mode is off, this is exactly the current CPAP set point.
                    this->AddEvent(new PRS1FlexPressureAverageEvent(t, value));
                }
                this->AddEvent(new PRS1IntervalBoundaryEvent(t));
                break;
            case 0x12:  // Snore count per pressure
                // Some sessions (with lots of ramps) have multiple of these, each with a
                // different pressure. The total snore count across all of them matches the
                // total found in the stats event.
                if (data[pos] != 0) {
                    CHECK_VALUES(data[pos], 1, 2);  // 0 = CPAP pressure, 1 = bi-level EPAP, 2 = bi-level IPAP
                }
                //CHECK_VALUE(data[pos+1], 0x78);  // pressure
                //CHECK_VALUE(data[pos+2], 1);  // 16-bit snore count
                //CHECK_VALUE(data[pos+3], 0);
                value = (data[pos+2] | (data[pos+3] << 8));
                this->AddEvent(new PRS1SnoresAtPressureEvent(t, data[pos], data[pos+1], value));
                break;
            //case 0x13:  // never seen
            case 0x0a:  // Hypopnea
                // TODO: Why does this hypopnea have a different event code?
                // fall through
            case 0x14:  // Hypopnea, new to F0V6
                // TODO: Why does this hypopnea have a different event code?
                // fall through
            case 0x15:  // Hypopnea, new to F0V6
                // 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;
            default:
                DUMP_EVENT();
                UNEXPECTED_VALUE(code, "known event code");
                this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
                break;
        }
        pos = startpos + size;
    } while (ok && pos < chunk_size);

    this->duration = t;

    return ok;
}