/* PRS1 Parsing for BiPAP autoSV (ASV) (Family 5)
 *
 * 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"

static QString hex(int i)
{
    return QString("0x") + QString::number(i, 16).toUpper();
}

//********************************************************************************************
// MARK: -
// MARK: 50 and 60 Series

// borrowed largely from ParseSummaryF0V4
bool PRS1DataChunk::ParseSummaryF5V012(void)
{
    if (this->family != 5 || (this->familyVersion > 2)) {
        qWarning() << "ParseSummaryF5V012 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();
    QVector<int> minimum_sizes;
    switch (this->familyVersion) {
        case 0: minimum_sizes = { 0x12, 4, 3, 0x1f, 0, 4, 0, 2, 2 }; break;
        case 1: minimum_sizes = { 0x13, 7, 5, 0x20, 0, 4, 0, 2, 2, 4 }; break;
        case 2: minimum_sizes = { 0x13, 7, 5, 0x22, 0, 4, 0, 2, 2, 4 }; break;
    }
    // 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 < minimum_sizes.length()) {
            // make sure the handlers below don't go past the end of the buffer
            size = minimum_sizes[code];
        } else {
            // We can't defer warning until later, because F5V0 doesn't have slice 9.
            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;
        }
        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_VALUE(data[pos] & 0xF0, 0);  // 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.
                }
                */
            // F5V012 doesn't have a separate settings record like F5V3 does, the settings just follow the EquipmentOn data.
                ok = this->ParseSettingsF5V012(data, size);
                /*
                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));
            // F5V012 doesn't have a separate stats record like F5V3 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));
                if (this->familyVersion == 0) {
                    //CHECK_VALUE(data[pos+2], 1);  // Usually 1, also seen 0, 6, and 7.
                    ParseHumidifierSetting50Series(data[pos+3]);
                }
                /* Possibly F5V12?
                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 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;
            case 7:  // Time Elapsed?
                tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
                break;
            case 8:  // Time Elapsed? How is this different from 7?
                tt += data[pos] | (data[pos+1] << 8);  // This also adds to the total duration (otherwise it won't match report)
                break;
            case 9:  // Humidifier setting change, F5V1 and F5V2 only
                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;
}


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

    float GAIN = PRS1PressureSettingEvent::GAIN;
    if (this->familyVersion == 2) GAIN = 0.125f;  // TODO: parameterize this somewhere better

    int imax_pressure = data[0x2];
    int imin_epap = data[0x3];
    int imax_epap = data[0x4];
    int imin_ps = data[0x5];
    int imax_ps = data[0x6];

    // Only one mode available, so apparently there's no byte in the settings that encodes it?
    cpapmode = PRS1_MODE_ASV;
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));

    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, imin_epap));
    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, imax_epap));
    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, imin_epap + imin_ps));
    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, imax_pressure));
    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, imin_ps));
    this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, imax_ps));

    //CHECK_VALUE(data[0x07], 1, 2);  // 1 = backup breath rate "Auto"; 2 = fixed BPM, see below
    //CHECK_VALUE(data[0x08], 0);     // backup "Breath Rate" in mode 2
    //CHECK_VALUE(data[0x09], 0);     // backup "Timed Inspiration" (gain 0.1) in mode 2
    int pos = 0x7;
    int backup_mode = data[pos];
    int breath_rate;
    int timed_inspiration;
    switch (backup_mode) {
        case 0:
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Off));
            break;
        case 1:
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Auto));
            CHECK_VALUE(data[pos+1], 0);
            CHECK_VALUE(data[pos+2], 0);
            break;
        case 2:
            breath_rate = data[pos+1];
            timed_inspiration = data[pos+2];
            if (breath_rate < 4 || breath_rate > 29) UNEXPECTED_VALUE(breath_rate, "4-29");
            if (timed_inspiration < 5 || timed_inspiration > 30) UNEXPECTED_VALUE(timed_inspiration, "5-30");
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Fixed));
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_RATE, breath_rate));
            this->AddEvent(new PRS1ScaledSettingEvent(PRS1_SETTING_BACKUP_TIMED_INSPIRATION, timed_inspiration, 0.1));
            break;
        default:
            UNEXPECTED_VALUE(backup_mode, "0-2");
            break;
    }

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

    quint8 flex = data[0x0c];
    this->ParseFlexSettingF5V012(flex, cpapmode);

    if (this->familyVersion == 0) {  // TODO: either split this into two functions or use size to differentiate like FV3 parsers do
        this->ParseHumidifierSetting50Series(data[0x0d], true);
        pos = 0xe;
    } else {
        // 60-Series machines have a 2-byte humidfier setting.
        this->ParseHumidifierSetting60Series(data[0x0d], data[0x0e], true);
        pos = 0xf;
    }

    // TODO: may differ between F5V0 and F5V12
    // 0x01, 0x41 = auto-on, view AHI, tubing type = 15
    // 0x41, 0x41 = auto-on, view AHI, tubing type = 15, resist lock
    // 0x42, 0x01 = (no auto-on), view AHI, tubing type = 22, resist lock, tubing lock
    // 0x00, 0x41 = auto-on, view AHI, tubing type = 22, no tubing lock
    // 0x0B, 0x41 = mask resist 1, tube lock, tubing type = 15, auto-on, view AHI
    // 0x09, 0x01 = mask resist 1, tubing 15, view AHI
    // 0x19, 0x41 = mask resist 3, tubing 15, auto-on, view AHI
    // 0x29, 0x41 = mask resist 5, tubing 15, auto-on, view AHI
    //          1 = view AHI
    //         4  = auto-on
    //    1       = tubing type: 0=22, 1=15
    //    2       = tubing lock
    //   38       = mask resist level
    //   4        = resist lock
    int resist_level = (data[pos] >> 3) & 7;  // 0x09 resist=1, 0x11 resist=2, 0x19=resist 3, 0x29=resist 5
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_LOCK, (data[pos] & 0x40) != 0));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_SETTING, resist_level));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HOSE_DIAMETER, (data[pos] & 0x01) ? 15 : 22));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TUBING_LOCK, (data[pos] & 0x02) != 0));
    CHECK_VALUE(data[pos] & (0x80|0x04), 0);
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_ON, (data[pos+1] & 0x40) != 0));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SHOW_AHI, (data[pos+1] & 1) != 0));
    CHECK_VALUE(data[pos+1] & ~(0x40|1), 0);
    
    int apnea_alarm = data[pos+2];
    int low_mv_alarm = data[pos+3];
    int disconnect_alarm = data[pos+4];
    if (apnea_alarm) {
        CHECK_VALUES(apnea_alarm, 1, 3);  // 1 = apnea alarm 10, 3 = apnea alarm 30
    }
    if (low_mv_alarm) {
        if (low_mv_alarm < 20 || low_mv_alarm > 99) {
            UNEXPECTED_VALUE(low_mv_alarm, "20-99");  // we've seen 20, 80 and 99, all of which correspond to the number on the report
        }
    }
    if (disconnect_alarm) {
        CHECK_VALUES(disconnect_alarm, 1, 2);  // 1 = disconnect alarm 15, 2 = disconnect alarm 60
    }

    return true;
}


// Flex F5V0 confirmed
// 0x81 = Bi-Flex 1 (ASV mode)
// 0x82 = Bi-Flex 2 (ASV mode)
// 0x83 = Bi-Flex 3 (ASV mode)

// Flex F5V1 confirmed
// 0x81 = Bi-Flex 1 (ASV mode)
// 0x82 = Bi-Flex 2 (ASV mode)
// 0x83 = Bi-Flex 3 (ASV mode)
// 0xC9 = Rise Time 1, Rise Time Lock (ASV mode)
// 0x8A = Rise Time 2 (ASV mode) (Shows "ASV - None" in mode summary, but then rise time in details)
// 0x8B = Rise Time 3 (ASV mode) (breath rate auto)
// 0x08 = Rise Time 2 (ASV mode) (falls back to level=2? bits encode level=0)

// Flex F5V2 confirmed
// 0x02 = Bi-Flex 2 (ASV mode) (breath rate auto, but min/max PS=0)
// this could be different from F5V01, or PS=0 could disable flex?

//   8  = ? (once was 0 when rise time was on and backup breathing was off, rise time level was also 0 in that case)
//          (was also 0 on F5V2)
//   4  = Rise Time Lock
//    8 = Rise Time (vs. Bi-Flex)
//    3 = level

void PRS1DataChunk::ParseFlexSettingF5V012(quint8 flex, int cpapmode)
{
    FlexMode flexmode = FLEX_Unknown;
    bool valid    = (flex & 0x80) != 0;
    bool lock     = (flex & 0x40) != 0;
    bool risetime = (flex & 0x08) != 0;
    int flexlevel = flex & 0x03;

    if (flex & (0x20 | 0x10 | 0x04)) UNEXPECTED_VALUE(flex, "known bits");
    CHECK_VALUE(cpapmode, PRS1_MODE_ASV);
    if (this->familyVersion == 0) {
        CHECK_VALUE(valid, true);
        CHECK_VALUE(lock, false);
        CHECK_VALUE(risetime, false);
    } else if (this->familyVersion == 1) {
        if (valid == false) {
            CHECK_VALUE(flex, 0x08);
            flexlevel = 2;  // These get reported as Rise Time 2
            valid = true;
        }
    } else {
        CHECK_VALUE(flex, 0x02);  // only seen one example, unsure if it matches F5V01; seems to encode Bi-Flex 2
        valid = true;  // add the flex mode and setting to the parsed settings
    }
    if (flexlevel == 0 || flexlevel >3) UNEXPECTED_VALUE(flexlevel, "1-3");

    CHECK_VALUE(valid, true);
    if (risetime) {
        flexmode = FLEX_RiseTime;
    } else {
        flexmode = FLEX_BiFlex;
    }
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, (int) flexmode));

    if (flexmode == FLEX_BiFlex) {
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, flexlevel));
        CHECK_VALUE(lock, 0);  // Flag any sample data that will let us confirm flex lock
        //this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, lock != 0));
    } else {
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME, flexlevel));
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME_LOCK, lock != 0));
    }
}


const QVector<PRS1ParsedEventType> ParsedEventsF5V0 = {
    PRS1EPAPSetEvent::TYPE,
    PRS1TimedBreathEvent::TYPE,
    PRS1ObstructiveApneaEvent::TYPE,
    PRS1ClearAirwayEvent::TYPE,
    PRS1HypopneaEvent::TYPE,
    PRS1FlowLimitationEvent::TYPE,
    PRS1VibratorySnoreEvent::TYPE,
    PRS1PeriodicBreathingEvent::TYPE,
    PRS1LargeLeakEvent::TYPE,
    PRS1IPAPAverageEvent::TYPE,
    PRS1IPAPLowEvent::TYPE,
    PRS1IPAPHighEvent::TYPE,
    PRS1TotalLeakEvent::TYPE,
    PRS1RespiratoryRateEvent::TYPE,
    PRS1PatientTriggeredBreathsEvent::TYPE,
    PRS1MinuteVentilationEvent::TYPE,
    PRS1TidalVolumeEvent::TYPE,
    PRS1SnoreEvent::TYPE,
    PRS1EPAPAverageEvent::TYPE,
};

// 950P is F5V0
bool PRS1DataChunk::ParseEventsF5V0(void)
{
    if (this->family != 5 || this->familyVersion != 0) {
        qWarning() << "ParseEventsF5V0 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 = { {1,2}, {3,4}, {8,4}, {0xa,2}, {0xb,5}, {0xc,5}, {0xd,0xc} };

    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;
    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;
        t += data[pos] | (data[pos+1] << 8);
        pos += 2;

        switch (code) {
            case 0x00:  // Humidifier setting change (logged in summary in 60 series)
                this->ParseHumidifierSetting50Series(data[pos]);
                break;
            //case 0x01:  // never seen on F5V0
            case 0x02:  // Pressure adjustment
                this->AddEvent(new PRS1EPAPSetEvent(t, data[pos++]));
                break;
            //case 0x03:  // never seen on F5V0; probably pressure pulse, see F5V1
            case 0x04:  // Timed Breath
                // TB events have a duration in 0.1s, based on the review of pressure waveforms.
                // TODO: Ideally the starting time here would be adjusted here, but PRS1ParsedEvents
                // currently assume integer seconds rather than ms, so that's done at import.
                duration = data[pos];
                this->AddEvent(new PRS1TimedBreathEvent(t, duration));
                break;
            case 0x05:  // Obstructive Apnea
                // OA events are instantaneous flags with no duration: reviewing waveforms
                // shows that the time elapsed between the flag and reporting often includes
                // non-apnea breathing.
                elapsed = data[pos];
                this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0));
                break;
            case 0x06:  // Clear Airway Apnea
                // CA events are instantaneous flags with no duration: reviewing waveforms
                // shows that the time elapsed between the flag and reporting often includes
                // non-apnea breathing.
                elapsed = data[pos];
                this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0));
                break;
            case 0x07:  // Hypopnea
                // NOTE: No additional (unknown) first byte as in F5V3 0x07, but see below.
                // This seems closer to F5V3 0x0d or 0x0e.
                elapsed = data[pos];  // based on sample waveform, the hypopnea is over after this
                this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
                break;
            case 0x08:  // Hypopnea, note this is 0x7 in F5V3
                // TODO: How is this hypopnea different from event 0x7?
                // 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 0x09:  // Flow Limitation, note this is 0x8 in F5V3
                // 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 0x0a:  // Vibratory Snore, note this is 0x9 in F5V3
                // VS events are instantaneous flags with no duration, drawn on the official waveform.
                // The current thinking is that these are the snores that cause a change in auto-titrating
                // pressure. The snoring statistic above seems to be a total count. It's unclear whether
                // the trigger for pressure change is severity or count or something else.
                // no data bytes
                this->AddEvent(new PRS1VibratorySnoreEvent(t, 0));
                break;
            case 0x0b:  // Periodic Breathing, note this is 0xa in F5V3
                // PB events are reported some time after they conclude, and they do have a reported duration.
                duration = 2 * (data[pos] | (data[pos+1] << 8));  // confirmed to double in F5V0
                elapsed = data[pos+2];
                this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
                break;
            case 0x0c:  // Large Leak, note this is 0xb in F5V3
                // LL events are reported some time after they conclude, and they do have a reported duration.
                duration = 2 * (data[pos] | (data[pos+1] << 8));  // confirmed to double in F5V0
                elapsed = data[pos+2];
                this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
                break;
            case 0x0d:  // Statistics
                // These appear every 2 minutes, so presumably summarize the preceding period.
                this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos+0]));              // 00=IPAP
                this->AddEvent(new PRS1IPAPLowEvent(t, data[pos+1]));                  // 01=IAP Low
                this->AddEvent(new PRS1IPAPHighEvent(t, data[pos+2]));                 // 02=IAP High
                this->AddEvent(new PRS1TotalLeakEvent(t, data[pos+3]));                // 03=Total leak (average?)
                this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos+4]));          // 04=Breaths Per Minute (average?)
                this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos+5]));  // 05=Patient Triggered Breaths (average?)
                this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos+6]));        // 06=Minute Ventilation (average?)
                this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos+7]));              // 07=Tidal Volume (average?)
                this->AddEvent(new PRS1SnoreEvent(t, data[pos+8]));                    // 08=Snore count  // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
                this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+9]));              // 09=EPAP average
                this->AddEvent(new PRS1IntervalBoundaryEvent(t));
                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 slices, 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;
}


const QVector<PRS1ParsedEventType> ParsedEventsF5V1 = {
    PRS1EPAPSetEvent::TYPE,
    PRS1PressurePulseEvent::TYPE,
    PRS1TimedBreathEvent::TYPE,
    PRS1ObstructiveApneaEvent::TYPE,
    PRS1ClearAirwayEvent::TYPE,
    PRS1HypopneaEvent::TYPE,
    PRS1FlowLimitationEvent::TYPE,
    PRS1VibratorySnoreEvent::TYPE,
    PRS1PeriodicBreathingEvent::TYPE,
    PRS1LargeLeakEvent::TYPE,
    PRS1IPAPAverageEvent::TYPE,
    PRS1IPAPLowEvent::TYPE,
    PRS1IPAPHighEvent::TYPE,
    PRS1TotalLeakEvent::TYPE,
    PRS1RespiratoryRateEvent::TYPE,
    PRS1PatientTriggeredBreathsEvent::TYPE,
    PRS1MinuteVentilationEvent::TYPE,
    PRS1TidalVolumeEvent::TYPE,
    PRS1SnoreEvent::TYPE,
    PRS1EPAPAverageEvent::TYPE,
    PRS1LeakEvent::TYPE,
};

// 960P and 961P are F5V1
bool PRS1DataChunk::ParseEventsF5V1(void)
{
    if (this->family != 5 || this->familyVersion != 1) {
        qWarning() << "ParseEventsF5V1 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 = { {1,2}, {8,4}, {9,3}, {0xa,2}, {0xb,5}, {0xc,5}, {0xd,0xd} };

    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;
    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 != 0) {  // Does this code really not have a timestamp? Never seen on F5V1, checked in F5V0.
            t += data[pos] | (data[pos+1] << 8);
            pos += 2;
        }

        switch (code) {
            //case 0x00:  // never seen on F5V1
            //case 0x01:  // never seen on F5V1
            case 0x02:  // Pressure adjustment
                this->AddEvent(new PRS1EPAPSetEvent(t, data[pos++]));
                break;
            case 0x03:  // Pressure Pulse
                duration = data[pos];  // TODO: is this a duration?
                this->AddEvent(new PRS1PressurePulseEvent(t, duration));
                break;
            case 0x04:  // Timed Breath
                // TB events have a duration in 0.1s, based on the review of pressure waveforms.
                // TODO: Ideally the starting time here would be adjusted here, but PRS1ParsedEvents
                // currently assume integer seconds rather than ms, so that's done at import.
                duration = data[pos];
                this->AddEvent(new PRS1TimedBreathEvent(t, duration));
                break;
            case 0x05:  // Obstructive Apnea
                // OA events are instantaneous flags with no duration: reviewing waveforms
                // shows that the time elapsed between the flag and reporting often includes
                // non-apnea breathing.
                elapsed = data[pos];
                this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0));
                break;
            case 0x06:  // Clear Airway Apnea
                // CA events are instantaneous flags with no duration: reviewing waveforms
                // shows that the time elapsed between the flag and reporting often includes
                // non-apnea breathing.
                elapsed = data[pos];
                this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0));
                break;
            case 0x07:  // Hypopnea
                // TODO: How is this hypopnea different from event 0x8?
                // NOTE: No additional (unknown) first byte as in F5V3 0x7, but see below.
                // This seems closer to F5V3 0x0d or 0x0e.
                elapsed = data[pos];  // based on sample waveform, the hypopnea is over after this
                this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
                break;
            case 0x08:  // Hypopnea, note this is 0x7 in F5V3
                // TODO: How is this hypopnea different from event 0x7?
                // 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 0x09:  // Flow Limitation, note this is 0x8 in F5V3
                // 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 0x0a:  // Vibratory Snore, note this is 0x9 in F5V3
                // VS events are instantaneous flags with no duration, drawn on the official waveform.
                // The current thinking is that these are the snores that cause a change in auto-titrating
                // pressure. The snoring statistic above seems to be a total count. It's unclear whether
                // the trigger for pressure change is severity or count or something else.
                // no data bytes
                this->AddEvent(new PRS1VibratorySnoreEvent(t, 0));
                break;
            case 0x0b:  // Periodic Breathing, note this is 0xa in F5V3
                // PB events are reported some time after they conclude, and they do have a reported duration.
                duration = 2 * (data[pos] | (data[pos+1] << 8));  // confirmed to double in F5V0
                elapsed = data[pos+2];
                this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
                break;
            case 0x0c:  // Large Leak, note this is 0xb in F5V3
                // LL events are reported some time after they conclude, and they do have a reported duration.
                duration = 2 * (data[pos] | (data[pos+1] << 8));  // confirmed to double in F5V0
                elapsed = data[pos+2];
                this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
                break;
            case 0x0d:  // Statistics
                // These appear every 2 minutes, so presumably summarize the preceding period.
                this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos+0]));              // 00=IPAP
                this->AddEvent(new PRS1IPAPLowEvent(t, data[pos+1]));                  // 01=IAP Low
                this->AddEvent(new PRS1IPAPHighEvent(t, data[pos+2]));                 // 02=IAP High
                this->AddEvent(new PRS1TotalLeakEvent(t, data[pos+3]));                // 03=Total leak (average?)
                this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos+4]));          // 04=Breaths Per Minute (average?)
                this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos+5]));  // 05=Patient Triggered Breaths (average?)
                this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos+6]));        // 06=Minute Ventilation (average?)
                this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos+7]));              // 07=Tidal Volume (average?)
                this->AddEvent(new PRS1SnoreEvent(t, data[pos+8]));                    // 08=Snore count  // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
                this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+9]));              // 09=EPAP average
                this->AddEvent(new PRS1LeakEvent(t, data[pos+0xa]));                   // 0A=Leak (average?) new to F5V1 (originally found in F5V3)
                this->AddEvent(new PRS1IntervalBoundaryEvent(t));
                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 slices, 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;
}


const QVector<PRS1ParsedEventType> ParsedEventsF5V2 = {
    PRS1EPAPSetEvent::TYPE,
    PRS1TimedBreathEvent::TYPE,
    PRS1ObstructiveApneaEvent::TYPE,
    //PRS1ClearAirwayEvent::TYPE,  // not yet seen
    PRS1HypopneaEvent::TYPE,
    PRS1FlowLimitationEvent::TYPE,
    //PRS1VibratorySnoreEvent::TYPE,  // not yet seen
    PRS1PeriodicBreathingEvent::TYPE,
    //PRS1LargeLeakEvent::TYPE,  // not yet seen
    PRS1IPAPAverageEvent::TYPE,
    PRS1IPAPLowEvent::TYPE,
    PRS1IPAPHighEvent::TYPE,
    PRS1TotalLeakEvent::TYPE,
    PRS1RespiratoryRateEvent::TYPE,
    PRS1PatientTriggeredBreathsEvent::TYPE,
    PRS1MinuteVentilationEvent::TYPE,
    PRS1TidalVolumeEvent::TYPE,
    PRS1SnoreEvent::TYPE,
    PRS1EPAPAverageEvent::TYPE,
    PRS1LeakEvent::TYPE,
};

// 960T is F5V2
bool PRS1DataChunk::ParseEventsF5V2(void)
{
    if (this->family != 5 || this->familyVersion != 2) {
        qWarning() << "ParseEventsF5V2 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}, {1,2}, {3,4}, {8,3}, {9,4}, {0xa,3}, {0xb,5}, {0xc,5}, {0xd,5}, {0xe,0xd}, {0xf,5}, {0x10,5}, {0x11,2}, {0x12,6} };

    if (chunk_size < 1) {
        // This does occasionally happen in F0V6.
        qDebug() << this->sessionid << "Empty event data";
        return false;
    }

    // F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
    static const float GAIN = 0.125;  // TODO: this should be parameterized somewhere more logical
    bool ok = true;
    int pos = 0, startpos;
    int code, size;
    int t = 0;
    int elapsed/*, duration, 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 != 0 && code != 0x12) {  // These two codes have no timestamp  TODO: verify this applies to F5V012
            t += data[pos] /*| (data[pos+1] << 8)*/;  // TODO: Is this really only 1 byte?
            if (data[pos+1] != 0) qWarning() << this->sessionid << "nonzero time? byte" << hex(startpos);
            CHECK_VALUE(data[pos+1], 0);
            pos += 2;
        }

        switch (code) {
/*
        case 0x00: // Unknown (ASV Pressure value)
            DUMP_EVENT();
            // offset?
            data0 = data[pos++];

            if (!data[pos - 1]) { // WTH???
                data1 = data[pos++];
            }

            if (!data[pos - 1]) {
                //data2 = data[pos++];
                pos++;
            }

            break;

        case 0x01: // Unknown
            DUMP_EVENT();
            this->AddEvent(new PRS1UnknownValueEvent(code, t, 0, 0.1F));
            break;
*/
            case 0x02:  // Pressure adjustment
                this->AddEvent(new PRS1EPAPSetEvent(t, data[pos++], GAIN));
                break;
/*
        case 0x03: // BIPAP Pressure
            DUMP_EVENT();
            qDebug() << "0x03 Observed in ASV data!!????";

            data0 = data[pos++];
            data1 = data[pos++];
            //            data0/=10.0;
            //            data1/=10.0;
            //            session->AddEvent(new Event(t,CPAP_EAP, 0, data, 1));
            //            session->AddEvent(new Event(t,CPAP_IAP, 0, &data1, 1));
            break;
*/
            case 0x04:  // Timed Breath
                // TB events have a duration in 0.1s, based on the review of pressure waveforms.
                // TODO: Ideally the starting time here would be adjusted here, but PRS1ParsedEvents
                // currently assume integer seconds rather than ms, so that's done at import.
                duration = data[pos];
                this->AddEvent(new PRS1TimedBreathEvent(t, duration));
                break;
            case 0x05:  // Obstructive Apnea
                // OA events are instantaneous flags with no duration: reviewing waveforms
                // shows that the time elapsed between the flag and reporting often includes
                // non-apnea breathing.
                elapsed = data[pos];
                this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0));
                break;
