/* SleepLib PRS1 Loader Implementation
 *
 * Copyright (c) 2019-2020 The OSCAR Team
 * 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 <QApplication>
#include <QString>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QDataStream>
#include <QMessageBox>
#include <QDebug>
#include <cmath>

#include "SleepLib/schema.h"
#include "prs1_loader.h"
#include "SleepLib/session.h"
#include "SleepLib/calcs.h"


// Disable this to cut excess debug messages

#define DEBUG_SUMMARY


//const int PRS1_MAGIC_NUMBER = 2;
//const int PRS1_SUMMARY_FILE=1;
//const int PRS1_EVENT_FILE=2;
//const int PRS1_WAVEFORM_FILE=5;

const int PRS1_HTYPE_NORMAL=0;
const int PRS1_HTYPE_INTERVAL=1;


//********************************************************************************************
/// IMPORTANT!!!
//********************************************************************************************
// Please INCREMENT the prs1_data_version in prs1_loader.h when making changes to this loader
// that change loader behaviour or modify channels.
//********************************************************************************************


// CRC-16/KERMIT, polynomial: 0x11021, bit reverse algorithm
// Table generated by crcmod (crc-kermit)

typedef quint16 crc16_t;
static crc16_t CRC16(unsigned char * data, size_t data_len, crc16_t crc=0)
{
    static const crc16_t table[256] = {
    0x0000U, 0x1189U, 0x2312U, 0x329bU, 0x4624U, 0x57adU, 0x6536U, 0x74bfU,
    0x8c48U, 0x9dc1U, 0xaf5aU, 0xbed3U, 0xca6cU, 0xdbe5U, 0xe97eU, 0xf8f7U,
    0x1081U, 0x0108U, 0x3393U, 0x221aU, 0x56a5U, 0x472cU, 0x75b7U, 0x643eU,
    0x9cc9U, 0x8d40U, 0xbfdbU, 0xae52U, 0xdaedU, 0xcb64U, 0xf9ffU, 0xe876U,
    0x2102U, 0x308bU, 0x0210U, 0x1399U, 0x6726U, 0x76afU, 0x4434U, 0x55bdU,
    0xad4aU, 0xbcc3U, 0x8e58U, 0x9fd1U, 0xeb6eU, 0xfae7U, 0xc87cU, 0xd9f5U,
    0x3183U, 0x200aU, 0x1291U, 0x0318U, 0x77a7U, 0x662eU, 0x54b5U, 0x453cU,
    0xbdcbU, 0xac42U, 0x9ed9U, 0x8f50U, 0xfbefU, 0xea66U, 0xd8fdU, 0xc974U,
    0x4204U, 0x538dU, 0x6116U, 0x709fU, 0x0420U, 0x15a9U, 0x2732U, 0x36bbU,
    0xce4cU, 0xdfc5U, 0xed5eU, 0xfcd7U, 0x8868U, 0x99e1U, 0xab7aU, 0xbaf3U,
    0x5285U, 0x430cU, 0x7197U, 0x601eU, 0x14a1U, 0x0528U, 0x37b3U, 0x263aU,
    0xdecdU, 0xcf44U, 0xfddfU, 0xec56U, 0x98e9U, 0x8960U, 0xbbfbU, 0xaa72U,
    0x6306U, 0x728fU, 0x4014U, 0x519dU, 0x2522U, 0x34abU, 0x0630U, 0x17b9U,
    0xef4eU, 0xfec7U, 0xcc5cU, 0xddd5U, 0xa96aU, 0xb8e3U, 0x8a78U, 0x9bf1U,
    0x7387U, 0x620eU, 0x5095U, 0x411cU, 0x35a3U, 0x242aU, 0x16b1U, 0x0738U,
    0xffcfU, 0xee46U, 0xdcddU, 0xcd54U, 0xb9ebU, 0xa862U, 0x9af9U, 0x8b70U,
    0x8408U, 0x9581U, 0xa71aU, 0xb693U, 0xc22cU, 0xd3a5U, 0xe13eU, 0xf0b7U,
    0x0840U, 0x19c9U, 0x2b52U, 0x3adbU, 0x4e64U, 0x5fedU, 0x6d76U, 0x7cffU,
    0x9489U, 0x8500U, 0xb79bU, 0xa612U, 0xd2adU, 0xc324U, 0xf1bfU, 0xe036U,
    0x18c1U, 0x0948U, 0x3bd3U, 0x2a5aU, 0x5ee5U, 0x4f6cU, 0x7df7U, 0x6c7eU,
    0xa50aU, 0xb483U, 0x8618U, 0x9791U, 0xe32eU, 0xf2a7U, 0xc03cU, 0xd1b5U,
    0x2942U, 0x38cbU, 0x0a50U, 0x1bd9U, 0x6f66U, 0x7eefU, 0x4c74U, 0x5dfdU,
    0xb58bU, 0xa402U, 0x9699U, 0x8710U, 0xf3afU, 0xe226U, 0xd0bdU, 0xc134U,
    0x39c3U, 0x284aU, 0x1ad1U, 0x0b58U, 0x7fe7U, 0x6e6eU, 0x5cf5U, 0x4d7cU,
    0xc60cU, 0xd785U, 0xe51eU, 0xf497U, 0x8028U, 0x91a1U, 0xa33aU, 0xb2b3U,
    0x4a44U, 0x5bcdU, 0x6956U, 0x78dfU, 0x0c60U, 0x1de9U, 0x2f72U, 0x3efbU,
    0xd68dU, 0xc704U, 0xf59fU, 0xe416U, 0x90a9U, 0x8120U, 0xb3bbU, 0xa232U,
    0x5ac5U, 0x4b4cU, 0x79d7U, 0x685eU, 0x1ce1U, 0x0d68U, 0x3ff3U, 0x2e7aU,
    0xe70eU, 0xf687U, 0xc41cU, 0xd595U, 0xa12aU, 0xb0a3U, 0x8238U, 0x93b1U,
    0x6b46U, 0x7acfU, 0x4854U, 0x59ddU, 0x2d62U, 0x3cebU, 0x0e70U, 0x1ff9U,
    0xf78fU, 0xe606U, 0xd49dU, 0xc514U, 0xb1abU, 0xa022U, 0x92b9U, 0x8330U,
    0x7bc7U, 0x6a4eU, 0x58d5U, 0x495cU, 0x3de3U, 0x2c6aU, 0x1ef1U, 0x0f78U,
    };

    for (size_t i=0; i < data_len; i++) {
        crc = table[(*data ^ (unsigned char)crc) & 0xFF] ^ (crc >> 8);
        data++;
    }
    return crc;
}


// CRC-32/MPEG-2, polynomial: 0x104C11DB7
// Table generated by crcmod (crc-32-mpeg)

typedef quint32 crc32_t;
static crc32_t CRC32(const unsigned char *data, size_t data_len, crc32_t crc=0xffffffffU)
{
    static const crc32_t table[256] = {
    0x00000000U, 0x04c11db7U, 0x09823b6eU, 0x0d4326d9U,
    0x130476dcU, 0x17c56b6bU, 0x1a864db2U, 0x1e475005U,
    0x2608edb8U, 0x22c9f00fU, 0x2f8ad6d6U, 0x2b4bcb61U,
    0x350c9b64U, 0x31cd86d3U, 0x3c8ea00aU, 0x384fbdbdU,
    0x4c11db70U, 0x48d0c6c7U, 0x4593e01eU, 0x4152fda9U,
    0x5f15adacU, 0x5bd4b01bU, 0x569796c2U, 0x52568b75U,
    0x6a1936c8U, 0x6ed82b7fU, 0x639b0da6U, 0x675a1011U,
    0x791d4014U, 0x7ddc5da3U, 0x709f7b7aU, 0x745e66cdU,
    0x9823b6e0U, 0x9ce2ab57U, 0x91a18d8eU, 0x95609039U,
    0x8b27c03cU, 0x8fe6dd8bU, 0x82a5fb52U, 0x8664e6e5U,
    0xbe2b5b58U, 0xbaea46efU, 0xb7a96036U, 0xb3687d81U,
    0xad2f2d84U, 0xa9ee3033U, 0xa4ad16eaU, 0xa06c0b5dU,
    0xd4326d90U, 0xd0f37027U, 0xddb056feU, 0xd9714b49U,
    0xc7361b4cU, 0xc3f706fbU, 0xceb42022U, 0xca753d95U,
    0xf23a8028U, 0xf6fb9d9fU, 0xfbb8bb46U, 0xff79a6f1U,
    0xe13ef6f4U, 0xe5ffeb43U, 0xe8bccd9aU, 0xec7dd02dU,
    0x34867077U, 0x30476dc0U, 0x3d044b19U, 0x39c556aeU,
    0x278206abU, 0x23431b1cU, 0x2e003dc5U, 0x2ac12072U,
    0x128e9dcfU, 0x164f8078U, 0x1b0ca6a1U, 0x1fcdbb16U,
    0x018aeb13U, 0x054bf6a4U, 0x0808d07dU, 0x0cc9cdcaU,
    0x7897ab07U, 0x7c56b6b0U, 0x71159069U, 0x75d48ddeU,
    0x6b93dddbU, 0x6f52c06cU, 0x6211e6b5U, 0x66d0fb02U,
    0x5e9f46bfU, 0x5a5e5b08U, 0x571d7dd1U, 0x53dc6066U,
    0x4d9b3063U, 0x495a2dd4U, 0x44190b0dU, 0x40d816baU,
    0xaca5c697U, 0xa864db20U, 0xa527fdf9U, 0xa1e6e04eU,
    0xbfa1b04bU, 0xbb60adfcU, 0xb6238b25U, 0xb2e29692U,
    0x8aad2b2fU, 0x8e6c3698U, 0x832f1041U, 0x87ee0df6U,
    0x99a95df3U, 0x9d684044U, 0x902b669dU, 0x94ea7b2aU,
    0xe0b41de7U, 0xe4750050U, 0xe9362689U, 0xedf73b3eU,
    0xf3b06b3bU, 0xf771768cU, 0xfa325055U, 0xfef34de2U,
    0xc6bcf05fU, 0xc27dede8U, 0xcf3ecb31U, 0xcbffd686U,
    0xd5b88683U, 0xd1799b34U, 0xdc3abdedU, 0xd8fba05aU,
    0x690ce0eeU, 0x6dcdfd59U, 0x608edb80U, 0x644fc637U,
    0x7a089632U, 0x7ec98b85U, 0x738aad5cU, 0x774bb0ebU,
    0x4f040d56U, 0x4bc510e1U, 0x46863638U, 0x42472b8fU,
    0x5c007b8aU, 0x58c1663dU, 0x558240e4U, 0x51435d53U,
    0x251d3b9eU, 0x21dc2629U, 0x2c9f00f0U, 0x285e1d47U,
    0x36194d42U, 0x32d850f5U, 0x3f9b762cU, 0x3b5a6b9bU,
    0x0315d626U, 0x07d4cb91U, 0x0a97ed48U, 0x0e56f0ffU,
    0x1011a0faU, 0x14d0bd4dU, 0x19939b94U, 0x1d528623U,
    0xf12f560eU, 0xf5ee4bb9U, 0xf8ad6d60U, 0xfc6c70d7U,
    0xe22b20d2U, 0xe6ea3d65U, 0xeba91bbcU, 0xef68060bU,
    0xd727bbb6U, 0xd3e6a601U, 0xdea580d8U, 0xda649d6fU,
    0xc423cd6aU, 0xc0e2d0ddU, 0xcda1f604U, 0xc960ebb3U,
    0xbd3e8d7eU, 0xb9ff90c9U, 0xb4bcb610U, 0xb07daba7U,
    0xae3afba2U, 0xaafbe615U, 0xa7b8c0ccU, 0xa379dd7bU,
    0x9b3660c6U, 0x9ff77d71U, 0x92b45ba8U, 0x9675461fU,
    0x8832161aU, 0x8cf30badU, 0x81b02d74U, 0x857130c3U,
    0x5d8a9099U, 0x594b8d2eU, 0x5408abf7U, 0x50c9b640U,
    0x4e8ee645U, 0x4a4ffbf2U, 0x470cdd2bU, 0x43cdc09cU,
    0x7b827d21U, 0x7f436096U, 0x7200464fU, 0x76c15bf8U,
    0x68860bfdU, 0x6c47164aU, 0x61043093U, 0x65c52d24U,
    0x119b4be9U, 0x155a565eU, 0x18197087U, 0x1cd86d30U,
    0x029f3d35U, 0x065e2082U, 0x0b1d065bU, 0x0fdc1becU,
    0x3793a651U, 0x3352bbe6U, 0x3e119d3fU, 0x3ad08088U,
    0x2497d08dU, 0x2056cd3aU, 0x2d15ebe3U, 0x29d4f654U,
    0xc5a92679U, 0xc1683bceU, 0xcc2b1d17U, 0xc8ea00a0U,
    0xd6ad50a5U, 0xd26c4d12U, 0xdf2f6bcbU, 0xdbee767cU,
    0xe3a1cbc1U, 0xe760d676U, 0xea23f0afU, 0xeee2ed18U,
    0xf0a5bd1dU, 0xf464a0aaU, 0xf9278673U, 0xfde69bc4U,
    0x89b8fd09U, 0x8d79e0beU, 0x803ac667U, 0x84fbdbd0U,
    0x9abc8bd5U, 0x9e7d9662U, 0x933eb0bbU, 0x97ffad0cU,
    0xafb010b1U, 0xab710d06U, 0xa6322bdfU, 0xa2f33668U,
    0xbcb4666dU, 0xb8757bdaU, 0xb5365d03U, 0xb1f740b4U,
    };
    
    for (size_t i=0; i < data_len; i++) {
        crc = table[(*data ^ (unsigned char)(crc >> 24)) & 0xFF] ^ (crc << 8);
        data++;
    }
    return crc;
}


// Strangely, the PRS1 CRC32 appears to consider every byte a 32-bit wchar_t.
// Nothing like trying a bunch of encodings and CRC32 variants on PROP.TXT files
// until you find a winner.

static crc32_t CRC32wchar(const unsigned char *data, size_t data_len, crc32_t crc=0xffffffffU)
{
    for (size_t i=0; i < data_len; i++) {
        static unsigned char wch[4] = { 0, 0, 0, 0 };
        wch[3] = *data++;
        crc = CRC32(wch, 4, crc);
    }
    return crc;
}


static QString ts(qint64 msecs)
{
    // TODO: make this UTC so that tests don't vary by where they're run
    return QDateTime::fromMSecsSinceEpoch(msecs).toString(Qt::ISODate);
}


// TODO: See the LogUnexpectedMessage TODO about generalizing this for other loaders.
// Right now this macro assumes that it's called within a method that has a "loader" member
// that points to the PRS1Loader* instance that's calling it.
#define UNEXPECTED_VALUE(SRC, VALS) { \
    QString message = QString("%1:%2: %3 = %4 != %5").arg(__func__).arg(__LINE__).arg(#SRC).arg(SRC).arg(VALS); \
    qWarning() << this->sessionid << message; \
    loader->LogUnexpectedMessage(message); \
    }
#define CHECK_VALUE(SRC, VAL) if ((SRC) != (VAL)) UNEXPECTED_VALUE(SRC, VAL)
#define CHECK_VALUES(SRC, VAL1, VAL2) if ((SRC) != (VAL1) && (SRC) != (VAL2)) UNEXPECTED_VALUE(SRC, #VAL1 " or " #VAL2)
// for more than 2 values, just write the test manually and use UNEXPECTED_VALUE if it fails
#define HEX(SRC) { qWarning() << this->sessionid << QString("%1:%2: %3 = %4").arg(__func__).arg(__LINE__).arg(#SRC).arg((SRC & 0xFF), 2, 16, QChar('0')); }

void PRS1Loader::LogUnexpectedMessage(const QString & message)
{
    m_importMutex.lock();
    m_unexpectedMessages += message;
    m_importMutex.unlock();
}


enum FlexMode { FLEX_None, FLEX_CFlex, FLEX_CFlexPlus, FLEX_AFlex, FLEX_RiseTime, FLEX_BiFlex, FLEX_PFlex, FLEX_Flex, FLEX_Unknown = -1  };

enum BackupBreathMode { PRS1Backup_Off, PRS1Backup_Auto, PRS1Backup_Fixed };

enum HumidMode { HUMID_Fixed, HUMID_Adaptive, HUMID_HeatedTube, HUMID_Passover, HUMID_Error };

ChannelID PRS1_Mode = 0;
ChannelID PRS1_TimedBreath = 0, PRS1_HumidMode = 0, PRS1_TubeTemp = 0;
ChannelID PRS1_FlexLock = 0, PRS1_TubeLock = 0, PRS1_RampType = 0;
ChannelID PRS1_BackupBreathMode = 0, PRS1_BackupBreathRate = 0, PRS1_BackupBreathTi = 0;
ChannelID PRS1_AutoTrial = 0, PRS1_EZStart = 0, PRS1_RiseTime = 0, PRS1_RiseTimeLock = 0;
ChannelID PRS1_PeakFlow = 0;
ChannelID PRS1_VariableBreathing = 0;  // TODO: UNCONFIRMED, but seems to match sample data

QString PRS1Loader::PresReliefLabel() { return QObject::tr(""); }
ChannelID PRS1Loader::PresReliefMode() { return PRS1_FlexMode; }
ChannelID PRS1Loader::PresReliefLevel() { return PRS1_FlexLevel; }
ChannelID PRS1Loader::CPAPModeChannel() { return PRS1_Mode; }
ChannelID PRS1Loader::HumidifierConnected() { return PRS1_HumidStatus; }
ChannelID PRS1Loader::HumidifierLevel() { return PRS1_HumidLevel; }


struct PRS1TestedModel
{
    QString model;
    int family;
    int familyVersion;
    const char* name;
};

static const PRS1TestedModel s_PRS1TestedModels[] = {
    // This first set says "(Philips Respironics)" intead of "(System One)" on official reports.
    { "251P", 0, 2, "REMstar Plus (System One)" },  // (brick)
    { "450P", 0, 2, "REMstar Pro (System One)" },
    { "450P", 0, 3, "REMstar Pro (System One)" },
    { "451P", 0, 2, "REMstar Pro (System One)" },
    { "451P", 0, 3, "REMstar Pro (System One)" },
    { "452P", 0, 3, "REMstar Pro (System One)" },
    { "550P", 0, 2, "REMstar Auto (System One)" },
    { "550P", 0, 3, "REMstar Auto (System One)" },
    { "551P", 0, 2, "REMstar Auto (System One)" },
    { "650P", 0, 2, "BiPAP Pro (System One)" },
    { "750P", 0, 2, "BiPAP Auto (System One)" },

    { "261CA",  0, 4, "REMstar Plus (System One 60 Series)" },  // (brick)
    { "460P",   0, 4, "REMstar Pro (System One 60 Series)" },
    { "461P",   0, 4, "REMstar Pro (System One 60 Series)" },
    { "462P",   0, 4, "REMstar Pro (System One 60 Series)" },
    { "461CA",  0, 4, "REMstar Pro (System One 60 Series)" },
    { "560P",   0, 4, "REMstar Auto (System One 60 Series)" },
    { "560PBT", 0, 4, "REMstar Auto (System One 60 Series)" },
    { "561P",   0, 4, "REMstar Auto (System One 60 Series)" },
    { "562P",   0, 4, "REMstar Auto (System One 60 Series)" },
    { "660P",   0, 4, "BiPAP Pro (System One 60 Series)" },
    { "760P",   0, 4, "BiPAP Auto (System One 60 Series)" },
    
    { "501V",   0, 5, "Dorma 500 Auto (System One 60 Series)" },  // (brick)

    { "200X110", 0, 6, "DreamStation CPAP" },  // (brick)
    { "400G110", 0, 6, "DreamStation Go" },
    { "400X110", 0, 6, "DreamStation CPAP Pro" },
    { "400X120", 0, 6, "DreamStation CPAP Pro" },
    { "400X130", 0, 6, "DreamStation CPAP Pro" },
    { "400X150", 0, 6, "DreamStation CPAP Pro" },
    { "500X110", 0, 6, "DreamStation Auto CPAP" },
    { "500X120", 0, 6, "DreamStation Auto CPAP" },
    { "500X130", 0, 6, "DreamStation Auto CPAP" },
    { "500X150", 0, 6, "DreamStation Auto CPAP" },
    { "500X180", 0, 6, "DreamStation Auto CPAP" },
    { "501X120", 0, 6, "DreamStation Auto CPAP with P-Flex" },
    { "500G110", 0, 6, "DreamStation Go Auto" },
    { "502G150", 0, 6, "DreamStation Go Auto" },
    { "600X110", 0, 6, "DreamStation BiPAP Pro" },
    { "600X150", 0, 6, "DreamStation BiPAP Pro" },
    { "700X110", 0, 6, "DreamStation Auto BiPAP" },
    { "700X120", 0, 6, "DreamStation Auto BiPAP" },
    { "700X150", 0, 6, "DreamStation Auto BiPAP" },
    
    { "950P",    5, 0, "BiPAP AutoSV Advanced System One" },
    { "951P",    5, 0, "BiPAP AutoSV Advanced System One" },
    { "960P",    5, 1, "BiPAP autoSV Advanced (System One 60 Series)" },
    { "961P",    5, 1, "BiPAP autoSV Advanced (System One 60 Series)" },
    { "960T",    5, 2, "BiPAP autoSV Advanced 30 (System One 60 Series)" },  // omits "(System One 60 Series)" on official reports
    { "900X110", 5, 3, "DreamStation BiPAP autoSV" },
    { "900X120", 5, 3, "DreamStation BiPAP autoSV" },
    { "900X150", 5, 3, "DreamStation BiPAP autoSV" },
    
    { "1061401",  3, 0, "BiPAP S/T (C Series)" },
    { "1061T",    3, 3, "BiPAP S/T 30 (System One 60 Series)" },
    { "1160P",    3, 3, "BiPAP AVAPS 30 (System One 60 Series)" },
    { "1030X110", 3, 6, "DreamStation BiPAP S/T 30" },
    { "1030X150", 3, 6, "DreamStation BiPAP S/T 30 with AAM" },
    { "1130X110", 3, 6, "DreamStation BiPAP AVAPS 30" },
    { "1131X150", 3, 6, "DreamStation BiPAP AVAPS 30 AE" },
    
    { "", 0, 0, "" },
};
PRS1ModelInfo s_PRS1ModelInfo;

PRS1ModelInfo::PRS1ModelInfo()
{
    for (int i = 0; !s_PRS1TestedModels[i].model.isEmpty(); i++) {
        const PRS1TestedModel & model = s_PRS1TestedModels[i];
        m_testedModels[model.family][model.familyVersion].append(model.model);
        
        m_modelNames[model.model] = model.name;
    }
    
    m_bricks = { "251P", "261CA", "200X110", "501V" };
}

bool PRS1ModelInfo::IsSupported(int family, int familyVersion) const
{
    if (m_testedModels.value(family).contains(familyVersion)) {
        return true;
    }
    return false;
}

bool PRS1ModelInfo::IsTested(const QString & model, int family, int familyVersion) const
{
    if (m_testedModels.value(family).value(familyVersion).contains(model)) {
        return true;
    }
    // Some 500X150 C0/C1 folders have contained this bogus model number in their PROP.TXT file,
    // with the same serial number seen in the main PROP.TXT file that shows the real model number.
    if (model == "100X100") {
#ifndef UNITTEST_MODE
        qDebug() << "Ignoring 100X100 for untested alert";
#endif
        return true;
    }
    return false;
};

bool PRS1ModelInfo::IsSupported(const QHash<QString,QString> & props) const
{
    bool ok;
    int family = props["Family"].toInt(&ok, 10);
    if (ok) {
        int familyVersion = props["FamilyVersion"].toInt(&ok, 10);
        if (ok) {
            ok = IsSupported(family, familyVersion);
        }
    }
    return ok;
}

bool PRS1ModelInfo::IsTested(const QHash<QString,QString> & props) const
{
    bool ok;
    int family = props["Family"].toInt(&ok, 10);
    if (ok) {
        int familyVersion = props["FamilyVersion"].toInt(&ok, 10);
        if (ok) {
            ok = IsTested(props["ModelNumber"], family, familyVersion);
        }
    }
    return ok;
};

bool PRS1ModelInfo::IsBrick(const QString & model) const
{
    bool is_brick;
    
    if (m_modelNames.contains(model)) {
        is_brick = m_bricks.contains(model);
    } else {
        // If we haven't seen it before, assume any 2xx is a brick.
        is_brick = (model.at(0) == QChar('2'));
    }
    
    return is_brick;
};

const char* PRS1ModelInfo::Name(const QString & model) const
{
    const char* name;
    if (m_modelNames.contains(model)) {
        name = m_modelNames[model];
    } else {
        name = "Unknown Model";
    }
    return name;
};

QMap<const char*,const char*> s_PRS1Series = {
    { "System One 60 Series", ":/icons/prs1_60s.png" },  // needs to come before following substring
    { "System One",           ":/icons/prs1.png" },
    { "C Series",             ":/icons/prs1vent.png" },
    { "DreamStation",         ":/icons/dreamstation.png" },
};

PRS1Loader::PRS1Loader()
{
#ifndef UNITTEST_MODE  // no QPixmap without a QGuiApplication
    for (auto & series : s_PRS1Series.keys()) {
        QString path = s_PRS1Series[series];
        m_pixmap_paths[series] = path;
        m_pixmaps[series] = QPixmap(path);
    }
#endif

    m_type = MT_CPAP;
}

PRS1Loader::~PRS1Loader()
{
}

bool isdigit(QChar c)
{
    if ((c >= '0') && (c <= '9')) { return true; }

    return false;
}


// Tests path to see if it has (what looks like) a valid PRS1 folder structure
// This is used both to detect newly inserted media and to decide which loader
// to use after the user selects a folder.
//
// TODO: Ideally there should be a way to handle the two scenarios slightly
// differently. In the latter case, it should clean up the selection and
// return the canonical path if it detects one, allowing us to remove the
// notification about selecting the root of the card. That kind of cleanup
// wouldn't be appropriate when scanning devices.
bool PRS1Loader::Detect(const QString & selectedPath)
{
    QString path = selectedPath;
    if (GetPSeriesPath(path).isEmpty()) {
        // Try up one level in case the user selected the P-Series folder within the SD card.
        path = QFileInfo(path).canonicalPath();
    }

    QStringList machines = FindMachinesOnCard(path);
    return !machines.isEmpty();
}

QString PRS1Loader::GetPSeriesPath(const QString & path)
{
    QString outpath = "";
    QDir root(path);
    QStringList dirs = root.entryList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Hidden | QDir::NoSymLinks);
    for (auto & dir : dirs) {
        // We've seen P-Series, P-SERIES, and p-series, so we need to search for the directory
        // in a way that won't break on a case-sensitive filesystem.
        if (dir.toUpper() == "P-SERIES") {
            outpath = path + QDir::separator() + dir;
            break;
        }
    }
    return outpath;
}

QStringList PRS1Loader::FindMachinesOnCard(const QString & cardPath)
{
    QStringList machinePaths;

    QString pseriesPath = this->GetPSeriesPath(cardPath);
    QDir pseries(pseriesPath);

    // If it contains a P-Series folder, it's a PRS1 SD card
    if (!pseriesPath.isEmpty() && pseries.exists()) {
        pseries.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
        pseries.setSorting(QDir::Name);
        QFileInfoList plist = pseries.entryInfoList();

        // Look for machine directories (containing a PROP.TXT or properties.txt)
        QFileInfoList propertyfiles;
        for (auto & pfi : plist) {
            if (pfi.isDir()) {
                QString machinePath = pfi.canonicalFilePath();
                QDir machineDir(machinePath);
                QFileInfoList mlist = machineDir.entryInfoList();
                for (auto & mfi : mlist) {
                    if (QDir::match("PROP*.TXT", mfi.fileName())) {
                        // Found a properties file, this is a machine folder
                        propertyfiles.append(mfi);
                    }
                }
            }
        }

        // Sort machines from oldest to newest.
        std::sort(propertyfiles.begin(), propertyfiles.end(),
            [](const QFileInfo & a, const QFileInfo & b)
        {
            return a.lastModified() < b.lastModified();
        });

        for (auto & propertyfile : propertyfiles) {
            machinePaths.append(propertyfile.canonicalPath());
        }
    }

    return machinePaths;
}


void parseModel(MachineInfo & info, const QString & modelnum)
{
    info.modelnumber = modelnum;

    const char* name = s_PRS1ModelInfo.Name(modelnum);
    const char* series = nullptr;
    for (auto & s : s_PRS1Series.keys()) {
        if (QString(name).contains(s)) {
            series = s;
            break;
        }
    }
    if (series == nullptr) {
        if (modelnum != "100X100") {  // Bogus model number seen in empty C0/Clear0 directories.
            qWarning() << "unknown series for" << name << modelnum;
        }
        series = "unknown";
    }
    
    info.model = QObject::tr(name);
    info.series = series;
}

bool PRS1Loader::PeekProperties(const QString & filename, QHash<QString,QString> & props)
{
    const static QMap<QString,QString> s_longFieldNames = {
        // CF?
        { "SN", "SerialNumber" },
        { "MN", "ModelNumber" },
        { "PT", "ProductType" },
        { "DF", "DataFormat" },
        { "DFV", "DataFormatVersion" },
        { "F", "Family" },
        { "FV", "FamilyVersion" },
        { "SV", "SoftwareVersion" },
        { "FD", "FirstDate" },
        { "LD", "LastDate" },
        // SID?
        // SK?
        { "BK", "BasicKey" },
        { "DK", "DetailsKey" },
        { "EK", "ErrorKey" },
        { "FN", "PatientFolderNum" },  // most recent Pn directory
        { "PFN", "PatientFileNum" },   // number of files in the most recent Pn directory
        { "EFN", "EquipFileNum" },     // number of .004 files in the E directory
        { "DFN", "DFileNum" },         // number of .003 files in the D directory
        { "VC", "ValidCheck" },
    };
    
    QFile f(filename);
    if (!f.open(QFile::ReadOnly)) {
        return false;
    }
    QTextStream in(&f);
    do {
        QString line = in.readLine();
        QStringList pair = line.split("=");

        if (s_longFieldNames.contains(pair[0])) {
            pair[0] = s_longFieldNames[pair[0]];
        }
        if (pair[0] == "Family") {
            if (pair[1] == "xPAP") {
                pair[1] = "0";
            } else if (pair[1] == "Ventilator") {
                pair[1] = "3";
            }
        }
        props[pair[0]] = pair[1];
    } while (!in.atEnd());
    
    return true;
}

bool PRS1Loader::PeekProperties(MachineInfo & info, const QString & filename, Machine * mach)
{
    QHash<QString,QString> props;
    if (!PeekProperties(filename, props)) {
        return false;
    }
    QString modelnum;
    for (auto & key : props.keys()) {
        bool skip = false;

        if (key == "ModelNumber") {
            modelnum = props[key];
            skip = true;
        }
        if (key == "SerialNumber") {
            info.serial = props[key];
            skip = true;
        }
        if (key == "ProductType") {
            bool ok;
            props[key].toInt(&ok, 16);
            if (!ok) qWarning() << "ProductType" << props[key];
            skip = true;
        }
        if (!mach || skip) continue;

        mach->properties[key] = props[key];
    };

    if (!modelnum.isEmpty()) {
        parseModel(info, modelnum);
    } else {
        qWarning() << "missing model number" << filename;
    }

    return true;
}


MachineInfo PRS1Loader::PeekInfo(const QString & path)
{
    QStringList machines = FindMachinesOnCard(path);
    if (machines.isEmpty()) {
        return MachineInfo();
    }

    // Present information about the newest machine on the card.
    QString newpath = machines.last();
    
    MachineInfo info = newInfo();
    if (!PeekProperties(info, newpath+"/properties.txt")) {
        if (!PeekProperties(info, newpath+"/PROP.TXT")) {
            qWarning() << "No properties file found in" << newpath;
        }
    }
    return info;
}


int PRS1Loader::Open(const QString & selectedPath)
{
    QString path = selectedPath;
    if (GetPSeriesPath(path).isEmpty()) {
        // Try up one level in case the user selected the P-Series folder within the SD card.
        path = QFileInfo(path).canonicalPath();
    }

    QStringList machines = FindMachinesOnCard(path);
    // Return an error if no machines were found.
    if (machines.isEmpty()) {
        qDebug() << "No PRS1 machines found at" << path;
        return -1;
    }

    // Import each machine, from oldest to newest.
    int c = 0;
    for (auto & machinePath : machines) {
        c += OpenMachine(machinePath);
    }
    return c;
}


int PRS1Loader::OpenMachine(const QString & path)
{
    if (p_profile == nullptr) {
        qWarning() << "PRS1Loader::OpenMachine() called without a valid p_profile object present";
        return 0;
    }

    qDebug() << "Opening PRS1 " << path;
    QDir dir(path);

    if (!dir.exists() || (!dir.isReadable())) {
        return 0;
    }
    m_abort = false;

    emit updateMessage(QObject::tr("Getting Ready..."));
    QCoreApplication::processEvents();

    emit setProgressValue(0);

    QStringList paths;
    QString propertyfile;
    int sessionid_base;
    sessionid_base = FindSessionDirsAndProperties(path, paths, propertyfile);

    Machine *m = CreateMachineFromProperties(propertyfile);
    if (m == nullptr) {
        return -1;
    }

    emit updateMessage(QObject::tr("Backing Up Files..."));
    QCoreApplication::processEvents();

    QString backupPath = m->getBackupPath() + path.section("/", -2);

    if (QDir::cleanPath(path).compare(QDir::cleanPath(backupPath)) != 0) {
        copyPath(path, backupPath);
    }

    emit updateMessage(QObject::tr("Scanning Files..."));
    QCoreApplication::processEvents();

    // Walk through the files and create an import task for each logical session.
    ScanFiles(paths, sessionid_base, m);

    int tasks = countTasks();

    emit updateMessage(QObject::tr("Importing Sessions..."));
    QCoreApplication::processEvents();

    runTasks(AppSetting->multithreading());

    emit updateMessage(QObject::tr("Finishing up..."));
    QCoreApplication::processEvents();

    finishAddingSessions();

    if (m_unexpectedMessages.count() > 0 && p_profile->session->warnOnUnexpectedData()) {
        // Compare this to the list of messages previously seen for this machine
        // and only alert if there are new ones.
        QSet<QString> newMessages = m_unexpectedMessages - m->previouslySeenUnexpectedData();
        if (newMessages.count() > 0) {
            // TODO: Rework the importer call structure so that this can become an
            // emit statement to the appropriate import job.
            QMessageBox::information(QApplication::activeWindow(),
                                     QObject::tr("Untested Data"),
                                     QObject::tr("Your Philips Respironics %1 (%2) generated data that OSCAR has never seen before.").arg(m->getInfo().model).arg(m->getInfo().modelnumber) +"\n\n"+
                                     QObject::tr("The imported data may not be entirely accurate, so the developers would like a .zip copy of this machine's SD card and matching Encore .pdf reports to make sure OSCAR is handling the data correctly.")
                                     ,QMessageBox::Ok);
            m->previouslySeenUnexpectedData() += newMessages;
        }
    }
    
    return m->unsupported() ? -1 : tasks;
}


int PRS1Loader::FindSessionDirsAndProperties(const QString & path, QStringList & paths, QString & propertyfile)
{
    QDir dir(path);
    dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
    dir.setSorting(QDir::Name);
    QFileInfoList flist = dir.entryInfoList();

    QString filename;

    int sessionid_base = 10;

    for (int i = 0; i < flist.size(); i++) {
        QFileInfo fi = flist.at(i);
        filename = fi.fileName();

        if (fi.isDir()) {
            if ((filename[0].toLower() == 'p') && (isdigit(filename[1]))) {
                // p0, p1, p2.. etc.. folders contain the session data
                paths.push_back(fi.canonicalFilePath());
            } else if (filename.toLower() == "e") {
                // Error files..
                // Reminder: I have been given some info about these. should check it over.
            }
        } else if (filename.compare("properties.txt",Qt::CaseInsensitive) == 0) {
            propertyfile = fi.canonicalFilePath();
        } else if (filename.compare("PROP.TXT",Qt::CaseInsensitive) == 0) {
            sessionid_base = 16;
            propertyfile = fi.canonicalFilePath();
        }
    }
    return sessionid_base;
}


Machine* PRS1Loader::CreateMachineFromProperties(QString propertyfile)
{
    QHash<QString,QString> props;
    PeekProperties(propertyfile, props);
    
    MachineInfo info = newInfo();
    // Have a peek first to get the model number.
    PeekProperties(info, propertyfile);

    if (true) {
        if (s_PRS1ModelInfo.IsBrick(info.modelnumber) && p_profile->cpap->brickWarning()) {
#ifndef UNITTEST_MODE
            QApplication::processEvents();
            QMessageBox::information(QApplication::activeWindow(),
                                     QObject::tr("Non Data Capable Machine"),
                                     QString(QObject::tr("Your Philips Respironics CPAP machine (Model %1) is unfortunately not a data capable model.")+"\n\n"+
                                             QObject::tr("I'm sorry to report that OSCAR can only track hours of use and very basic settings for this machine.")).
                                     arg(info.modelnumber),QMessageBox::Ok);
#endif
            p_profile->cpap->setBrickWarning(false);

        }

        if (!s_PRS1ModelInfo.IsSupported(props)) {
            qWarning() << info.modelnumber << "unsupported";
#ifndef UNITTEST_MODE
            QMessageBox::information(QApplication::activeWindow(),
                                     QObject::tr("Machine Unsupported"),
                                     QObject::tr("Sorry, your Philips Respironics CPAP machine (Model %1) is not supported yet.").arg(info.modelnumber) +"\n\n"+
                                     QObject::tr("The developers needs a .zip copy of this machine's SD card and matching Encore .pdf reports to make it work with OSCAR.")
                                     ,QMessageBox::Ok);

#endif
            return nullptr;
        }
    }

    // Which is needed to get the right machine record..
    Machine *m = p_profile->CreateMachine(info);

    // This time supply the machine object so it can populate machine properties..
    PeekProperties(m->info, propertyfile, m);
    
    if (!s_PRS1ModelInfo.IsTested(props)) {
        qDebug() << info.modelnumber << "untested";
        if (p_profile->session->warnOnUntestedMachine() && m->warnOnUntested()) {
            m->suppressWarnOnUntested();  // don't warn the user more than once
#ifndef UNITTEST_MODE
            // TODO: Rework the importer call structure so that this can become an
            // emit statement to the appropriate import job.
            QMessageBox::information(QApplication::activeWindow(),
                                 QObject::tr("Machine Untested"),
                                 QObject::tr("Your Philips Respironics CPAP machine (Model %1) has not been tested yet.").arg(info.modelnumber) +"\n\n"+
                                 QObject::tr("It seems similar enough to other machines that it might work, but the developers would like a .zip copy of this machine's SD card and matching Encore .pdf reports to make sure it works with OSCAR.")
                                 ,QMessageBox::Ok);

#endif
        }
    }

    // Mark the machine in the profile as unsupported.
    if (!s_PRS1ModelInfo.IsSupported(props)) {
        if (!m->unsupported()) {
            unsupported(m);
        }
    }
    
    return m;
}


static QString relativePath(const QString & inpath)
{
    QStringList pathlist = QDir::toNativeSeparators(inpath).split(QDir::separator(), QString::SkipEmptyParts);
    QString relative = pathlist.mid(pathlist.size()-3).join(QDir::separator());
    return relative;
}

static bool chunksIdentical(const PRS1DataChunk* a, const PRS1DataChunk* b)
{
    return (a->hash() == b->hash());
}

static QString chunkComparison(const PRS1DataChunk* a, const PRS1DataChunk* b)
{
    return QString("Session %1 in %2 @ %3 %4 %5 @ %6, skipping")
            .arg(a->sessionid)
            .arg(relativePath(a->m_path)).arg(a->m_filepos)
            .arg(chunksIdentical(a, b) ? "is identical to" : "differs from")
            .arg(relativePath(b->m_path)).arg(b->m_filepos);

}

void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machine * m)
{
    SessionID sid;
    long ext;

    QDir dir;
    dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
    dir.setSorting(QDir::Name);

    int size = paths.size();

    sesstasks.clear();
    new_sessions.clear(); // this hash is used by OpenFile
    m_unexpectedMessages.clear();


    PRS1Import * task = nullptr;
    // Note, I have observed p0/p1/etc folders containing duplicates session files (in Robin Sanders data.)

    QDateTime datetime;

    qint64 ignoreBefore = p_profile->session->ignoreOlderSessionsDate().toMSecsSinceEpoch()/1000;
    bool ignoreOldSessions = p_profile->session->ignoreOlderSessions();
    QSet<SessionID> skipped;

    // for each p0/p1/p2/etc... folder
    for (int p=0; p < size; ++p) {
        dir.setPath(paths.at(p));

        if (!dir.exists() || !dir.isReadable()) {
            qWarning() << dir.canonicalPath() << "can't read directory";
            continue;
        }

        QFileInfoList flist = dir.entryInfoList();

        // Scan for individual session files
        for (int i = 0; i < flist.size(); i++) {
#ifndef UNITTEST_MODE
            QCoreApplication::processEvents();
#endif
            if (isAborted()) {
                qDebug() << "received abort signal";
                break;
            }
            QFileInfo fi = flist.at(i);
            QString path = fi.canonicalFilePath();
            bool ok;

            if (fi.fileName() == ".DS_Store") {
                continue;
            }

            QString ext_s = fi.fileName().section(".", -1);
            ext = ext_s.toInt(&ok);
            if (!ok) {
                // not a numerical extension
                qInfo() << path << "unexpected filename";
                continue;
            }

            QString session_s = fi.fileName().section(".", 0, -2);
            sid = session_s.toInt(&ok, sessionid_base);
            if (!ok) {
                // not a numerical session ID
                qInfo() << path << "unexpected filename";
                continue;
            }

            // TODO: BUG: This isn't right, since files can have multiple session
            // chunks, which might not correspond to the filename. But before we can
            // fix this we need to come up with a reasonably fast way to filter previously
            // imported files without re-reading all of them.
            if (m->SessionExists(sid)) {
                // Skip already imported session
                qDebug() << path << "session already exists, skipping" << sid;
                continue;
            }

            if ((ext == 5) || (ext == 6)) {
                if (skipped.contains(sid)) {
                    // We don't know the timestamp until the file is parsed, which we only do for
                    // waveform data at import (after scanning) since it's so large. If we relied
                    // solely on the chunks' timestamps at that point, we'd get half of an otherwise
                    // skipped session (the half after midnight).
                    //
                    // So we skip the entire file here based on the session's other data.
                    continue;
                }

                // Waveform files aren't grouped... so we just want to add the filename for later
                QHash<SessionID, PRS1Import *>::iterator it = sesstasks.find(sid);
                if (it != sesstasks.end()) {
                    task = it.value();
                } else {
                    // Should probably check if session already imported has this data missing..

                    // Create the group if we see it first..
                    task = new PRS1Import(this, sid, m, sessionid_base);
                    sesstasks[sid] = task;
                    queTask(task);
                }

                if (ext == 5) {
                    // Occasionally waveforms in a session can be split into multiple files.
                    //
                    // This seems to happen when the machine begins writing the waveform file
                    // before realizing that it will hit its 500-file-per-directory limit
                    // for the remaining session files, at which point it appears to write
                    // the rest of the waveform data along with the summary and event files
                    // in the next directory.
                    //
                    // All samples exhibiting this behavior are DreamStations.
                    task->m_wavefiles.append(fi.canonicalFilePath());
                } else if (ext == 6) {
                    // Oximetry data can also be split into multiple files, see waveform
                    // comment above.
                    task->m_oxifiles.append(fi.canonicalFilePath());
                }

                continue;
            }

            // Parse the data chunks and read the files..
            QList<PRS1DataChunk *> Chunks = ParseFile(fi.canonicalFilePath());

            for (int i=0; i < Chunks.size(); ++i) {
                if (isAborted()) {
                    qDebug() << "received abort signal 2";
                    break;
                }

                PRS1DataChunk * chunk = Chunks.at(i);

                SessionID chunk_sid = chunk->sessionid;
                if (i == 0 && chunk_sid != sid) {  // log session ID mismatches
                    // This appears to be benign, probably when a card is out of the machine one night and
                    // then inserted in the morning. It writes out all of the still-in-memory summaries and
                    // events up through the last night (and no waveform data).
                    //
                    // This differs from the first time a card is inserted, because in that case the filename
                    // *is* equal to the first session contained within it, and then filenames for the
                    // remaining sessions contained in that file are skipped.
                    //
                    // Because the card was present and previous sessions were written with their filenames,
                    // the first available filename isn't the first session contained in the file.
                    //qDebug() << fi.canonicalFilePath() << "first session is" << chunk_sid << "instead of" << sid;
                }
                if (m->SessionExists(chunk_sid)) {
                    qDebug() << path << "session already imported, skipping" << sid << chunk_sid;
                    delete chunk;
                    continue;
                }
                if (ignoreOldSessions && chunk->timestamp < ignoreBefore) {
                    qDebug().noquote() << relativePath(path) << "skipping session" << chunk_sid << ":"
                        << QDateTime::fromMSecsSinceEpoch(chunk->timestamp*1000).toString() << "older than"
                        << QDateTime::fromMSecsSinceEpoch(ignoreBefore*1000).toString();
                    skipped += chunk_sid;
                    delete chunk;
                    continue;
                }

                task = nullptr;
                QHash<SessionID, PRS1Import *>::iterator it = sesstasks.find(chunk_sid);
                if (it != sesstasks.end()) {
                    task = it.value();
                } else {
                    task = new PRS1Import(this, chunk_sid, m, sessionid_base);
                    sesstasks[chunk_sid] = task;
                    // save a loop an que this now
                    queTask(task);
                }
                switch (ext) {
                case 0:
                    if (task->compliance) {
                        if (chunksIdentical(chunk, task->summary)) {
                            // Never seen identical compliance chunks, so keep logging this for now.
                            qDebug() << chunkComparison(chunk, task->summary);
                        } else {
                            qWarning() << chunkComparison(chunk, task->summary);
                        }
                        delete chunk;
                        continue; // (skipping to avoid duplicates)
                    }
                    task->compliance = chunk;
                    break;
                case 1:
                    if (task->summary) {
                        if (chunksIdentical(chunk, task->summary)) {
                            // This seems to be benign. It happens most often when a single file contains
                            // a bunch of chunks and subsequent files each contain a single chunk that was
                            // already covered by the first file. It also sometimes happens with entirely
                            // duplicate files between e.g. a P1 and P0 directory.
                            //
                            // It's common enough that we don't emit a message about it by default.
                            //qDebug() << chunkComparison(chunk, task->summary);
                        } else {
                            // Warn about any non-identical duplicate session IDs.
                            //
                            // This seems to happen with F5V1 slice 8, which is the only slice in a session,
                            // and which doesn't update the session ID, so the following slice 7 session
                            // (which can be hours later) has the same session ID. Neither affects import.
                            qWarning() << chunkComparison(chunk, task->summary);
                        }
                        delete chunk;
                        continue;
                    }
                    task->summary = chunk;
                    break;
                case 2:
                    if (task->m_event_chunks.count() > 0) {
                        PRS1DataChunk* previous;
                        if (chunk->family == 3 && chunk->familyVersion <= 3) {
                            // F3V0 and F3V3 events are formatted as waveforms, with one chunk per mask-on slice,
                            // and thus multiple chunks per session.
                            previous = task->m_event_chunks[chunk->timestamp];
                            if (previous != nullptr) {
                                // Skip any chunks with identical timestamps.
                                qWarning() << chunkComparison(chunk, previous);
                                delete chunk;
                                continue;
                            }
                            // fall through to add the new chunk
                        } else {
                            // Nothing else should have multiple event chunks per session.
                            previous = task->m_event_chunks.first();
                            if (chunksIdentical(chunk, previous)) {
                                // See comment above regarding identical summary chunks.
                                //qDebug() << chunkComparison(chunk, previous);
                            } else {
                                qWarning() << chunkComparison(chunk, previous);
                            }
                            delete chunk;
                            continue;
                        }
                    }
                    task->m_event_chunks[chunk->timestamp] = chunk;
                    break;
                default:
                    qWarning() << path << "unexpected file";
                    break;
                }
            }
        }
        if (isAborted()) {
            qDebug() << "received abort signal 3";
            break;
        }
    }
}


//********************************************************************************************
// Internal PRS1 parsed data types
//********************************************************************************************

// For new events, add an enum here and then a class below with an PRS1_*_EVENT macro
enum PRS1ParsedEventType
{
    EV_PRS1_RAW = -1,     // these only get logged
    EV_PRS1_UNKNOWN = 0,  // these have their value graphed
    EV_PRS1_TB,
    EV_PRS1_OA,
    EV_PRS1_CA,
    EV_PRS1_FL,
    EV_PRS1_PB,
    EV_PRS1_LL,
    EV_PRS1_VB,           // UNCONFIRMED
    EV_PRS1_HY,
    EV_PRS1_OA_COUNT,     // F3V3 only
    EV_PRS1_CA_COUNT,     // F3V3 only
    EV_PRS1_HY_COUNT,     // F3V3 only
    EV_PRS1_TOTLEAK,
    EV_PRS1_LEAK,  // unintentional leak
    EV_PRS1_AUTO_PRESSURE_SET,
    EV_PRS1_PRESSURE_SET,
    EV_PRS1_IPAP_SET,
    EV_PRS1_EPAP_SET,
    EV_PRS1_PRESSURE_AVG,
    EV_PRS1_FLEX_PRESSURE_AVG,
    EV_PRS1_IPAP_AVG,
    EV_PRS1_IPAPLOW,
    EV_PRS1_IPAPHIGH,
    EV_PRS1_EPAP_AVG,
    EV_PRS1_RR,
    EV_PRS1_PTB,
    EV_PRS1_MV,
    EV_PRS1_TV,
    EV_PRS1_SNORE,
    EV_PRS1_VS,
    EV_PRS1_PP,
    EV_PRS1_RERA,
    EV_PRS1_FLOWRATE,
    EV_PRS1_TEST1,
    EV_PRS1_TEST2,
    EV_PRS1_SETTING,
    EV_PRS1_SLICE,
    EV_PRS1_DISCONNECT_ALARM,
    EV_PRS1_APNEA_ALARM,
    EV_PRS1_LOW_MV_ALARM,
    EV_PRS1_SNORES_AT_PRESSURE,
    EV_PRS1_INTERVAL_BOUNDARY,  // An artificial internal-only event used to separate stat intervals
};

enum PRS1ParsedEventUnit
{
    PRS1_UNIT_NONE,
    PRS1_UNIT_CMH2O,
    PRS1_UNIT_ML,
    PRS1_UNIT_S,
};

enum PRS1ParsedSettingType
{
    PRS1_SETTING_CPAP_MODE,
    PRS1_SETTING_AUTO_TRIAL,
    PRS1_SETTING_PRESSURE,
    PRS1_SETTING_PRESSURE_MIN,
    PRS1_SETTING_PRESSURE_MAX,
    PRS1_SETTING_EPAP,
    PRS1_SETTING_EPAP_MIN,
    PRS1_SETTING_EPAP_MAX,
    PRS1_SETTING_IPAP,
    PRS1_SETTING_IPAP_MIN,
    PRS1_SETTING_IPAP_MAX,
    PRS1_SETTING_PS,
    PRS1_SETTING_PS_MIN,
    PRS1_SETTING_PS_MAX,
    PRS1_SETTING_BACKUP_BREATH_MODE,
    PRS1_SETTING_BACKUP_BREATH_RATE,
    PRS1_SETTING_BACKUP_TIMED_INSPIRATION,
    PRS1_SETTING_TIDAL_VOLUME,
    PRS1_SETTING_EZ_START,
    PRS1_SETTING_FLEX_LOCK,
    PRS1_SETTING_FLEX_MODE,
    PRS1_SETTING_FLEX_LEVEL,
    PRS1_SETTING_RISE_TIME,
    PRS1_SETTING_RISE_TIME_LOCK,
    PRS1_SETTING_RAMP_TYPE,
    PRS1_SETTING_RAMP_TIME,
    PRS1_SETTING_RAMP_PRESSURE,
    PRS1_SETTING_HUMID_STATUS,
    PRS1_SETTING_HUMID_MODE,
    PRS1_SETTING_HEATED_TUBE_TEMP,
    PRS1_SETTING_HUMID_LEVEL,
    PRS1_SETTING_MASK_RESIST_LOCK,
    PRS1_SETTING_MASK_RESIST_SETTING,
    PRS1_SETTING_HOSE_DIAMETER,
    PRS1_SETTING_TUBING_LOCK,
    PRS1_SETTING_AUTO_ON,
    PRS1_SETTING_AUTO_OFF,
    PRS1_SETTING_APNEA_ALARM,
    PRS1_SETTING_DISCONNECT_ALARM,  // Is this any different from mask alert?
    PRS1_SETTING_LOW_MV_ALARM,
    PRS1_SETTING_LOW_TV_ALARM,
    PRS1_SETTING_MASK_ALERT,
    PRS1_SETTING_SHOW_AHI,
};


static QString timeStr(int t);
static QString byteList(QByteArray data, int limit=-1);
static QString hex(int i);
static QString parsedSettingTypeName(PRS1ParsedSettingType t);
static QString parsedModeName(int m);


class PRS1ParsedEvent
{
public:
    PRS1ParsedEventType m_type;
    int m_start;     // seconds relative to chunk timestamp at which this event began
    int m_duration;
    int m_value;
    float m_offset;
    float m_gain;
    PRS1ParsedEventUnit m_unit;

    inline float value(void) { return (m_value * m_gain) + m_offset; }
    
    static const PRS1ParsedEventType TYPE = EV_PRS1_UNKNOWN;
    static constexpr float GAIN = 1.0;
    static const PRS1ParsedEventUnit UNIT = PRS1_UNIT_NONE;
    
    virtual QMap<QString,QString> contents(void) = 0;

protected:
    PRS1ParsedEvent(PRS1ParsedEventType type, int start)
    : m_type(type), m_start(start), m_duration(0), m_value(0), m_offset(0.0), m_gain(GAIN), m_unit(UNIT)
    {
    }
public:
    virtual ~PRS1ParsedEvent()
    {
    }
};


class PRS1IntervalBoundaryEvent : public PRS1ParsedEvent
{
public:
    virtual QMap<QString,QString> contents(void)
    {
        QMap<QString,QString> out;
        out["start"] = timeStr(m_start);
        return out;
    }

    static const PRS1ParsedEventType TYPE = EV_PRS1_INTERVAL_BOUNDARY;

    PRS1IntervalBoundaryEvent(int start) : PRS1ParsedEvent(TYPE, start) {}
};


class PRS1ParsedDurationEvent : public PRS1ParsedEvent
{
public:
    virtual QMap<QString,QString> contents(void)
    {
        QMap<QString,QString> out;
        out["start"] = timeStr(m_start);
        out["duration"] = timeStr(m_duration);
        return out;
    }

    static const PRS1ParsedEventUnit UNIT = PRS1_UNIT_S;
    
    PRS1ParsedDurationEvent(PRS1ParsedEventType type, int start, int duration) : PRS1ParsedEvent(type, start) { m_duration = duration; }
};
const PRS1ParsedEventUnit PRS1ParsedDurationEvent::UNIT;


class PRS1ParsedValueEvent : public PRS1ParsedEvent
{
public:
    virtual QMap<QString,QString> contents(void)
    {
        QMap<QString,QString> out;
        out["start"] = timeStr(m_start);
        out["value"] = QString::number(value());
        return out;
    }

protected:
    PRS1ParsedValueEvent(PRS1ParsedEventType type, int start, int value) : PRS1ParsedEvent(type, start) { m_value = value; }
};

/*
class PRS1UnknownValueEvent : public PRS1ParsedValueEvent
{
public:
    virtual QMap<QString,QString> contents(void)
    {
        QMap<QString,QString> out;
        out["start"] = timeStr(m_start);
        out["code"] = hex(m_code);
        out["value"] = QString::number(value());
        return out;
    }

    int m_code;
    PRS1UnknownValueEvent(int code, int start, int value, float gain=1.0) : PRS1ParsedValueEvent(TYPE, start, value), m_code(code) { m_gain = gain; }
};
*/