/*
        case 0x06:
            DUMP_EVENT();
            //code=CPAP_ClearAirway;
            data0 = data[pos++];
            this->AddEvent(new PRS1ClearAirwayEvent(t - data0, data0));
            break;
*/

            case 0x07:  // Hypopnea
                // NOTE: No additional (unknown) first byte as in F5V3 0x07, but see below.
                // This seems closer to F5V3 0x0d or 0x0e.
                // What's different about this an 0x08? This was seen in a PB at least once?
                elapsed = data[pos];  // based on sample waveform, the hypopnea is over after this
                this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
                break;
            case 0x08:  // Hypopnea, note this is 0x7 in F5V1
                // TODO: How is this hypopnea different from event 0x9 and 0x7?
                // NOTE: No additional (unknown) first byte as in F5V3 0x7, but see below.
                // This seems closer to F5V3 0x0d or 0x0e.
                elapsed = data[pos];  // based on sample waveform, the hypopnea is over after this
                this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
                break;
/*
        case 0x09: // ASV Codes
            DUMP_EVENT();
            / *
            if (this->familyVersion<2) {
                //code=CPAP_FlowLimit;
                data0 = data[pos++];

                this->AddEvent(new PRS1FlowLimitationEvent(t - data0, data0));
            } else {
            * /
                data0 = data[pos++];
                data1 = data[pos++];
            break;
*/
            case 0x0a:  // Flow Limitation, note this is 0x9 in F5V1 and 0x8 in F5V3
                // 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 0x0b: // Cheyne Stokes
            DUMP_EVENT();
            data0 = ((unsigned char *)data)[pos + 1] << 8 | ((unsigned char *)data)[pos];
            //data0*=2;
            pos += 2;
            data1 = ((unsigned char *)data)[pos]; //|data[pos+1] << 8
            pos += 1;
            //tt-=delta;
            this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0));
            break;