class PRS1UnknownDataEvent : public PRS1ParsedEvent
{
public:
    virtual QMap<QString,QString> contents(void)
    {
        QMap<QString,QString> out;
        out["pos"] = QString::number(m_pos);
        out["data"] = byteList(m_data);
        return out;
    }

    static const PRS1ParsedEventType TYPE = EV_PRS1_RAW;
    
    int m_pos;
    unsigned char m_code;
    QByteArray m_data;
    
    PRS1UnknownDataEvent(const QByteArray & data, int pos, int len=18)
        : PRS1ParsedEvent(TYPE, 0)
    {
        m_pos = pos;
        m_data = data.mid(pos, len);
        Q_ASSERT(m_data.size() >= 1);
        m_code = m_data.at(0);
    }
};

class PRS1PressureEvent : public PRS1ParsedValueEvent
{
public:
    static constexpr float GAIN = 0.1;
    static const PRS1ParsedEventUnit UNIT = PRS1_UNIT_CMH2O;
    
    PRS1PressureEvent(PRS1ParsedEventType type, int start, int value, float gain=GAIN)
        : PRS1ParsedValueEvent(type, start, value) 
    { 
        m_gain = gain;
        m_unit = UNIT;
    }
};

class PRS1TidalVolumeEvent : public PRS1ParsedValueEvent
{
public:
    static const PRS1ParsedEventType TYPE = EV_PRS1_TV;

    static constexpr float GAIN = 10.0;
    static const PRS1ParsedEventUnit UNIT = PRS1_UNIT_ML;
    
    PRS1TidalVolumeEvent(int start, int value)
        : PRS1ParsedValueEvent(TYPE, start, value)
    {
        m_gain = GAIN;
        m_unit = UNIT;
    }
};
const PRS1ParsedEventType PRS1TidalVolumeEvent::TYPE;

class PRS1ParsedSettingEvent : public PRS1ParsedValueEvent
{
public:
    virtual QMap<QString,QString> contents(void)
    {
        QMap<QString,QString> out;
        QString v;
        if (m_setting == PRS1_SETTING_CPAP_MODE) {
            v = parsedModeName(value());
        } else {
            v = QString::number(value());
        }
        out[parsedSettingTypeName(m_setting)] = v;
        return out;
    }
    
    static const PRS1ParsedEventType TYPE = EV_PRS1_SETTING;
    PRS1ParsedSettingType m_setting;
    
    PRS1ParsedSettingEvent(PRS1ParsedSettingType setting, int value) : PRS1ParsedValueEvent(TYPE, 0, value), m_setting(setting) {}
};

class PRS1ScaledSettingEvent : public PRS1ParsedSettingEvent
{
public:
    PRS1ScaledSettingEvent(PRS1ParsedSettingType setting, int value, float gain)
        : PRS1ParsedSettingEvent(setting, value)
    {
        m_gain = gain;
    }
};

class PRS1PressureSettingEvent : public PRS1ScaledSettingEvent
{
public:
    static constexpr float GAIN = PRS1PressureEvent::GAIN;
    static const PRS1ParsedEventUnit UNIT = PRS1PressureEvent::UNIT;
    
    PRS1PressureSettingEvent(PRS1ParsedSettingType setting, int value, float gain=GAIN)
        : PRS1ScaledSettingEvent(setting, value, gain)
    { 
        m_unit = UNIT;
    }
};

class PRS1ParsedSliceEvent : public PRS1ParsedValueEvent
{
public:
    virtual QMap<QString,QString> contents(void)
    {
        QMap<QString,QString> out;
        out["start"] = timeStr(m_start);
        QString s;
        switch ((SliceStatus) m_value) {
            case MaskOn: s = "MaskOn"; break;
            case MaskOff: s = "MaskOff"; break;
            case EquipmentOff: s = "EquipmentOff"; break;
            case UnknownStatus: s = "Unknown"; break;
        }
        out["status"] = s;
        return out;
    }

    static const PRS1ParsedEventType TYPE = EV_PRS1_SLICE;
    
    PRS1ParsedSliceEvent(int start, SliceStatus status) : PRS1ParsedValueEvent(TYPE, start, (int) status) {}
};


class PRS1ParsedAlarmEvent : public PRS1ParsedEvent
{
public:
    virtual QMap<QString,QString> contents(void)
    {
        QMap<QString,QString> out;
        out["start"] = timeStr(m_start);
        return out;
    }

protected:
    PRS1ParsedAlarmEvent(PRS1ParsedEventType type, int start, int /*unused*/) : PRS1ParsedEvent(type, start) {}
};


class PRS1SnoresAtPressureEvent : public PRS1PressureEvent
{
public:
    static const PRS1ParsedEventType TYPE = EV_PRS1_SNORES_AT_PRESSURE;

    PRS1SnoresAtPressureEvent(int start, int kind, int pressure, int count, float gain=GAIN)
        : PRS1PressureEvent(TYPE, start, pressure, gain)
    {
        m_kind = kind;
        m_count = count;
    }

    virtual QMap<QString,QString> contents(void)
    {
        QString label;
        switch (m_kind) {
        case 0: label = "pressure"; break;
        case 1: label = "epap"; break;
        case 2: label = "ipap"; break;
        default: label = "unknown_pressure"; break;
        }
        
        QMap<QString,QString> out;
        out["start"] = timeStr(m_start);
        out[label] = QString::number(value());
        out["count"] = QString::number(m_count);
        return out;
    }

protected:
    int m_kind;
    // m_value is pressure
    int m_count;
};
const PRS1ParsedEventType PRS1SnoresAtPressureEvent::TYPE;


#define _PRS1_EVENT(T, E, P, ARG) \
class T : public P \
{ \
public: \
    static const PRS1ParsedEventType TYPE = E; \
    T(int start, int ARG) : P(TYPE, start, ARG) {} \
}; \
const PRS1ParsedEventType T::TYPE
#define PRS1_DURATION_EVENT(T, E) _PRS1_EVENT(T, E, PRS1ParsedDurationEvent, duration)
#define PRS1_VALUE_EVENT(T, E)    _PRS1_EVENT(T, E, PRS1ParsedValueEvent, value)
#define PRS1_ALARM_EVENT(T, E)    _PRS1_EVENT(T, E, PRS1ParsedAlarmEvent, value)
#define PRS1_PRESSURE_EVENT(T, E) \
class T : public PRS1PressureEvent \
{ \
public: \
    static const PRS1ParsedEventType TYPE = E; \
    T(int start, int value, float gain=PRS1PressureEvent::GAIN) : PRS1PressureEvent(TYPE, start, value, gain) {} \
}; \
const PRS1ParsedEventType T::TYPE

PRS1_DURATION_EVENT(PRS1TimedBreathEvent, EV_PRS1_TB);
PRS1_DURATION_EVENT(PRS1ObstructiveApneaEvent, EV_PRS1_OA);
PRS1_DURATION_EVENT(PRS1ClearAirwayEvent, EV_PRS1_CA);
PRS1_DURATION_EVENT(PRS1FlowLimitationEvent, EV_PRS1_FL);
PRS1_DURATION_EVENT(PRS1PeriodicBreathingEvent, EV_PRS1_PB);
PRS1_DURATION_EVENT(PRS1LargeLeakEvent, EV_PRS1_LL);
PRS1_DURATION_EVENT(PRS1VariableBreathingEvent, EV_PRS1_VB);
PRS1_DURATION_EVENT(PRS1HypopneaEvent, EV_PRS1_HY);

PRS1_VALUE_EVENT(PRS1TotalLeakEvent, EV_PRS1_TOTLEAK);
PRS1_VALUE_EVENT(PRS1LeakEvent, EV_PRS1_LEAK);

PRS1_PRESSURE_EVENT(PRS1AutoPressureSetEvent, EV_PRS1_AUTO_PRESSURE_SET);
PRS1_PRESSURE_EVENT(PRS1PressureSetEvent, EV_PRS1_PRESSURE_SET);
PRS1_PRESSURE_EVENT(PRS1IPAPSetEvent, EV_PRS1_IPAP_SET);
PRS1_PRESSURE_EVENT(PRS1EPAPSetEvent, EV_PRS1_EPAP_SET);
PRS1_PRESSURE_EVENT(PRS1PressureAverageEvent, EV_PRS1_PRESSURE_AVG);
PRS1_PRESSURE_EVENT(PRS1FlexPressureAverageEvent, EV_PRS1_FLEX_PRESSURE_AVG);
PRS1_PRESSURE_EVENT(PRS1IPAPAverageEvent, EV_PRS1_IPAP_AVG);
PRS1_PRESSURE_EVENT(PRS1IPAPHighEvent, EV_PRS1_IPAPHIGH);
PRS1_PRESSURE_EVENT(PRS1IPAPLowEvent, EV_PRS1_IPAPLOW);
PRS1_PRESSURE_EVENT(PRS1EPAPAverageEvent, EV_PRS1_EPAP_AVG);

PRS1_VALUE_EVENT(PRS1RespiratoryRateEvent, EV_PRS1_RR);
PRS1_VALUE_EVENT(PRS1PatientTriggeredBreathsEvent, EV_PRS1_PTB);
PRS1_VALUE_EVENT(PRS1MinuteVentilationEvent, EV_PRS1_MV);
PRS1_VALUE_EVENT(PRS1SnoreEvent, EV_PRS1_SNORE);
PRS1_VALUE_EVENT(PRS1VibratorySnoreEvent, EV_PRS1_VS);
PRS1_VALUE_EVENT(PRS1PressurePulseEvent, EV_PRS1_PP);
PRS1_VALUE_EVENT(PRS1RERAEvent, EV_PRS1_RERA);  // TODO: should this really be a duration event?
PRS1_VALUE_EVENT(PRS1FlowRateEvent, EV_PRS1_FLOWRATE);  // TODO: is this a single event or an index/hour?
PRS1_VALUE_EVENT(PRS1Test1Event, EV_PRS1_TEST1);
PRS1_VALUE_EVENT(PRS1Test2Event, EV_PRS1_TEST2);
PRS1_VALUE_EVENT(PRS1HypopneaCount, EV_PRS1_HY_COUNT);  // F3V3 only
PRS1_VALUE_EVENT(PRS1ClearAirwayCount, EV_PRS1_CA_COUNT);  // F3V3 only
PRS1_VALUE_EVENT(PRS1ObstructiveApneaCount, EV_PRS1_OA_COUNT);  // F3V3 only

PRS1_ALARM_EVENT(PRS1DisconnectAlarmEvent, EV_PRS1_DISCONNECT_ALARM);
PRS1_ALARM_EVENT(PRS1ApneaAlarmEvent, EV_PRS1_APNEA_ALARM);
PRS1_ALARM_EVENT(PRS1LowMinuteVentilationAlarmEvent, EV_PRS1_LOW_MV_ALARM);

enum PRS1Mode {
    PRS1_MODE_UNKNOWN = -1,
    PRS1_MODE_CPAPCHECK = 0,    // "CPAP-Check"
    PRS1_MODE_CPAP,             // "CPAP"
    PRS1_MODE_AUTOCPAP,         // "AutoCPAP"
    PRS1_MODE_AUTOTRIAL,        // "Auto-Trial"
    PRS1_MODE_BILEVEL,          // "Bi-Level"
    PRS1_MODE_AUTOBILEVEL,      // "AutoBiLevel"
    PRS1_MODE_ASV,              // "ASV"
    PRS1_MODE_S,                // "S"
    PRS1_MODE_ST,               // "S/T"
    PRS1_MODE_PC,               // "PC"
    PRS1_MODE_ST_AVAPS,         // "S/T - AVAPS"
    PRS1_MODE_PC_AVAPS,         // "PC - AVAPS"
};

// Returns the set of all channels ever reported/supported by the parser for the given chunk.
const QVector<PRS1ParsedEventType> & GetSupportedEvents(const PRS1DataChunk* chunk);

// The set of PRS1 "on-demand" channels that only get created on import if the session
// contains events of that type.  Any channels not on this list always get created if
// they're reported/supported by the parser.
static const QVector<PRS1ParsedEventType> PRS1OnDemandChannels =
{
    PRS1TimedBreathEvent::TYPE,
    PRS1PressurePulseEvent::TYPE,
    
    // Pressure initialized on-demand for F0 due to the possibility of bilevel vs. single pressure.
    PRS1PressureSetEvent::TYPE,
    PRS1IPAPSetEvent::TYPE,
    PRS1EPAPSetEvent::TYPE,
    
    // Pressure average initialized on-demand for F0 due to the different semantics of bilevel vs. single pressure.
    PRS1PressureAverageEvent::TYPE,
    PRS1FlexPressureAverageEvent::TYPE,
};

// The set of "non-slice" channels are independent of mask-on slices, i.e. they
// are continuously reported and charted regardless of whether the mask is on.
static const QSet<PRS1ParsedEventType> PRS1NonSliceChannels =
{
    PRS1PressureSetEvent::TYPE,
    PRS1IPAPSetEvent::TYPE,
    PRS1EPAPSetEvent::TYPE,
    PRS1SnoresAtPressureEvent::TYPE,
};

// The channel ID (referenced by pointer because their values aren't initialized
// prior to runtime) to which a given PRS1 event should be added.  Events with
// no channel IDs are silently dropped, and events with more than one channel ID
// must be handled specially.
static const QHash<PRS1ParsedEventType,QVector<ChannelID*>> PRS1ImportChannelMap =
{
    { PRS1ClearAirwayEvent::TYPE,       { &CPAP_ClearAirway } },
    { PRS1ObstructiveApneaEvent::TYPE,  { &CPAP_Obstructive } },
    { PRS1HypopneaEvent::TYPE,          { &CPAP_Hypopnea } },
    { PRS1FlowLimitationEvent::TYPE,    { &CPAP_FlowLimit } },
    { PRS1SnoreEvent::TYPE,             { &CPAP_Snore, &CPAP_VSnore2 } },  // VSnore2 is calculated from snore count, used to flag nonzero intervals on overview
    { PRS1VibratorySnoreEvent::TYPE,    { &CPAP_VSnore } },
    { PRS1RERAEvent::TYPE,              { &CPAP_RERA } },

    { PRS1PeriodicBreathingEvent::TYPE, { &CPAP_PB } },
    { PRS1LargeLeakEvent::TYPE,         { &CPAP_LargeLeak } },
    { PRS1TotalLeakEvent::TYPE,         { &CPAP_LeakTotal, &CPAP_Leak } },  // TODO: Remove CPAP_Leak if we get rid of unintentional leak calculation in the importer.
    { PRS1LeakEvent::TYPE,              { &CPAP_Leak } },

    { PRS1RespiratoryRateEvent::TYPE,   { &CPAP_RespRate } },
    { PRS1TidalVolumeEvent::TYPE,       { &CPAP_TidalVolume } },
    { PRS1MinuteVentilationEvent::TYPE, { &CPAP_MinuteVent } },
    { PRS1PatientTriggeredBreathsEvent::TYPE, { &CPAP_PTB } },
    { PRS1TimedBreathEvent::TYPE,       { &PRS1_TimedBreath } },
    { PRS1FlowRateEvent::TYPE,          { &PRS1_PeakFlow } },  // Only reported by F3V0 and F3V3  // TODO: should this stat be calculated from flow waveforms on other models?

    { PRS1PressureSetEvent::TYPE,       { &CPAP_PressureSet } },
    { PRS1IPAPSetEvent::TYPE,           { &CPAP_IPAPSet, &CPAP_PS } },  // PS is calculated from IPAPset and EPAPset when both are supported (F0) TODO: Should this be a separate channel since it's not a 2-minute average?
    { PRS1EPAPSetEvent::TYPE,           { &CPAP_EPAPSet } },            // EPAPset is supported on F5 without any corresponding IPAPset, so it shouldn't always create a PS channel
    { PRS1PressureAverageEvent::TYPE,   { &CPAP_Pressure } },           // This is the time-weighted average pressure in bilevel modes.
    { PRS1FlexPressureAverageEvent::TYPE, { &CPAP_EPAP } },             // This is effectively EPAP due to Flex reduced pressure in single-pressure modes.
    { PRS1IPAPAverageEvent::TYPE,       { &CPAP_IPAP } },
    { PRS1EPAPAverageEvent::TYPE,       { &CPAP_EPAP, &CPAP_PS } },     // PS is calculated from IPAP and EPAP averages (F3 and F5)
    { PRS1IPAPLowEvent::TYPE,           { &CPAP_IPAPLo } },
    { PRS1IPAPHighEvent::TYPE,          { &CPAP_IPAPHi } },

    { PRS1Test1Event::TYPE,             { &CPAP_Test1 } },  // ??? F3V6
    { PRS1Test2Event::TYPE,             { &CPAP_Test2 } },  // ??? F3V6

    { PRS1PressurePulseEvent::TYPE,     { &CPAP_PressurePulse } },
    { PRS1ApneaAlarmEvent::TYPE,        { /* Not imported */ } },
    { PRS1SnoresAtPressureEvent::TYPE,  { /* Not imported */ } },
    { PRS1AutoPressureSetEvent::TYPE,   { /* Not imported */ } },
    { PRS1VariableBreathingEvent::TYPE, { &PRS1_VariableBreathing } },  // UNCONFIRMED
    
    { PRS1HypopneaCount::TYPE,          { &CPAP_Hypopnea } },     // F3V3 only, generates individual events on import
    { PRS1ObstructiveApneaCount::TYPE,  { &CPAP_Obstructive } },  // F3V3 only, generates individual events on import
    { PRS1ClearAirwayCount::TYPE,       { &CPAP_ClearAirway } },  // F3V3 only, generates individual events on import
};

//********************************************************************************************

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

#define ENUMSTRING(ENUM) case ENUM: s = #ENUM; break
static QString parsedEventTypeName(PRS1ParsedEventType t)
{
    QString s;
    switch (t) {
        ENUMSTRING(EV_PRS1_RAW);
        ENUMSTRING(EV_PRS1_UNKNOWN);
        ENUMSTRING(EV_PRS1_TB);
        ENUMSTRING(EV_PRS1_OA);
        ENUMSTRING(EV_PRS1_CA);
        ENUMSTRING(EV_PRS1_FL);
        ENUMSTRING(EV_PRS1_PB);
        ENUMSTRING(EV_PRS1_LL);
        ENUMSTRING(EV_PRS1_VB);
        ENUMSTRING(EV_PRS1_HY);
        ENUMSTRING(EV_PRS1_OA_COUNT);
        ENUMSTRING(EV_PRS1_CA_COUNT);
        ENUMSTRING(EV_PRS1_HY_COUNT);
        ENUMSTRING(EV_PRS1_TOTLEAK);
        ENUMSTRING(EV_PRS1_LEAK);
        ENUMSTRING(EV_PRS1_AUTO_PRESSURE_SET);
        ENUMSTRING(EV_PRS1_PRESSURE_SET);
        ENUMSTRING(EV_PRS1_IPAP_SET);
        ENUMSTRING(EV_PRS1_EPAP_SET);
        ENUMSTRING(EV_PRS1_PRESSURE_AVG);
        ENUMSTRING(EV_PRS1_FLEX_PRESSURE_AVG);
        ENUMSTRING(EV_PRS1_IPAP_AVG);
        ENUMSTRING(EV_PRS1_IPAPLOW);
        ENUMSTRING(EV_PRS1_IPAPHIGH);
        ENUMSTRING(EV_PRS1_EPAP_AVG);
        ENUMSTRING(EV_PRS1_RR);
        ENUMSTRING(EV_PRS1_PTB);
        ENUMSTRING(EV_PRS1_MV);
        ENUMSTRING(EV_PRS1_TV);
        ENUMSTRING(EV_PRS1_SNORE);
        ENUMSTRING(EV_PRS1_VS);
        ENUMSTRING(EV_PRS1_PP);
        ENUMSTRING(EV_PRS1_RERA);
        ENUMSTRING(EV_PRS1_FLOWRATE);
        ENUMSTRING(EV_PRS1_TEST1);
        ENUMSTRING(EV_PRS1_TEST2);
        ENUMSTRING(EV_PRS1_SETTING);
        ENUMSTRING(EV_PRS1_SLICE);
        ENUMSTRING(EV_PRS1_DISCONNECT_ALARM);
        ENUMSTRING(EV_PRS1_APNEA_ALARM);
        ENUMSTRING(EV_PRS1_LOW_MV_ALARM);
        ENUMSTRING(EV_PRS1_SNORES_AT_PRESSURE);
        ENUMSTRING(EV_PRS1_INTERVAL_BOUNDARY);
        default:
            s = hex(t);
            qDebug() << "Unknown PRS1ParsedEventType type:" << qPrintable(s);
            return s;
    }
    return s.mid(8).toLower();  // lop off initial EV_PRS1_
}

static QString parsedSettingTypeName(PRS1ParsedSettingType t)
{
    QString s;
    switch (t) {
        ENUMSTRING(PRS1_SETTING_CPAP_MODE);
        ENUMSTRING(PRS1_SETTING_AUTO_TRIAL);
        ENUMSTRING(PRS1_SETTING_PRESSURE);
        ENUMSTRING(PRS1_SETTING_PRESSURE_MIN);
        ENUMSTRING(PRS1_SETTING_PRESSURE_MAX);
        ENUMSTRING(PRS1_SETTING_EPAP);
        ENUMSTRING(PRS1_SETTING_EPAP_MIN);
        ENUMSTRING(PRS1_SETTING_EPAP_MAX);
        ENUMSTRING(PRS1_SETTING_IPAP);
        ENUMSTRING(PRS1_SETTING_IPAP_MIN);
        ENUMSTRING(PRS1_SETTING_IPAP_MAX);
        ENUMSTRING(PRS1_SETTING_PS);
        ENUMSTRING(PRS1_SETTING_PS_MIN);
        ENUMSTRING(PRS1_SETTING_PS_MAX);
        ENUMSTRING(PRS1_SETTING_BACKUP_BREATH_MODE);
        ENUMSTRING(PRS1_SETTING_BACKUP_BREATH_RATE);
        ENUMSTRING(PRS1_SETTING_BACKUP_TIMED_INSPIRATION);
        ENUMSTRING(PRS1_SETTING_TIDAL_VOLUME);
        ENUMSTRING(PRS1_SETTING_EZ_START);
        ENUMSTRING(PRS1_SETTING_FLEX_LOCK);
        ENUMSTRING(PRS1_SETTING_FLEX_MODE);
        ENUMSTRING(PRS1_SETTING_FLEX_LEVEL);
        ENUMSTRING(PRS1_SETTING_RISE_TIME);
        ENUMSTRING(PRS1_SETTING_RISE_TIME_LOCK);
        ENUMSTRING(PRS1_SETTING_RAMP_TYPE);
        ENUMSTRING(PRS1_SETTING_RAMP_TIME);
        ENUMSTRING(PRS1_SETTING_RAMP_PRESSURE);
        ENUMSTRING(PRS1_SETTING_HUMID_STATUS);
        ENUMSTRING(PRS1_SETTING_HUMID_MODE);
        ENUMSTRING(PRS1_SETTING_HEATED_TUBE_TEMP);
        ENUMSTRING(PRS1_SETTING_HUMID_LEVEL);
        ENUMSTRING(PRS1_SETTING_MASK_RESIST_LOCK);
        ENUMSTRING(PRS1_SETTING_MASK_RESIST_SETTING);
        ENUMSTRING(PRS1_SETTING_HOSE_DIAMETER);
        ENUMSTRING(PRS1_SETTING_TUBING_LOCK);
        ENUMSTRING(PRS1_SETTING_AUTO_ON);
        ENUMSTRING(PRS1_SETTING_AUTO_OFF);
        ENUMSTRING(PRS1_SETTING_APNEA_ALARM);
        ENUMSTRING(PRS1_SETTING_DISCONNECT_ALARM);
        ENUMSTRING(PRS1_SETTING_LOW_MV_ALARM);
        ENUMSTRING(PRS1_SETTING_LOW_TV_ALARM);
        ENUMSTRING(PRS1_SETTING_MASK_ALERT);
        ENUMSTRING(PRS1_SETTING_SHOW_AHI);
        default:
            s = hex(t);
            qDebug() << "Unknown PRS1ParsedSettingType type:" << qPrintable(s);
            return s;
    }
    return s.mid(13).toLower();  // lop off initial PRS1_SETTING_
}

static QString parsedModeName(int m)
{
    QString s;
    switch ((PRS1Mode) m) {
        ENUMSTRING(PRS1_MODE_UNKNOWN);  // TODO: Remove this when all the parsers are complete.
        ENUMSTRING(PRS1_MODE_CPAP);
        ENUMSTRING(PRS1_MODE_CPAPCHECK);
        ENUMSTRING(PRS1_MODE_AUTOTRIAL);
        ENUMSTRING(PRS1_MODE_AUTOCPAP);
        ENUMSTRING(PRS1_MODE_BILEVEL);
        ENUMSTRING(PRS1_MODE_AUTOBILEVEL);
        ENUMSTRING(PRS1_MODE_ASV);
        ENUMSTRING(PRS1_MODE_S);
        ENUMSTRING(PRS1_MODE_ST);
        ENUMSTRING(PRS1_MODE_PC);
        ENUMSTRING(PRS1_MODE_ST_AVAPS);
        ENUMSTRING(PRS1_MODE_PC_AVAPS);
        default:
            s = hex(m);
            qDebug() << "Unknown PRS1Mode:" << qPrintable(s);
            return s;
    }
    return s.mid(10).toLower();  // lop off initial PRS1_MODE_
}

static QString timeStr(int t)
{
    int h = t / 3600;
    int m = (t - (h * 3600)) / 60;
    int s = t % 60;
    return QString("%1:%2:%3").arg(h, 2, 10, QChar('0')).arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0'));
}

static QString byteList(QByteArray data, int limit)
{
    int count = data.size();
    if (limit == -1 || limit > count) limit = count;
    QStringList l;
    for (int i = 0; i < limit; i++) {
        l.push_back(QString( "%1" ).arg((int) data[i] & 0xFF, 2, 16, QChar('0') ).toUpper());
    }
    if (limit < count) l.push_back("...");
    QString s = l.join(" ");
    return s;
}

QString _PRS1ParsedEventName(PRS1ParsedEvent* e)
{
    return parsedEventTypeName(e->m_type);
}

QMap<QString,QString> _PRS1ParsedEventContents(PRS1ParsedEvent* e)
{
    return e->contents();
}

//********************************************************************************************


static QString DumpEvent(int t, int code, const unsigned char* data, int size)
{
    int s = t;
    int h = s / 3600; s -= h * 3600;
    int m = s / 60; s -= m * 60;
    QString dump = QString("%1:%2:%3 ")
        .arg(h, 2, 10, QChar('0'))
        .arg(m, 2, 10, QChar('0'))
        .arg(s, 2, 10, QChar('0'));
    dump = dump + " " + hex(code) + ":";
    for (int i = 0; i < size; i++) {
        dump = dump + QString(" %1").arg(data[i]);
    }
    return dump;
}
#define DUMP_EVENT() qWarning() << this->sessionid << DumpEvent(t, code, data + pos, size - (pos - startpos)) + "  @ " + hex(startpos-1)


void PRS1DataChunk::AddEvent(PRS1ParsedEvent* const event)
{
    m_parsedData.push_back(event);
}


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


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


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


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


void PRS1Import::CreateEventChannels(const PRS1DataChunk* chunk)
{
    const QVector<PRS1ParsedEventType> & supported = GetSupportedEvents(chunk);

    // Generate the list of channels created by non-slice events for this machine.
    // We can't just use the full list of non-slice events, since on some machines
    // PS is generated by slice events (EPAP/IPAP average).
    // TODO: convert supported to QSet and clean this up.
    QSet<PRS1ParsedEventType> supportedNonSliceEvents = QSet<PRS1ParsedEventType>::fromList(QList<PRS1ParsedEventType>::fromVector(supported));
    supportedNonSliceEvents.intersect(PRS1NonSliceChannels);
    QSet<ChannelID> supportedNonSliceChannels;
    for (auto & e : supportedNonSliceEvents) {
        for (auto & pChannelID : PRS1ImportChannelMap[e]) {
            supportedNonSliceChannels += *pChannelID;
        }
    }

    // Clear channels to prepare for a new slice, except for channels created by
    // non-slice events.
    for (auto & c : m_importChannels.keys()) {
        if (supportedNonSliceChannels.contains(c) == false) {
            m_importChannels.remove(c);
        }
    }
    
    // Create all supported channels (except for on-demand ones that only get created if an event appears)
    for (auto & e : supported) {
        if (!PRS1OnDemandChannels.contains(e)) {
            for (auto & pChannelID : PRS1ImportChannelMap[e]) {
                GetImportChannel(*pChannelID);
            }
        }
    }
}


EventList* PRS1Import::GetImportChannel(ChannelID channel)
{
    if (!channel) {
        qCritical() << this->sessionid << "channel in import table has not been added to schema!";
    }
    EventList* C = m_importChannels[channel];
    if (C == nullptr) {
        C = session->AddEventList(channel, EVL_Event);
        Q_ASSERT(C);  // Once upon a time AddEventList could return nullptr, but not any more.
        m_importChannels[channel] = C;
    }
    return C;
}


void PRS1Import::AddEvent(ChannelID channel, qint64 t, float value, float gain)
{
    EventList* C = GetImportChannel(channel);
    Q_ASSERT(C);
    if (C->count() == 0) {
        // Initialize the gain (here, since required channels are created with default gain).
        C->setGain(gain);
    } else {
        // Any change in gain is a programming error.
        if (gain != C->gain()) {
            qWarning() << "gain mismatch for channel" << channel << "at" << ts(t);
        }
    }
    
    // Add the event
    C->AddEvent(t, value, gain);
}


bool PRS1Import::UpdateCurrentSlice(PRS1DataChunk* chunk, qint64 t)
{
    bool updated = false;
    
    if (!m_currentSliceInitialized) {
        m_currentSliceInitialized = true;
        m_currentSlice = m_slices.constBegin();
        m_lastIntervalEvents.clear();  // there was no previous slice, so there are no pending end-of-slice events
        m_lastIntervalEnd = 0;
        updated = true;
    }

    // Update the slice iterator to point to the mask-on slice encompassing time t.
    while ((*m_currentSlice).status != MaskOn || t > (*m_currentSlice).end) {
        m_currentSlice++;
        updated = true;
        if (m_currentSlice == m_slices.constEnd()) {
            qWarning() << sessionid << "Events after last mask-on slice?";
            m_currentSlice--;
            break;
        }
    }
    
    if (updated) {
        // Write out any pending end-of-slice events.
        FinishSlice();
    }
    
    if (updated && (*m_currentSlice).status == MaskOn) {
        // Set the interval start times based on the new slice's start time.
        m_statIntervalStart = 0;
        StartNewInterval((*m_currentSlice).start);

        // Create a new eventlist for this new slice, to allow for a gap in the data between slices.
        CreateEventChannels(chunk);
    }
    
    return updated;
}


void PRS1Import::FinishSlice()
{
    qint64 t = m_lastIntervalEnd;  // end of the slice (at least of its interval data)

    // If the most recently recorded interval stats aren't at the end of the slice,
    // import additional events marking the end of the data.
    if (t != m_prevIntervalStart) {
        // Make sure to use the same pressure used to import the original events,
        // otherwise calculated channels (such as PS or LEAK) will be wrong.
        EventDataType orig_pressure = m_currentPressure;
        m_currentPressure = m_intervalPressure;

        // Import duplicates of each event with the end-of-slice timestamp.
        for (auto & e : m_lastIntervalEvents) {
            ImportEvent(t, e);
        }

        // Restore the current pressure.
        m_currentPressure = orig_pressure;
    }
    m_lastIntervalEvents.clear();
}


void PRS1Import::StartNewInterval(qint64 t)
{
    if (t == m_prevIntervalStart) {
        qWarning() << sessionid << "Multiple zero-length intervals at end of slice?";
    }
    m_prevIntervalStart = m_statIntervalStart;
    m_statIntervalStart = t;
}


bool PRS1Import::IsIntervalEvent(PRS1ParsedEvent* e)
{
    bool intervalEvent = false;

    // Statistical timestamps are reported at the end of a (generally) 2-minute
    // interval, rather than the start time that OSCAR expects for its imported
    // events.  (When a session or slice ends, there will be a shorter interval,
    // the previous statistics to the end of the session/slice.)
    switch (e->m_type) {
        case PRS1PressureAverageEvent::TYPE:
        case PRS1FlexPressureAverageEvent::TYPE:
        case PRS1IPAPAverageEvent::TYPE:
        case PRS1IPAPLowEvent::TYPE:
        case PRS1IPAPHighEvent::TYPE:
        case PRS1EPAPAverageEvent::TYPE:
        case PRS1TotalLeakEvent::TYPE:
        case PRS1LeakEvent::TYPE:
        case PRS1RespiratoryRateEvent::TYPE:
        case PRS1PatientTriggeredBreathsEvent::TYPE:
        case PRS1MinuteVentilationEvent::TYPE:
        case PRS1TidalVolumeEvent::TYPE:
        case PRS1FlowRateEvent::TYPE:
        case PRS1Test1Event::TYPE:
        case PRS1Test2Event::TYPE:
        case PRS1SnoreEvent::TYPE:
        case PRS1HypopneaCount::TYPE:
        case PRS1ClearAirwayCount::TYPE:
        case PRS1ObstructiveApneaCount::TYPE:
            intervalEvent = true;
            break;
        default:
            break;
    }
    
    return intervalEvent;
}