*/
            case 0x0c:  // Periodic Breathing, note this is 0xb in F5V1 and 0xa in F5V3
                // PB events are reported some time after they conclude, and they do have a reported duration.
                duration = 2 * (data[pos] | (data[pos+1] << 8));  // confirmed to double in F5V0
                elapsed = data[pos+2];
                this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
                break;
/*
        case 0x0d:
            DUMP_EVENT();

                data0 = (data[pos + 1] << 8 | data[pos]);
                data0 *= 2;
                pos += 2;
                data1 = data[pos++];
                //tt = t - qint64(data1) * 1000L;
            break;
*/
            case 0x0e:  // Statistics, note this was 0x0d in F5V0 and F5V1
                // These appear every 2 minutes, so presumably summarize the preceding period.
                this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos+0], GAIN));        // 00=IPAP
                this->AddEvent(new PRS1IPAPLowEvent(t, data[pos+1], GAIN));            // 01=IAP Low
                this->AddEvent(new PRS1IPAPHighEvent(t, data[pos+2], GAIN));           // 02=IAP High
                this->AddEvent(new PRS1TotalLeakEvent(t, data[pos+3]));                // 03=Total leak (average?)
                this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos+4]));          // 04=Breaths Per Minute (average?)
                this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos+5]));  // 05=Patient Triggered Breaths (average?)
                this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos+6]));        // 06=Minute Ventilation (average?)
                this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos+7]));              // 07=Tidal Volume (average?)
                this->AddEvent(new PRS1SnoreEvent(t, data[pos+8]));                    // 08=Snore count  // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
                this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+9], GAIN));        // 09=EPAP average
                this->AddEvent(new PRS1LeakEvent(t, data[pos+0xa]));                   // 0A=Leak (average?) new to F5V1 (originally found in F5V3)
                this->AddEvent(new PRS1IntervalBoundaryEvent(t));
                break;