bool PRS1Import::ImportEventChunk(PRS1DataChunk* event)
{
    m_currentPressure=0;

    const QVector<PRS1ParsedEventType> & supported = GetSupportedEvents(event);
    
    // Calculate PS from IPAP/EPAP set events only when both are supported. This includes F0, but excludes
    // F5, which only reports EPAP set events, but both IPAP/EPAP average, from which PS will be calculated.
    m_calcPSfromSet = supported.contains(PRS1IPAPSetEvent::TYPE) && supported.contains(PRS1EPAPSetEvent::TYPE);
    
    // Unintentional leak calculation, see zMaskProfile:calcLeak in calcs.cpp for explanation
    m_calcLeaks = p_profile->cpap->calculateUnintentionalLeaks();
    if (m_calcLeaks) {
        // Only needed for machines that don't support it directly.
        m_calcLeaks = (supported.contains(PRS1LeakEvent::TYPE) == false);
    }
    m_lpm4 = p_profile->cpap->custom4cmH2OLeaks();
    EventDataType lpm20 = p_profile->cpap->custom20cmH2OLeaks();
    EventDataType lpm = lpm20 - m_lpm4;
    m_ppm = lpm / 16.0;

    qint64 t = qint64(event->timestamp) * 1000L;
    if (session->first() == 0) {
        qWarning() << sessionid << "Start time not set by summary?";
    } else if (t < session->first()) {
        qWarning() << sessionid << "Events start before summary?";
    }

    bool ok;
    ok = event->ParseEvents();
    
    // Set up the (possibly initial) slice based on the chunk's starting timestamp.
    UpdateCurrentSlice(event, t);

    for (int i=0; i < event->m_parsedData.count(); i++) {
        PRS1ParsedEvent* e = event->m_parsedData.at(i);
        t = qint64(event->timestamp + e->m_start) * 1000L;

        // Skip unknown events with no timestamp
        if (e->m_type == PRS1UnknownDataEvent::TYPE) {
            continue;
        }
        
        // Skip zero-length PB or LL or VB events
        if ((e->m_type == PRS1PeriodicBreathingEvent::TYPE || e->m_type == PRS1LargeLeakEvent::TYPE || e->m_type == PRS1VariableBreathingEvent::TYPE) &&
            (e->m_duration == 0)) {
            // LL occasionally appear about a minute before a new mask-on slice
            // begins, when the previous mask-on slice ended with a large leak.
            // This probably indicates the end of LL and beginning
            // of breath detection, but we don't get any real data until mask-on.
            //
            // It has also happened once in a similar scenario for PB and VB, even when
            // the two mask-on slices are in different sessions!
            continue;
        }
        
        if (e->m_type == PRS1IntervalBoundaryEvent::TYPE) {
            StartNewInterval(t);
            continue;  // these internal pseudo-events don't get imported
        }
        
        bool intervalEvent = IsIntervalEvent(e);
        qint64 interval_end_t = 0;
        if (intervalEvent) {
            // Deal with statistics that are reported at the end of an interval, but which need to be imported
            // at the start of the interval.

            if (event->family == 3 && event->familyVersion <= 3) {
                // In F3V0 and F3V3, each slice has its own chunk, so the initial call to UpdateCurrentSlice()
                // for this chunk is all that's needed.
                //
                // We can't just call it again here for simplicity, since the timestamps of F3V3 stat events
                // can go past the end of the slice.
            } else {
                // For all other machines, the event's time stamp will be within bounds of its slice, so
                // we can use it to find the current slice.
                UpdateCurrentSlice(event, t);
            }
            // Clamp this interval's end time to the end of the slice.
            interval_end_t = min(t, (*m_currentSlice).end);
            // Set this event's timestamp as the start of the interval, since that what OSCAR assumes.
            t = m_statIntervalStart;
            // TODO: ideally we would also set the duration of the event, but OSCAR doesn't have any notion of that yet.
        } else {
            // Advance the slice if needed for the regular event's timestamp.
            if (!PRS1NonSliceChannels.contains(e->m_type)) {
                UpdateCurrentSlice(event, t);
            }
        }

        // Sanity check: warn if a (non-slice) event is earlier than the current mask-on slice
        if (t < (*m_currentSlice).start && (*m_currentSlice).status == MaskOn) {
            if (!PRS1NonSliceChannels.contains(e->m_type)) {
                // LL and VB at the beginning of a mask-on session sometimes start 1 second early,
                // so suppress that warning.
                if ((*m_currentSlice).start - t > 1000 || (e->m_type != PRS1LargeLeakEvent::TYPE && e->m_type != PRS1VariableBreathingEvent::TYPE)) {
                    qWarning() << sessionid << "Event" << e->m_type << "before mask-on slice:" << ts(t);
                }
            }
        }

        // Import the event.
        switch (e->m_type) {
            // F3V3 doesn't have individual HY/CA/OA events, only counts every 2 minutes, where
            // nonzero counts show up as overview flags. Currently OSCAR doesn't have a way to
            // chart those numeric statistics, so we generate events based on the count.
            //
            // TODO: This (and VS2) would probably be better handled as numeric charts only,
            // along with enhancing overview flags to be drawn when channels have nonzero values,
            // instead of the fictitious "events" that are currently generated.
            case PRS1HypopneaCount::TYPE:
            case PRS1ClearAirwayCount::TYPE:
            case PRS1ObstructiveApneaCount::TYPE:
                // Make sure PRS1ClearAirwayEvent/etc. isn't supported before generating events from counts.
                CHECK_VALUE(supported.contains(PRS1HypopneaEvent::TYPE), false);
                CHECK_VALUE(supported.contains(PRS1ClearAirwayEvent::TYPE), false);
                CHECK_VALUE(supported.contains(PRS1ObstructiveApneaEvent::TYPE), false);

                // Divide each count into events evenly spaced over the interval.
                // NOTE: This is slightly fictional, but there's no waveform data for F3V3, so it won't
                // incorrectly associate specific events with specific flow or pressure events.
                if (e->m_value > 0) {
                    qint64 blockduration = interval_end_t - m_statIntervalStart;
                    qint64 div = blockduration / e->m_value;
                    qint64 tt = t;
                    PRS1ParsedDurationEvent ee(e->m_type, t, 0);
                    for (int i=0; i < e->m_value; ++i) {
                        ImportEvent(tt, &ee);
                        tt += div;
                    }
                }
                
                // TODO: Consider whether to have a numeric channel for HY/CA/OA that gets charted like VS does,
                // in which case we can fall through.
                break;
                
            default:
                ImportEvent(t, e);

                // Cache the most recently imported interval events so that we can import duplicate end-of-slice events if needed.
                // We can't write them here because we don't yet know if they're the last in the slice.
                if (intervalEvent) {
                    // Reset the list of pending events when we encounter a stat event in a new interval.
                    //
                    // This logic has grown sufficiently complex that it may eventually be worth encapsulating
                    // each batch of parsed interval events into a composite interval event when parsing,
                    // rather than requiring timestamp-based gymnastics to infer that structure on import.
                    if (m_lastIntervalEnd != interval_end_t) {
                        m_lastIntervalEvents.clear();
                        m_lastIntervalEnd = interval_end_t;
                        m_intervalPressure = m_currentPressure;
                    }
                    // The events need to be in order so that any dynamically calculated channels (such as PS) are properly computed.
                    m_lastIntervalEvents.append(e);
                }
                break;
        }
    }
    
    // Write out any pending end-of-slice events.
    FinishSlice();

    if (!ok) {
        return false;
    }

    // TODO: This needs to be special-cased for F3V0 and F3V3 due to their weird interval-based event format
    // until there's a way for its parser to correctly set the timestamps for truncated
    // intervals in sessions that don't end on a 2-minute boundary.
    if (!(event->family == 3 && event->familyVersion <= 3)) {
        // If the last event has a non-zero duration, t will not reflect the full duration of the chunk, so update it.
        t = qint64(event->timestamp + event->duration) * 1000L;
        if (session->last() == 0) {
            qWarning() << sessionid << "End time not set by summary?";
        } else if (t > session->last()) {
            // This has only been seen in two instances:
            // 1. Once with corrupted data, in which the summary and event files each contained
            //    multiple conflicting sessions (all brief) with the same ID.
            // 2. On one 500G110, multiple PRS1PressureSetEvents appear after the end of the session,
            //    across roughtly two dozen sessions. These seem to be discarded on official reports,
            //    see ImportEvent() below.
            qWarning() << sessionid << "Events continue after summary?";
        }
        // Events can end before the session if the mask was off before the equipment turned off.
    }

    return true;
}


void PRS1Import::ImportEvent(qint64 t, PRS1ParsedEvent* e)
{
    qint64 duration;

    // TODO: Filter out duplicate/overlapping PB and RE events.
    //
    // These actually get reported by the machines, but they cause "unordered time" warnings
    // and they throw off the session statistics. Even official reports show the wrong stats,
    // for example counting each of 3 duplicate PBs towards the total time in PB.
    //
    // It's not clear whether filtering can reasonably be done here or whether it needs
    // to be done in ImportEventChunk.
    
    const QVector<ChannelID*> & channels = PRS1ImportChannelMap[e->m_type];
    ChannelID channel = NoChannel, PS, VS2, LEAK;
    if (channels.count() > 0) {
        channel = *channels.at(0);
    }
    
    switch (e->m_type) {
        case PRS1PressureSetEvent::TYPE:  // currentPressure is used to calculate unintentional leak, not just PS
            // TODO: These have sometimes been observed with t > session->last() on a 500G110.
            // Official reports seem to discard such events, OSCAR currently doesn't.
            // Test this more thoroughly before changing behavior here.
            // fall through
        case PRS1IPAPSetEvent::TYPE:
        case PRS1IPAPAverageEvent::TYPE:
            AddEvent(channel, t, e->m_value, e->m_gain);
            m_currentPressure = e->m_value;
            break;
        case PRS1EPAPSetEvent::TYPE:
            AddEvent(channel, t, e->m_value, e->m_gain);
            if (m_calcPSfromSet) {
                PS = *(PRS1ImportChannelMap[PRS1IPAPSetEvent::TYPE].at(1));
                AddEvent(PS, t, m_currentPressure - e->m_value, e->m_gain);  // Pressure Support
            }
            break;
        case PRS1EPAPAverageEvent::TYPE:
            PS = *channels.at(1);
            AddEvent(channel, t, e->m_value, e->m_gain);
            AddEvent(PS, t, m_currentPressure - e->m_value, e->m_gain);  // Pressure Support
            break;

        case PRS1TimedBreathEvent::TYPE:
            // The duration appears to correspond to the length of the timed breath in seconds when multiplied by 0.1 (100ms)!
            // TODO: consider changing parsers to use milliseconds for time, since it turns out there's at least one way
            // they can express durations less than 1 second.
            // TODO: consider allowing OSCAR to record millisecond durations so that the display will say "2.1" instead of "21" or "2".
            duration = e->m_duration * 100L;  // for now do this here rather than in parser, since parser events don't use milliseconds
            AddEvent(*channels.at(0), t - duration, e->m_duration * 0.1F, 0.1F);  // TODO: a gain of 0.1 should render this unnecessary, but gain doesn't seem to work currently
            break;

        case PRS1ObstructiveApneaEvent::TYPE:
        case PRS1ClearAirwayEvent::TYPE:
        case PRS1HypopneaEvent::TYPE:
        case PRS1FlowLimitationEvent::TYPE:
            AddEvent(channel, t, e->m_duration, e->m_gain);
            break;

        case PRS1PeriodicBreathingEvent::TYPE:
        case PRS1LargeLeakEvent::TYPE:
        case PRS1VariableBreathingEvent::TYPE:
            // TODO: The graphs silently treat the timestamp of a span as an end time rather than start (see gFlagsLine::paint).
            // Decide whether to preserve that behavior or change it universally and update either this code or comment.
            duration = e->m_duration * 1000L;
            AddEvent(channel, t + duration, e->m_duration, e->m_gain);
            break;

        case PRS1TotalLeakEvent::TYPE:
            AddEvent(channel, t, e->m_value, e->m_gain);
            // F0 up through F0V6 doesn't appear to report unintentional leak.
            // TODO: decide whether to keep this here, shouldn't keep it here just because it's "quicker".
            // TODO: compare this value for the reported value for F5V1 and higher?
            // TODO: Fix this for 0.125 gain: it assumes 0.1 (dividing by 10.0)...
            //   Or omit, because machines with 0.125 gain report unintentional leak directly.
            if (m_calcLeaks) { // Much Quicker doing this here than the recalc method.
                EventDataType leak = e->m_value;
                leak -= (((m_currentPressure/10.0f) - 4.0) * m_ppm + m_lpm4);
                if (leak < 0) leak = 0;
                LEAK = *channels.at(1);
                AddEvent(LEAK, t, leak, 1);
            }
            break;

        case PRS1SnoreEvent::TYPE:  // snore count that shows up in flags but not waveform
            // TODO: The numeric snore graph is the right way to present this information,
            // but it needs to be shifted left 2 minutes, since it's not a starting value
            // but a past statistic.
            AddEvent(channel, t, e->m_value, e->m_gain);  // Snore count, continuous data
            if (e->m_value > 0) {
                // TODO: currently these get drawn on our waveforms, but they probably shouldn't,
                // since they don't have a precise timestamp. They should continue to be drawn
                // on the flags overview. See the comment in ImportEventChunk regarding flags
                // for numeric channels.
                //
                // We need to pass the count along so that the VS2 index will tabulate correctly.
                VS2 = *channels.at(1);
                AddEvent(VS2, t, e->m_value, 1);
            }
            break;
        case PRS1VibratorySnoreEvent::TYPE:  // real VS marker on waveform
            // TODO: These don't need to be drawn separately on the flag overview, since
            // they're presumably included in the overall snore count statistic. They should
            // continue to be drawn on the waveform, due to their precise timestamp.
            AddEvent(channel, t, e->m_value, e->m_gain);
            break;

        default:
            if (channels.count() == 1) {
                // For most events, simply pass the value through to the mapped channel.
                AddEvent(channel, t, e->m_value, e->m_gain);
            } else if (channels.count() > 1) {
                // Anything mapped to more than one channel must have a case statement above.
                qWarning() << "Missing import handler for PRS1 event type" << (int) e->m_type;
                break;
            } else {
                // Not imported, no channels mapped to this event
                // These will show up in chunk YAML and any user alerts will be driven by the parser.
            }
            break;
    }
}


static const QVector<PRS1ParsedEventType> ParsedEventsF3V6 = {
    PRS1TimedBreathEvent::TYPE,
    PRS1IPAPAverageEvent::TYPE,
    PRS1EPAPAverageEvent::TYPE,
    PRS1TotalLeakEvent::TYPE,
    PRS1RespiratoryRateEvent::TYPE,
    PRS1PatientTriggeredBreathsEvent::TYPE,
    PRS1MinuteVentilationEvent::TYPE,
    PRS1TidalVolumeEvent::TYPE,
    PRS1Test2Event::TYPE,
    PRS1Test1Event::TYPE,
    PRS1SnoreEvent::TYPE,  // No individual VS, only snore count
    PRS1LeakEvent::TYPE,
    PRS1PressurePulseEvent::TYPE,
    PRS1ObstructiveApneaEvent::TYPE,
    PRS1ClearAirwayEvent::TYPE,
    PRS1HypopneaEvent::TYPE,
    PRS1PeriodicBreathingEvent::TYPE,
    PRS1RERAEvent::TYPE,
    PRS1LargeLeakEvent::TYPE,
    PRS1ApneaAlarmEvent::TYPE,
    // No FL?
};

// 1030X, 11030X series
// based on ParseEventsF5V3, updated for F3V6
bool PRS1DataChunk::ParseEventsF3V6(void)
{
    if (this->family != 3 || this->familyVersion != 6) {
        qWarning() << "ParseEventsF3V6 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, 0xe, 3, 3, 3, 4, 5, 3, 5, 3, 3, 2, 2, 2, 2 };
    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;
    }

    // F3V6 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 0x00?
            case 1:  // 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];
                // TODO: make sure F3 import logic matches F5 in adjusting TB start time
                this->AddEvent(new PRS1TimedBreathEvent(t, duration));
                break;
            case 2:  // Statistics
                // These appear every 2 minutes, so presumably summarize the preceding period.
                //data[pos+0];  // TODO: 0 = ???
                this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos+2], GAIN));        // 02=IPAP
                this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+1], GAIN));        // 01=EPAP, needs to be added second to calculate PS
                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 PRS1Test2Event(t, data[pos+8]));                    // 08=Flow???
                this->AddEvent(new PRS1Test1Event(t, data[pos+9]));                    // 09=TMV???
                this->AddEvent(new PRS1SnoreEvent(t, data[pos+0xa]));                  // 0A=Snore count  // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
                this->AddEvent(new PRS1LeakEvent(t, data[pos+0xb]));                   // 0B=Leak (average?)
                this->AddEvent(new PRS1IntervalBoundaryEvent(t));
                break;
            case 0x03:  // Pressure Pulse
                duration = data[pos];  // TODO: is this a duration?
                this->AddEvent(new PRS1PressurePulseEvent(t, duration));
                break;
            case 0x04:  // 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 0x05:  // 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 0x06:  // 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 0x07:  // 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 0x08:  // RERA
                elapsed = data[pos];  // based on sample waveform, the RERA is over after this
                this->AddEvent(new PRS1RERAEvent(t - elapsed, 0));
                break;
            case 0x09:  // 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 0x0a:  // Hypopnea
                // TODO: Why does this hypopnea have a different event code?
                // fall through
            case 0x0b:  // 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 0x0c:  // Apnea Alarm
                // no additional data
                this->AddEvent(new PRS1ApneaAlarmEvent(t, 0));
                break;
            case 0x0d:  // Low MV Alarm
                // no additional data
                this->AddEvent(new PRS1LowMinuteVentilationAlarmEvent(t, 0));
                break;
            // case 0x0e?
            // case 0x0f?
            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;
}


static const QVector<PRS1ParsedEventType> ParsedEventsF3V0 = {
    PRS1IPAPAverageEvent::TYPE,
    PRS1EPAPAverageEvent::TYPE,
    PRS1TotalLeakEvent::TYPE,
    PRS1TidalVolumeEvent::TYPE,
    PRS1FlowRateEvent::TYPE,
    PRS1PatientTriggeredBreathsEvent::TYPE,
    PRS1RespiratoryRateEvent::TYPE,
    PRS1MinuteVentilationEvent::TYPE,
    // No LEAK, unlike F3V3
    PRS1HypopneaCount::TYPE,
    PRS1ClearAirwayCount::TYPE,  // TODO
    PRS1ObstructiveApneaCount::TYPE,  // TODO
    // No PP, FL, VS, RERA, PB, LL
    // No TB
};

static const QVector<PRS1ParsedEventType> ParsedEventsF3V3 = {
    PRS1IPAPAverageEvent::TYPE,
    PRS1EPAPAverageEvent::TYPE,
    PRS1TotalLeakEvent::TYPE,
    PRS1TidalVolumeEvent::TYPE,
    PRS1FlowRateEvent::TYPE,
    PRS1PatientTriggeredBreathsEvent::TYPE,
    PRS1RespiratoryRateEvent::TYPE,
    PRS1MinuteVentilationEvent::TYPE,
    PRS1LeakEvent::TYPE,
    PRS1HypopneaCount::TYPE,
    PRS1ClearAirwayCount::TYPE,
    PRS1ObstructiveApneaCount::TYPE,
    // No PP, FL, VS, RERA, PB, LL
    // No TB
};

// 1061, 1061T, 1160P series
bool PRS1DataChunk::ParseEventsF3V03(void)
{
    // NOTE: Older ventilators (BiPAP S/T and AVAPS) machines don't use timestamped events like everything else.
    // Instead, they use a fixed interval format like waveforms do (see PRS1_HTYPE_INTERVAL).

    if (this->family != 3 || (this->familyVersion != 0 && this->familyVersion != 3)) {
        qWarning() << "ParseEventsF3V03 called with family" << this->family << "familyVersion" << this->familyVersion;
        return false;
    }
    if (this->fileVersion == 3) {
        // NOTE: The original comment in the header for ParseF3EventsV3 said there was a 1060P with fileVersion 3.
        // We've never seen that, so warn if it ever shows up.
        qWarning() << "F3V3 event file with fileVersion 3?";
    }
    
    int t = 0;
    static const int record_size = 0x10;
    int size = this->m_data.size()/record_size;
    CHECK_VALUE(this->m_data.size() % record_size, 0);
    unsigned char * h = (unsigned char *)this->m_data.data();

    static const qint64 block_duration = 120;

    // Make sure the assumptions here agree with the header
    CHECK_VALUE(this->htype, PRS1_HTYPE_INTERVAL);
    CHECK_VALUE(this->interval_count, size);
    CHECK_VALUE(this->interval_seconds, block_duration);
    for (auto & channel : this->waveformInfo) {
        CHECK_VALUE(channel.interleave, 1);
    }
    
    for (int x=0; x < size; x++) {
        // Use the timestamp of the end of this interval, to be consistent with other parsers,
        // but see note below regarding the duration of the final interval.
        t += block_duration;

        // TODO: The duration of the final interval isn't clearly defined in this format:
        // there appears to be no way (apart from looking at the summary or waveform data)
        // to determine the end time, which may truncate the last interval.
        //
        // TODO: What if there are multiple "final" intervals in a session due to multiple
        // mask-on slices?
        this->AddEvent(new PRS1IPAPAverageEvent(t, h[0] | (h[1] << 8)));
        this->AddEvent(new PRS1EPAPAverageEvent(t, h[2] | (h[3] << 8)));
        this->AddEvent(new PRS1TotalLeakEvent(t, h[4]));
        this->AddEvent(new PRS1TidalVolumeEvent(t, h[5]));
        this->AddEvent(new PRS1FlowRateEvent(t, h[6]));
        this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, h[7]));
        this->AddEvent(new PRS1RespiratoryRateEvent(t, h[8]));
        if (this->familyVersion == 0) {
            if (h[9] < 4 || h[9] > 65) UNEXPECTED_VALUE(h[9], "4-65");
        } else {
            if (h[9] < 13 || h[9] > 84) UNEXPECTED_VALUE(h[9], "13-84");  // not sure what this is.. encore doesn't graph it.
        }
        if (this->familyVersion == 0) {
            // 1 shows as Apnea (AP) alarm
            // 2 shows as a Patient Disconnect (PD) alarm
            // 4 shows as a Low Minute Vent (LMV) alarm
            // 8 shows as a Low Pressure (LP) alarm
            // 10 shows as PD + LP in the same interval
            if (h[10] & ~(0x01 | 0x02 | 0x04 | 0x08)) UNEXPECTED_VALUE(h[10], "known bits");
        } else {
            // This is probably the same as F3V0, but we don't yet have the sample data to confirm.
            CHECK_VALUES(h[10], 0, 8);  // 8 shows as a Low Pressure (LP) alarm
        }
        this->AddEvent(new PRS1MinuteVentilationEvent(t, h[11]));
        if (this->familyVersion == 0) {
            CHECK_VALUE(h[12], 0);
            this->AddEvent(new PRS1HypopneaCount(t, h[13]));          // count of hypopnea events
            this->AddEvent(new PRS1ClearAirwayCount(t, h[14]));       // count of clear airway events
            this->AddEvent(new PRS1ObstructiveApneaCount(t, h[15]));  // count of obstructive events
        } else {
            this->AddEvent(new PRS1HypopneaCount(t, h[12]));          // count of hypopnea events
            this->AddEvent(new PRS1ClearAirwayCount(t, h[13]));       // count of clear airway events
            this->AddEvent(new PRS1ObstructiveApneaCount(t, h[14]));  // count of obstructive events
            this->AddEvent(new PRS1LeakEvent(t, h[15]));
        }
        this->AddEvent(new PRS1IntervalBoundaryEvent(t));

        h += record_size;
    }
    
    this->duration = t;

    return true;
}


#if 0
// Currently unused, apparently an abandoned effort to massage F0 pressure/IPAP/EPAP data.
extern EventDataType CatmullRomSpline(EventDataType p0, EventDataType p1, EventDataType p2, EventDataType p3, EventDataType t = 0.5);

void SmoothEventList(Session * session, EventList * ev, ChannelID code)
{
    if (!ev) return;
    int cnt = ev->count();
    if (cnt > 4) {
        EventList * smooth = new EventList(EVL_Event, ev->gain());

        smooth->setFirst(ev->first());
        smooth->AddEvent(ev->time(0), ev->raw(0));

        EventDataType p0, p1, p2, p3, v;
        for (int i=1; i<cnt-2; ++i) {
            qint64 time = ev->time(i);
            qint64 time2 = ev->time(i+1);
            qint64 diff = time2 - time;

            // these aren't evenly spaced... spline won't work here.
            p0 = ev->raw(i-1);
            p1 = ev->raw(i);
            p2 = ev->raw(i+1);
            p3 = ev->raw(i+2);

            smooth->AddEvent(time, p1);

//            int df = p2-p1;
//            if (df > 0) {
//                qint64 inter = diff/(df+1);
//                qint64 t = time+inter;
//                for (int j=0; j<df; ++j) {
//                    smooth->AddEvent(t, p1+j);
//                    t+=inter;
//                }
//            } else if (df<0) {
//                df = abs(df);
//                qint64 inter = diff/(df+1);
//                qint64 t = time+inter;
//                for (int j=0; j<df; ++j) {
//                    smooth->AddEvent(t, p1-j);
//                    t+=inter;
//                }
//            }
            // don't want to use Catmull here...


            v = CatmullRomSpline(p0, p1, p2, p3, 0.25);
            smooth->AddEvent(time+diff*0.25, v);
            v = CatmullRomSpline(p0, p1, p2, p3, 0.5);
            smooth->AddEvent(time+diff*0.5, v);
            v = CatmullRomSpline(p0, p1, p2, p3, 0.75);
            smooth->AddEvent(time+diff*0.75, v);

        }
        smooth->AddEvent(ev->time(cnt-2), ev->raw(cnt-2));
        smooth->AddEvent(ev->time(cnt-1), ev->raw(cnt-1));


        session->eventlist[code].removeAll(ev);
        delete ev;
        session->eventlist[code].append(smooth);
    }

}
#endif


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


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


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


// TODO: This really should be in some kind of class hierarchy, once we figure out
// the right one.
const QVector<PRS1ParsedEventType> & GetSupportedEvents(const PRS1DataChunk* chunk)
{
    static const QVector<PRS1ParsedEventType> none;
    
    switch (chunk->family) {
        case 0:
            switch (chunk->familyVersion) {
                case 2: return ParsedEventsF0V23; break;
                case 3: return ParsedEventsF0V23; break;
                case 4: return ParsedEventsF0V4; break;
                case 6: return ParsedEventsF0V6; break;
            }
            break;
        case 3:
            switch (chunk->familyVersion) {
                case 0: return ParsedEventsF3V0; break;
                case 3: return ParsedEventsF3V3; break;
                case 6: return ParsedEventsF3V6; break;
            }
            break;
        case 5:
            switch (chunk->familyVersion) {
                case 0: return ParsedEventsF5V0; break;
                case 1: return ParsedEventsF5V1; break;
                case 2: return ParsedEventsF5V2; break;
                case 3: return ParsedEventsF5V3; break;
            }
            break;
    }
    qWarning() << "Missing supported event list for family" << chunk->family << "version" << chunk->familyVersion;
    return none;
}


CPAPMode PRS1Import::importMode(int prs1mode)
{
    CPAPMode mode = MODE_UNKNOWN;
    
    switch (prs1mode) {
        case PRS1_MODE_CPAPCHECK:   mode = MODE_CPAP; break;
        case PRS1_MODE_CPAP:        mode = MODE_CPAP; break;
        case PRS1_MODE_AUTOCPAP:    mode = MODE_APAP; break;
        case PRS1_MODE_AUTOTRIAL:   mode = MODE_APAP; break;
        case PRS1_MODE_BILEVEL:     mode = MODE_BILEVEL_FIXED; break;
        case PRS1_MODE_AUTOBILEVEL: mode = MODE_BILEVEL_AUTO_VARIABLE_PS; break;
        case PRS1_MODE_ASV:         mode = MODE_ASV_VARIABLE_EPAP; break;
        case PRS1_MODE_S:           mode = MODE_BILEVEL_FIXED; break;
        case PRS1_MODE_ST:          mode = MODE_BILEVEL_FIXED; break;
        case PRS1_MODE_PC:          mode = MODE_BILEVEL_FIXED; break;
        case PRS1_MODE_ST_AVAPS:    mode = MODE_AVAPS; break;
        case PRS1_MODE_PC_AVAPS:    mode = MODE_AVAPS; break;
        default:
            UNEXPECTED_VALUE(prs1mode, "known PRS1 mode");
            break;
    }

    return mode;
}


bool PRS1Import::ImportCompliance()
{
    bool ok;
    ok = compliance->ParseCompliance();
    qint64 start = qint64(compliance->timestamp) * 1000L;
    
    for (int i=0; i < compliance->m_parsedData.count(); i++) {
        PRS1ParsedEvent* e = compliance->m_parsedData.at(i);
        if (e->m_type == PRS1ParsedSliceEvent::TYPE) {
            AddSlice(start, e);
            continue;
        } else if (e->m_type != PRS1ParsedSettingEvent::TYPE) {
            qWarning() << "Compliance had non-setting event:" << (int) e->m_type;
            continue;
        }
        PRS1ParsedSettingEvent* s = (PRS1ParsedSettingEvent*) e;
        switch (s->m_setting) {
            case PRS1_SETTING_CPAP_MODE:
                session->settings[PRS1_Mode] = (PRS1Mode) e->m_value;
                session->settings[CPAP_Mode] = importMode(e->m_value);
                break;
            case PRS1_SETTING_PRESSURE:
                session->settings[CPAP_Pressure] = e->value();
                break;
            case PRS1_SETTING_PRESSURE_MIN:
                session->settings[CPAP_PressureMin] = e->value();
                break;
            case PRS1_SETTING_PRESSURE_MAX:
                session->settings[CPAP_PressureMax] = e->value();
                break;
            case PRS1_SETTING_FLEX_MODE:
                session->settings[PRS1_FlexMode] = e->m_value;
                break;
            case PRS1_SETTING_FLEX_LEVEL:
                session->settings[PRS1_FlexLevel] = e->m_value;
                break;
            case PRS1_SETTING_FLEX_LOCK:
                session->settings[PRS1_FlexLock] = (bool) e->m_value;
                break;
            case PRS1_SETTING_RAMP_TIME:
                session->settings[CPAP_RampTime] = e->m_value;
                break;
            case PRS1_SETTING_RAMP_PRESSURE:
                session->settings[CPAP_RampPressure] = e->value();
                break;
            case PRS1_SETTING_RAMP_TYPE:
                session->settings[PRS1_RampType] = e->m_value;
                break;
            case PRS1_SETTING_HUMID_STATUS:
                session->settings[PRS1_HumidStatus] = (bool) e->m_value;
                break;
            case PRS1_SETTING_HUMID_MODE:
                session->settings[PRS1_HumidMode] = e->m_value;
                break;
            case PRS1_SETTING_HEATED_TUBE_TEMP:
                session->settings[PRS1_TubeTemp] = e->m_value;
                break;
            case PRS1_SETTING_HUMID_LEVEL:
                session->settings[PRS1_HumidLevel] = e->m_value;
                break;
            case PRS1_SETTING_MASK_RESIST_LOCK:
                session->settings[PRS1_MaskResistLock] = (bool) e->m_value;
                break;
            case PRS1_SETTING_MASK_RESIST_SETTING:
                session->settings[PRS1_MaskResistSet] = e->m_value;
                break;
            case PRS1_SETTING_HOSE_DIAMETER:
                session->settings[PRS1_HoseDiam] = e->m_value;
                break;
            case PRS1_SETTING_TUBING_LOCK:
                session->settings[PRS1_TubeLock] = (bool) e->m_value;
                break;
            case PRS1_SETTING_AUTO_ON:
                session->settings[PRS1_AutoOn] = (bool) e->m_value;
                break;
            case PRS1_SETTING_AUTO_OFF:
                session->settings[PRS1_AutoOff] = (bool) e->m_value;
                break;
            case PRS1_SETTING_MASK_ALERT:
                session->settings[PRS1_MaskAlert] = (bool) e->m_value;
                break;
            case PRS1_SETTING_SHOW_AHI:
                session->settings[PRS1_ShowAHI] = (bool) e->m_value;
                break;
            default:
                qWarning() << "Unknown PRS1 setting type" << (int) s->m_setting;
                break;
        }
    }

    if (!ok) {
        return false;
    }
    if (compliance->duration == 0) {
        // This does occasionally happen and merely indicates a brief session with no useful data.
        // This requires the use of really_set_last below, which otherwise rejects 0 length.
        qDebug() << compliance->sessionid << "compliance duration == 0";
    }
    session->setSummaryOnly(true);
    session->set_first(start);
    session->really_set_last(qint64(compliance->timestamp + compliance->duration) * 1000L);

    return true;
}


bool PRS1DataChunk::ParseCompliance(void)
{
    switch (this->family) {
    case 0:
        switch (this->familyVersion) {
        case 2:
        case 3:
            return this->ParseComplianceF0V23();
        case 4:
            return this->ParseComplianceF0V4();
        case 5:
            return this->ParseComplianceF0V5();
        case 6:
            return this->ParseComplianceF0V6();
        }
    default:
        ;
    }

    qWarning() << "unexpected compliance family" << this->family << "familyVersion" << this->familyVersion;
    return false;
}


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


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


// F0V4 confirmed:
// B3 0A = HT=5, H=3, HT
// A3 0A = HT=5, H=2, HT
// 33 0A = HT=4, H=3, HT
// 23 4A = HT=4, H=2, HT
// B3 09 = HT=3, H=3, HT
// A4 09 = HT=3, H=2, HT
// A3 49 = HT=3, H=2, HT
// 22 09 = HT=2, H=2, HT
// 33 09 = HT=2, H=3, HT
// 21 09 = HT=2, H=2, HT
// 13 09 = HT=2, H=1, HT
// B5 08 = HT=1, H=3, HT
// 03 08 = HT=off,    HT; data=tube t=0,h=0
// 05 24 =       H=5, S1
// 95 06 =       H=5, S1
// 95 05 =       H=5, S1
// 94 05 =       H=4, S1
// 04 24 =       H=4, S1
// A3 05 =       H=3, S1
// 92 05 =       H=2, S1
// A2 05 =       H=2, S1
// 01 24 =       H=1, S1
// 90 05 =       H=off, S1
// 30 05 =       H=off, S1
// 95 41 =       H=5, Classic
// A4 61 =       H=4, Classic
// A3 61 =       H=3, Classic
// A2 61 =       H=2, Classic
// A1 61 =       H=1, Classic
// 90 41 =       H=Off, Classic; data=classic h=0
// 94 11 =       H=3, S1, no data [note that bits encode H=4, so no data falls back to H=3]
// 93 11 =       H=3, S1, no data
// 04 30 =       H=3, S1, no data

// F0V5 confirmed:
// 00 60 =       H=Off, Classic
// 02 60 =       H=2, Classic
// 05 60 =       H=5, Classic
// 00 70 =       H=Off, no data in chart

// F5V1 confirmed:
// A0 4A = HT=5, H=2, HT
// B1 09 = HT=3, H=3, HT
// 91 09 = HT=3, H=1, HT
// 32 09 = HT=2, H=3, HT
// B2 08 = HT=1, H=3, HT
// 00 48 = HT=off, data=tube t=0,h=0
// 95 05 =       H=5, S1
// 94 05 =       H=4, S1
// 93 05 =       H=3, S1
// 92 05 =       H=2, S1
// 91 05 =       H=1, S1
// 90 05 =       H=Off, S1
// 95 41 =       H=5, Classic
// 94 41 =       H=4, Classic
// 93 41 =       H=3, Classic
// 92 41 =       H=2, Classic
// 01 60 =       H=1, Classic
// 00 60 =       H=Off, Classic
// 00 70 =       H=3, S1, no data [no data ignores Classic mode, H bits, falls back to S1 H=3]

// F5V2 confirmed:
// 00 48 = HT=off, data=tube t=0,h=0
// 93 09 = HT=3, H=1, HT
// 00 10 =       H=3, S1, no data

// XX XX = 60-Series Humidifier bytes
//  7    = humidity level without tube [on tube disconnect / system one with 22mm hose / classic]  :  0 = humidifier off
//  8    = [never seen]
// 3     = humidity level with tube
// 4     = maybe part of humidity level? [never seen]
// 8   3 = tube temperature (high bit of humid 1 is low bit of temp)
//     4 = "System One" mode (valid even when humidifier is off)
//     8 = heated tube present
//    10 = no data in chart, maybe no humidifier attached? Seems to fall back on System One = 3 despite other (humidity level and S1) bits.
//    20 = unknown, something tube related since whenever it's set tubepresent is false
//    40 = "Classic" mode (valid even when humidifier is off, ignored when heated tube is present)
//    80 = [never seen]

void PRS1DataChunk::ParseHumidifierSetting60Series(unsigned char humid1, unsigned char humid2, bool add_setting)
{
    int humidlevel = humid1 & 7;  // Ignored when heated tube is present: humidifier setting on tube disconnect is always reported as 3
    if (humidlevel > 5) UNEXPECTED_VALUE(humidlevel, "<= 5");
    CHECK_VALUE(humid1 & 8, 0);  // never seen
    int tubehumidlevel = (humid1 >> 4) & 7;  // This mask is a best guess based on other masks.
    if (tubehumidlevel > 5) UNEXPECTED_VALUE(tubehumidlevel, "<= 5");
    CHECK_VALUE(tubehumidlevel & 4, 0);  // never seen, but would clarify whether above mask is correct

    int tubetemp = (humid1 >> 7) | ((humid2 & 3) << 1);
    if (tubetemp > 5) UNEXPECTED_VALUE(tubetemp, "<= 5");

    CHECK_VALUE(humid2 & 0x80, 0);  // never seen
    bool humidclassic = (humid2 & 0x40) != 0;  // Set on classic mode reports; evidently ignored (sometimes set!) when tube is present
    //bool no_tube? = (humid2 & 0x20) != 0;  // Something tube related: whenever it is set, tube is never present (inverse is not true)
    bool no_data = (humid2 & 0x10) != 0;  // As described in chart, settings still show up
    int tubepresent = (humid2 & 0x08) != 0;
    bool humidsystemone = (humid2 & 0x04) != 0;  // Set on "System One" humidification mode reports when tubepresent is false
    if (humidsystemone && tubepresent) {
        // On a 560P, we've observed a spurious tubepresent bit being set during two sessions.
        // Those sessions (and the ones that followed) used a 22mm hose.
        CHECK_VALUE(add_setting, false);  // We've only seen this appear during a session, not in the initial settings.
        tubepresent = false;
    }

    // When no_data, reports always say "System One" with humidity level 3, regardless of humidlevel and humidsystemone

    if (humidsystemone + tubepresent + no_data == 0) CHECK_VALUE(humidclassic, true);  // Always set when everything else is off
    if (humidsystemone + tubepresent + no_data > 1) UNEXPECTED_VALUE(humid2, "one bit set");  // Only one of these ever seems to be set at a time
    if (tubepresent && tubetemp == 0) CHECK_VALUE(tubehumidlevel, 0);  // When the heated tube is off, tube humidity seems to be 0
    
    if (tubepresent) humidclassic = false;  // Classic mode bit is evidently ignored when tube is present
    if (no_data) humidclassic = false;  // Classic mode bit is evidently ignored when tube is present

    //qWarning() << this->sessionid << (humidclassic ? "C" : ".") << (humid2 & 0x20 ? "?" : ".") << (tubepresent ? "T" : ".") << (no_data ? "X" : ".") << (humidsystemone ? "1" : ".");
    /*
    if (tubepresent) {
        if (tubetemp) {
            qWarning() << this->sessionid << "tube temp" << tubetemp << "tube humidity" << tubehumidlevel << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
        } else {
            qWarning() << this->sessionid << "heated tube off" << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
        }
    } else {
        qWarning() << this->sessionid << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
    }
    */
    HumidMode humidmode = HUMID_Fixed;
    if (tubepresent) {
        humidmode = HUMID_HeatedTube;
    } else {
        if (humidsystemone + humidclassic > 1) UNEXPECTED_VALUE(humid2, "fixed or adaptive");
        if (humidsystemone) humidmode = HUMID_Adaptive;
    }

    if (add_setting) {
        bool humidifier_present = (no_data == 0);
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_STATUS, humidifier_present));
        if (humidifier_present) {
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_MODE, humidmode));
            if (humidmode == HUMID_HeatedTube) {
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HEATED_TUBE_TEMP, tubetemp));
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, tubehumidlevel));
            } else {
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, humidlevel));
            }
        }
    }

    // Check for previously unseen data that we expect to be normal:
    if (this->family == 0) {
        // F0V4
        if (tubetemp && (tubehumidlevel < 1 || tubehumidlevel > 3)) UNEXPECTED_VALUE(tubehumidlevel, "1-3");
    } else if (this->familyVersion == 1) {
        // F5V1
        if (tubepresent) {
            // all tube temperatures seen
            if (tubetemp) {
                if (tubehumidlevel == 0 || tubehumidlevel > 3) UNEXPECTED_VALUE(tubehumidlevel, "1-3");
            }
        }
    } else if (this->familyVersion == 2) {
        // F5V2
        if (tubepresent) {
            CHECK_VALUES(tubetemp, 0, 3);
            if (tubetemp) {
                CHECK_VALUE(tubehumidlevel, 1);
            }
        }
        CHECK_VALUE(humidsystemone, false);
        CHECK_VALUE(humidclassic, false);
    }
}


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


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


// XX XX = F3V3 Humidifier bytes
// 43 15 = heated tube temp 5, humidity 2
// 43 14 = heated tube temp 4, humidity 2
// 63 13 = heated tube temp 3, humidity 3
// 63 11 = heated tube temp 1, humidity 3
// 45 08 = system one 5
// 44 08 = system one 4
// 43 08 = system one 3
// 42 08 = system one 2
// 41 08 = system one 1
// 40 08 = system one 0 (off)
// 40 60 = system one 3, no data
// 40 90 = heated tube, tube off, data=tube t=0,h=0
// 45 80 = classic 5
// 44 80 = classic 4
// 43 80 = classic 3
// 42 80 = classic 2

// 40 80 = classic 0 (off)
//
//  7    = humidity level without tube
//  8    = ? (never seen)
// 1     = ? (never seen)
// 6     = heated tube humidity level (when tube present, 0x40 all other times? including when tube is off?)
// 8     = ? (never seen)
//     7 = tube temp
//     8 = "System One" mode
//    1  = tube present
//    6  = no data, seems to show system one 3 in settings, only seen in session 1 briefly
//    8  = (classic mode; also seen when heated tube present but off, possibly ignored in that case)
//
// Note that, while containing similar fields as ParseHumidifierSetting60Series, the bit arrangement is different for F3V3!

void PRS1DataChunk::ParseHumidifierSettingF3V3(unsigned char humid1, unsigned char humid2, bool add_setting)
{
    if (false) qWarning() << this->sessionid << "humid" << hex(humid1) << hex(humid2) << add_setting;

    int humidlevel = humid1 & 7;  // Ignored when heated tube is present: humidifier setting on tube disconnect is always reported as 3
    if (humidlevel > 5) UNEXPECTED_VALUE(humidlevel, "<= 5");
    CHECK_VALUE(humid1 & 0x40, 0x40);  // seems always set, even without heated tube
    CHECK_VALUE(humid1 & 0x98, 0);  // never seen
    int tubehumidlevel = (humid1 >> 5) & 7;  // This mask is a best guess based on other masks.
    if (tubehumidlevel > 5) UNEXPECTED_VALUE(tubehumidlevel, "<= 5");
    CHECK_VALUE(tubehumidlevel & 4, 0);  // never seen, but would clarify whether above mask is correct

    int tubetemp = humid2 & 7;
    if (tubetemp > 5) UNEXPECTED_VALUE(tubetemp, "<= 5");

    if (this->sessionid != 1) CHECK_VALUE(humid2 & 0x60, 0);  // Only seen on 1-second session 1 of several machines, no humidifier data on chart.
    bool humidclassic = (humid2 & 0x80) != 0;  // Set on classic mode reports; evidently ignored (sometimes set!) when tube is present
    //bool no_tube? = (humid2 & 0x20) != 0;  // Something tube related: whenever it is set, tube is never present (inverse is not true)
    bool no_data = (humid2 & 0x60) == 0x60;  // As described in chart, settings still show up
    int tubepresent = (humid2 & 0x10) != 0;
    bool humidsystemone = (humid2 & 0x08) != 0;  // Set on "System One" humidification mode reports when tubepresent is false

    if (humidsystemone + tubepresent + no_data == 0) CHECK_VALUE(humidclassic, true);  // Always set when everything else is off in F0V4
    if (humidsystemone + tubepresent /*+ no_data*/ > 1) UNEXPECTED_VALUE(humid2, "one bit set");  // Only one of these ever seems to be set at a time
    //if (tubepresent && tubetemp == 0) CHECK_VALUE(tubehumidlevel, 0);  // When the heated tube is off, tube humidity seems to be 0 in F0V4, but not F3V3

    if (tubepresent) humidclassic = false;  // Classic mode bit is evidently ignored when tube is present

    //qWarning() << this->sessionid << (humidclassic ? "C" : ".") << (humid2 & 0x20 ? "?" : ".") << (tubepresent ? "T" : ".") << (no_data ? "X" : ".") << (humidsystemone ? "1" : ".");
    /*
    if (tubepresent) {
        if (tubetemp) {
            qWarning() << this->sessionid << "tube temp" << tubetemp << "tube humidity" << tubehumidlevel << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
        } else {
            qWarning() << this->sessionid << "heated tube off" << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
        }
    } else {
        qWarning() << this->sessionid << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
    }
    */
    HumidMode humidmode = HUMID_Fixed;
    if (tubepresent) {
        humidmode = HUMID_HeatedTube;
    } else {
        if (humidsystemone + humidclassic > 1) UNEXPECTED_VALUE(humid2, "fixed or adaptive");
        if (humidsystemone) humidmode = HUMID_Adaptive;
    }

    if (add_setting) {
        bool humidifier_present = (no_data == 0);
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_STATUS, humidifier_present));
        if (humidifier_present) {
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_MODE, humidmode));
            if (humidmode == HUMID_HeatedTube) {
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HEATED_TUBE_TEMP, tubetemp));
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, tubehumidlevel));
            } else {
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, humidlevel));
            }
        }
    }
    
    // Check for previously unseen data that we expect to be normal:
    if (humidclassic && humidlevel == 1) UNEXPECTED_VALUE(humidlevel, "!= 1");
    if (tubepresent) {
        if (tubetemp) CHECK_VALUES(tubehumidlevel, 2, 3);
        if (tubetemp == 2) UNEXPECTED_VALUE(tubetemp, "!= 2");
    }
}


// Support for 1061, 1061T, 1160P
// logic largely borrowed from ParseSettingsF3V6, values based on sample data
bool PRS1DataChunk::ParseSettingsF3V03(const unsigned char* data, int /*size*/)
{
    PRS1Mode cpapmode = PRS1_MODE_UNKNOWN;
    FlexMode flexmode = FLEX_Unknown;

    // data[0] is the event code
    // data[1] is checked in the calling function

    switch (data[2]) {
        case 0: cpapmode = PRS1_MODE_CPAP; break;  // "CPAP" mode
        case 1: cpapmode = PRS1_MODE_S; break;   // "S" mode
        case 2: cpapmode = PRS1_MODE_ST; break;  // "S/T" mode; pressure seems variable?
        case 4: cpapmode = PRS1_MODE_PC; break;  // "PC" mode? Usually "PC - AVAPS", see setting 1 below
        default:
            UNEXPECTED_VALUE(data[2], "known device mode");
            break;
    }

    switch (data[3]) {
        case 0:  // 0 = None
            switch (cpapmode) {
                case PRS1_MODE_CPAP: flexmode = FLEX_None; break;
                case PRS1_MODE_S:    flexmode = FLEX_RiseTime; break;  // reports say "None" but then list a rise time setting
                case PRS1_MODE_ST:   flexmode = FLEX_RiseTime; break;  // reports say "None" but then list a rise time setting
                default:
                    UNEXPECTED_VALUE(cpapmode, "CPAP, S, or S/T");
                    break;
            }
            break;
        case 1:  // 1 = Bi-Flex, only seen with "S - Bi-Flex"
            flexmode = FLEX_BiFlex;
            CHECK_VALUE(cpapmode, PRS1_MODE_S);
            break;
        case 2:  // 2 = AVAPS: usually "PC - AVAPS", sometimes "S/T - AVAPS"
            switch (cpapmode) {
                case PRS1_MODE_ST: cpapmode = PRS1_MODE_ST_AVAPS; break;
                case PRS1_MODE_PC: cpapmode = PRS1_MODE_PC_AVAPS; break;
                default:
                    UNEXPECTED_VALUE(cpapmode, "S/T or PC");
                    break;
            }
            flexmode = FLEX_RiseTime;  // reports say "AVAPS" but then list a rise time setting
            break;
        default:
            UNEXPECTED_VALUE(data[3], "known flex mode");
            break;
    }
    if (this->familyVersion == 0) {
        // Confirm F3V0 setting encoding
        switch (cpapmode) {
        case PRS1_MODE_CPAP: break;  // CPAP has been confirmed
        case PRS1_MODE_S: break;     // S bi-flex and rise time have been confirmed
        case PRS1_MODE_ST:
            CHECK_VALUE(flexmode, FLEX_RiseTime);  // only rise time has been confirmed
            break;
        default:
            UNEXPECTED_VALUE(cpapmode, "tested modes");
        }
    }
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, (int) flexmode));

    int epap     = data[4] + (data[5] << 8);   // 0x82 = EPAP 13 cmH2O; 0x78 = EPAP 12 cmH2O; 0x50 = EPAP 8 cmH2O
    int min_ipap = data[6] + (data[7] << 8);   // 0xA0 = IPAP 16 cmH2O; 0xBE = 19 cmH2O min IPAP (in AVAPS); 0x78 = IPAP 12 cmH2O
    int max_ipap = data[8] + (data[9] << 8);   // 0xAA = ???;          0x12C = 30 cmH2O max IPAP (in AVAPS); 0x78 = ???
    switch (cpapmode) {
        case PRS1_MODE_CPAP:
            this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, epap));
            CHECK_VALUE(min_ipap, 0);
            CHECK_VALUE(max_ipap, 0);
            break;
        case PRS1_MODE_S:
        case PRS1_MODE_ST:
            this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, epap));
            this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP, min_ipap));
            this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS, min_ipap - epap));
            //CHECK_VALUES(max_ipap, 170, 300);
            break;
        case PRS1_MODE_ST_AVAPS:
        case PRS1_MODE_PC_AVAPS:
            this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, epap));
            this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_ipap));
            this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, max_ipap));
            this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ipap - epap));
            this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, max_ipap - epap));
            break;
        default:
            UNEXPECTED_VALUE(cpapmode, "expected mode");
            break;
    }

    if (cpapmode == PRS1_MODE_CPAP) {
        CHECK_VALUE(flexmode, FLEX_None);
        CHECK_VALUE(data[0xa], 0);
        CHECK_VALUE(data[0xb], 0);
        CHECK_VALUE(data[0xc], 0);
        CHECK_VALUE(data[0xd], 0);
    }
    if (flexmode == FLEX_RiseTime) {
        int rise_time = data[0xa];  // 1 = Rise Time Setting 1, 2 = Rise Time Setting 2, 3 = Rise Time Setting 3
        if (rise_time < 1 || rise_time > 6) UNEXPECTED_VALUE(rise_time, "1-6");  // TODO: what is 0?
        CHECK_VALUES(data[0xb], 0, 1);  // 1 = Rise Time Lock (in "None" and AVAPS flex mode)
        CHECK_VALUE(data[0xc], 0);
        CHECK_VALUES(data[0xd], 0, 1);  // TODO: What is this? It's usually 0.
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME, rise_time));
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME_LOCK, data[0xb] == 1));
    } else if (flexmode == FLEX_BiFlex) {
        CHECK_VALUES(data[0xa], 2, 3);  // TODO: May also be Bi-Flex level? But how is this different from [0xc] below?
        CHECK_VALUES(data[0xb], 0, 1);  // TODO: What is this? It doesn't always match [0xd].
        CHECK_VALUES(data[0xc], 2, 3);
        CHECK_VALUE(data[0x0a], data[0xc]);
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[0xc]));  // 3 = Bi-Flex 3, 2 = Bi-Flex 2 (in bi-flex mode)
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, data[0xd] == 1));
    }

    if (flexmode == FLEX_None) CHECK_VALUE(data[0xe], 0);
    if (cpapmode == PRS1_MODE_ST_AVAPS || cpapmode == PRS1_MODE_PC_AVAPS) {
        if (data[0xe] < 24 || data[0xe] > 65) UNEXPECTED_VALUE(data[0xe], "24-65");
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TIDAL_VOLUME, data[0xe] * 10.0));
    } else if (flexmode == FLEX_BiFlex || flexmode == FLEX_RiseTime) {
        CHECK_VALUE(data[0xe], 0x14);  // 0x14 = ???
    }


    int breath_rate = data[0xf];
    int timed_inspiration = data[0x10];
    bool backup = false;
    switch (cpapmode) {
        case PRS1_MODE_CPAP:
            CHECK_VALUE(breath_rate, 0);
            CHECK_VALUE(timed_inspiration, 0);
            break;
        case PRS1_MODE_S:
            if (this->familyVersion == 0) {
                CHECK_VALUE(breath_rate, 10);
                CHECK_VALUE(timed_inspiration, 10);
            } else {
                CHECK_VALUE(breath_rate, 0);
                CHECK_VALUE(timed_inspiration, 0);
            }
            break;
        case PRS1_MODE_PC_AVAPS:
            CHECK_VALUE(breath_rate, 0);  // only ever seen 0 on reports so far
            CHECK_VALUE(timed_inspiration, 30);
            backup = true;
            break;
        case PRS1_MODE_ST_AVAPS:
            if (breath_rate) {  // can be 0 on reports
                CHECK_VALUES(breath_rate, 9, 10);
            }
            if (timed_inspiration < 10 || timed_inspiration > 30) UNEXPECTED_VALUE(timed_inspiration, "10-30");
            backup = true;
            break;
        case PRS1_MODE_ST:
            if (breath_rate < 10 || breath_rate > 18) UNEXPECTED_VALUE(breath_rate, "10-18");  // can this be 0?
            if (timed_inspiration < 10 || timed_inspiration > 20) UNEXPECTED_VALUE(timed_inspiration, "10-20");  // 16 = 1.6s
            backup = true;
            break;
        default:
            UNEXPECTED_VALUE(cpapmode, "CPAP, S, S/T, or PC");
            break;
    }
    if (backup) {
        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));
    }

    CHECK_VALUE(data[0x11], 0);

    //CHECK_VALUE(data[0x12], 0x1E, 0x0F);  // 0x1E = ramp time 30 minutes, 0x0F = ramp time 15 minutes
    //CHECK_VALUE(data[0x13], 0x3C, 0x5A, 0x28);  // 0x3C = ramp pressure 6 cmH2O, 0x28 = ramp pressure 4 cmH2O, 0x5A = ramp pressure 9 cmH2O
    CHECK_VALUE(data[0x14], 0);  // the ramp pressure is probably a 16-bit value like the ones above are
    int ramp_time = data[0x12];
    int ramp_pressure = data[0x13];
    if (ramp_time > 0) {
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, ramp_time));
        this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, ramp_pressure));
    }

    int pos;
    if (this->familyVersion == 0) {
        ParseHumidifierSetting50Series(data[0x15], true);
        pos = 0x16;
    } else {
        this->ParseHumidifierSettingF3V3(data[0x15], data[0x16], true);

        // Menu options?
        CHECK_VALUES(data[0x17], 0x10, 0x90);  // 0x10 = resist 1; 0x90 = resist 1, resist lock
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_LOCK, (data[0x17] & 0x80) != 0));
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_SETTING, 1));  // only value seen so far, CHECK_VALUES above will flag any others
        
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TUBING_LOCK, (data[0x18] & 0x80) != 0));
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HOSE_DIAMETER, (data[0x18] & 0x7f)));
        CHECK_VALUES(data[0x18] & 0x7f, 22, 15);  // 0x16 = tubing 22; 0x0F = tubing 15, 0x96 = tubing 22 with lock
        pos = 0x19;
    }
    
    // Alarms?
    if (this->familyVersion == 0) {
        if (data[pos] != 0) {
            CHECK_VALUES(data[pos], 10, 30);  // Apnea alarm on F3V0
        }
        CHECK_VALUES(data[pos+1], 0, 15);  // Disconnect alarm on F3V0
        CHECK_VALUES(data[pos+2], 0, 17);  // Low MV alarm on F3V0
    } else {
        CHECK_VALUE(data[pos], 0);
        CHECK_VALUE(data[pos+1], 0);
        CHECK_VALUE(data[pos+2], 0);
    }
    return true;
}