/*
        case 0x0f:
            DUMP_EVENT();
            qDebug() << "0x0f Observed in ASV data!!????";

            data0 = data[pos + 1] << 8 | data[pos];
            pos += 2;
            data1 = data[pos]; //|data[pos+1] << 8
            pos += 1;
            //tt -= qint64(data1) * 1000L;
            //session->AddEvent(new Event(tt,cpapcode, 0, data, 2));
            break;

        case 0x10: // Unknown
            DUMP_EVENT();
            data0 = data[pos + 1] << 8 | data[pos];
            pos += 2;
            data1 = data[pos++];

            this->AddEvent(new PRS1LargeLeakEvent(t - data1, data0));

//            qDebug() << "0x10 Observed in ASV data!!????";
//            data0 = data[pos++]; // << 8) | data[pos];
//            data1 = data[pos++];
//            data2 = data[pos++];
            //session->AddEvent(new Event(t,cpapcode, 0, data, 3));
            break;
        case 0x11: // Not Leak Rate
            DUMP_EVENT();
            qDebug() << "0x11 Observed in ASV data!!????";
            //if (!Code[24]) {
            //   Code[24]=new EventList(cpapcode,EVL_Event);
            //}
            //Code[24]->AddEvent(t,data[pos++]);
            break;


        case 0x12: // Summary
            DUMP_EVENT();
            qDebug() << "0x12 Observed in ASV data!!????";
            data0 = data[pos++];
            data1 = data[pos++];
            //data2 = data[pos + 1] << 8 | data[pos];
            pos += 2;
            //session->AddEvent(new Event(t,cpapcode, 0, data,3));
            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 slices, 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;
}


//********************************************************************************************
// MARK: -
// MARK: DreamStation

// Originally based on ParseSummaryF0V6, with changes observed in ASV sample data
// based on size, slices 0-5 look similar, and it looks like F0V6 slides 8-B are equivalent to 6-9
//
// TODO: surely there will be a way to merge these loops and abstract the machine-specific
// encodings into another function or class, but that's probably worth pursuing only after
// the details have been figured out.
bool PRS1DataChunk::ParseSummaryF5V3(void)
{
    if (this->family != 5 || this->familyVersion != 3) {
        qWarning() << "ParseSummaryF5V3 called with family" << this->family << "familyVersion" << this->familyVersion;
        return false;
    }
    const unsigned char * data = (unsigned char *)this->m_data.constData();
    int chunk_size = this->m_data.size();
    static const int minimum_sizes[] = { 1, 0x35, 9, 4, 2, 4, 0x1e, 2, 4, 9 };
    static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
    // NOTE: The sizes contained in hblock can vary, even within a single machine, as can the length of hblock itself!

    // TODO: hardcoding this is ugly, think of a better approach
    if (chunk_size < minimum_sizes[0] + minimum_sizes[1] + minimum_sizes[2]) {
        qWarning() << this->sessionid << "summary data too short:" << chunk_size;
        return false;
    }
    // We've once seen a short summary with no mask-on/off: just equipment-on, settings, 9, equipment-off
    // (And we've seen something similar in F3V6.)
    if (chunk_size < 75) UNEXPECTED_VALUE(chunk_size, ">= 75");

    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;
        }

        int alarm;
        switch (code) {
            case 0:  // Equipment On
                CHECK_VALUE(pos, 1);  // Always first?
                //CHECK_VALUES(data[pos], 1, 7);  // or 3, or 0?  3 when machine turned on via auto-on, 1 when turned on via button
                CHECK_VALUE(size, 1);
                break;
            case 1:  // Settings
                ok = this->ParseSettingsF5V3(data + pos, size);
                break;
            case 9:  // new to F5V3 vs. F0V6, comes right after settings, before mask on?
                CHECK_VALUE(data[pos], 0);
                CHECK_VALUE(data[pos+1], 1);
                CHECK_VALUE(data[pos+2], 0);
                CHECK_VALUE(data[pos+3], 1);
                CHECK_VALUE(data[pos+4], 1);
                CHECK_VALUE(data[pos+5], 0);
                CHECK_VALUE(data[pos+6], 2);
                CHECK_VALUE(data[pos+7], 1);

                alarm = 0;
                switch (data[pos+8]) {
                    case 1: alarm = 15; break;  // 15 sec
                    case 2: alarm = 60; break;  // 60 sec
                    case 0: break;
                    default:
                        UNEXPECTED_VALUE(data[pos+8], "0-2");
                        break;
                }
                if (alarm) {
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_DISCONNECT_ALARM, alarm));
                }

                CHECK_VALUE(size, 9);
                break;
            case 3:  // Mask On
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
                this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
                break;
            case 4:  // Mask Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
                break;
            case 5:  // ASV pressure stats per mask-on slice
                //CHECK_VALUE(data[pos], 0x28);  // 90% EPAP
                //CHECK_VALUE(data[pos+1], 0x23);  // average EPAP
                //CHECK_VALUE(data[pos+2], 0x24);  // 90% PS
                //CHECK_VALUE(data[pos+3], 0x17);  // average PS
                break;
            case 6:  // Patient statistics per mask-on slice
                // These get averaged on a time-weighted basis in the final report.
                // Where is H count?
                //CHECK_VALUE(data[pos], 0x00);  // probably 16-bit value
                CHECK_VALUE(data[pos+1], 0x00);
                //CHECK_VALUE(data[pos+2], 0x00);  // 16-bit OA count
                //CHECK_VALUE(data[pos+3], 0x00);
                //CHECK_VALUE(data[pos+4], 0x00);  // probably 16-bit value
                CHECK_VALUE(data[pos+5], 0x00);
                //CHECK_VALUE(data[pos+6], 0x00);  // 16-bit CA count
                //CHECK_VALUE(data[pos+7], 0x00);
                //CHECK_VALUE(data[pos+8], 0x00);  // 16-bit minutes in LL
                //CHECK_VALUE(data[pos+9], 0x00);
                //CHECK_VALUE(data[pos+0xa], 0x0f);  // 16-bit minutes in PB
                //CHECK_VALUE(data[pos+0xb], 0x00);
                //CHECK_VALUE(data[pos+0xc], 0x14);  // 16-bit VS count
                //CHECK_VALUE(data[pos+0xd], 0x00);
                //CHECK_VALUE(data[pos+0xe], 0x05);  // 16-bit H count for type 0xd
                //CHECK_VALUE(data[pos+0xf], 0x00);
                //CHECK_VALUE(data[pos+0x10], 0x00);  // 16-bit H count for type 7
                //CHECK_VALUE(data[pos+0x11], 0x00);
                //CHECK_VALUE(data[pos+0x12], 0x02);  // 16-bit FL count
                //CHECK_VALUE(data[pos+0x13], 0x00);
                //CHECK_VALUE(data[pos+0x14], 0x28);  // 0x69 (105)
                //CHECK_VALUE(data[pos+0x15], 0x17);  // average total leak
                //CHECK_VALUE(data[pos+0x16], 0x5b);  // 0x7d (125)
                //CHECK_VALUE(data[pos+0x17], 0x09);  // 16-bit H count for type 0xe
                //CHECK_VALUE(data[pos+0x18], 0x00);
                //CHECK_VALUE(data[pos+0x19], 0x10);  // average breath rate
                //CHECK_VALUE(data[pos+0x1a], 0x2d);  // average TV / 10
                //CHECK_VALUE(data[pos+0x1b], 0x63);  // average % PTB
                //CHECK_VALUE(data[pos+0x1c], 0x07);  // average minute vent
                //CHECK_VALUE(data[pos+0x1d], 0x06);  // average leak
                break;
            case 2:  // Equipment Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
                //CHECK_VALUE(data[pos+2], 0x01);  // 0x08
                //CHECK_VALUE(data[pos+3], 0x17);  // 0x16, 0x18
                //CHECK_VALUE(data[pos+4], 0x00);
                //CHECK_VALUE(data[pos+5], 0x29);  // 0x2a, 0x28, 0x26, 0x36
                //CHECK_VALUE(data[pos+6], 0x01);  // 0x00
                CHECK_VALUE(data[pos+7], 0x00);
                CHECK_VALUE(data[pos+8], 0x00);
                break;
            case 8:  // Humidier setting change
                tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
                this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
                break;
            default:
                UNEXPECTED_VALUE(code, "known slice code");
                break;
        }
        pos += size;
    }

    this->duration = tt;

    return ok;
}


// Based initially on ParseSettingsF0V6. Many of the codes look the same, like always starting with 0, 0x35 looking like
// a humidifier setting, etc., but the contents are sometimes a bit different, such as mode values and pressure settings.
//
// new settings to find: breath rate, tubing lock, alarms,
bool PRS1DataChunk::ParseSettingsF5V3(const unsigned char* data, int size)
{
    static const QMap<int,int> expected_lengths = { {0x0a,5}, /*{0x0c,3}, {0x0d,2}, {0x0e,2}, {0x0f,4}, {0x10,3},*/ {0x14,3}, {0x2e,2}, {0x35,2} };
    bool ok = true;

    PRS1Mode cpapmode = PRS1_MODE_UNKNOWN;

    // F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
    static const float GAIN = 0.125;  // TODO: parameterize this somewhere better

    int max_pressure = 0;
    int min_ps   = 0;
    int max_ps   = 0;
    int min_epap = 0;
    int max_epap = 0;
    int rise_time;
    int breath_rate;
    int timed_inspiration;

    // 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_ASV; break;
                default:
                    UNEXPECTED_VALUE(data[pos], "known device mode");
                    break;
                }
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
                break;
            case 1: // ???
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0, 1);  // 1 when when Opti-Start is on? 0 when off?
                /*
                if (data[pos] != 0 && data[pos] != 3) {
                    CHECK_VALUES(data[pos], 1, 2);  // 1 when EZ-Start is enabled? 2 when Auto-Trial? 3 when Auto-Trial is off or Opti-Start isn't off?
                }
                */
                break;
            case 0x0a:  // ASV with variable EPAP pressure setting
                CHECK_VALUE(len, 5);
                CHECK_VALUE(cpapmode, PRS1_MODE_ASV);
                max_pressure = data[pos];
                min_epap = data[pos+1];
                max_epap = data[pos+2];
                min_ps = data[pos+3];
                max_ps = data[pos+4];
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, min_epap, GAIN));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, max_epap, GAIN));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_epap + min_ps, GAIN));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, qMin(max_pressure, max_epap + max_ps), GAIN));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ps, GAIN));
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, max_ps, GAIN));
                break;
            case 0x14:  // ASV backup rate
                CHECK_VALUE(len, 3);
                CHECK_VALUE(cpapmode, PRS1_MODE_ASV);
                switch (data[pos]) {
                //case 0:  // Breath Rate Off in F3V6 setting 0x1e
                case 1:  // Breath Rate Auto
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Auto));
                    CHECK_VALUE(data[pos+1], 0);  // 0 for auto
                    CHECK_VALUE(data[pos+2], 0);  // 0 for auto
                    break;
                case 2:  // Breath Rate (fixed BPM)
                    breath_rate = data[pos+1];
                    timed_inspiration = data[pos+2];
                    if (breath_rate < 4 || breath_rate > 16) UNEXPECTED_VALUE(breath_rate, "4-16");
                    if (timed_inspiration < 12 || timed_inspiration > 25) UNEXPECTED_VALUE(timed_inspiration, "12-25");
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Fixed));
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_RATE, breath_rate));  // BPM
                    this->AddEvent(new PRS1ScaledSettingEvent(PRS1_SETTING_BACKUP_TIMED_INSPIRATION, timed_inspiration, 0.1));
                    break;
                default:
                    CHECK_VALUES(data[pos], 1, 2);  // 1 = auto, 2 = fixed BPM (0 = off in F3V6 setting 0x1e)
                    break;
                }
                break;
            /*
            case 0x2a:  // EZ-Start
                CHECK_VALUE(data[pos], 0x80);  // EZ-Start enabled
                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 (with ASV pressure encoding)
                CHECK_VALUE(len, 1);
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, data[pos], GAIN));
                break;
            case 0x2e:  // Flex mode and level (ASV variant)
                CHECK_VALUE(len, 2);
                switch (data[pos]) {
                case 0:  // Bi-Flex
                    // [0x00, N] for Bi-Flex level N
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, FLEX_BiFlex));
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[pos+1]));
                    break;
                case 0x20:  // Rise Time
                    // [0x20, 0x03] for no flex, rise time setting = 3, no rise lock
                    rise_time = data[pos+1];
                    if (rise_time < 1 || rise_time > 6) UNEXPECTED_VALUE(rise_time, "1-6");
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME, rise_time));
                    break;
                default:
                    CHECK_VALUES(data[pos], 0, 0x20);
                    break;
                }
                break;
            case 0x2f:  // Flex lock? (was on F0V6, 0x80 for locked)
                CHECK_VALUE(len, 1);
                CHECK_VALUE(data[pos], 0);
                //this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, data[pos] != 0));
                break;
            //case 0x30: ASV puts the flex level in the 0x2e setting for some reason
            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);  // 0x80 = locked
                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:
                CHECK_VALUE(len, 1);
                CHECK_VALUE(data[pos], 0);  // 0x80 maybe auto-trial in F0V6?
                break;
            case 0x3b:  // Tubing Type
                CHECK_VALUE(len, 1);
                if (data[pos] > 2) UNEXPECTED_VALUE(data[pos], "0-2");  // 15HT = 2, 15 = 1, 22 = 0, though report only says "15" for 15HT
                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 0x3d:  // Auto On (ASV variant)
                CHECK_VALUE(len, 1);
                CHECK_VALUES(data[pos], 0, 0x80);
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_ON, data[pos] != 0));
                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> ParsedEventsF5V3 = {
    PRS1EPAPSetEvent::TYPE,
    PRS1TimedBreathEvent::TYPE,
    PRS1IPAPAverageEvent::TYPE,
    PRS1IPAPLowEvent::TYPE,
    PRS1IPAPHighEvent::TYPE,
    PRS1TotalLeakEvent::TYPE,
    PRS1RespiratoryRateEvent::TYPE,
    PRS1PatientTriggeredBreathsEvent::TYPE,
    PRS1MinuteVentilationEvent::TYPE,
    PRS1TidalVolumeEvent::TYPE,
    PRS1SnoreEvent::TYPE,
    PRS1EPAPAverageEvent::TYPE,
    PRS1LeakEvent::TYPE,
    PRS1PressurePulseEvent::TYPE,
    PRS1ObstructiveApneaEvent::TYPE,
    PRS1ClearAirwayEvent::TYPE,
    PRS1HypopneaEvent::TYPE,
    PRS1FlowLimitationEvent::TYPE,
    PRS1VibratorySnoreEvent::TYPE,
    PRS1PeriodicBreathingEvent::TYPE,
    PRS1LargeLeakEvent::TYPE,
};

// Outer loop based on ParseSummaryF5V3 along with hint as to event codes from old ParseEventsF5V3,
// except this actually does something with the data.
bool PRS1DataChunk::ParseEventsF5V3(void)
{
    if (this->family != 5 || this->familyVersion != 3) {
        qWarning() << "ParseEventsF5V3 called with family" << this->family << "familyVersion" << this->familyVersion;
        return false;
    }
    const unsigned char * data = (unsigned char *)this->m_data.constData();
    int chunk_size = this->m_data.size();
    static const int minimum_sizes[] = { 2, 3, 3, 0xd, 3, 3, 3, 4, 3, 2, 5, 5, 3, 3, 3, 3 };
    static const int ncodes = sizeof(minimum_sizes) / sizeof(int);

    if (chunk_size < 1) {
        // This does occasionally happen.
        qDebug() << this->sessionid << "Empty event data";
        return false;
    }

    // F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
    static const float GAIN = 0.125;  // TODO: this should be parameterized somewhere more logical
    bool ok = true;
    int pos = 0, startpos;
    int code, size;
    int t = 0;
    int elapsed, duration;
    do {
        code = data[pos++];
        if (!this->hblock.contains(code)) {
            qWarning() << this->sessionid << "missing hblock entry for event" << code;
            ok = false;
            break;
        }
        size = this->hblock[code];
        if (code < ncodes) {
            // make sure the handlers below don't go past the end of the buffer
            if (size < minimum_sizes[code]) {
                qWarning() << this->sessionid << "event" << code << "too small" << size << "<" << minimum_sizes[code];
                ok = false;
                break;
            }
        } // else if it's past ncodes, we'll log its information below (rather than handle it)
        if (pos + size > chunk_size) {
            qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk";
            ok = false;
            break;
        }
        startpos = pos;
        t += data[pos] | (data[pos+1] << 8);
        pos += 2;

        switch (code) {
            case 1:  // Pressure adjustment
                this->AddEvent(new PRS1EPAPSetEvent(t, data[pos++], GAIN));
                this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));  // TODO: what is this?
                break;
            case 2:  // Timed Breath
                // TB events have a duration in 0.1s, based on the review of pressure waveforms.
                // TODO: Ideally the starting time here would be adjusted here, but PRS1ParsedEvents
                // currently assume integer seconds rather than ms, so that's done at import.
                duration = data[pos];
                this->AddEvent(new PRS1TimedBreathEvent(t, duration));
                break;
            case 3:  // Statistics
                // These appear every 2 minutes, so presumably summarize the preceding period.
                this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos+0], GAIN));        // 00=IPAP
                this->AddEvent(new PRS1IPAPLowEvent(t, data[pos+1], GAIN));            // 01=IAP Low
                this->AddEvent(new PRS1IPAPHighEvent(t, data[pos+2], GAIN));           // 02=IAP High
                this->AddEvent(new PRS1TotalLeakEvent(t, data[pos+3]));                // 03=Total leak (average?)
                this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos+4]));          // 04=Breaths Per Minute (average?)
                this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos+5]));  // 05=Patient Triggered Breaths (average?)
                this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos+6]));        // 06=Minute Ventilation (average?)
                this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos+7]));              // 07=Tidal Volume (average?)
                this->AddEvent(new PRS1SnoreEvent(t, data[pos+8]));                    // 08=Snore count  // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
                this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+9], GAIN));        // 09=EPAP average
                this->AddEvent(new PRS1LeakEvent(t, data[pos+0xa]));                   // 0A=Leak (average?)
                this->AddEvent(new PRS1IntervalBoundaryEvent(t));
                break;
            case 0x04:  // Pressure Pulse
                duration = data[pos];  // TODO: is this a duration?
                this->AddEvent(new PRS1PressurePulseEvent(t, duration));
                break;
            case 0x05:  // Obstructive Apnea
                // OA events are instantaneous flags with no duration: reviewing waveforms
                // shows that the time elapsed between the flag and reporting often includes
                // non-apnea breathing.
                elapsed = data[pos];
                this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0));
                break;
            case 0x06:  // Clear Airway Apnea
                // CA events are instantaneous flags with no duration: reviewing waveforms
                // shows that the time elapsed between the flag and reporting often includes
                // non-apnea breathing.
                elapsed = data[pos];
                this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0));
                break;
            case 0x07:  // Hypopnea
                // TODO: How is this hypopnea different from events 0xd and 0xe?
                // TODO: What is the first byte?
                //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 0x08:  // Flow Limitation
                // TODO: We should revisit whether this is elapsed or duration once (if)
                // we start calculating flow limitations ourselves. Flow limitations aren't
                // as obvious as OA/CA when looking at a waveform.
                elapsed = data[pos];
                this->AddEvent(new PRS1FlowLimitationEvent(t - elapsed, 0));
                break;
            case 0x09:  // Vibratory Snore
                // VS events are instantaneous flags with no duration, drawn on the official waveform.
                // The current thinking is that these are the snores that cause a change in auto-titrating
                // pressure. The snoring statistic above seems to be a total count. It's unclear whether
                // the trigger for pressure change is severity or count or something else.
                // no data bytes
                this->AddEvent(new PRS1VibratorySnoreEvent(t, 0));
                break;
            case 0x0a:  // Periodic Breathing
                // PB events are reported some time after they conclude, and they do have a reported duration.
                duration = 2 * (data[pos] | (data[pos+1] << 8));
                elapsed = data[pos+2];
                this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
                break;
            case 0x0b:  // Large Leak
                // LL events are reported some time after they conclude, and they do have a reported duration.
                duration = 2 * (data[pos] | (data[pos+1] << 8));
                elapsed = data[pos+2];
                this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
                break;
            case 0x0d:  // Hypopnea
                // TODO: Why does this hypopnea have a different event code?
                // fall through
            case 0x0e:  // Hypopnea
                // TODO: We should revisit whether this is elapsed or duration once (if)
                // we start calculating hypopneas ourselves. Their official definition
                // is 40% reduction in flow lasting at least 10s.
                duration = data[pos];
                this->AddEvent(new PRS1HypopneaEvent(t - duration, 0));
                break;
            case 0x0f:
                // TODO: some other pressure adjustment?
                // Appears near the beginning and end of a session when Opti-Start is on, at least once in middle
                //CHECK_VALUES(data[pos], 0x20, 0x28);
                this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
                break;
            default:
                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;
}