// borrowed largely from ParseSummaryF5V012
bool PRS1DataChunk::ParseSummaryF3V03(void)
{
    if (this->family != 3 || (this->familyVersion > 3)) {
        qWarning() << "ParseSummaryF3V03 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;
    if (this->familyVersion == 0) {
        minimum_sizes = { 0x19, 3, 3, 9 };
    } else {
        minimum_sizes = { 0x1b, 3, 5, 9 };
    }
    // 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 F3V6.
        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 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;
        }
        
        // NOTE: F3V3 doesn't use 16-bit time deltas in its summary events, it uses absolute timestamps!
        // It's possible that these are 24-bit, but haven't yet seen a timestamp that large.
        
        const unsigned char * ondata = data;
        switch (code) {
            case 0:  // Equipment On
                CHECK_VALUE(pos, 1);  // Always first
                if (this->familyVersion == 0) {
                    // F3V0 inserts an extra byte in front
                    CHECK_VALUE(data[pos], 1);
                    ondata = ondata + 1;
                }
                CHECK_VALUE(ondata[pos], 0);
                /*
                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.
                }
                */
            // F3V3 doesn't have a separate settings record like F3V6 does, the settings just follow the EquipmentOn data.
                ok = this->ParseSettingsF3V03(ondata, size);
                break;
            case 2:  // Mask On
                tt = data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
                CHECK_VALUE(data[pos+2], 0);  // may be high byte of timestamp
                if (size > 3) {  // F3V3 records the humidifier setting at each mask-on, F3V0 only records the initial setting.
                    this->ParseHumidifierSettingF3V3(data[pos+3], data[pos+4]);
                }
                break;
            case 3:  // Mask Off
                tt = data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
            // F3V3 doesn't have a separate stats record like F3V6 does, the stats just follow the MaskOff data.
                CHECK_VALUE(data[pos+0x2], 0);  // may be high byte of timestamp
                if (this->familyVersion == 0) {
                    //CHECK_VALUES(data[pos+0x3], 0, 1);  // OA count
                } else {
                    CHECK_VALUE(data[pos+0x3], 0);  // probably OA count, but the only F3V3 sample data is missing .002 files, so we can't yet verify
                }
                CHECK_VALUE(data[pos+0x4], 0);
                //CHECK_VALUE(data[pos+0x5], 0);  // CA count, probably 16-bit
                CHECK_VALUE(data[pos+0x6], 0);
                //CHECK_VALUE(data[pos+0x7], 0);  // H count, probably 16-bit
                CHECK_VALUE(data[pos+0x8], 0);
                break;
            case 1:  // Equipment Off
                tt = data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
                CHECK_VALUE(data[pos+2], 0);  // may be high byte of timestamp
                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:  // ???
                tt += data[pos] | (data[pos+1] << 8);  // This adds to the total duration (otherwise it won't match report)
                break;
            case 8:  // ???
                tt += data[pos] | (data[pos+1] << 8);  // Since 7 and 8 seem to occur near each other, let's assume 8 also has a timestamp
                CHECK_VALUE(pos, 1);
                CHECK_VALUE(chunk_size, 3);
                CHECK_VALUE(data[pos], 0);  // and alert us if the timestamp is nonzero
                CHECK_VALUE(data[pos+1], 0);
                break;
            case 9:  // Humidifier setting change
                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;
}


// Originally based on ParseSummaryF5V3, with changes observed in ventilator sample data
//
// TODO: surely there will be a way to merge ParseSummary (FV3) 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::ParseSummaryF3V6(void)
{
    if (this->family != 3 || this->familyVersion != 6) {
        qWarning() << "ParseSummaryF3V6 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, 0x25, 9, 7, 4, 2, 1, 2, 2, 1, 0x18, 2, 4 };  // F5V3 = { 1, 0x38, 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, 2, equipment-off
    // (And we've seen something similar in F5V3.)
    if (chunk_size < 58) UNEXPECTED_VALUE(chunk_size, ">= 58");

    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_VALUE(data[pos], 0x10);  // usually 0x10 for 1030X, sometimes 0x40 or 0x80 are set in addition or instead
                CHECK_VALUE(size, 1);
                break;
            case 1:  // Settings
                ok = this->ParseSettingsF3V6(data + pos, size);
                break;
            case 2:  // seems equivalent to F5V3 #9, comes right after settings, usually 9 bytes, identical values
                // TODO: This may be structurally similar to settings: a list of (code, length, value).
                CHECK_VALUE(data[pos], 0);
                CHECK_VALUE(data[pos+1], 1);
                //CHECK_VALUE(data[pos+2], 0);  // Apnea Alarm (0=off, 1=10, 2=20)
                if (data[pos+2] != 0) {
                    CHECK_VALUES(data[pos+2], 1, 2);
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_APNEA_ALARM, data[pos+2] * 10));
                }
                CHECK_VALUE(data[pos+3], 1);
                CHECK_VALUE(data[pos+4], 1);
                CHECK_VALUES(data[pos+5], 0, 1);  // 1 = Low Minute Ventilation Alarm set to 1
                CHECK_VALUE(data[pos+6], 2);
                CHECK_VALUE(data[pos+7], 1);
                CHECK_VALUE(data[pos+8], 0);  // 1 = patient disconnect alarm of 15 sec on F5V3, not sure where time is encoded
                if (size > 9) {
                    CHECK_VALUE(data[pos+9],  3);
                    CHECK_VALUE(data[pos+10], 1);
                    CHECK_VALUE(data[pos+11], 0);
                    CHECK_VALUE(size, 12);
                }
                break;
            case 4:  // 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 5:  // Mask Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
                break;
            case 6:  // Ventilator CPAP stats, presumably per mask-on slice
                //CHECK_VALUE(data[pos], 0x3C);  // Average CPAP
                break;
            case 7:  // Ventilator EPAP stats, presumably per mask-on slice
                //CHECK_VALUE(data[pos], 0x69);  // Average EPAP
                //CHECK_VALUE(data[pos+1], 0x80);  // Average 90% EPAP
                break;
            case 8:  // Ventilator IPAP stats, presumably per mask-on slice
                //CHECK_VALUE(data[pos], 0x86);  // Average IPAP
                //CHECK_VALUE(data[pos+1], 0xA8);  // Average 90% IPAP
                break;
            case 0xa:  // Patient statistics, presumably per mask-on slice
                //CHECK_VALUE(data[pos], 0x00);  // 16-bit OA count
                CHECK_VALUE(data[pos+1], 0x00);
                //CHECK_VALUE(data[pos+2], 0x00);  // 16-bit CA count
                CHECK_VALUE(data[pos+3], 0x00);
                //CHECK_VALUE(data[pos+4], 0x00);  // 16-bit minutes in LL
                CHECK_VALUE(data[pos+5], 0x00);
                //CHECK_VALUE(data[pos+6], 0x0A);  // 16-bit VS count
                //CHECK_VALUE(data[pos+7], 0x00);  // We've actually seen someone with more than 255 VS in a night!
                //CHECK_VALUE(data[pos+8], 0x01);  // 16-bit H count (partial)
                CHECK_VALUE(data[pos+9], 0x00);
                //CHECK_VALUE(data[pos+0xa], 0x00);  // 16-bit H count (partial)
                CHECK_VALUE(data[pos+0xb], 0x00);
                //CHECK_VALUE(data[pos+0xc], 0x00);  // 16-bit RE count
                CHECK_VALUE(data[pos+0xd], 0x00);
                //CHECK_VALUE(data[pos+0xe], 0x3e);  // average total leak
                //CHECK_VALUE(data[pos+0xf], 0x03);  // 16-bit H count (partial)
                CHECK_VALUE(data[pos+0x10], 0x00);
                //CHECK_VALUE(data[pos+0x11], 0x11);  // average breath rate
                //CHECK_VALUE(data[pos+0x12], 0x41);  // average TV / 10
                //CHECK_VALUE(data[pos+0x13], 0x60);  // average % PTB
                //CHECK_VALUE(data[pos+0x14], 0x0b);  // average minute vent
                //CHECK_VALUE(data[pos+0x15], 0x1d);  // average leak? (similar position to F5V3, similar delta to total leak)
                //CHECK_VALUE(data[pos+0x16], 0x00);  // 16-bit minutes in PB
                CHECK_VALUE(data[pos+0x17], 0x00);
                break;
            case 3:  // Equipment Off
                tt += data[pos] | (data[pos+1] << 8);
                this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
                //CHECK_VALUES(data[pos+2], 1, 4);  // bitmask, have seen 1, 4, 6, 0x41
                //CHECK_VALUE(data[pos+3], 0x17);  // 0x16, etc.
                //CHECK_VALUES(data[pos+4], 0, 1);  // or 2
                //CHECK_VALUE(data[pos+5], 0x15);  // 0x16, etc.
                //CHECK_VALUES(data[pos+6], 0, 1);  // or 2
                break;
            case 0xc:  // 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 ParseSettingsF5V3. 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: ...
bool PRS1DataChunk::ParseSettingsF3V6(const unsigned char* data, int size)
{
    static const QMap<int,int> expected_lengths = { {0x1e,3}, {0x35,2} };
    bool ok = true;

    PRS1Mode cpapmode = PRS1_MODE_UNKNOWN;
    FlexMode flexmode = FLEX_Unknown;

    // F5V3 and F3V6 use 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 fixed_pressure = 0;
    int fixed_epap = 0;
    int fixed_ipap = 0;
    int min_ipap = 0;
    int max_ipap = 0;
    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_CPAP; break; // "CPAP" mode
                case 1: cpapmode = PRS1_MODE_S; break;   // "S" mode
                case 2: cpapmode = PRS1_MODE_ST; break;  // "S/T" mode; pressure seems variable?
                case 4: cpapmode = PRS1_MODE_PC; break;  // "PC" mode? Usually "PC - AVAPS", see setting 1 below
                default:
                    UNEXPECTED_VALUE(data[pos], "known device mode");
                    break;
                }
                break;
            case 1: // Flex Mode
                CHECK_VALUE(len, 1);
                switch (data[pos]) {
                    case 0:  // 0 = None
                        switch (cpapmode) {
                            case PRS1_MODE_CPAP: flexmode = FLEX_None; break;
                            case PRS1_MODE_S:    flexmode = FLEX_RiseTime; break;  // reports say "None" but then list a rise time setting
                            case PRS1_MODE_ST:   flexmode = FLEX_RiseTime; break;  // reports say "None" but then list a rise time setting
                            default:
                                UNEXPECTED_VALUE(cpapmode, "CPAP, S, or S/T");
                                break;
                        }
                        break;
                    case 1:  // 1 = Bi-Flex, only seen with "S - Bi-Flex"
                        flexmode = FLEX_BiFlex;
                        CHECK_VALUE(cpapmode, PRS1_MODE_S);
                        break;
                    case 2:  // 2 = AVAPS: usually "PC - AVAPS", sometimes "S/T - AVAPS"
                        switch (cpapmode) {
                            case PRS1_MODE_ST: cpapmode = PRS1_MODE_ST_AVAPS; break;
                            case PRS1_MODE_PC: cpapmode = PRS1_MODE_PC_AVAPS; break;
                            default:
                                UNEXPECTED_VALUE(cpapmode, "S/T or PC");
                                break;
                        }
                        flexmode = FLEX_RiseTime;  // reports say "AVAPS" but then list a rise time setting
                        break;
                    default:
                        UNEXPECTED_VALUE(data[pos], "known flex mode");
                        break;
                }
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, (int) flexmode));
                break;
            case 2: // ??? Maybe AAM?
                CHECK_VALUE(len, 1);
                CHECK_VALUE(data[pos], 0);
                break;
            case 3: // CPAP Pressure
                CHECK_VALUE(len, 1);
                CHECK_VALUE(cpapmode, PRS1_MODE_CPAP);
                fixed_pressure = data[pos];
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, fixed_pressure, GAIN));
                break;
            case 4: // EPAP Pressure
                CHECK_VALUE(len, 1);
                if (cpapmode == PRS1_MODE_CPAP) UNEXPECTED_VALUE(cpapmode, "!cpap");
                // pressures seem variable on practice, maybe due to ramp or leaks?
                fixed_epap = data[pos];
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, fixed_epap, GAIN));
                break;
            case 7: // IPAP Pressure
                CHECK_VALUE(len, 1);
                CHECK_VALUES(cpapmode, PRS1_MODE_S, PRS1_MODE_ST);
                // pressures seem variable on practice, maybe due to ramp or leaks?
                fixed_ipap = data[pos];
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP, fixed_ipap, GAIN));
                // TODO: We need to revisit whether PS should be shown as a setting.
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS, fixed_ipap - fixed_epap, GAIN));
                if (fixed_epap == 0) UNEXPECTED_VALUE(fixed_epap, ">0");
                break;
            case 8:  // Min IPAP
                CHECK_VALUE(len, 1);
                CHECK_VALUE(fixed_ipap, 0);
                CHECK_VALUES(cpapmode, PRS1_MODE_ST_AVAPS, PRS1_MODE_PC_AVAPS);
                min_ipap = data[pos];
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_ipap, GAIN));
                // TODO: We need to revisit whether PS should be shown as a setting.
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ipap - fixed_epap, GAIN));
                if (fixed_epap == 0) UNEXPECTED_VALUE(fixed_epap, ">0");
                break;
            case 9:  // Max IPAP
                CHECK_VALUE(len, 1);
                CHECK_VALUE(fixed_ipap, 0);
                CHECK_VALUES(cpapmode, PRS1_MODE_ST_AVAPS, PRS1_MODE_PC_AVAPS);
                if (min_ipap == 0) UNEXPECTED_VALUE(min_ipap, ">0");
                max_ipap = data[pos];
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, max_ipap, GAIN));
                // TODO: We need to revisit whether PS should be shown as a setting.
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, max_ipap - fixed_epap, GAIN));
                if (fixed_epap == 0) UNEXPECTED_VALUE(fixed_epap, ">0");
                break;
            case 0x19:  // Tidal Volume (AVAPS)
                CHECK_VALUE(len, 1);
                CHECK_VALUES(cpapmode, PRS1_MODE_ST_AVAPS, PRS1_MODE_PC_AVAPS);
                //CHECK_VALUE(data[pos], 47);  // gain 10.0
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TIDAL_VOLUME, data[pos] * 10.0));
                break;
            case 0x1e:  // (Backup) Breath Rate (S/T and PC)
                CHECK_VALUE(len, 3);
                if (cpapmode == PRS1_MODE_CPAP || cpapmode == PRS1_MODE_S) UNEXPECTED_VALUE(cpapmode, "S/T or PC");
                switch (data[pos]) {
                case 0:  // Breath Rate Off
                    // TODO: Is this mode essentially bilevel? The pressure graphs are confusing.
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Off));
                    CHECK_VALUE(data[pos+1], 0);
                    CHECK_VALUE(data[pos+2], 0);
                    break;
                //case 1:  // Breath Rate Auto in F5V3 setting 0x14
                case 2:  // Breath Rate (fixed BPM)
                    breath_rate = data[pos+1];
                    timed_inspiration = data[pos+2];
                    if (breath_rate < 9 || breath_rate > 12) UNEXPECTED_VALUE(breath_rate, "9-12");
                    if (timed_inspiration < 8 || timed_inspiration > 20) UNEXPECTED_VALUE(timed_inspiration, "8-20");
                    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:
                    CHECK_VALUES(data[pos], 0, 2);  // 0 = Breath Rate off (S), 2 = fixed BPM (1 = auto on F5V3 setting 0x14, haven't seen it on F3V6 yet)
                    break;
                }
                break;
            //0x2b: Ramp type sounds like it's linear for F3V6 unless AAM is enabled, so no setting may be needed.
            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/ventilator pressure encoding), only present when ramp is on
                CHECK_VALUE(len, 1);
                this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, data[pos], GAIN));
                break;
            case 0x2e:  // Bi-Flex level or Rise Time
                // On F5V3 the first byte could specify Bi-Flex or Rise Time, and second byte contained the value.
                // On F3V6 there's only one byte, which seems to correspond to Rise Time on the reports with flex
                // mode None or AVAPS and to Bi-Flex Setting (level) in Bi-Flex mode.
                CHECK_VALUE(len, 1);
                if (flexmode == FLEX_BiFlex) {
                    // Bi-Flex level
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[pos]));
                } else if (flexmode == FLEX_RiseTime) {
                    // Rise time
                    if (data[pos] < 1 || data[pos] > 6) UNEXPECTED_VALUE(data[pos], "1-6");  // 1-6 have been seen
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME, data[pos]));
                }
                // Timed inspiration specified in the backup breath rate.
                break;
            case 0x2f:  // Rise Time lock? (was flex lock on F0V6, 0x80 for locked)
                CHECK_VALUE(len, 1);
                if (cpapmode == PRS1_MODE_S) {
                    CHECK_VALUES(data[pos], 0, 0x80);  // Bi-Flex Lock
                    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, data[pos] != 0));
                } else {
                    CHECK_VALUE(data[pos], 0);  // Rise Time Lock? not yet observed on F3V6
                    //this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME_LOCK, data[pos] != 0));
                }
                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, 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;
            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;
}


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


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


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


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


// Humid F0V2 confirmed
// 0x00 = Off (presumably no humidifier present)
// 0x80 = Off
// 0x81 = 1
// 0x82 = 2
// 0x83 = 3
// 0x84 = 4
// 0x85 = 5

// Humid F3V0 confirmed
// 0x03 = 3 (but no humidification shown on hours of usage chart)
// 0x04 = 4 (but no humidification shown on hours of usage chart)
// 0x80 = Off
// 0x81 = 1
// 0x82 = 2
// 0x83 = 3
// 0x84 = 4
// 0x85 = 5

// Humid F5V0 confirmed
// 0x00 = Off (presumably no humidifier present)
// 0x80 = Off
// 0x81 = 1, bypass = no
// 0x82 = 2, bypass = no
// 0x83 = 3, bypass = no
// 0x84 = 4, bypass = no
// 0x85 = 5, bypass = no
// 0xA0 = Off, bypass = yes

void PRS1DataChunk::ParseHumidifierSetting50Series(int humid, bool add_setting)
{
    if (humid & (0x40 | 0x10 | 0x08)) UNEXPECTED_VALUE(humid, "known bits");
    if (humid & 0x20) {
        if (this->family == 5) {
            CHECK_VALUE(humid, 0xA0);  // only example of bypass set, unsure whether it can appear otherwise
        } else {
            CHECK_VALUE(humid & 0x20, 0);  // only ever seen on 950P, where "Bypass System One humidification" is "Yes"
        }
    }
    
    bool humidifier_present = ((humid & 0x80) != 0);  // humidifier connected
    int humidlevel = humid & 7;  // humidification level

    HumidMode humidmode = HUMID_Fixed;  // 50-Series didn't have adaptive or heated tube humidification
    if (add_setting) {
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_STATUS, humidifier_present));
        if (humidifier_present) {
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_MODE, humidmode));
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, humidlevel));
        }
    }

    // Check for truly unexpected values:
    if (humidlevel > 5) UNEXPECTED_VALUE(humidlevel, "<= 5");
    //if (!humidifier_present) CHECK_VALUES(humidlevel, 0, 1);  // Some machines appear to encode the humidlevel setting even when the humidifier is not present.
}


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


void PRS1DataChunk::ParseTubingTypeV3(unsigned char type)
{
    int diam;
    switch (type) {
    case 0: diam = 22; break;
    case 1: diam = 15; break;
    case 2: diam = 15; break;  // 15HT, though the reports only say "15" for DreamStation models
    case 3: diam = 12; break;  // seen on DreamStation Go models
    default:
        UNEXPECTED_VALUE(type, "known tubing type");
        return;
    }
    this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HOSE_DIAMETER, diam));
}


// F0V6 confirmed
// 90 B0 = HT=3!,H=3!,data=none [no humidifier appears to ignore HT and H bits and show HT=3,H=3 in details]
// 8C 6C = HT=3, H=3, data=none
// 80 00 = nothing listed in details, data=none, only seen on 400G and 502G
// 54 B4 = HT=5, H=5, data=tube
// 50 90 = HT=4, H=4, data=tube
// 4C 6C = HT=3, H=3, data=tube
// 48 68 = HT=3, H=2, data=tube
// 40 60 = HT=3, H=Off, data=tube t=3,h=0
// 50 50 = HT=2, H=4, data=tube
// 4C 4C = HT=2, H=3, data=tube
// 50 30 = HT=1, H=4, data=tube
// 4C 0C = HT=off, H=3, data=tube t=0,h=3
// 34 74 = HT=3, H=5, data=adaptive (5)
// 50 B0 = HT=5, H=4, adaptive
// 30 B0 = HT=3, H=4, data=adaptive (4)
// 30 50 = HT=3, H=4, data=adaptive (4)
// 30 10 = HT=3!,H=4, data=adaptive (4) [adaptive mode appears to ignore HT bits and show HT=3 in details]
// 30 70 = HT=3, H=4, data=adaptive (4)
// 2C 6C = HT=3, H=3, data=adaptive (3)
// 28 08 =       H=2, data=adaptive (2), no details (400G)
// 28 48 = HT=3!,H=2, data=adaptive (2) [adaptive mode appears to ignore HT bits and show HT=3 in details]
// 28 68 = HT=3, H=2, data=adaptive (2)
// 24 64 = HT=3, H=1, data=adaptive (1)
// 20 60 = HT=3, H=off, data=adaptive (0)
// 14 74 = HT=3, H=5, data=fixed (5)
// 10 70 = HT=3, H=4, data=fixed (4)
// 0C 6C = HT=3, H=3, data=fixed (3)
// 08 48 = HT=3, H=2, data=fixed (2)
// 08 68 = HT=3, H=2, data=fixed (2)
// 04 64 = HT=3, H=1, data=fixed (1)
// 00 00 = HT=3, H=off, data=fixed (0)

// F5V3 confirmed:
// 90 70 = HT=3, H=3, adaptive, data=no data
// 54 14 = HT=Off, H=5, adaptive, data=tube t=0,h=5
// 54 34 = HT=1, H=5, adaptive, data=tube t=1,h=5
// 50 70 = HT=3, H=4, adaptive, data=tube t=3,h=4
// 4C 6C = HT=3, H=3, adaptive, data=tube t=3,h=3
// 4C 4C = HT=2, H=3, adaptive, data=tube t=2,h=3
// 4C 2C = HT=1, H=3, adaptive, data=tube t=1,h=3
// 4C 0C = HT=off, H=3, adaptive, data=tube t=0,h=3
// 48 08 = HT=off, H=2, adaptive, data=tube t=0,h=2
// 44 04 = HT=off, H=1, adaptive, data=tube t=0,h=1
// 40 00 = HT=off,H=off, adaptive, data=tube t=0,h=0
// 34 74 = HT=3, H=5, adaptive, data=s1 (5)
// 30 70 = HT=3, H=4, adaptive, data=s1 (4)
// 2C 6C = HT=3, H=3, adaptive, data=s1 (3)
// 28 68 = HT=3, H=2, adaptive, data=s1 (2)
// 24 64 = HT=3, H=1, adaptive, data=s1 (1)

// F3V6 confirmed:
// 84 24 = HT=3, H=3, disconnect=adaptive, data=no data
// 50 90 = HT=4, H=4, disconnect=adaptive, data=tube t=4,h=4
// 44 84 = HT=4, H=1, disconnect=adaptive, data=tube t=4,h=1
// 40 80 = HT=4, H=Off,disconnect=adaptive, data=tube t=4,h=0
// 4C 6C = HT=3, H=3, disconnect=adaptive, data=tube t=3,h=3
// 48 68 = HT=3, H=2, disconnect=adaptive, data=tube t=3,h=2
// 44 44 = HT=2, H=1, disconnect=adaptive, data=tube t=2,h=1
// 48 28 = HT=1, H=2, disconnect=adaptive, data=tube t=1,h=2
// 54 14 = HT=Off,H=5, disconnect=adaptive data=tube t=0,h=5
// 34 14 = HT=3, H=5, disconnect=adaptive, data=s1 (5)
// 30 70 = HT=3, H=4, disconnect=adaptive, data=s1 (4)
// 2C 6C = HT=3, H=3, disconnect=adaptive, data=s1 (3)
// 28 08 = HT=3, H=2, disconnect=adaptive, data=s1 (2)
// 20 20 = HT=3, H=Off, disconnect=adaptive, data=s1 (0)
// 14 14 = HT=3, H=3, disconnect=fixed, data=classic (5)
// 10 10 = HT=3, H=4, disconnect=fixed, data=classic (4) [fixed mode appears to ignore HT bits and show HT=3 in details]
// 0C 0C = HT=3, H=3, disconnect=fixed, data=classic (3)
// 08 08 = HT=3, H=2, disconnect=fixed, data=classic (2)
// 04 64 = HT=3, H=1, disconnect=fixed, data=classic (1)

// The data is consistent among all fileVersion 3 models: F0V6, F5V3, F3V6.
//
// NOTE: F5V3 and F3V6 charts report the "Adaptive" setting as "System One" and the "Fixed"
// setting as "Classic", despite labeling the settings "Adaptive" and "Fixed" just like F0V6.
// F0V6 is consistent and labels both settings and chart as "Adaptive" and "Fixed".
//
// 400G and 502G appear to omit the humidifier settings in their details, though they
// do support humidifiers, and will show the humidification in the charts.

void PRS1DataChunk::ParseHumidifierSettingV3(unsigned char byte1, unsigned char byte2, bool add_setting)
{
    bool humidifier_present = true;
    bool humidfixed = false;  // formerly called "Classic"
    bool humidadaptive = false;  // formerly called "System One"
    bool tubepresent = false;
    bool passover = false;
    bool error = false;

    // Byte 1: 0x90 (no humidifier data), 0x50 (15ht, tube 4/5, humid 4), 0x54 (15ht, tube 5, humid 5) 0x4c (15ht, tube temp 3, humidifier 3)
    // 0x0c (15, tube 3, humid 3, fixed)
    // 0b1001 0000 no humidifier data
    // 0b0101 0000 tube 4 and 5, humidifier 4
    // 0b0101 0100 15ht, tube 5, humidifier 5
    // 0b0100 1100 15ht, tube 3, humidifier 3
    // 0b1011 0000 15, tube 3, humidifier 3, "Error" on humidification chart with asterisk at 4
    // 0b0111 0000 15, tube 3, humidifier 3, "Passover" on humidification chart with notch at 4
    //   842       = humidifier status
    //      1 84   = humidifier setting
    //          ??
    CHECK_VALUE(byte1 & 3, 0);
    int humid = byte1 >> 5;
    switch (humid) {
    case 0: humidfixed = true; break;  // fixed, ignores tubetemp bits and reports tubetemp=3
    case 1: humidadaptive = true; break;  // adaptive, ignores tubetemp bits and reports tubetemp=3
    case 2: tubepresent = true; break;  // heated tube
    case 3: passover = true; break;  // passover mode (only visible in chart)
    case 4: humidifier_present = false; break;  // no humidifier, reports tubetemp=3 and humidlevel=3
    case 5: error = true; break;  // "Error" in humidification chart, reports tubetemp=3 and humidlevel=3 in settings
    default:
        UNEXPECTED_VALUE(humid, "known value");
        break;
    }
    int humidlevel = (byte1 >> 2) & 7;

    // Byte 2: 0xB4 (15ht, tube 5, humid 5), 0xB0 (15ht, tube 5, humid 4), 0x90 (tube 4, humid 4), 0x6C (15ht, tube temp 3, humidifier 3)
    // 0x80?
    // 0b1011 0100 15ht, tube 5, humidifier 5
    // 0b1011 0000 15ht, tube 5, humidifier 4
    // 0b1001 0000 tube 4, humidifier 4
    // 0b0110 1100 15ht, tube 3, humidifier 3
    //   842       = tube temperature
    //      1 84   = humidity level when using heated tube, thus far always identical to humidlevel
    //          ??
    CHECK_VALUE(byte2 & 3, 0);
    int tubehumidlevel = (byte2 >> 2) & 7;
    CHECK_VALUE(humidlevel, tubehumidlevel);  // thus far always the same
    int tubetemp = (byte2 >> 5) & 7;
    if (humidifier_present) {
        if (humidlevel > 5 || humidlevel < 0) UNEXPECTED_VALUE(humidlevel, "0-5");  // 0=off is valid when a humidifier is attached
        if (humid == 2) {  // heated tube
            if (tubetemp > 5 || tubetemp < 0) UNEXPECTED_VALUE(tubetemp, "0-5");  // TODO: maybe this is only if heated tube? 0=off is valid even in heated tube mode
        }
    }

    // TODO: move this up into the switch statement above, given how many modes there now are.
    HumidMode humidmode = HUMID_Fixed;
    if (tubepresent) {
        humidmode = HUMID_HeatedTube;
    } else if (humidadaptive) {
        humidmode = HUMID_Adaptive;
    } else if (passover) {
        humidmode = HUMID_Passover;
    } else if (error) {
        humidmode = HUMID_Error;
    }

    if (add_setting) {
        this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_STATUS, humidifier_present));
        if (humidifier_present) {
            this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_MODE, humidmode));
            if (humidmode == HUMID_HeatedTube) {
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HEATED_TUBE_TEMP, tubetemp));
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, tubehumidlevel));
            } else {
                this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, humidlevel));
            }
        }
    }

    // Check for previously unseen data that we expect to be normal:
    if (family == 0) {
        // All variations seen.
    } else if (family == 5) {
        if (tubepresent) {
            // All tube temperature and humidity levels seen.
        } else if (humidadaptive) {
            // All humidity levels seen.
        } else if (humidfixed) {
            if (humidlevel < 3) UNEXPECTED_VALUE(humidlevel, "3-5");
        }
    } else if (family == 3) {
        if (tubepresent) {
            // All tube temperature and humidity levels seen.
        } else if (humidadaptive) {
            // All humidity levels seen.
        } else if (humidfixed) {
            if (humidlevel == 0) UNEXPECTED_VALUE(humidlevel, "1-5");
        }
    }
}


// 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) {
                    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:  // new to 400G and 500G, appears right after 0x35 (humidifier setting)
                CHECK_VALUE(len, 1);
                CHECK_VALUES(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;
}


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


// 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 > 24) UNEXPECTED_VALUE(timed_inspiration, "12-24");
                    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;
}


void PRS1Import::AddSlice(qint64 start, PRS1ParsedEvent* e)
{
    // Cache all slices and incrementally calculate their durations.
    PRS1ParsedSliceEvent* s = (PRS1ParsedSliceEvent*) e;
    qint64 tt = start + qint64(s->m_start) * 1000L;
    if (!m_slices.isEmpty()) {
        SessionSlice & prevSlice = m_slices.last();
        prevSlice.end = tt;
    }
    m_slices.append(SessionSlice(tt, tt, (SliceStatus) s->m_value));
}


bool PRS1Import::ImportSummary()
{
    if (!summary) {
        qWarning() << "ImportSummary() called with no summary?";
        return false;
    }

    qint64 start = qint64(summary->timestamp) * 1000L;
    session->set_first(start);

    // TODO: The below max pressures aren't right for the 30 cmH2O models.
    session->setPhysMax(CPAP_LeakTotal, 120);
    session->setPhysMin(CPAP_LeakTotal, 0);
    session->setPhysMax(CPAP_Pressure, 25);
    session->setPhysMin(CPAP_Pressure, 4);
    session->setPhysMax(CPAP_IPAP, 25);
    session->setPhysMin(CPAP_IPAP, 4);
    session->setPhysMax(CPAP_EPAP, 25);
    session->setPhysMin(CPAP_EPAP, 4);
    session->setPhysMax(CPAP_PS, 25);
    session->setPhysMin(CPAP_PS, 0);
    
    bool ok;
    ok = summary->ParseSummary();
    
    PRS1Mode nativemode = PRS1_MODE_UNKNOWN;
    CPAPMode cpapmode = MODE_UNKNOWN;
    for (int i=0; i < summary->m_parsedData.count(); i++) {
        PRS1ParsedEvent* e = summary->m_parsedData.at(i);
        if (e->m_type == PRS1ParsedSliceEvent::TYPE) {
            AddSlice(start, e);
            continue;
        } else if (e->m_type != PRS1ParsedSettingEvent::TYPE) {
            qWarning() << "Summary had non-setting event:" << (int) e->m_type;
            continue;
        }
        PRS1ParsedSettingEvent* s = (PRS1ParsedSettingEvent*) e;
        switch (s->m_setting) {
            case PRS1_SETTING_CPAP_MODE:
                nativemode = (PRS1Mode) e->m_value;
                cpapmode = importMode(e->m_value);
                break;
            case PRS1_SETTING_PRESSURE:
                session->settings[CPAP_Pressure] = e->value();
                break;
            case PRS1_SETTING_PRESSURE_MIN:
                session->settings[CPAP_PressureMin] = e->value();
                break;
            case PRS1_SETTING_PRESSURE_MAX:
                session->settings[CPAP_PressureMax] = e->value();
                break;
            case PRS1_SETTING_EPAP:
                session->settings[CPAP_EPAP] = e->value();
                break;
            case PRS1_SETTING_IPAP:
                session->settings[CPAP_IPAP] = e->value();
                break;
            case PRS1_SETTING_PS:
                session->settings[CPAP_PS] = e->value();
                break;
            case PRS1_SETTING_EPAP_MIN:
                session->settings[CPAP_EPAPLo] = e->value();
                break;
            case PRS1_SETTING_EPAP_MAX:
                session->settings[CPAP_EPAPHi] = e->value();
                break;
            case PRS1_SETTING_IPAP_MIN:
                session->settings[CPAP_IPAPLo] = e->value();
                break;
            case PRS1_SETTING_IPAP_MAX:
                session->settings[CPAP_IPAPHi] = e->value();
                break;
            case PRS1_SETTING_PS_MIN:
                session->settings[CPAP_PSMin] = e->value();
                break;
            case PRS1_SETTING_PS_MAX:
                session->settings[CPAP_PSMax] = e->value();
                break;
            case PRS1_SETTING_FLEX_MODE:
                session->settings[PRS1_FlexMode] = e->m_value;
                break;
            case PRS1_SETTING_FLEX_LEVEL:
                session->settings[PRS1_FlexLevel] = e->m_value;
                break;
            case PRS1_SETTING_FLEX_LOCK:
                session->settings[PRS1_FlexLock] = (bool) e->m_value;
                break;
            case PRS1_SETTING_RAMP_TIME:
                session->settings[CPAP_RampTime] = e->m_value;
                break;
            case PRS1_SETTING_RAMP_PRESSURE:
                session->settings[CPAP_RampPressure] = e->value();
                break;
            case PRS1_SETTING_RAMP_TYPE:
                session->settings[PRS1_RampType] = e->m_value;
                break;
            case PRS1_SETTING_HUMID_STATUS:
                session->settings[PRS1_HumidStatus] = (bool) e->m_value;
                break;
            case PRS1_SETTING_HUMID_MODE:
                session->settings[PRS1_HumidMode] = e->m_value;
                break;
            case PRS1_SETTING_HEATED_TUBE_TEMP:
                session->settings[PRS1_TubeTemp] = e->m_value;
                break;
            case PRS1_SETTING_HUMID_LEVEL:
                session->settings[PRS1_HumidLevel] = e->m_value;
                break;
            case PRS1_SETTING_MASK_RESIST_LOCK:
                session->settings[PRS1_MaskResistLock] = (bool) e->m_value;
                break;
            case PRS1_SETTING_MASK_RESIST_SETTING:
                session->settings[PRS1_MaskResistSet] = e->m_value;
                break;
            case PRS1_SETTING_HOSE_DIAMETER:
                session->settings[PRS1_HoseDiam] = e->m_value;
                break;
            case PRS1_SETTING_TUBING_LOCK:
                session->settings[PRS1_TubeLock] = (bool) e->m_value;
                break;
            case PRS1_SETTING_AUTO_ON:
                session->settings[PRS1_AutoOn] = (bool) e->m_value;
                break;
            case PRS1_SETTING_AUTO_OFF:
                session->settings[PRS1_AutoOff] = (bool) e->m_value;
                break;
            case PRS1_SETTING_MASK_ALERT:
                session->settings[PRS1_MaskAlert] = (bool) e->m_value;
                break;
            case PRS1_SETTING_SHOW_AHI:
                session->settings[PRS1_ShowAHI] = (bool) e->m_value;
                break;
            case PRS1_SETTING_BACKUP_BREATH_MODE:
                session->settings[PRS1_BackupBreathMode] = e->m_value;
                break;
            case PRS1_SETTING_BACKUP_BREATH_RATE:
                session->settings[PRS1_BackupBreathRate] = e->m_value;
                break;
            case PRS1_SETTING_BACKUP_TIMED_INSPIRATION:
                session->settings[PRS1_BackupBreathTi] = e->value();
                break;
            case PRS1_SETTING_TIDAL_VOLUME:
                session->settings[CPAP_TidalVolume] = e->m_value;
                break;
            case PRS1_SETTING_AUTO_TRIAL:  // new to F0V6
                session->settings[PRS1_AutoTrial] = e->m_value;
                nativemode = PRS1_MODE_AUTOTRIAL;  // Note: F0V6 reports show the underlying CPAP mode rather than Auto-Trial.
                cpapmode = importMode(nativemode);
                break;
            case PRS1_SETTING_EZ_START:
                session->settings[PRS1_EZStart] = (bool) e->m_value;
                break;
            case PRS1_SETTING_RISE_TIME:
                session->settings[PRS1_RiseTime] = e->m_value;
                break;
            case PRS1_SETTING_RISE_TIME_LOCK:
                session->settings[PRS1_RiseTimeLock] = (bool) e->m_value;
                break;
            case PRS1_SETTING_APNEA_ALARM:
            case PRS1_SETTING_DISCONNECT_ALARM:
            case PRS1_SETTING_LOW_MV_ALARM:
            case PRS1_SETTING_LOW_TV_ALARM:
                // TODO: define and add new channels for alarms once we have more samples and can reliably parse them.
                break;
            default:
                qWarning() << "Unknown PRS1 setting type" << (int) s->m_setting;
                break;
        }
    }

    if (!ok) {
        return false;
    }
    
    if (summary->m_parsedData.count() > 0) {
        if (nativemode == PRS1_MODE_UNKNOWN) UNEXPECTED_VALUE(nativemode, "known mode");
        if (cpapmode == MODE_UNKNOWN) UNEXPECTED_VALUE(cpapmode, "known mode");
        session->settings[PRS1_Mode] = nativemode;
        session->settings[CPAP_Mode] = cpapmode;
    }

    if (summary->duration == 0) {
        // This does occasionally happen and merely indicates a brief session with no useful data.
        // This requires the use of really_set_last below, which otherwise rejects 0 length.
        //qDebug() << summary->sessionid << "session duration == 0";
    }
    session->really_set_last(qint64(summary->timestamp + summary->duration) * 1000L);
    
    return true;
}


bool PRS1DataChunk::ParseSummary()
{
    switch (this->family) {
    case 0:
        if (this->familyVersion == 6) {
            return this->ParseSummaryF0V6();
        } else if (this->familyVersion == 4) {
            return this->ParseSummaryF0V4();
        } else {
            return this->ParseSummaryF0V23();
        }
    case 3:
        switch (this->familyVersion) {
            case 0: return this->ParseSummaryF3V03();
            case 3: return this->ParseSummaryF3V03();
            case 6: return this->ParseSummaryF3V6();
        }
        break;
    case 5:
        if (this->familyVersion == 1) {
            return this->ParseSummaryF5V012();
        } else if (this->familyVersion == 0) {
            return this->ParseSummaryF5V012();
        } else if (this->familyVersion == 2) {
            return this->ParseSummaryF5V012();
        } else if (this->familyVersion == 3) {
            return this->ParseSummaryF5V3();
        }
    default:
        ;
    }

    qWarning() << "unexpected family" << this->family << "familyVersion" << this->familyVersion;
    return false;
}


// TODO: The nested switch statement below just begs for per-version subclasses.
bool PRS1DataChunk::ParseEvents()
{
    bool ok = false;
    switch (this->family) {
        case 0:
            switch (this->familyVersion) {
                case 2: ok = this->ParseEventsF0V23(); break;
                case 3: ok = this->ParseEventsF0V23(); break;
                case 4: ok = this->ParseEventsF0V4(); break;
                case 6: ok = this->ParseEventsF0V6(); break;
            }
            break;
        case 3:
            switch (this->familyVersion) {
                case 0: ok = this->ParseEventsF3V03(); break;
                case 3: ok = this->ParseEventsF3V03(); break;
                case 6: ok = this->ParseEventsF3V6(); break;
            }
            break;
        case 5:
            switch (this->familyVersion) {
                case 0: ok = this->ParseEventsF5V0(); break;
                case 1: ok = this->ParseEventsF5V1(); break;
                case 2: ok = this->ParseEventsF5V2(); break;
                case 3: ok = this->ParseEventsF5V3(); break;
            }
            break;
        default:
            qDebug() << "Unknown PRS1 family" << this->family << "familyVersion" << this->familyVersion;
    }
    return ok;
}


bool PRS1Import::ImportEvents()
{
    bool ok = true;
    
    for (auto & event : m_event_chunks.values()) {
        bool chunk_ok = this->ImportEventChunk(event);
        if (!chunk_ok && m_event_chunks.count() > 1) {
            // Specify which chunk had problems if there's more than one. ParseSession will warn about the overall result.
            qWarning() << event->sessionid << QString("Error parsing events in %1 @ %2, continuing")
                .arg(relativePath(event->m_path))
                .arg(event->m_filepos);
        }
        ok &= chunk_ok;
    }

    if (ok) {
        // Sanity check: warn if channels' eventlists don't line up with the final mask-on slices.
        // First make a list of the mask-on slices that will be imported (nonzero duration)
        QVector<SessionSlice> maskOn;
        for (auto & slice : m_slices) {
            if (slice.status == MaskOn) {
                if (slice.end > slice.start) {
                    maskOn.append(slice);
                } else {
                    qWarning() << this->sessionid << "Dropping empty mask-on slice:" << ts(slice.start);
                }
            }
        }
        // Then go through each required channel and make sure each eventlist is within
        // the bounds of the corresponding slice, warn if not.
        if (maskOn.count() > 0 && m_event_chunks.count() > 0) {
            QVector<SessionSlice> maskOnWithEvents = maskOn;
            if (m_event_chunks.first()->family == 3 && m_event_chunks.first()->familyVersion <= 3) {
                // F3V0 and F3V3 sometimes omit (empty) event chunks if the mask-on slice is shorter than 2 minutes.
                // Specifically, 1061401 and 1061T always do, but 1160P usually doesn't. Sometimes 1160P will omit
                // just the first event chunk if the first mask-on slice is shorter than 2 minutes.
                int empty = maskOn.count() - m_event_chunks.count();
                if (empty > 0) {
                    // If there are fewer event chunks than mask-on slices, filter the list to have just the
                    // mask-on slices that we expect to have events.
                    int skipped = 0;
                    maskOnWithEvents.clear();
                    for (auto & slice : maskOn) {
                        if (skipped < empty && slice.end - slice.start < 120 * 1000L) {
                            skipped++;
                            continue;
                        }
                        maskOnWithEvents.append(slice);
                    }
                }
            }
            if (maskOnWithEvents.count() < m_event_chunks.count()) {
                qWarning() << sessionid << "has more event chunks than mask-on slices!";
            }
            const QVector<PRS1ParsedEventType> & supported = GetSupportedEvents(m_event_chunks.first());
            for (auto & e : supported) {
                if (!PRS1OnDemandChannels.contains(e) && !PRS1NonSliceChannels.contains(e)) {
                    for (auto & pChannelID : PRS1ImportChannelMap[e]) {
                        auto & eventlists = session->eventlist[*pChannelID];
                        if (eventlists.count() != maskOnWithEvents.count()) {
                            qWarning() << sessionid << "has" << maskOnWithEvents.count() << "mask-on slices, channel"
                                << *pChannelID << "has" << eventlists.count() << "eventlists";
                            continue;
                        }
                        for (int i = 0; i < eventlists.count(); i++) {
                            if (eventlists[i]->count() == 0) continue;  // no first/last timestamp
                            auto & list = eventlists[i];
                            auto & slice = maskOnWithEvents[i];
                            if (list->first() < slice.start || list->first() > slice.end ||
                                list->last() < slice.start || list->last() > slice.end) {
                                qWarning() << sessionid << "channel" << *pChannelID << "has events outside of mask-on slice" << i;
                            }
                        }
                    }
                }
            }
        }
        // The above is just sanity-checking the results of our import process, that discontinuous
        // data is fully contained within mask-on slices.
    
        session->m_cnt.clear();
        session->m_cph.clear();

        session->m_valuesummary[CPAP_Pressure].clear();
        session->m_valuesummary.erase(session->m_valuesummary.find(CPAP_Pressure));
    }

    return ok;
}


QList<PRS1DataChunk *> PRS1Import::CoalesceWaveformChunks(QList<PRS1DataChunk *> & allchunks)
{
    QList<PRS1DataChunk *> coalesced;
    PRS1DataChunk *chunk = nullptr, *lastchunk = nullptr;
    int num;
    
    for (int i=0; i < allchunks.size(); ++i) {
        chunk = allchunks.at(i);
        
        // Log mismatched waveform session IDs
        QFileInfo fi(chunk->m_path);
        bool numeric;
        QString session_s = fi.fileName().section(".", 0, -2);
        qint32 sid = session_s.toInt(&numeric, m_sessionid_base);
        if (!numeric || sid != chunk->sessionid) {
            qWarning() << chunk->m_path << "@" << chunk->m_filepos << "session ID mismatch:" << chunk->sessionid;
        }

        if (lastchunk != nullptr) {
            // A handful of 960P waveform files have been observed to have multiple sessions.
            //
            // This breaks the current approach of deferring waveform parsing until the (multithreaded)
            // import, since each session is in a separate import task and could be in a separate
            // thread, or already imported by the time it is discovered that this file contains
            // more than one session.
            //
            // For now, we just dump the chunks that don't belong to the session currently
            // being imported in this thread, since this happens so rarely.
            //
            // TODO: Rework the import process to handle waveform data after compliance/summary/
            // events (since we're no longer inferring session information from it) and add it to the
            // newly imported sessions.
            if (lastchunk->sessionid != chunk->sessionid) {
                qWarning() << chunk->m_path << "@" << chunk->m_filepos
                    << "session ID" << lastchunk->sessionid << "->" << chunk->sessionid
                    << ", skipping" << allchunks.size() - i << "remaining chunks";
                // Free any remaining chunks
                for (int j=i; j < allchunks.size(); ++j) {
                    chunk = allchunks.at(j);
                    delete chunk;
                }
                break;
            }
            
            // Check whether the data format is the same between the two chunks
            bool same_format = (lastchunk->waveformInfo.size() == chunk->waveformInfo.size());
            if (same_format) {
                num = chunk->waveformInfo.size();
                for (int n=0; n < num; n++) {
                    const PRS1Waveform &a = lastchunk->waveformInfo.at(n);
                    const PRS1Waveform &b = chunk->waveformInfo.at(n);
                    if (a.interleave != b.interleave) {
                        // We've never seen this before
                        qWarning() << chunk->m_path << "format change?" << a.interleave << b.interleave;
                        same_format = false;
                        break;
                    }
                }
            } else {
                // We've never seen this before
                qWarning() << chunk->m_path << "channels change?" << lastchunk->waveformInfo.size() << chunk->waveformInfo.size();
            }
            
            qint64 diff = (chunk->timestamp - lastchunk->timestamp) - lastchunk->duration;
            if (same_format && diff == 0) {
                // Same format and in sync, so append waveform data to previous chunk
                lastchunk->m_data.append(chunk->m_data);
                lastchunk->duration += chunk->duration;
                delete chunk;
                continue;
            }
            // else start a new chunk to resync
        }
        
        // Report any formats we haven't seen before
        num = chunk->waveformInfo.size();
        if (num > 2) {
            qDebug() << chunk->m_path << num << "channels";
        }
        for (int n=0; n < num; n++) {
            int interleave = chunk->waveformInfo.at(n).interleave;
            switch (chunk->ext) {
                case 5:  // flow data, 5 samples per second
                    if (interleave != 5) {
                        qDebug() << chunk->m_path << "interleave?" << interleave;
                    }
                    break;
                case 6:  // oximetry, 1 sample per second
                    if (interleave != 1) {
                        qDebug() << chunk->m_path << "interleave?" << interleave;
                    }
                    break;
                default:
                    qWarning() << chunk->m_path << "unknown waveform?" << chunk->ext;
                    break;
            }
        }
        
        coalesced.append(chunk);
        lastchunk = chunk;
    }
    
    // In theory there could be broken sessions that have waveform data but no summary or events.
    // Those waveforms won't be skipped by the scanner, so we have to check for them here.
    //
    // This won't be perfect, since any coalesced chunks starting after midnight of the threshhold
    // date will also be imported, but those should be relatively few, and tolerable imprecision.
    QList<PRS1DataChunk *> coalescedAndFiltered;
    qint64 ignoreBefore = p_profile->session->ignoreOlderSessionsDate().toMSecsSinceEpoch()/1000;
    bool ignoreOldSessions = p_profile->session->ignoreOlderSessions();

    for (auto & chunk : coalesced) {
        if (ignoreOldSessions && chunk->timestamp < ignoreBefore) {
            qWarning().noquote() << relativePath(chunk->m_path) << "skipping session" << chunk->sessionid << ":"
                << QDateTime::fromMSecsSinceEpoch(chunk->timestamp*1000).toString() << "older than"
                << QDateTime::fromMSecsSinceEpoch(ignoreBefore*1000).toString();
            delete chunk;
            continue;
        }
        coalescedAndFiltered.append(chunk);
    }

    return coalescedAndFiltered;
}


void PRS1Import::ImportOximetry()
{
    int size = oximetry.size();

    for (int i=0; i < size; ++i) {
        PRS1DataChunk * oxi = oximetry.at(i);
        int num = oxi->waveformInfo.size();
        CHECK_VALUE(num, 2);

        int size = oxi->m_data.size();
        if (size == 0) {
            qDebug() << oxi->sessionid << oxi->timestamp << "empty?";
            continue;
        }
        quint64 ti = quint64(oxi->timestamp) * 1000L;
        qint64 dur = qint64(oxi->duration) * 1000L;

        if (num > 1) {
            CHECK_VALUE(oxi->waveformInfo.at(0).interleave, 1);
            CHECK_VALUE(oxi->waveformInfo.at(1).interleave, 1);
            
            // Process interleaved samples
            QVector<QByteArray> data;
            data.resize(num);

            int pos = 0;
            do {
                for (int n=0; n < num; n++) {
                    int interleave = oxi->waveformInfo.at(n).interleave;
                    data[n].append(oxi->m_data.mid(pos, interleave));
                    pos += interleave;
                }
            } while (pos < size);
            CHECK_VALUE(data[0].size(), data[1].size());

            ImportOximetryChannel(OXI_Pulse, data[0], ti, dur);

            ImportOximetryChannel(OXI_SPO2, data[1], ti, dur);
        }
    }
}


void PRS1Import::ImportOximetryChannel(ChannelID channel, QByteArray & data, quint64 ti, qint64 dur)
{
    if (data.size() == 0)
        return;

    unsigned char* raw = (unsigned char*) data.data();
    qint64 step = dur / data.size();
    CHECK_VALUE(dur % data.size(), 0);
    
    bool pending_samples = false;
    quint64 start_ti;
    int start_i;
    
    // Split eventlist on invalid values (254-255)
    for (int i=0; i < data.size(); i++) {
        unsigned char value = raw[i];
        bool valid = (value < 254);

        if (valid) {
            if (pending_samples == false) {
                pending_samples = true;
                start_i  = i;
                start_ti = ti;
            }
            
            if (channel == OXI_Pulse) {
                // Values up through 253 are confirmed to be reported as valid on official reports.
            } else {
                if (value > 100) UNEXPECTED_VALUE(value, "<= 100%");
            }
        } else {
            if (pending_samples) {
                // Create the pending event list
                EventList* el = session->AddEventList(channel, EVL_Waveform, 1.0, 0.0, 0.0, 0.0, step);
                el->AddWaveform(start_ti, &raw[start_i], i - start_i, ti - start_ti);
                pending_samples = false;
            }
        }
        ti += step;
    }

    if (pending_samples) {
        // Create the pending event list
        EventList* el = session->AddEventList(channel, EVL_Waveform, 1.0, 0.0, 0.0, 0.0, step);
        el->AddWaveform(start_ti, &raw[start_i], data.size() - start_i, ti - start_ti);
        pending_samples = false;
    }
}


void PRS1Import::ImportWaveforms()
{
    int size = waveforms.size();
    quint64 s1, s2;

    int discontinuities = 0;
    qint64 lastti=0;

    for (int i=0; i < size; ++i) {
        PRS1DataChunk * waveform = waveforms.at(i);
        int num = waveform->waveformInfo.size();

        int size = waveform->m_data.size();
        if (size == 0) {
            qDebug() << waveform->sessionid << waveform->timestamp << "empty?";
            continue;
        }
        quint64 ti = quint64(waveform->timestamp) * 1000L;
        quint64 dur = qint64(waveform->duration) * 1000L;

        qint64 diff = ti - lastti;
        if ((lastti != 0) && (diff == 1000 || diff == -1000)) {
            // TODO: Handle discontinuities properly.
            // Option 1: preserve the discontinuity and make it apparent:
            // - In the case of a 1-sec overlap, truncate the previous waveform by 1s (+1 sample).
            // - Then start a new eventlist for the new section.
            // > The down side of this approach is gaps in the data.
            // Option 2: slide the waveform data a fraction of a second to avoid the discontinuity
            // - In the case of a single discontinuity, simply adjust the timestamps of each section by 0.5s so they meet.
            // - In the case of multiple discontinuities, fitting them is more complicated
            // > The down side of this approach is that events won't line up exactly the same as official reports.
            //
            // Evidently the machines' internal clock drifts slightly, and in some sessions that
            // means two adjacent (5-minute) waveform chunks have have a +/- 1 second difference in
            // their notion of the correct time, since the machines only record time at 1-second
            // resolution. Presumably the real drift is fractional, but there's no way to tell from
            // the data.
            //
            // Encore apparently drops the second chunk entirely if it overlaps with the first
            // (even by 1 second), and inserts a 1-second gap in the data if it's 1 second later than
            // the first ended.
            //
            // At worst in the former case it seems preferable to drop the overlap and then one
            // additional second to mark the discontinuity. But depending how often these drifts
            // occur, it may be possible to adjust all the data so that it's continuous. "Overlapping"
            // data is not identical, so it seems like these discontinuities are simply an artifact
            // of timestamping at 1-second intervals right around the 1-second boundary.

            //qDebug() << waveform->sessionid << "waveform discontinuity:" << (diff / 1000L) << "s @" << ts(waveform->timestamp * 1000L);
            discontinuities++;
        }

        if (num > 1) {
            float pressure_gain = 0.1F;  // standard pressure gain
            if ((waveform->family == 5 && (waveform->familyVersion == 2 || waveform->familyVersion == 3)) ||
                (waveform->family == 3 && waveform->familyVersion == 6)){
                // F5V2, F5V3, and F3V6 use a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
                pressure_gain = 0.125F;  // TODO: this should be parameterized somewhere better, once we have a clear idea of which machines use this
            }
            
            // Process interleaved samples
            QVector<QByteArray> data;
            data.resize(num);

            int pos = 0;
            do {
                for (int n=0; n < num; n++) {
                    int interleave = waveform->waveformInfo.at(n).interleave;
                    data[n].append(waveform->m_data.mid(pos, interleave));
                    pos += interleave;
                }
            } while (pos < size);

            s1 = data[0].size();
            s2 = data[1].size();

            if (s1 > 0) {
                EventList * flow = session->AddEventList(CPAP_FlowRate, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(dur) / double(s1));
                flow->AddWaveform(ti, (char *)data[0].data(), data[0].size(), dur);
            }

            if (s2 > 0) {
                // NOTE: The 900X (F5V3) clamps the values at 127 (15.875 cmH2O) for some reason.
                //
                // Previous autoSV machines (950P-961P, F5V0-F5V2) didn't, nor do 1030X (F3V6).
                // 1130X (also F3V6) is unknown, but likely follows the 1030X. Older ventilators
                // (F3V3) are also unknown.
                EventList * pres = session->AddEventList(CPAP_MaskPressureHi, EVL_Waveform, pressure_gain, 0.0f, 0.0f, 0.0f, double(dur) / double(s2));
                pres->AddWaveform(ti, (unsigned char *)data[1].data(), data[1].size(), dur);
            }

        } else {
            // Non interleaved, so can process it much faster
            EventList * flow = session->AddEventList(CPAP_FlowRate, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(dur) / double(waveform->m_data.size()));
            flow->AddWaveform(ti, (char *)waveform->m_data.data(), waveform->m_data.size(), dur);
        }
        lastti = dur+ti;
    }
    
    if (discontinuities > 1) {
        qWarning() << session->session() << "multiple discontinuities!" << discontinuities;
    }
}

void PRS1Import::run()
{
    if (mach->unsupported())
        return;

    if (ParseSession()) {
        SaveSessionToDatabase();
    }
}


bool PRS1Import::ParseSession(void)
{
    bool ok = false;
    bool save = false;
    session = new Session(mach, sessionid);

    do {
        if (compliance != nullptr) {
            ok = ImportCompliance();
            if (!ok) {
                // We don't see any parse errors with our test data, so warn if there's ever an error encountered.
                qWarning() << sessionid << "Error parsing compliance, skipping session";
                break;
            }
        }
        if (summary != nullptr) {
            if (compliance != nullptr) {
                qWarning() << sessionid << "Has both compliance and summary?!";
                // Never seen this, but try the summary anyway.
            }
            ok = ImportSummary();
            if (!ok) {
                // We don't see any parse errors with our test data, so warn if there's ever an error encountered.
                qWarning() << sessionid << "Error parsing summary, skipping session";
                break;
            }
        }
        if (compliance == nullptr && summary == nullptr) {
            // With one exception, the only time we've seen missing .000 or .001 data has been with a corrupted card,
            // or occasionally with partial cards where the .002 is the first file in the Pn directory
            // and we're missing the preceding directory. Since the lack of compliance or summary means we
            // don't know the therapy settings or if the mask was ever off, we just skip this very rare case.
            qWarning() << sessionid << "No compliance or summary, skipping session";
            break;
        }
        
        // Import the slices into the session
        for (auto & slice : m_slices) {
            // Filter out 0-length slices, since they cause problems for Day::total_time().
            if (slice.end > slice.start) {
                // Filter out everything except mask on/off, since gSessionTimesChart::paint assumes those are the only options.
                if (slice.status == MaskOn) {
                    session->m_slices.append(slice);
                } else if (slice.status == MaskOff) {
                    // Mark this slice as BND
                    AddEvent(PRS1_BND, slice.end, (slice.end - slice.start) / 1000L, 1.0);
                    session->m_slices.append(slice);
                }
            }
        }
        
        // If are no mask-on slices, then there's not any meaningful event or waveform data for the session.
        // If there's no no event or waveform data, mark this session as a summary.
        if (session->m_slices.count() == 0 || (m_event_chunks.count() == 0 && m_wavefiles.isEmpty() && m_oxifiles.isEmpty())) {
            session->setSummaryOnly(true);
            save = true;
            break;  // and skip the occasional fragmentary event or waveform data
        }

        // TODO: There should be a way to distinguish between no-data-to-import vs. parsing errors
        // (once we figure out what's benign and what isn't).
        if (m_event_chunks.count() > 0) {
            ok = ImportEvents();
            if (!ok) {
                qWarning() << sessionid << "Error parsing events, proceeding anyway?";
            }
        }

        if (!m_wavefiles.isEmpty()) {
            // Parse .005 Waveform files
            waveforms = ReadWaveformData(m_wavefiles, "Waveform");

            // Extract and import raw data into channels.
            ImportWaveforms();
        }

        if (!m_oxifiles.isEmpty()) {
            // Parse .006 Waveform files
            oximetry = ReadWaveformData(m_oxifiles, "Oximetry");

            // Extract and import raw data into channels.
            ImportOximetry();
        }

        save = true;
    } while (false);
    
    return save;
}


QList<PRS1DataChunk *> PRS1Import::ReadWaveformData(QList<QString> & files, const char* label)
{
    QMap<qint64,PRS1DataChunk *> waveform_chunks;
    QList<PRS1DataChunk *> result;

    if (files.count() > 1) {
        qDebug() << session->session() << label << "data split across multiple files";
    }
    
    for (auto & f : files) {
        // Parse a single .005 or .006 waveform file
        QList<PRS1DataChunk *> file_chunks = loader->ParseFile(f);
        for (auto & chunk : file_chunks) {
            PRS1DataChunk* previous = waveform_chunks[chunk->timestamp];
            if (previous != nullptr) {
                // Skip any chunks with identical timestamps. Never yet seen, so warn.
                qWarning() << chunkComparison(chunk, previous);
                delete chunk;
                continue;
            }
            waveform_chunks[chunk->timestamp] = chunk;
        }
    }
    
    // Get the list of pointers sorted by timestamp.
    result = waveform_chunks.values();

    // Coalesce contiguous waveform chunks into larger chunks.
    result = CoalesceWaveformChunks(result);

    return result;
}


void PRS1Import::SaveSessionToDatabase(void)
{
    // Make sure it's saved
    session->SetChanged(true);

    // Add the session to the database
    loader->addSession(session);

    // Update indexes, process waveform and perform flagging
    session->UpdateSummaries();

    // Save is not threadsafe
    loader->saveMutex.lock();
    session->Store(mach->getDataPath());
    loader->saveMutex.unlock();

    // Unload them from memory
    session->TrashEvents();
}


QList<PRS1DataChunk *> PRS1Loader::ParseFile(const QString & path)
{
    QList<PRS1DataChunk *> CHUNKS;

    if (path.isEmpty()) {
        // ParseSession passes empty filepaths for waveforms if none exist.
        //qWarning() << path << "ParseFile given empty path";
        return CHUNKS;
    }

    QFile f(path);

    if (!f.exists()) {
        qWarning() << path << "missing";
        return CHUNKS;
    }

    if (!f.open(QIODevice::ReadOnly)) {
        qWarning() << path << "can't open";
        return CHUNKS;
    }

    PRS1DataChunk *chunk = nullptr, *lastchunk = nullptr;

    int cnt = 0;

    do {
        chunk = PRS1DataChunk::ParseNext(f, this);
        if (chunk == nullptr) {
            break;
        }
        chunk->SetIndex(cnt);  // for logging/debugging purposes

        if (lastchunk != nullptr) {
            if ((lastchunk->fileVersion != chunk->fileVersion)
                    || (lastchunk->ext != chunk->ext)
                    || (lastchunk->family != chunk->family)
                    || (lastchunk->familyVersion != chunk->familyVersion)
                    || (lastchunk->htype != chunk->htype)) {
                QString message = "*** unexpected change in header data";
                qWarning() << path << message;
                LogUnexpectedMessage(message);
                // There used to be error-recovery code here, written before we checked CRCs.
                // If we ever encounter data with a valid CRC that triggers the above warnings,
                // we can then revisit how to handle it.
            }
        }

        CHUNKS.append(chunk);
        lastchunk = chunk;
        cnt++;
    } while (!f.atEnd());

    return CHUNKS;
}


PRS1DataChunk::PRS1DataChunk(QFile & f, PRS1Loader* in_loader) : loader(in_loader)
{
    m_path = QFileInfo(f).canonicalFilePath();
}

PRS1DataChunk::~PRS1DataChunk()
{
    for (int i=0; i < m_parsedData.count(); i++) {
        PRS1ParsedEvent* e = m_parsedData.at(i);
        delete e;
    }
}


PRS1DataChunk* PRS1DataChunk::ParseNext(QFile & f, PRS1Loader* loader)
{
    PRS1DataChunk* out_chunk = nullptr;
    PRS1DataChunk* chunk = new PRS1DataChunk(f, loader);

    do {
        // Parse the header and calculate its checksum.
        bool ok = chunk->ReadHeader(f);
        if (!ok) {
            break;
        }

        // Make sure the calculated checksum matches the stored checksum.
        if (chunk->calcChecksum != chunk->storedChecksum) {
            qWarning() << chunk->m_path << "header checksum calc" << chunk->calcChecksum << "!= stored" << chunk->storedChecksum;
            break;
        }

        // Read the block's data and calculate the block CRC.
        ok = chunk->ReadData(f);
        if (!ok) {
            break;
        }
        
        // Make sure the calculated CRC over the entire chunk (header and data) matches the stored CRC.
        if (chunk->calcCrc != chunk->storedCrc) {
            // Corrupt data block, warn about it.
            qWarning() << chunk->m_path << "@" << chunk->m_filepos << "block CRC calc" << hex << chunk->calcCrc << "!= stored" << hex << chunk->storedCrc;
            
            // TODO: When this happens, it's usually because the chunk was truncated and another chunk header
            // exists within the blockSize bytes. In theory it should be possible to rewing and resync by
            // looking for another chunk header with the same fileVersion, htype, family, familyVersion, and
            // ext (blockSize and other fields could vary).
            //
            // But this is quite rare, so for now we bail on the rest of the file.
            break;
        }

        // Only return the chunk if it has passed all tests above.
        out_chunk = chunk;
    } while (false);

    if (out_chunk == nullptr) delete chunk;
    return out_chunk;
}


bool PRS1DataChunk::ReadHeader(QFile & f)
{
    bool ok = false;
    do {
        // Read common header fields.
        this->m_filepos = f.pos();
        this->m_header = f.read(15);
        if (this->m_header.size() != 15) {
            if (this->m_header.size() == 0) {
                qWarning() << this->m_path << "empty, skipping";
            } else {
                qWarning() << this->m_path << "file too short?";
            }
            break;
        }
        
        unsigned char * header = (unsigned char *)this->m_header.data();
        this->fileVersion = header[0];    // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed
        this->blockSize = (header[2] << 8) | header[1];
        this->htype = header[3];      // 00 = normal, 01=waveform
        this->family = header[4];
        this->familyVersion = header[5];
        this->ext = header[6];
        this->sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7];
        this->timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11];

        // Do a few early sanity checks before any variable-length header data.
        if (this->blockSize == 0) {
            qWarning() << this->m_path << "@" << hex << this->m_filepos << "blocksize 0, skipping remainder of file";
            break;
        }
        if (this->fileVersion < 2 || this->fileVersion > 3) {
            if (this->m_filepos > 0) {
                qWarning() << this->m_path << "@" << hex << this->m_filepos << "corrupt PRS1 header, skipping remainder of file";
            } else {
                qWarning() << this->m_path << "unsupported PRS1 header version" << this->fileVersion;
            }
            break;
        }
        if (this->htype != PRS1_HTYPE_NORMAL && this->htype != PRS1_HTYPE_INTERVAL) {
            qWarning() << this->m_path << "unexpected htype:" << this->htype;
            break;
        }

        // Read format-specific variable-length header data.
        bool hdr_ok = false;
        if (this->htype != PRS1_HTYPE_INTERVAL) {  // Not just waveforms: the 1160P uses this for its .002 events file.
            // Not a waveform/interval chunk
            switch (this->fileVersion) {
                case 2:
                    hdr_ok = ReadNormalHeaderV2(f);
                    break;
                case 3:
                    hdr_ok = ReadNormalHeaderV3(f);
                    break;
                default:
                    //hdr_ok remains false, warning is above
                    break;
            }
        } else {
            // Waveform/interval chunk
            hdr_ok = ReadWaveformHeader(f);
        }
        if (!hdr_ok) {
            break;
        }

        // The 8bit checksum comes at the end.
        QByteArray checksum = f.read(1);
        if (checksum.size() < 1) {
            qWarning() << this->m_path << "read error header checksum";
            break;
        }
        this->storedChecksum = checksum.data()[0];

        // Calculate 8bit additive header checksum.
        header = (unsigned char *)this->m_header.data(); // important because its memory location could move
        int header_size = this->m_header.size();
        quint8 achk=0;
        for (int i=0; i < header_size; i++) {
            achk += header[i];
        }
        this->calcChecksum = achk;
        
        // Append the stored checksum to the raw data *after* calculating the checksum on the preceding data.
        this->m_header.append(checksum);

        ok = true;
    } while (false);

    return ok;
}


bool PRS1DataChunk::ReadNormalHeaderV2(QFile & /*f*/)
{
    this->m_headerblock = QByteArray();
    return true;  // always OK
}


bool PRS1DataChunk::ReadNormalHeaderV3(QFile & f)
{
    bool ok = false;
    unsigned char * header;
    QByteArray headerB2;

    // This is a new machine, byte 15 is header data block length
    // followed by variable, data byte pairs
    do {
        QByteArray extra = f.read(1);
        if (extra.size() < 1) {
            qWarning() << this->m_path << "read error extended header";
            break;
        }
        this->m_header.append(extra);
        header = (unsigned char *)this->m_header.data();

        int hdb_len = header[15];
        int hdb_size = hdb_len * 2;

        headerB2 = f.read(hdb_size);
        if (headerB2.size() != hdb_size) {
            qWarning() << this->m_path << "read error in extended header";
            break;
        }
        this->m_headerblock = headerB2;
        
        this->m_header.append(headerB2);
        header = (unsigned char *)this->m_header.data();
        const unsigned char * hd = (unsigned char *)headerB2.constData();
        int pos = 0;
        int recs = header[15];
        for (int i=0; i<recs; i++) {
            this->hblock[hd[pos]] = hd[pos+1];
            pos += 2;
        }
        
        ok = true;
    } while (false);

    return ok;
}


bool PRS1DataChunk::ReadWaveformHeader(QFile & f)
{
    bool ok = false;
    unsigned char * header;
    do {
        // Read the fixed-length waveform header.
        QByteArray extra = f.read(4);
        if (extra.size() != 4) {
            qWarning() << this->m_path << "read error in waveform header";
            break;
        }
        this->m_header.append(extra);
        header = (unsigned char *)this->m_header.data();

        // Parse the fixed-length portion.
        this->interval_count = header[0x0f] | header[0x10] << 8;
        this->interval_seconds = header[0x11];  // not always 1 after all
        this->duration = this->interval_count * this->interval_seconds;  // ??? the last entry doesn't always seem to be a full interval?
        quint8 wvfm_signals = header[0x12];

        // Read the variable-length data + trailing byte.
        int ws_size = (this->fileVersion == 3) ? 4 : 3;
        int sbsize = wvfm_signals * ws_size + 1;

        extra = f.read(sbsize);
        if (extra.size() != sbsize) {
            qWarning() << this->m_path << "read error in waveform header 2";
            break;
        }
        this->m_header.append(extra);
        header = (unsigned char *)this->m_header.data();

        // Parse the variable-length waveform information.
        // TODO: move these checks into the parser, after the header checksum has been verified
        // For now just skip them for the one known sample with a bad checksum.
        if (this->sessionid == 268962649) return true;

        int pos = 0x13;
        for (int i = 0; i < wvfm_signals; ++i) {
            quint8 kind = header[pos];
            CHECK_VALUE(kind, i);  // always seems to range from 0...wvfm_signals-1, alert if not
            quint16 interleave = header[pos + 1] | header[pos + 2] << 8;  // samples per interval
            if (this->fileVersion == 2) {
                this->waveformInfo.push_back(PRS1Waveform(interleave, kind));
                pos += 3;
            } else if (this->fileVersion == 3) {
                int always_8 = header[pos + 3];  // sample size in bits?
                CHECK_VALUE(always_8, 8);
                this->waveformInfo.push_back(PRS1Waveform(interleave, kind));
                pos += 4;
            }
        }
        
        // And the trailing byte, whatever it is.
        int always_0 = header[pos];
        CHECK_VALUE(always_0, 0);
       
        ok = true;
    } while (false);

    return ok;
}


bool PRS1DataChunk::ReadData(QFile & f)
{
    bool ok = false;
    do {
        // Read data block
        int data_size = this->blockSize - this->m_header.size();
        if (data_size < 0) {
            qWarning() << this->m_path << "chunk size smaller than header";
            break;
        }
        this->m_data = f.read(data_size);
        if (this->m_data.size() < data_size) {
            qWarning() << this->m_path << "less data in file than specified in header";
            break;
        }

        // Extract the stored CRC from the data buffer and calculate the current CRC.
        if (this->fileVersion==3) {
            // The last 4 bytes contain a CRC32 checksum of the data.
            if (!ExtractStoredCrc(4)) {
                break;
            }
            this->calcCrc = CRC32wchar((unsigned char *)this->m_data.data(), this->m_data.size());
        } else {
            // The last 2 bytes contain a CRC16 checksum of the data.
            if (!ExtractStoredCrc(2)) {
                break;
            }
            this->calcCrc = CRC16((unsigned char *)this->m_data.data(), this->m_data.size());
        }
        
        ok = true;
    } while (false);

    return ok;
}


bool PRS1DataChunk::ExtractStoredCrc(int size)
{
    // Make sure there's enough data for the CRC.
    int offset = this->m_data.size() - size;
    if (offset < 0) {
        qWarning() << this->m_path << "chunk truncated";
        return false;
    }
    
    // Read the last 16- or 32-bit little-endian integer.
    quint32 storedCrc = 0;
    unsigned char* data = (unsigned char*)this->m_data.data();
    for (int i=0; i < size; i++) {
        storedCrc |= data[offset+i] << (8*i);
    }
    this->storedCrc = storedCrc;

    // Drop the CRC from the data.
    this->m_data.chop(size);
    
    return true;
}
        

bool initialized = false;

using namespace schema;

Channel PRS1Channels;

void PRS1Loader::initChannels()
{
    Channel * chan = nullptr;

    channel.add(GRP_CPAP, new Channel(CPAP_PressurePulse = 0x1009, MINOR_FLAG,  MT_CPAP,   SESSION,
        "PressurePulse",
        QObject::tr("Pressure Pulse"),
        QObject::tr("A pulse of pressure 'pinged' to detect a closed airway."),
        QObject::tr("PP"),
        STR_UNIT_EventsPerHour,    DEFAULT,    QColor("dark red")));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_Mode = 0xe120, SETTING,  MT_CPAP,  SESSION,
        "PRS1Mode", QObject::tr("Mode"),
        QObject::tr("PAP Mode"),
        QObject::tr("Mode"),
        "", LOOKUP, Qt::green));
    chan->addOption(PRS1_MODE_CPAPCHECK, QObject::tr("CPAP-Check"));
    chan->addOption(PRS1_MODE_CPAP,      QObject::tr("CPAP"));
    chan->addOption(PRS1_MODE_AUTOCPAP,  QObject::tr("AutoCPAP"));
    chan->addOption(PRS1_MODE_AUTOTRIAL, QObject::tr("Auto-Trial"));
    chan->addOption(PRS1_MODE_BILEVEL,   QObject::tr("Bi-Level"));
    chan->addOption(PRS1_MODE_AUTOBILEVEL, QObject::tr("AutoBiLevel"));
    chan->addOption(PRS1_MODE_ASV,       QObject::tr("ASV"));
    chan->addOption(PRS1_MODE_S,         QObject::tr("S"));
    chan->addOption(PRS1_MODE_ST,        QObject::tr("S/T"));
    chan->addOption(PRS1_MODE_PC,        QObject::tr("PC"));
    chan->addOption(PRS1_MODE_ST_AVAPS,  QObject::tr("S/T - AVAPS"));
    chan->addOption(PRS1_MODE_PC_AVAPS,  QObject::tr("PC - AVAPS"));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_FlexMode = 0xe105, SETTING,  MT_CPAP,  SESSION,
        "PRS1FlexMode", QObject::tr("Flex Mode"),
        QObject::tr("PRS1 pressure relief mode."),
        QObject::tr("Flex Mode"),
        "", LOOKUP, Qt::green));
    chan->addOption(FLEX_None, STR_TR_None);
    chan->addOption(FLEX_CFlex, QObject::tr("C-Flex"));
    chan->addOption(FLEX_CFlexPlus, QObject::tr("C-Flex+"));
    chan->addOption(FLEX_AFlex, QObject::tr("A-Flex"));
    chan->addOption(FLEX_PFlex, QObject::tr("P-Flex"));
    chan->addOption(FLEX_RiseTime, QObject::tr("Rise Time"));
    chan->addOption(FLEX_BiFlex, QObject::tr("Bi-Flex"));
    //chan->addOption(FLEX_AVAPS, QObject::tr("AVAPS"));  // Converted into AVAPS PRS1_Mode with FLEX_RiseTime
    chan->addOption(FLEX_Flex, QObject::tr("Flex"));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_FlexLevel = 0xe106, SETTING, MT_CPAP,   SESSION,
        "PRS1FlexSet",
        QObject::tr("Flex Level"),
        QObject::tr("PRS1 pressure relief setting."),
        QObject::tr("Flex Level"),
        "", LOOKUP, Qt::blue));
    chan->addOption(0, STR_TR_Off);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_FlexLock = 0xe111, SETTING, MT_CPAP,   SESSION,
        "PRS1FlexLock",
        QObject::tr("Flex Lock"),
        QObject::tr("Whether Flex settings are available to you."),
        QObject::tr("Flex Lock"),
        "", LOOKUP, Qt::black));
    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, STR_TR_On);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_RiseTime = 0xe119, SETTING, MT_CPAP,   SESSION,
        "PRS1RiseTime",
        QObject::tr("Rise Time"),
        QObject::tr("Amount of time it takes to transition from EPAP to IPAP, the higher the number the slower the transition"),
        QObject::tr("Rise Time"),
        "", LOOKUP, Qt::blue));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_RiseTimeLock = 0xe11a, SETTING, MT_CPAP,   SESSION,
        "PRS1RiseTimeLock",
        QObject::tr("Rise Time Lock"),
        QObject::tr("Whether Rise Time settings are available to you."),
        QObject::tr("Rise Lock"),
        "", LOOKUP, Qt::black));
    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, STR_TR_On);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_HumidStatus = 0xe101, SETTING, MT_CPAP, SESSION,
        "PRS1HumidStat",
        QObject::tr("Humidifier Status"),
        QObject::tr("PRS1 humidifier connected?"),
        QObject::tr("Humidifier"),
        "", LOOKUP, Qt::green));
    chan->addOption(0, QObject::tr("Disconnected"));
    chan->addOption(1, QObject::tr("Connected"));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_HumidMode = 0xe110, SETTING, MT_CPAP, SESSION,
        "PRS1HumidMode",
        QObject::tr("Humidification Mode"),
        QObject::tr("PRS1 Humidification Mode"),
        QObject::tr("Humid. Mode"),
        "", LOOKUP, Qt::green));
    chan->addOption(HUMID_Fixed, QObject::tr("Fixed (Classic)"));
    chan->addOption(HUMID_Adaptive, QObject::tr("Adaptive (System One)"));
    chan->addOption(HUMID_HeatedTube, QObject::tr("Heated Tube"));
    chan->addOption(HUMID_Passover, QObject::tr("Passover"));
    chan->addOption(HUMID_Error, QObject::tr("Error"));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_TubeTemp = 0xe10f, SETTING, MT_CPAP,  SESSION,
        "PRS1TubeTemp",
        QObject::tr("Tube Temperature"),
        QObject::tr("PRS1 Heated Tube Temperature"),
        QObject::tr("Tube Temp."),
        "", LOOKUP, Qt::red));
    chan->addOption(0, STR_TR_Off);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_HumidLevel = 0xe102, SETTING,  MT_CPAP,  SESSION,
        "PRS1HumidLevel",
        QObject::tr("Humidifier"),  // label varies in reports, "Humidifier Setting" in 50-series, "Humidity Level" in 60-series, "Humidifier" in DreamStation
        QObject::tr("PRS1 Humidifier Setting"),
        QObject::tr("Humid. Lvl"),
        "", LOOKUP, Qt::blue));
    chan->addOption(0, STR_TR_Off);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_MaskResistSet = 0xe104, SETTING, MT_CPAP,   SESSION,
        "MaskResistSet",
        QObject::tr("Mask Resistance Setting"),
        QObject::tr("Mask Resistance Setting"),
        QObject::tr("Mask Resist."),
        "", LOOKUP, Qt::green));
    chan->addOption(0, STR_TR_Off);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_HoseDiam = 0xe107, SETTING,  MT_CPAP,  SESSION,
        "PRS1HoseDiam",
        QObject::tr("Hose Diameter"),
        QObject::tr("Diameter of primary CPAP hose"),
        QObject::tr("Hose Diam."),
        "", LOOKUP, Qt::green));
    chan->addOption(22, QObject::tr("22mm"));
    chan->addOption(15, QObject::tr("15mm"));
    chan->addOption(12, QObject::tr("12mm"));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_TubeLock = 0xe112, SETTING, MT_CPAP,   SESSION,
        "PRS1TubeLock",
        QObject::tr("Tubing Type Lock"),
        QObject::tr("Whether tubing type settings are available to you."),
        QObject::tr("Tube Lock"),
        "", LOOKUP, Qt::black));
    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, STR_TR_On);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_MaskResistLock = 0xe108, SETTING,  MT_CPAP,  SESSION,
        "MaskResistLock",
        QObject::tr("Mask Resistance Lock"),
        QObject::tr("Whether mask resistance settings are available to you."),
        QObject::tr("Mask Res. Lock"),
        "", LOOKUP, Qt::black));
    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, STR_TR_On);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_AutoOn = 0xe109, SETTING, MT_CPAP,   SESSION,
        "PRS1AutoOn",
        QObject::tr("Auto On"),
        QObject::tr("A few breaths automatically starts machine"),
        QObject::tr("Auto On"),
        "", LOOKUP, Qt::green));
    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, STR_TR_On);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_AutoOff = 0xe10a, SETTING, MT_CPAP,   SESSION,
        "PRS1AutoOff",
        QObject::tr("Auto Off"),
        QObject::tr("Machine automatically switches off"),
        QObject::tr("Auto Off"),
        "", LOOKUP, Qt::green));
    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, STR_TR_On);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_MaskAlert = 0xe10b, SETTING,  MT_CPAP,  SESSION,
        "PRS1MaskAlert",
        QObject::tr("Mask Alert"),
        QObject::tr("Whether or not machine allows Mask checking."),
        QObject::tr("Mask Alert"),
        "", LOOKUP, Qt::green));
    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, STR_TR_On);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_ShowAHI = 0xe10c, SETTING, MT_CPAP,   SESSION,
        "PRS1ShowAHI",
        QObject::tr("Show AHI"),
        QObject::tr("Whether or not machine shows AHI via built-in display."),
        QObject::tr("Show AHI"),
        "", LOOKUP, Qt::green));
    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, STR_TR_On);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_RampType = 0xe113, SETTING, MT_CPAP,   SESSION,
        "PRS1RampType",
        QObject::tr("Ramp Type"),
        QObject::tr("Type of ramp curve to use."),
        QObject::tr("Ramp Type"),
        "", LOOKUP, Qt::black));
    chan->addOption(0, QObject::tr("Linear"));
    chan->addOption(1, QObject::tr("SmartRamp"));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_BackupBreathMode = 0xe114, SETTING, MT_CPAP,   SESSION,
        "PRS1BackupBreathMode",
        QObject::tr("Backup Breath Mode"),
        QObject::tr("The kind of backup breath rate in use: none (off), automatic, or fixed"),
        QObject::tr("Breath Rate"),
        "", LOOKUP, Qt::black));
    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, QObject::tr("Auto"));
    chan->addOption(2, QObject::tr("Fixed"));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_BackupBreathRate = 0xe115, SETTING, MT_CPAP,   SESSION,
        "PRS1BackupBreathRate",
        QObject::tr("Fixed Backup Breath BPM"),
        QObject::tr("Minimum breaths per minute (BPM) below which a timed breath will be initiated"),
        QObject::tr("Breath BPM"),
        STR_UNIT_BreathsPerMinute, LOOKUP, Qt::black));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_BackupBreathTi = 0xe116, SETTING, MT_CPAP,   SESSION,
        "PRS1BackupBreathTi",
        QObject::tr("Timed Inspiration"),
        QObject::tr("The time that a timed breath will provide IPAP before transitioning to EPAP"),
        QObject::tr("Timed Insp."),
        STR_UNIT_Seconds, DEFAULT, Qt::blue));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_AutoTrial = 0xe117, SETTING, MT_CPAP,   SESSION,
        "PRS1AutoTrial",
        QObject::tr("Auto-Trial Duration"),
        QObject::tr("The number of days in the Auto-CPAP trial period, after which the machine will revert to CPAP"),
        QObject::tr("Auto-Trial Dur."),
        "", LOOKUP, Qt::black));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_EZStart = 0xe118, SETTING, MT_CPAP,   SESSION,
        "PRS1EZStart",
        QObject::tr("EZ-Start"),
        QObject::tr("Whether or not EZ-Start is enabled"),
        QObject::tr("EZ-Start"),
        "", LOOKUP, Qt::black));
    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, STR_TR_On);

    channel.add(GRP_CPAP, chan = new Channel(PRS1_VariableBreathing = 0x1156, SPAN, MT_CPAP,    SESSION,
        "PRS1_VariableBreathing",
        QObject::tr("Variable Breathing"),
        QObject::tr("UNCONFIRMED: Possibly variable breathing, which are periods of high deviation from the peak inspiratory flow trend"),
        "VB",
        STR_UNIT_Seconds,
        DEFAULT,    QColor("#ffe8f0")));
    chan->setEnabled(false);  // disable by default

    channel.add(GRP_CPAP, new Channel(PRS1_BND = 0x1159, SPAN,  MT_CPAP,   SESSION,
        "PRS1_BND",
        QObject::tr("Breathing Not Detected"),
        QObject::tr("A period during a session where the machine could not detect flow."),
        QObject::tr("BND"),
        STR_UNIT_Unknown,
        DEFAULT,    QColor("light purple")));
    channel.add(GRP_CPAP, new Channel(PRS1_TimedBreath = 0x1180, MINOR_FLAG, MT_CPAP,    SESSION,
        "PRS1TimedBreath",
        QObject::tr("Timed Breath"),
        QObject::tr("Machine Initiated Breath"),
        QObject::tr("TB"),
        STR_UNIT_Seconds,
        DEFAULT,    QColor("black")));

    channel.add(GRP_CPAP, chan = new Channel(PRS1_PeakFlow = 0x115a, WAVEFORM, MT_CPAP, SESSION,
        "PRS1PeakFlow",
        QObject::tr("Peak Flow"),
        QObject::tr("Peak flow during a 2-minute interval"),
        QObject::tr("Peak Flow"),
        STR_UNIT_LPM,
        DEFAULT,    QColor("red")));
    chan->setShowInOverview(true);
}

void PRS1Loader::Register()
{
    if (initialized) { return; }

    qDebug() << "Registering PRS1Loader";
    RegisterLoader(new PRS1Loader());
    initialized = true;
}

/* Thanks to SleepyCPAP :)
CODE ERROR DESCRIPTION ERROR TYPE ERROR CATEGORY
1 SOFTWARE STOP STOP General Errors
2 Not Used General Errors
3 INT RAM REBOOT General Errors
4 NULL PTR REBOOT General Errors
5 DATA REBOOT General Errors
6 STATE MACHINE REBOOT General Errors
7 SOFTWARE REBOOT General Errors
8-9 Not Used General Errors
10 WDOG TEST RAM REBOOT Watchdog & Timer Errors
11 WDOG TEST REBOOT Watchdog & Timer Errors
12 BACKGROUND WDOG NO CARD REBOOT Watchdog & Timer Errors
13 BACKGROUND WDOG SD CARD REBOOT Watchdog & Timer Errors
14 WDOG LOWRES TIMER REBOOT Watchdog & Timer Errors
15 CYCLE HANDLER OVERRUN REBOOT Watchdog & Timer Errors
16 RASP RESTORE TIMEOUT CONTINUE Watchdog & Timer Errors
17 ONEMS HANDLER OVERRUN REBOOT Watchdog & Timer Errors
18 Not Used Watchdog & Timer Errors
19 WDOG TIMEOUT REBOOT Watchdog & Timer Errors
20 MOTOR SPINUP FLUX LOW REBOOT Motor/Blower Errors
21 MOTOR VBUS HIGH STOP Motor/Blower Errors
22 MOTOR FLUX MAGNITUDE REBOOT Motor/Blower Errors
23 MOTOR OVERSPEED REBOOT Motor/Blower Errors
24 MOTOR SPEED REVERSE REBOOT Motor/Blower Errors
25 MOTOR THERMISTOR OPEN CONTINUE Motor/Blower Errors
26 MOTOR THERMISTOR SHORTED CONTINUE Motor/Blower Errors
27 MOTOR RL NOCONVERGE STOP Motor/Blower Errors
28 NEGATIVE QUADRATURE VOLTAGE VECTOR REBOOT Motor/Blower Errors
29 VBUS GAIN ZERO: REBOOT Motor/Blower Errors
30 MOTOR SPINUP FLUX HIGH REBOOT Motor/Blower Errors
31 (incorrect power supply - 60series) Motor/Blower Errors
32-39 Not Used Motor/Blower Errors
40 NVRAM REBOOT NVRAM Low Level Errors
41 STORAGE UNIT RAM REBOOT NVRAM Low Level Errors
42 UNABLE TO OBTAIN BUS REBOOT NVRAM Low Level Errors
43 NVRAM NO CALLBACK OCCURRED REBOOT NVRAM Low Level Errors
44 NV BUFFER NULL REBOOT NVRAM Low Level Errors
45 NV CALLBACK NULL REBOOT NVRAM Low Level Errors
46 NV ZERO LENGTH REBOOT NVRAM Low Level Errors
47 NVRAM INVALID BYTES XFRRED REBOOT NVRAM Low Level Errors
48-49 Not Used NVRAM Low Level Errors
50 DAILY VALUES CORRUPT LOG ONLY NVRAM Unit Related Errors
51 CORRUPT COMPLIANCE LOG CONTINUE NVRAM Unit Related Errors
52 CORRUPT COMPLIANCE CB CONTINUE NVRAM Unit Related Errors
53 COMP LOG SEM TIMEOUT CONTINUE NVRAM Unit Related Errors
54 COMPLOG REQS OVERFLOW REBOOT NVRAM Unit Related Errors
55 THERAPY QUEUE FULL CONTINUE NVRAM Unit Related Errors
56 COMPLOG PACKET STATUS REBOOT NVRAM Unit Related Errors
57 SESS OBS QUEUE OVF REBOOT NVRAM Unit Related Errors
58 SESS OBS NO CALLBACK REBOOT NVRAM Unit Related Errors
59 Not Used NVRAM Unit Related Errors
60 UNSUPPORTED HARDWARE REBOOT General Hardware Errors
61 PLL UNLOCKED REBOOT General Hardware Errors
62 STUCK RAMP KEY CONTINUE General Hardware Errors
63 STUCK KNOB KEY CONTINUE General Hardware Errors
64 DSP OVERTIME PWM REBOOT General Hardware Errors
65 STUCK ENCODER A CONTINUE General Hardware Errors
66 STUCK ENCODER B CONTINUE General Hardware Errors
67-69 Not Used General Hardware Errors
70 PRESSURE SENSOR ABSENT STOP Pressure Sensor Errors
71 Not Used Pressure Sensor Errors
72 PSENS UNABLE TO OBTAIN BUS REBOOT Pressure Sensor Errors
73 SENSOR PRESS OFFSET STOP STOP Pressure Sensor Errors
74-79 Not Used Pressure Sensor Errors
80 UNABLE TO INIT FLOW SENSOR REBOOT Flow Sensor Errors
81 FLOW SENSOR TABLE CONTINUE Flow Sensor Errors
82 FLOW SENSOR OFFSET CONTINUE Flow Sensor Errors
83 FSENS UNABLE TO OBTAIN BUS REBOOT / 2nd failure=STOP Flow Sensor Errors
84 FLOW SENSOR STOP STOP Flow Sensor Errors
85 FLOW SENSOR OCCLUDED CONTINUE Flow Sensor Errors
86 FLOW SENSOR ABSENT CONTINUE Flow Sensor Errors
87 FLOW SENSOR BUS CONTINUE Flow Sensor Errors
88-89 Not Used Flow Sensor Errors
90 OTP NOT CONFIGURED STOP OTP & RTC Errors
91 OTP INCORRECTLY CONFIGURED STOP OTP & RTC Errors
92 Not Used OTP & RTC Errors
93 RTC VALUE CONTINUE OTP & RTC Errors
94 RTC STOPPED CONTINUE OTP & RTC Errors
95-99 Not Used OTP & RTC Errors
100 HUMID NO HEAT CONTINUE Humidifier Errors
101 HUMID TEMP MAX STOP Humidifier Errors
102 THERMISTOR HIGH CONTINUE Humidifier Errors
103 THERMISTOR LOW CONTINUE Humidifier Errors
104 HUMID AMBIENT OFF CONTINUE Humidifier Errors
105 HUMID AMBIENT COMM CONTINUE Humidifier Errors
106-109 Not Used Humidifier Errors
110 STACK REBOOT Stack & Exception Handler Errors
111 EXCEPTION STACK OVERFLOW REBOOT Stack & Exception Handler Errors
112 EXCEPTION STACK RESERVE LOG ONLY Stack & Exception Handler Errors
113 EXCEPTION STACK UNDERFLOW REBOOT Stack & Exception Handler Errors
114 FIQ STACK OVERFLOW REBOOT Stack & Exception Handler Errors
115 FIQ STACK RESERVE LOG ONLY Stack & Exception Handler Errors
116 FIQ STACK UNDERFLOW REBOOT Stack & Exception Handler Errors
117 IRQ STACK OVERFLOW REBOOT Stack & Exception Handler Errors
118 IRQ STACK RESERVE LOG ONLY Stack & Exception Handler Errors
119 IRQ STACK UNDERFLOW REBOOT Stack & Exception Handler Errors
120 SVC STACK OVERFLOW REBOOT Stack & Exception Handler Errors
121 SVC STACK RESERVE LOG ONLY Stack & Exception Handler Errors
122 SVC STACK UNDERFLOW REBOOT Stack & Exception Handler Errors
123 DATA ABORT EXCEPTION REBOOT Stack & Exception Handler Errors
124 PREFETCH EXCEPTION REBOOT Stack & Exception Handler Errors
125 ILLEGAL INSTRUCTION EXCEPTION REBOOT Stack & Exception Handler Errors
126 SWI ABORT EXCEPTION REBOOT Stack & Exception Handler Errors
*/

