/* SleepLib (DeVilbiss) Intellipap Loader Implementation
 *
 * Notes: Intellipap DV54 requires the SmartLink attachment to access this data.
 *
 * Copyright (c) 2011-2018 Mark Watkins <mark@jedimark.net>
 * Copyright (c) 2020 The OSCAR Team
 *
 * 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 <QDir>
#include <QCoreApplication>

#include "intellipap_loader.h"

//#define DEBUG6

ChannelID INTP_SmartFlexMode, INTP_SmartFlexLevel;

Intellipap::Intellipap(Profile *profile, MachineID id)
    : CPAP(profile, id)
{
}

Intellipap::~Intellipap()
{
}

IntellipapLoader::IntellipapLoader()
{
    const QString INTELLIPAP_ICON = ":/icons/intellipap.png";
    const QString DV6_ICON = ":/icons/dv64.png";

    QString s = newInfo().series;
    m_pixmap_paths[s] = INTELLIPAP_ICON;
    m_pixmaps[s] = QPixmap(INTELLIPAP_ICON);
    m_pixmap_paths["DV6"] = DV6_ICON;
    m_pixmaps["DV6"] = QPixmap(DV6_ICON);

    m_buffer = nullptr;
    m_type = MT_CPAP;
}

IntellipapLoader::~IntellipapLoader()
{
}

const QString SET_BIN = "SET.BIN";
const QString SET1 = "SET1";
const QString DV6 = "DV6";
const QString SL = "SL";

const QString DV6_DIR = "/" + DV6;
const QString SL_DIR = "/" + SL;

bool IntellipapLoader::Detect(const QString & givenpath)
{
    QString path = givenpath;
    if (path.endsWith(SL_DIR)) {
        path.chop(3);
    }
    if (path.endsWith(DV6_DIR)) {
        path.chop(4);
    }

    QDir dir(path);

    if (!dir.exists()) {
        return false;
    }

    // Intellipap DV54 has a folder called SL in the root directory, DV64 has DV6
    if (dir.cd(SL)) {
        // Test for presence of settings file
        return dir.exists(SET1) ? true : false;
    }

    if (dir.cd(DV6)) { // DV64
        return dir.exists(SET_BIN) ? true : false;
    }
    return false;
}

enum INTPAP_Type { INTPAP_Unknown, INTPAP_DV5, INTPAP_DV6 };


int IntellipapLoader::OpenDV5(const QString & path)
{
    QString newpath = path + SL_DIR;
    QString filename;

    qDebug() << "DV5 Loader started";

    //////////////////////////
    // Parse the Settings File
    //////////////////////////
    filename = newpath + "/" + SET1;
    QFile f(filename);

    if (!f.exists()) {
        return -1;
    }

    f.open(QFile::ReadOnly);
    QTextStream tstream(&f);

    const QString INT_PROP_Serial = "Serial";
    const QString INT_PROP_Model = "Model";
    const QString INT_PROP_Mode = "Mode";
    const QString INT_PROP_MaxPressure = "Max Pressure";
    const QString INT_PROP_MinPressure = "Min Pressure";
    const QString INT_PROP_IPAP = "IPAP";
    const QString INT_PROP_EPAP = "EPAP";
    const QString INT_PROP_PS = "PS";
    const QString INT_PROP_RampPressure = "Ramp Pressure";
    const QString INT_PROP_RampTime = "Ramp Time";

    const QString INT_PROP_HourMeter = "Usage Hours";
    const QString INT_PROP_ComplianceMeter = "Compliance Hours";
    const QString INT_PROP_ErrorCode = "Error";
    const QString INT_PROP_LastErrorCode = "Long Error";
    const QString INT_PROP_LowUseThreshold = "Low Usage";
    const QString INT_PROP_SmartFlex = "SmartFlex";
    const QString INT_PROP_SmartFlexMode = "SmartFlexMode";


    QHash<QString, QString> lookup;
        lookup["Sn"] = INT_PROP_Serial;
        lookup["Mn"] = INT_PROP_Model;
        lookup["Mo"] = INT_PROP_Mode;     // 0 cpap, 1 auto
        //lookup["Pn"]="??";
        lookup["Pu"] = INT_PROP_MaxPressure;
        lookup["Pl"] = INT_PROP_MinPressure;
        lookup["Pi"] = INT_PROP_IPAP;
        lookup["Pe"] = INT_PROP_EPAP;  // == WF on Auto models
        lookup["Ps"] = INT_PROP_PS;             // == WF on Auto models, Pressure support
        //lookup["Ds"]="??";
        //lookup["Pc"]="??";
        lookup["Pd"] = INT_PROP_RampPressure;
        lookup["Dt"] = INT_PROP_RampTime;
        //lookup["Ld"]="??";
        //lookup["Lh"]="??";
        //lookup["FC"]="??";
        //lookup["FE"]="??";
        //lookup["FL"]="??";
        lookup["A%"]="ApneaThreshold";
        lookup["Ad"]="ApneaDuration";
        lookup["H%"]="HypopneaThreshold";
        lookup["Hd"]="HypopneaDuration";
        //lookup["Pi"]="??";
        //lookup["Pe"]="??";
        lookup["Ri"]="SmartFlexIRnd";  // Inhale Rounding (0-5)
        lookup["Re"]="SmartFlexERnd"; // Inhale Rounding (0-5)
        //lookup["Bu"]="??"; // WF
        //lookup["Ie"]="??"; // 20
        //lookup["Se"]="??"; // 05    //Inspiratory trigger?
        //lookup["Si"]="??"; // 05    // Expiratory Trigger?
        //lookup["Mi"]="??"; // 0
        lookup["Uh"]="HoursMeter"; // 0000.0
        lookup["Up"]="ComplianceMeter"; // 0000.00
        //lookup["Er"]="ErrorCode";, // E00
        //lookup["El"]="LongErrorCode"; // E00 00/00/0000
        //lookup["Hp"]="??";, // 1
        //lookup["Hs"]="??";, // 02
        //lookup["Lu"]="LowUseThreshold"; // defaults to 0 (4 hours)
        lookup["Sf"] = INT_PROP_SmartFlex;
        lookup["Sm"] = INT_PROP_SmartFlexMode;
        lookup["Ks=s"]="Ks_s";
        lookup["Ks=i"]="ks_i";

    QHash<QString, QString> set1;
    QHash<QString, QString>::iterator hi;

    Machine *mach = nullptr;

    MachineInfo info = newInfo();


    bool ok;

    //EventDataType min_pressure = 0, max_pressure = 0, set_ipap = 0, set_ps = 0,
    EventDataType ramp_pressure = 0, set_epap = 0, ramp_time = 0;

    int papmode = 0, smartflex = 0, smartflexmode = 0;
    while (1) {
        QString line = tstream.readLine();

        if ((line.length() <= 2) ||
                (line.isNull())) { break; }

        QString key = line.section("\t", 0, 0).trimmed();
        hi = lookup.find(key);

        if (hi != lookup.end()) {
            key = hi.value();
        }

        QString value = line.section("\t", 1).trimmed();

        if (key == INT_PROP_Mode) {
            papmode = value.toInt(&ok);
        } else if (key == INT_PROP_Serial) {
            info.serial = value;
        } else if (key == INT_PROP_Model) {
            info.model = value;
        } else if (key == INT_PROP_MinPressure) {
            //min_pressure = value.toFloat() / 10.0;
        } else if (key == INT_PROP_MaxPressure) {
            //max_pressure = value.toFloat() / 10.0;
        } else if (key == INT_PROP_IPAP) {
            //set_ipap = value.toFloat() / 10.0;
        } else if (key == INT_PROP_EPAP) {
            set_epap = value.toFloat() / 10.0;
        } else if (key == INT_PROP_PS) {
            //set_ps = value.toFloat() / 10.0;
        } else if (key == INT_PROP_RampPressure) {
            ramp_pressure = value.toFloat() / 10.0;
        } else if (key == INT_PROP_RampTime) {
            ramp_time = value.toFloat() / 10.0;
        } else if (key == INT_PROP_SmartFlex) {
            smartflex = value.toInt();
        } else if (key == INT_PROP_SmartFlexMode) {
            smartflexmode = value.toInt();
        } else {
            set1[key] = value;
        }
        qDebug() << key << "=" << value;
    }

    CPAPMode mode = MODE_UNKNOWN;

    switch (papmode) {
    case 0:
        mode = MODE_CPAP;
        break;
    case 1:
        mode = (set_epap > 0) ? MODE_BILEVEL_FIXED : MODE_APAP;
        break;
    default:
        qDebug() << "New machine mode";
    }

    if (!info.serial.isEmpty()) {
        mach = p_profile->CreateMachine(info);
    }

    if (!mach) {
        qDebug() << "Couldn't get Intellipap machine record";
        return -1;
    }

    QString backupPath = mach->getBackupPath();
    QString copypath = path;

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


    // Refresh properties data..
    for (QHash<QString, QString>::iterator i = set1.begin(); i != set1.end(); i++) {
        mach->properties[i.key()] = i.value();
    }

    f.close();

    ///////////////////////////////////////////////
    // Parse the Session Index (U File)
    ///////////////////////////////////////////////
    unsigned char buf[27];
    filename = newpath + "/U";
    f.setFileName(filename);

    if (!f.exists()) { return -1; }

    QVector<quint32> SessionStart;
    QVector<quint32> SessionEnd;
    QHash<SessionID, Session *> Sessions;

    quint32 ts1, ts2;//, length;
    //unsigned char cs;
    f.open(QFile::ReadOnly);
    int cnt = 0;
    QDateTime epoch(QDate(2002, 1, 1), QTime(0, 0, 0), Qt::UTC); // Intellipap Epoch
    int ep = epoch.toTime_t();

    do {
        cnt = f.read((char *)buf, 9);
        // big endian
        ts1 = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
        ts2 = (buf[4] << 24) | (buf[5] << 16) | (buf[6] << 8) | buf[7];
        // buf[8] == ??? What is this byte? A Bit Field? A checksum?
        ts1 += ep;
        ts2 += ep;
        SessionStart.append(ts1);
        SessionEnd.append(ts2);
    } while (cnt > 0);

    qDebug() << "U file logs" << SessionStart.size() << "sessions.";
    f.close();

    ///////////////////////////////////////////////
    // Parse the Session Data (L File)
    ///////////////////////////////////////////////
    filename = newpath + "/L";
    f.setFileName(filename);

    if (!f.exists()) { return -1; }

    f.open(QFile::ReadOnly);
    long size = f.size();
    int recs = size / 26;
    m_buffer = new unsigned char [size];

    if (size != f.read((char *)m_buffer, size)) {
        qDebug()  << "Couldn't read 'L' data" << filename;
        return -1;
    }

    Session *sess;
    SessionID sid;
    QHash<SessionID,qint64> rampstart;
    QHash<SessionID,qint64> rampend;

    for (int i = 0; i < SessionStart.size(); i++) {
        sid = SessionStart[i];

        if (mach->SessionExists(sid)) {
            // knock out the already imported sessions..
            SessionStart[i] = 0;
            SessionEnd[i] = 0;
        } else if (!Sessions.contains(sid)) {
            sess = Sessions[sid] = new Session(mach, sid);

            sess->really_set_first(qint64(sid) * 1000L);
         //   sess->really_set_last(qint64(SessionEnd[i]) * 1000L);

            rampstart[sid] = 0;
            rampend[sid] = 0;
            sess->SetChanged(true);
            if (mode >= MODE_BILEVEL_FIXED) {
                sess->AddEventList(CPAP_IPAP, EVL_Event);
                sess->AddEventList(CPAP_EPAP, EVL_Event);
                sess->AddEventList(CPAP_PS, EVL_Event);
            } else {
                sess->AddEventList(CPAP_Pressure, EVL_Event);
            }

            sess->AddEventList(INTELLIPAP_Unknown1, EVL_Event);
            sess->AddEventList(INTELLIPAP_Unknown2, EVL_Event);

            sess->AddEventList(CPAP_LeakTotal, EVL_Event);
            sess->AddEventList(CPAP_MaxLeak, EVL_Event);
            sess->AddEventList(CPAP_TidalVolume, EVL_Event);
            sess->AddEventList(CPAP_MinuteVent, EVL_Event);
            sess->AddEventList(CPAP_RespRate, EVL_Event);
            sess->AddEventList(CPAP_Snore, EVL_Event);

            sess->AddEventList(CPAP_Obstructive, EVL_Event);
            sess->AddEventList(INTP_SnoreFlag, EVL_Event);
            sess->AddEventList(CPAP_Hypopnea, EVL_Event);
            sess->AddEventList(CPAP_NRI, EVL_Event);
            sess->AddEventList(CPAP_LeakFlag, EVL_Event);
            sess->AddEventList(CPAP_ExP, EVL_Event);


        } else {
            // If there is a double up, null out the earlier session
            // otherwise there will be a crash on shutdown.
            for (int z = 0; z < SessionStart.size(); z++) {
                if (SessionStart[z] == (quint32)sid) {
                    SessionStart[z] = 0;
                    SessionEnd[z] = 0;
                    break;
                }
            }

            QDateTime d = QDateTime::fromSecsSinceEpoch(sid);
            qDebug() << sid << "has double ups" << d;
            /*Session *sess=Sessions[sid];
            Sessions.erase(Sessions.find(sid));
            delete sess;
            SessionStart[i]=0;
            SessionEnd[i]=0; */
        }
    }

    long pos = 0;
    int rampval = 0;
    sid = 0;
    //SessionID lastsid = 0;

    //int last_minp=0, last_maxp=0, last_ps=0, last_pres = 0;

    for (int i = 0; i < recs; i++) {
        // convert timestamp to real epoch
        ts1 = ((m_buffer[pos] << 24) | (m_buffer[pos + 1] << 16) | (m_buffer[pos + 2] << 8) | m_buffer[pos + 3]) + ep;

        for (int j = 0; j < SessionStart.size(); j++) {
            sid = SessionStart[j];

            if (!sid) { continue; }

            if ((ts1 >= (quint32)sid) && (ts1 <= SessionEnd[j])) {
                Session *sess = Sessions[sid];

                qint64 time = quint64(ts1) * 1000L;
                sess->really_set_last(time);
                sess->settings[CPAP_Mode] = mode;

                int minp = m_buffer[pos + 0x13];
                int maxp = m_buffer[pos + 0x14];
                int ps = m_buffer[pos + 0x15];
                int pres = m_buffer[pos + 0xd];

                if (mode >= MODE_BILEVEL_FIXED) {
                    rampval = maxp;
                } else {
                    rampval = minp;
                }

                qint64 rs = rampstart[sid];

                if (pres < rampval) {
                    if (!rs) {
                        // ramp started


//                        int rv = pres-rampval;
//                        double ramp =

                        rampstart[sid] = time;
                    }
                    rampend[sid] = time;
                } else {
                    if (rs > 0) {
                        if (!sess->eventlist.contains(CPAP_Ramp)) {
                            sess->AddEventList(CPAP_Ramp, EVL_Event);
                        }
                        int duration = (time - rs) / 1000L;
                        sess->eventlist[CPAP_Ramp][0]->AddEvent(time, duration);

                        rampstart.remove(sid);
                        rampend.remove(sid);
                    }
                }


                // Do this after ramp, because ramp calcs might need to insert interpolated pressure samples
                if (mode >= MODE_BILEVEL_FIXED) {

                    sess->settings[CPAP_EPAP] = float(minp) / 10.0;
                    sess->settings[CPAP_IPAP] = float(maxp) / 10.0;

                    sess->settings[CPAP_PS] = float(ps) / 10.0;


                    sess->eventlist[CPAP_IPAP][0]->AddEvent(time, float(pres) / 10.0);
                    sess->eventlist[CPAP_EPAP][0]->AddEvent(time, float(pres-ps) / 10.0);
//                   rampval = maxp;

                } else {
                    sess->eventlist[CPAP_Pressure][0]->AddEvent(time, float(pres) / 10.0); // current pressure
//                    rampval = minp;

                    if (mode == MODE_APAP) {
                        sess->settings[CPAP_PressureMin] = float(minp) / 10.0;
                        sess->settings[CPAP_PressureMax] = float(maxp) / 10.0;
                    } else if (mode == MODE_CPAP) {
                        sess->settings[CPAP_Pressure] = float(maxp) / 10.0;
                    }
                }


                sess->eventlist[CPAP_LeakTotal][0]->AddEvent(time, m_buffer[pos + 0x7]); // "Average Leak"
                sess->eventlist[CPAP_MaxLeak][0]->AddEvent(time, m_buffer[pos + 0x6]); // "Max Leak"

                int rr = m_buffer[pos + 0xa];
                sess->eventlist[CPAP_RespRate][0]->AddEvent(time, rr); // Respiratory Rate
               // sess->eventlist[INTELLIPAP_Unknown1][0]->AddEvent(time, m_buffer[pos + 0xf]); //
                sess->eventlist[INTELLIPAP_Unknown1][0]->AddEvent(time, m_buffer[pos + 0xc]);

                sess->eventlist[CPAP_Snore][0]->AddEvent(time, m_buffer[pos + 0x4]); //4/5??

                if (m_buffer[pos+0x4] > 0) {
                    sess->eventlist[INTP_SnoreFlag][0]->AddEvent(time, m_buffer[pos + 0x5]);
                }

                // 0x0f == Leak Event
                // 0x04 == Snore?
                if (m_buffer[pos + 0xf] > 0) { // Leak Event
                    sess->eventlist[CPAP_LeakFlag][0]->AddEvent(time, m_buffer[pos + 0xf]);
                }

                if (m_buffer[pos + 0x5] > 4) { // This matches Exhale Puff.. not sure why 4
                    //MW: Are the lower 2 bits something else?

                    sess->eventlist[CPAP_ExP][0]->AddEvent(time, m_buffer[pos + 0x5]);
                }

                if (m_buffer[pos + 0x10] > 0) {
                    sess->eventlist[CPAP_Obstructive][0]->AddEvent(time, m_buffer[pos + 0x10]);
                }

                if (m_buffer[pos + 0x11] > 0) {
                    sess->eventlist[CPAP_Hypopnea][0]->AddEvent(time, m_buffer[pos + 0x11]);
                }

                if (m_buffer[pos + 0x12] > 0) { // NRI // is this == to RERA?? CA??
                    sess->eventlist[CPAP_NRI][0]->AddEvent(time, m_buffer[pos + 0x12]);
                }

                quint16 tv = (m_buffer[pos + 0x8] << 8) | m_buffer[pos + 0x9]; // correct

                sess->eventlist[CPAP_TidalVolume][0]->AddEvent(time, tv);

                EventDataType mv = tv * rr; // MinuteVent=TidalVolume * Respiratory Rate
                sess->eventlist[CPAP_MinuteVent][0]->AddEvent(time, mv / 1000.0);
                break;
            } else {
            }
            //lastsid = sid;
        }

        pos += 26;
    }

    // Close any open ramps and store the event.
    QHash<SessionID,qint64>::iterator rit;
    QHash<SessionID,qint64>::iterator rit_end = rampstart.end();

    for (rit = rampstart.begin(); rit != rit_end; ++rit) {
        qint64 rs = rit.value();
        SessionID sid = rit.key();
        if (rs > 0) {
            qint64 re = rampend[rit.key()];

            Session *sess = Sessions[sid];
            if (!sess->eventlist.contains(CPAP_Ramp)) {
                sess->AddEventList(CPAP_Ramp, EVL_Event);
            }

            int duration = (re - rs) / 1000L;
            sess->eventlist[CPAP_Ramp][0]->AddEvent(re, duration);
            rit.value() = 0;

        }
    }

    for (int i = 0; i < SessionStart.size(); i++) {
        SessionID sid = SessionStart[i];

        if (sid) {
            sess = Sessions[sid];

            if (!sess) continue;

//            quint64 first = qint64(sid) * 1000L;
            //quint64 last = qint64(SessionEnd[i]) * 1000L;

            if (sess->last() > 0) {
             //   sess->really_set_last(last);


                sess->settings[INTP_SmartFlexLevel] = smartflex;

                if (smartflexmode == 0) {
                    sess->settings[INTP_SmartFlexMode] = PM_FullTime;
                } else {
                    sess->settings[INTP_SmartFlexMode] = PM_RampOnly;
                }

                sess->settings[CPAP_RampPressure] = ramp_pressure;
                sess->settings[CPAP_RampTime] = ramp_time;

                sess->UpdateSummaries();

                addSession(sess);
            } else {
                delete sess;
            }

        }
    }

    finishAddingSessions();
    mach->Save();


    delete [] m_buffer;

    f.close();

    int c = Sessions.size();
    return c;
}

////////////////////////////////////////////////////////////////////////////
// Devilbiss DV64 Notes
// 1)  High resolution data (flow and pressure) is kept on SD for only 100 hours
// 1a) Flow graph for days without high resolution data is absent
// 1b) Pressure graph is high resolution when high res data is available and
//     only 1 per minute when using low resolution data.
// 2)  Max and Average leak rates are as reported by DV64 machine but we're
//     not sure how those measures relate to other machine's data. Leak rate
//     seems to include the intentional mask leak.
// 2a) Not sure how SmartLink calculates the pct of time of poor mask fit.
//     May be same as what we call large leak time for other machines?
////////////////////////////////////////////////////////////////////////////

struct DV6TestedModel
{
    QString model;
    QString name;
};

static const DV6TestedModel testedModels[] = {
    { "DV64D", "Blue StandardPlus" },
    { "DV64E", "Blue AutoPlus" },
    { "DV63E", "Blue (IntelliPAP 2) AutoPlus" },
    { "", "unknown product" } // List stopper -- must be last entry
};

struct DV6_S_Data // Daily summary
{
/***    
    Session * sess;
    unsigned char u1;            //00 (position)
***/    
    unsigned int start_time;     //01 Start time for date
    unsigned int stop_time;      //05 End time
    unsigned int written;        //09 timestamp when this record was written
    EventDataType hours;         //13
//    EventDataType unknown14;   //14
    EventDataType pressureAvg;   //15
    EventDataType pressureMax;   //16
    EventDataType pressure50;    //17 50th percentile
    EventDataType pressure90;    //18 90th percentile
    EventDataType pressure95;    //19 95th percentile
    EventDataType pressureStdDev;//20 std deviation
//    EventDataType unknown_21;    //21
    EventDataType leakAvg;       //22
    EventDataType leakMax;       //23
    EventDataType leak50;        //24 50th percentile
    EventDataType leak90;        //25 90th percentile
    EventDataType leak95;        //26 95th percentile
    EventDataType leakStdDev;    //27 std deviation
    EventDataType tidalVolume;   //28 & 0x29
    EventDataType avgBreathRate; //30
    EventDataType unknown_31;    //31
    EventDataType snores;        //32 snores / hypopnea per minute
    EventDataType timeInExPuf;   //33 Time in Expiratory Puff
    EventDataType timeInFL;      //34 Time in Flow Limitation
    EventDataType timeInPB;      //35 Time in Periodic Breathing
    EventDataType maskFit;       //36 mask fit (or rather, not fit) percentage
    EventDataType indexOA;       //37 Obstructive
    EventDataType indexCA;       //38 Central index
    EventDataType indexHyp;      //39 Hypopnea Index
    EventDataType unknown_40;    //40 Reserved?
    EventDataType unknown_41;    //40 Reserved?
                                 //42-48 unknown
    EventDataType pressureSetMin;   //49
    EventDataType pressureSetMax;   //50
};

#ifdef _MSC_VER
#define PACK( __Declaration__ ) __pragma( pack(push, 1) ) __Declaration__ __pragma( pack(pop) )
#else
#define PACK( __Declaration__ ) __Declaration__ __attribute__((__packed__))
#endif


// DV6_S_REC is the day structure in the S.BIN file
PACK (struct DV6_S_REC{
    unsigned char begin[4];         //0 Beginning of day
    unsigned char end[4];           //4 End of day
    unsigned char written[4];       //8 When this record was written??
    unsigned char hours;            //12 Hours in session * 10
    unsigned char unknown_13;       //13
    unsigned char pressureAvg;      //14 All pressure settings are * 10
    unsigned char pressureMax;      //15
    unsigned char pressure50;       //16 50th percentile
    unsigned char pressure90;       //17 90th percentile
    unsigned char pressure95;       //18 95th percentile
    unsigned char pressureStdDev;   //19 std deviation
    unsigned char unknown_20;       //20
    unsigned char leakAvg;          //21
    unsigned char leakMax;          //22
    unsigned char leak50;           //23 50th percentile
    unsigned char leak90;           //24 90th percentile
    unsigned char leak95;           //25 95th percentile
    unsigned char leakStdDev;       //26 std deviation
    unsigned char tv1;              //27 tidal volume = tv2 * 256 + tv1
    unsigned char tv2;              //28
    unsigned char avgBreathRate;    //29
    unsigned char unknown_30;       //30
    unsigned char snores;           //31 snores / hypopnea per minute
    unsigned char timeInExPuf;      //32 % Time in Expiratory Puff * 2
    unsigned char timeInFL;         //33 % Time in Flow Limitation * 2
    unsigned char timeInPB;         //34 % Time in Periodic Breathing * 2
    unsigned char maskFit;          //35 mask fit (or rather, not fit) percentage * 2
    unsigned char indexOA;          //36 Obstructive index * 4
    unsigned char indexCA;          //37 Central index * 4
    unsigned char indexHyp;         //38 Hypopnea Index * 4
    unsigned char unknown_39;       //39 Reserved?
    unsigned char unknown_40;       //40 Reserved?
    unsigned char unknown_41;       //41
    unsigned char unknown_42;       //42
    unsigned char unknown_43;       //43
    unsigned char unknown_44;       //44 % time snoring *4
    unsigned char unknown_45;       //45
    unsigned char unknown_46;       //46
    unsigned char unknown_47;       //47 (related to smartflex and flow rounding?)
    unsigned char pressureSetMin;   //48
    unsigned char pressureSetMax;   //49
    unsigned char unknown_50;       //50
    unsigned char unknown_51;       //51
    unsigned char unknown_52;       //52
    unsigned char unknown_53;       //53
    unsigned char checksum;         //54
});

// DV6 SET.BIN - structure of the entire settings file
PACK (struct SET_BIN_REC {
    char unknown_00;                        // assuming file version
    char serial[11];                        // null terminated
    unsigned char language;
    unsigned char capabilities;             // CPAP or APAP
    unsigned char unknown_11;
    unsigned char cpap_pressure;
    unsigned char unknown_12;
    unsigned char max_pressure;
    unsigned char unknown_13;
    unsigned char min_pressure;
    unsigned char alg_apnea_threshhold;     // always locked at 00
    unsigned char alg_apnea_duration;
    unsigned char alg_hypop_threshold;
    unsigned char alg_hypop_duration;
    unsigned char ramp_pressure;
    unsigned char unknown_01;
    unsigned char ramp_duration;
    unsigned char unknown_02[3];
    unsigned char smartflex_setting;
    unsigned char smartflex_when;
    unsigned char inspFlowRounding;
    unsigned char expFlowRounding;
    unsigned char complianceHours;
    unsigned char unknown_03;
    unsigned char tubing_diameter;
    unsigned char autostart_setting;
    unsigned char unknown_04;
    unsigned char show_hide;
    unsigned char unknown_05;
    unsigned char lock_flags;
    unsigned char unknown_06;
    unsigned char humidifier_setting;  // 0-5
    unsigned char unknown_7;
    unsigned char possible_alg_apnea;
    unsigned char unknown_8[7];
    unsigned char bacteria_filter;
    unsigned char unused[73];
    unsigned char checksum;
}); // http://digitalvampire.org/blog/index.php/2006/07/31/why-you-shouldnt-use-__attribute__packed/

// Unless explicitly noted, all other DV6_x_REC are definitions for the repeating data structure that follows the header
PACK (struct DV6_HEADER {
    unsigned char unknown;          // 0 always zero
    unsigned char filetype;         // 1 e.g. "R" for a R.BIN file
    unsigned char serial[11];       // 2 serial number
    unsigned char numRecords[4];    // 13 Number of records in file (always fixed, 180,000 for R.BIN)
    unsigned char recordLength;     // 17 Length of data record (always 117)
    unsigned char recordStart[4];   // 18 First record in wrap-around buffer
    unsigned char unknown_22[21];   // 22 Unknown values
    unsigned char unknown_43[8];    // 43 Seems always to be zero
    unsigned char lasttime[4];      // 51 OSCAR only: Last timestamp, in history files only
    unsigned char checksum;         // 55 Checksum
});

// DV6 E.BIN - event data
struct DV6_E_REC { // event log record
    unsigned char begin[4];
    unsigned char end[4];
    unsigned char unknown_01;
    unsigned char unknown_02;
    unsigned char unknown_03;
    unsigned char unknown_04;
    unsigned char event_type;
    unsigned char event_severity;
    unsigned char value;
    unsigned char reduction;
    unsigned char duration;
    unsigned char unknown[7];
    unsigned char checksum;
};

// DV6 U.BIN - session start and stop times
struct DV6_U_REC {
    unsigned char begin[4];
    unsigned char end[4];
    unsigned char checksum;  // possible checksum? Not really sure
};

// DV6 R.BIN - High resolution data (breath) and moderate resolution (pressure, flags)
struct DV6_R_REC {
    unsigned char timestamp[4];
    qint16 breath[50];          // 50 breath flow records at 25 Hz
    unsigned char pressure1;    // pressure in first second of frame
    unsigned char pressure2;    // pressure in second second of frame
    unsigned char unknown106;
    unsigned char unknown107;
    unsigned char flags1[4];    // flags for first second of frame
    unsigned char flags2[4];    // flags for second second of frame
    unsigned char checksum;
};

// DV6 L.BIN - Low resolution data
PACK (struct DV6_L_REC {
    unsigned char timestamp[4];     // 0 timestamp
    unsigned char maxLeak;          // 4 lpm
    unsigned char avgLeak;          // 5 lpm
    unsigned char tidalVolume6;     // 6
    unsigned char tidalVolume7;     // 7
    unsigned char breathRate;       // 8 breaths per minute
    unsigned char unknown9;         // 9
    unsigned char avgPressure;      // 10 pressure * 10
    unsigned char unknown11;        // 11 always zero?
    unsigned char unknown12;        // 12
    unsigned char pressureLimitLow; // 13 pressure * 10
    unsigned char pressureLimitHigh;// 14 pressure * 10
    unsigned char timeSnoring;      // 15
    unsigned char snoringSeverity;  // 16
    unsigned char timeEP;           // 17
    unsigned char epSeverity;       // 18
    unsigned char timeX1;           // 19 ??
    unsigned char x1Severity;       // 20 ??
    unsigned char timeX2;           // 21 ??
    unsigned char x2Severity;       // 22 ??
    unsigned char timeX3;           // 23 ??
    unsigned char x3Severity;       // 24 ??
    unsigned char apSeverity;       // 25
    unsigned char TimeApnea;        // 26
    unsigned char noaSeverity;      // 27
    unsigned char timeNOA;          // 28
    unsigned char ukSeverity;       // 29 ??
    unsigned char timeUk;           // 30 ??
    unsigned char unknown31;        // 31
    unsigned char unknown32;        // 32
    unsigned char unknown33;        // 33
    unsigned char unknownFlag34;    // 34
    unsigned char unknownTime35;    // 35
    unsigned char unknownFlag36;    // 36
    unsigned char unknown37;        // 37
    unsigned char unknown38;        // 38
    unsigned char unknown39;        // 39
    unsigned char unknown40;        // 40
    unsigned char unknown41;        // 41
    unsigned char unknown42;        // 42
    unsigned char unknown43;        // 43
    unsigned char checksum;         // 44
});

// Our structure for managing sessions
struct DV6_SessionInfo {
    Session * sess;
    DV6_S_Data *dailyData;
    SET_BIN_REC * dv6Settings;

    unsigned int begin;
    unsigned int end;
    unsigned int written;
//    bool haveHighResData;
    unsigned int firstHighRes;
    unsigned int lastHighRes;
    CPAPMode mode = MODE_UNKNOWN;
};

QString card_path;
QString backup_path;
QString history_path;
QString rebuild_path;

MachineInfo info;
Machine * mach = nullptr;

bool rebuild_from_backups = false;
bool create_backups = false;

QStringList inputFilePaths;

QMap<SessionID, DV6_S_Data> DailySummaries;
QMap<SessionID, DV6_SessionInfo> SessionData;
SET_BIN_REC * settings;

unsigned int ep = 0;

// Convert a 4-character number in DV6 data file to a standard int
unsigned int convertNum (unsigned char num[]) {
    return ((num[3] << 24) + (num[2] << 16) + (num[1] << 8) + num[0]);
}

// Convert a timestamp in DV6 data file to a standard Unix epoch timestamp as used in OSCAR
unsigned int convertTime (unsigned char time[]) {
    if (ep == 0) {
        QDateTime epoch(QDate(2002, 1, 1), QTime(0, 0, 0), Qt::UTC); // Intellipap Epoch
        ep = epoch.toTime_t();
    }
    return ((time[3] << 24) + (time[2] << 16) + (time[1] << 8) + time[0]) + ep; // Time as Unix epoch time
}

class RollingBackup
{
public:
    RollingBackup () {}
    ~RollingBackup () {
    }

    bool open (const QString filetype, DV6_HEADER * newhdr, QByteArray * startTime); // Open the file
    bool close();           // close the file
    bool save(const QByteArray &dataBA);  // save the next record in the file

private:
    DV6_HEADER hdr;       // file header
    QString filetype;
    QFile histfile;

    const qint64 maxHistFileSize = 10000000;    // Maximum size of file before we create a new file, in MB (40 MB)
                                                // (While 40e6 would be easier to understand, 40e6 is a double, not an int)

    unsigned int lastTimeInFile;    // Timestamp of last data record in history file
    int numWritten;     // Number of records written
};

QStringList getHistoryFileNames (const QString filetype, bool reversed = false) {
    QStringList filters;
    QDir hpath(history_path);

    filters.append(filetype);     // Assume one-letter file name like "S.BIN"
    filters[0].insert(1, "_*");   // Add a wild card like "S_*.BIN"
    hpath.setNameFilters(filters);
    hpath.setFilter(QDir::Files);
    hpath.setSorting(QDir::Name);
    if (reversed) hpath.setSorting(QDir::Name | QDir::Reversed);

    return hpath.entryList(); // Get list of files
}

QString getNewFileName (QString filetype, QByteArray * startTime, int offset=0) {
    unsigned char startTimeChar[5];
    for (int i = 0; i < 4; i++)
        startTimeChar[i] = startTime->at(offset+i);
    unsigned int ts = convertTime(startTimeChar);
    QString newfile = filetype.left(1) + "_" + QDateTime::fromSecsSinceEpoch(ts).toString("yyyyMMdd") + ".BIN";
    qDebug() << "DV6 getNewFileName returns" << newfile;
    return newfile;
}

bool RollingBackup::open (const QString filetype, DV6_HEADER * inputhdr, QByteArray * startTimeOfBackup) {
    if (!create_backups)
        return true;

    QDir hpath(history_path);
    QString historypath = hpath.absolutePath() + "/";
    int histfilesize = 0;

    this->filetype = filetype;

    bool needNewFile = false;
    memcpy (&hdr, inputhdr, sizeof(DV6_HEADER));
    numWritten = 0;

    QStringList fileNames = getHistoryFileNames(filetype, true);

    // Handle first time a history file is being created
    if (fileNames.isEmpty()) {
        for (int i = 0; i < 4; i++) {
            hdr.recordStart[i] = 0;
            hdr.lasttime[i] = 0;
        }
        lastTimeInFile = 0;
        histfile.setFileName(historypath + getNewFileName (filetype, startTimeOfBackup));
        needNewFile= true;
    }
    
    // We have an existing history record
    if (! fileNames.isEmpty()) {
        histfile.setFileName(historypath + fileNames.first());      // File names are in reverse order, so latest is first

        // Open and read history file header and save the header
        if (!histfile.open(QIODevice::ReadWrite)) {
            qWarning() << "DV6 rb(open) could not open" << fileNames.first() << "for readwrite, error code" << histfile.error() << histfile.errorString();
            return false;
        }
        histfilesize = histfile.size();
        QByteArray dataBA = histfile.read(sizeof(DV6_HEADER));
        memcpy (&hdr, dataBA.data(), sizeof(DV6_HEADER));
        lastTimeInFile = convertTime(hdr.lasttime);

        // See if this file is large enough that we want to create a new file
        // If it is large, we'll start a new file.
        if (histfile.size() > maxHistFileSize) {
            QString nextFile = historypath + getNewFileName (filetype, &dataBA, 51);
            QString hh = histfile.fileName();

            if (hh != nextFile) {
                lastTimeInFile = convertTime(hdr.lasttime);
                histfile.close();
                // Update header saying we are starting at record 0
                for (int i = 0; i < 4; i++)
                    hdr.recordStart[i] = 0;
                histfile.setFileName(nextFile);
                needNewFile = true;
            }
        }
    }

    if (needNewFile) {
        if (!histfile.open(QIODevice::ReadWrite)) {
            qWarning() << "DV6 rb(open) could not create new file" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString();
            return false;
        }
        if (histfile.write((char *)&hdr.unknown, sizeof(DV6_HEADER)) != sizeof(DV6_HEADER)) {
            qWarning() << "DV6 rb(open) could not write header to new file" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString();
            histfile.close();
            return false;
        }
    } else {
//        qDebug() << "DV6 rb(open) history file size" << histfilesize;
        histfile.seek(histfilesize);
    }

    return true;
}

bool RollingBackup::close() {
    if (!create_backups)
        return true;

    qint32 size = histfile.size();

    if (!histfile.seek(0)) {
        qWarning() << "DV6 rb(close) unable to seek to file beginning" << histfile.error() << histfile.errorString();
        histfile.close();
        return false;
    }

    quint32 sizehdr = sizeof(DV6_HEADER);
    quint32 reclen = hdr.recordLength;
    quint32 wrap_point = (size - sizehdr) / reclen;

    hdr.recordStart[0] = wrap_point & 0xff;
    hdr.recordStart[1] = (wrap_point >> 8)  & 0xff;
    hdr.recordStart[2] = (wrap_point >> 16) & 0xff;
    hdr.recordStart[3] = (wrap_point >> 24) & 0xff;

    if (histfile.write((char *)&hdr, sizeof(DV6_HEADER)) != sizeof(DV6_HEADER)) {
        qWarning() << "DV6 rb(close) could not write header to file" << histfile.fileName() << "error code" << histfile.error() << histfile.errorString();
        histfile.close();
        return false;
    }

    histfile.close();
    qDebug() << "DV6 rb(close) wrote" << numWritten << "records.";
    return true;
}

bool RollingBackup::save(const QByteArray &dataBA) {

    if (!create_backups)
        return true;

    unsigned char * data = (unsigned char *)dataBA.data();
    unsigned int thisTimeStamp = convertTime(data);

    if (thisTimeStamp > lastTimeInFile) {   // Is this data new to us?
        // If so, save it to the history file.
        if (histfile.write(dataBA) == -1) {
            qWarning() << "DV6 rb(save) could not save record" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString();
            histfile.close();
            return false;
        }
        memcpy(&hdr.lasttime, data, 4);
//        if (!histfile.seek(histfile.pos() + dataBA.length()))
//            qWarning() << "DV6 rb(save) failed respositioning" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString();
        numWritten++;
    }
/***
    else {
        qDebug() << "DV6 rb(save) skipping record" << numWritten << QDateTime::fromSecsSinceEpoch(thisTimeStamp).toString("MM/dd/yyyy hh:mm:ss")
                 << "last in file" << QDateTime::fromSecsSinceEpoch(lastTimeInFile).toString("MM/dd/yyyy hh:mm:ss");
    }
***/

    return true;
}

class RollingFile
{
public:
    RollingFile () { }

    ~RollingFile () {
        if (data)
            delete [] data;
        data = nullptr;
        if (hdr)
            delete hdr;
        hdr = nullptr;
    }

    bool open (QString fn, bool getNext = false); // Open the file
    bool close();           // close the file
    unsigned char * get();  // read the next record in the file

    int numread () {return number_read;};   // Return number of records read
    int recnum () {return record_number;};  // Return last-read record number

    RollingBackup rb;

private:
    QString filename;
    QFile file;
    int record_length;
    int wrap_record;
    bool wrapping = false;

    int number_read = 0;    // Number of records read

    int record_number = 0;  // Number of record.  First record in the file  is #1. First record read is wrap_record;

    DV6_HEADER * hdr;       // file header

    unsigned char * data = nullptr; // record pointer
};

bool RollingFile::open(QString filetype, bool getNext) {

    filename = filetype;

    if (rebuild_from_backups) {
        // Building from backup
        if (!getNext) { // Initialize on first call
            inputFilePaths.clear();
            QStringList histFileNames = getHistoryFileNames(filetype);
            qDebug() << "DV6 rf(open) History file names" << histFileNames;
            for (int i=0; i < histFileNames.size(); i++) {
                file.setFileName(history_path + "/" + histFileNames.at(i));
                inputFilePaths.append(file.fileName());
            }
        }
        if (inputFilePaths.empty())
            return false;
        file.setFileName(inputFilePaths.at(0));
        inputFilePaths.removeAt(0);

    } else {
        file.setFileName(card_path + "/" +filetype);
        inputFilePaths.clear();
    }

    if (!file.open(QIODevice::ReadOnly)) {
        qWarning() << "DV6 rf(open) could not open" << filename << "for reading, error code" << file.error() << file.errorString();
        return false;
    }

    // Save header for use in making backups of data
    hdr = new DV6_HEADER;
    QByteArray dataBA = file.read(sizeof(DV6_HEADER));
    memcpy (hdr, dataBA.data(), sizeof(DV6_HEADER));

    // Extract control information from header
    record_length = hdr->recordLength;
    wrap_record = convertNum(hdr->recordStart);
    record_number = wrap_record;
    number_read = 0;
    wrapping = false;

    // Create buffer to hold each record as it is read
    data = new unsigned char[record_length];

    // Seek to oldest data record in file, which is always at the wrap point
    // wrap_record is the C offset where the next data record is to be written.
    // Since C offsets begin with zero, it is also the number of records in the file.
    int seekpos = sizeof(DV6_HEADER) + wrap_record * record_length;
    if (!file.seek(seekpos)) {
        qWarning() << "DV6 rf(open) unable to make initial seek to record" << wrap_record << "in" + filename << file.error() << file.errorString();
        file.close();
        return false;
    }
    qDebug() << "DV6 rf(open)" << filetype << "positioning to oldest record at pos" << seekpos << "after seek" << file.pos();

    if (file.atEnd()) {
        file.seek(sizeof(DV6_HEADER));
    }
    dataBA = file.read(4);      // Read timestamp of newest data record
    file.seek(seekpos);  // Reset read position before what we just read so we start reading data records here
    if (!rb.open(filetype, hdr, &dataBA)) {
        qWarning() << "DV6 rf(open) failed";
        file.close();
        return false;
    }

    qDebug() << "DV6 rf(open)" << filename << "at wrap record" << wrap_record << "now at pos" << file.pos();
    return true;
}

bool RollingFile::close() {

/*** Works for backing up but prevents chart appearing for the last day
    // Flush any additional input that has not been backed up
    if (create_backups) {
        do {
            DV6_U_REC * rec = (DV6_U_REC *) get();
            if (rec == nullptr)
                break;
        } while (true);
    }
***/

    file.close();

    rb.close();

    if (data)
        delete [] data;
    data = nullptr;
    if (hdr)
        delete hdr;
    hdr = nullptr;

    return true;
}

unsigned char * RollingFile::get() {

//    int readpos;
    record_number++;

    // If we have found the wrap record again, we are done
    if (wrapping && record_number == wrap_record) {
        // Unless we are rebuilding from backup and may have more files
        if (rebuild_from_backups && !inputFilePaths.empty()) {
            qDebug() << "DV6 rf(get) closing" << file.fileName();
            file.close();
            open(inputFilePaths.at(0), true);
        } else {
            return nullptr;
        }
    }

    // Have we reached end of file and need to wrap around to beginning?
    if (file.atEnd()) {
        if (wrapping) {
            qDebug() << "DV6 rf(get) wrap - second time through";
            return nullptr;
        }
        qDebug() << "DV6 rf(get) wrapping to beginning of data in" << filename << "record number is" << record_number-1 << "records read" << number_read;
        record_number = 1;
        wrapping = true;
        if (!file.seek(sizeof(DV6_HEADER))) {
            file.close();
            qWarning() << "DV6 rf(get) unable to seek to first data record in file";
            return nullptr;
        }
        qDebug() << "DV6 rf(get) #" << record_number << "now at pos" << file.pos();
    }

    QByteArray dataBA;
//    readpos = file.pos();
    dataBA=file.read(record_length); // read next record
    if (dataBA.size() != record_length) {
        qWarning() << "DV6 rf(get) #" << record_number << "wrong length";
        file.close();
        return nullptr;
    }

    if (!rb.save(dataBA)) {
        qWarning() << "DV6 rf(get) failed";
    }

    number_read++;

//    qDebug() << "DV6 rf(get)" << filename << "at start pos" << readpos << "end pos" << file.pos() << "record number" << record_number << "of length" << record_length << "number read so far" << number_read;
    memcpy (data, (unsigned char *) dataBA.data(), record_length);
    return data;
}

// Returns empty QByteArray() on failure.
QByteArray fileChecksum(const QString &fileName,
                        QCryptographicHash::Algorithm hashAlgorithm)
{
    QFile f(fileName);
    if (f.open(QFile::ReadOnly)) {
        QCryptographicHash hash(hashAlgorithm);
        bool res = hash.addData(&f);
        f.close();
        if (res) {
            return hash.result();
        }
    }
    return QByteArray();
}

// Return date used within OSCAR, assuming day ends at split time in preferences (usually noon)
QDate getNominalDate (QDateTime dt) {
    QDate d = dt.date();
    QTime tm = dt.time();
    QTime daySplitTime = p_profile->session->getPref(STR_IS_DaySplitTime).toTime();
    if (tm < daySplitTime)
        d = d.addDays(-1);
    return d;
}
QDate getNominalDate (unsigned int dt) {
    QDateTime xdt = QDateTime::fromSecsSinceEpoch(dt);
    return getNominalDate(xdt);
}

///////////////////////////////////////////////
// U.BIN - Open and parse session list and create session data structures
//         with session start and stop times.
///////////////////////////////////////////////

bool load6Sessions () {

    RollingFile rf;
    unsigned int ts1,ts2;

    SessionData.clear();

    qDebug() << "Parsing U.BIN";

    if (!rf.open("U.BIN")) {
        qWarning() << "Unable to open U.BIN";
        return false;
    }

    do {
        DV6_U_REC * rec = (DV6_U_REC *) rf.get();
        if (rec == nullptr)
            break;
        DV6_SessionInfo sinfo;
        // big endian
        ts1 = convertTime(rec->begin); // session start time (this is also the session id)
        ts2 = convertTime(rec->end); // session end time
//#ifdef DEBUG6
        qDebug() << "U.BIN Session" << QDateTime::fromSecsSinceEpoch(ts1).toString("MM/dd/yyyy hh:mm:ss") << ts1 << "to" << QDateTime::fromSecsSinceEpoch(ts2).toString("MM/dd/yyyy hh:mm:ss") << ts2;
//#endif
        sinfo.sess = nullptr;
        sinfo.dailyData = nullptr;
        sinfo.begin = ts1;
        sinfo.end = ts2;
        sinfo.written = 0;
//        sinfo.haveHighResData = false;
        sinfo.firstHighRes = 0;
        sinfo.lastHighRes = 0;

        SessionData[ts1] = sinfo;
    } while (true);

    rf.close();
    qDebug() << "DV6 U.BIN processed" << rf.numread() << "records";

    return true;
}

/////////////////////////////////////////////////////////////////////////////////
// Parse SET.BIN settings file
/////////////////////////////////////////////////////////////////////////////////

bool load6Settings (const QString & path) {

    QByteArray dataBA;

    QFile f(path+"/"+SET_BIN);
    if (rebuild_from_backups)
        f.setFileName(rebuild_path+"/"+SET_BIN);

    if (f.open(QIODevice::ReadOnly)) {
        // Read and parse entire SET.BIN file
        dataBA = f.readAll();
        f.close();

        settings = (SET_BIN_REC *)dataBA.data();

    } else { // if f.open settings file
        // Settings file open failed, return
        qWarning() << "Unable to open SET.BIN file";
        return false;
    }
    return true;
}

////////////////////////////////////////////////////////////////////////////////////////
// S.BIN - Open and load day summary list
////////////////////////////////////////////////////////////////////////////////////////

bool load6DailySummaries () {
    RollingFile rf;

    DailySummaries.clear();

    if (!rf.open("S.BIN")) {
        qWarning() << "Unable to open S.BIN";
        return false;
    }

    qDebug() << "Reading S.BIN summaries";

    do {
        DV6_S_REC * rec = (DV6_S_REC *) rf.get();
        if (rec == nullptr)
            break;

        DV6_S_Data dailyData;

        dailyData.start_time = convertTime(rec->begin);
        dailyData.stop_time = convertTime(rec->end);
        dailyData.written = convertTime(rec->written);

#ifdef DEBUG6
        qDebug() << "DV6 S.BIN start" << dailyData.start_time
                 << "stop" << dailyData.stop_time
                 << "written" << dailyData.written;
#endif

        dailyData.hours = float(rec->hours) / 10.0F;
        dailyData.pressureSetMin = float(rec->pressureSetMin) / 10.0F;
        dailyData.pressureSetMax = float(rec->pressureSetMax) / 10.0F;

        // The following stuff is not necessary to decode, but can be used to verify we are on the right track
        dailyData.pressureAvg = float(rec->pressureAvg) / 10.0F;
        dailyData.pressureMax = float(rec->pressureMax) / 10.0F;
        dailyData.pressure50 = float(rec->pressure50) / 10.0F;
        dailyData.pressure90 = float(rec->pressure90) / 10.0F;
        dailyData.pressure95 = float(rec->pressure95) / 10.0F;
        dailyData.pressureStdDev = float(rec->pressureStdDev) / 10.0F;

        dailyData.leakAvg = float(rec->leakAvg) / 10.0F;
        dailyData.leakMax = float(rec->leakMax) / 10.0F;
        dailyData.leak50= float(rec->leak50) / 10.0F;
        dailyData.leak90 = float(rec->leak90) / 10.0F;
        dailyData.leak95 = float(rec->leak95) / 10.0F;
        dailyData.leakStdDev = float(rec->leakStdDev) / 10.0F;

        dailyData.tidalVolume = float(rec->tv1 | rec->tv2 << 8);
        dailyData.avgBreathRate = float(rec->avgBreathRate);

        dailyData.snores = float(rec->snores);
        dailyData.timeInExPuf = float(rec->timeInExPuf) / 2.0F;
        dailyData.timeInFL = float(rec->timeInFL) / 2.0F;
        dailyData.timeInPB = float(rec->timeInPB) / 2.0F;
        dailyData.maskFit = float(rec->maskFit) / 2.0F;
        dailyData.indexOA = float(rec->indexOA) / 4.0F;
        dailyData.indexCA = float(rec->indexCA) / 4.0F;
        dailyData.indexHyp = float(rec->indexHyp) / 4.0F;

        DailySummaries[dailyData.start_time] = dailyData;

/**** Previous loader did this:
        if (!mach->sessionlist.contains(ts1)) { // Check if already imported
            qDebug() << "Detected new Session" << ts1;
            R.sess = new Session(mach, ts1);
            R.sess->SetChanged(true);

            R.sess->really_set_first(qint64(ts1) * 1000L);
            R.sess->really_set_last(qint64(ts2) * 1000L);

            if (data[49] != data[50]) {
                R.sess->settings[CPAP_PressureMin] = R.pressureSetMin;
                R.sess->settings[CPAP_PressureMax] = R.pressureSetMax;
                R.sess->settings[CPAP_Mode] = MODE_APAP;
            } else {
                R.sess->settings[CPAP_Mode] = MODE_CPAP;
                R.sess->settings[CPAP_Pressure] = R.pressureSetMin;
            }
            R.hasMaskPressure = false;
***/

    } while (true);

    rf.close();
    qDebug() << "DV6 S.BIN processed" << rf.numread() << "records";

    return true;
}

////////////////////////////////////////////////////////////////////////////////////////
// Parse VER.BIN for model number, serial, etc.
////////////////////////////////////////////////////////////////////////////////////////

bool load6VersionInfo(const QString & path) {

    QByteArray dataBA;
    QByteArray str;

    QFile f(path+"/VER.BIN");
    if (rebuild_from_backups)
        f.setFileName(rebuild_path+"/VER.BIN");
    info.series = "DV6";
    info.brand = "DeVilbiss";

    if (f.open(QIODevice::ReadOnly)) {
        dataBA = f.readAll();
        f.close();

        int cnt = 0;
        for (int i=0; i< dataBA.size(); ++i) { // deliberately going one further to catch end condition
            if ((dataBA.at(i) == 0) || (i >= dataBA.size()-1)) { // if null terminated or last byte

                switch(cnt) {
                case 1: // serial
                    info.serial = str;
                    break;
                case 2: // modelnumber
//                    info.model = str;
                    info.modelnumber = str;
                    info.modelnumber = info.modelnumber.trimmed();
                    for (int i = 0; i < (int)sizeof(testedModels); i++) {
                        if (   testedModels[i].model == info.modelnumber
                            || testedModels[i].model.isEmpty()) {
                            info.model = testedModels[i].name;
                            break;
                        }
                    }
                    break;
                case 7: // ??? V025RN20170
                    break;
                case 9: // ??? V014BL20150630
                    break;
                case 11: // ??? 01 09
                    break;
                case 12: // ??? 0C 0C
                    break;
                case 14: // ??? BA 0C
                    break;
                default:
                    break;

                }

                // Clear and start a new data record
                str.clear();
                cnt++;
            } else {
                // Add the character to the current string
                str.append(dataBA[i]);
            }
        }
        return true;

    } else { // if (f.open(...)
        // VER.BIN open failed
        qWarning() << "Unable to open VER.BIN";
        return false;
    }

}

////////////////////////////////////////////////////////////////////////////////////////
// Create DV6_SessionInfo structures for each session and store in SessionData qmap
////////////////////////////////////////////////////////////////////////////////////////

int create6Sessions() {
    SessionID sid = 0;
    Session * sess;
    
    for (auto sinfo=SessionData.begin(), end=SessionData.end(); sinfo != end; ++sinfo) {
        sid = sinfo->begin;

        if (mach->SessionExists(sid)) {
            // skip already imported sessions..
            qDebug() << "Session already exists" << QDateTime::fromSecsSinceEpoch(sid).toString("MM/dd/yyyy hh:mm:ss");

        } else if (sinfo->sess == nullptr) {
            // process new sessions
            sess = new Session(mach, sid);
#ifdef DEBUG6
            qDebug() << "Creating session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "to" << QDateTime::fromSecsSinceEpoch(sinfo->end).toString("MM/dd/yyyy hh:mm:ss");
#endif

            sinfo->sess = sess;
            sinfo->dailyData = nullptr;
            sinfo->written = 0;
//            sinfo->haveHighResData = false;
            sinfo->firstHighRes = 0;
            sinfo->lastHighRes = 0;

            sess->really_set_first(quint64(sinfo->begin) * 1000L);
            sess->really_set_last(quint64(sinfo->end) * 1000L);

//            rampstart[sid] = 0;
//            rampend[sid] = 0;
            sess->SetChanged(true);
            
            sess->AddEventList(INTELLIPAP_Unknown1, EVL_Event);
            sess->AddEventList(INTELLIPAP_Unknown2, EVL_Event);

            sess->AddEventList(CPAP_LeakTotal, EVL_Event);
            sess->AddEventList(CPAP_MaxLeak, EVL_Event);
            sess->AddEventList(CPAP_TidalVolume, EVL_Event);
            sess->AddEventList(CPAP_MinuteVent, EVL_Event);
            sess->AddEventList(CPAP_RespRate, EVL_Event);
            sess->AddEventList(CPAP_Snore, EVL_Event);
            sess->AddEventList(INTP_SnoreFlag, EVL_Event);

            sess->AddEventList(CPAP_Obstructive, EVL_Event);
            sess->AddEventList(CPAP_Hypopnea, EVL_Event);
            sess->AddEventList(CPAP_NRI, EVL_Event);
//            sess->AddEventList(CPAP_LeakFlag, EVL_Event);
            sess->AddEventList(CPAP_ExP, EVL_Event);
            sess->AddEventList(CPAP_FlowLimit, EVL_Event);


        } else {
            // If there is a duplicate session, null out the earlier session
            // otherwise there will be a crash on shutdown.
//??            for (int z = 0; z < SessionStart.size(); z++) {
//??                if (SessionStart[z] == sid) {
//??                    SessionStart[z] = 0;
//??                    SessionEnd[z] = 0;
//??                    break;
//??                }
            qDebug() << sid << "has double ups" << QDateTime::fromSecsSinceEpoch(sid).toString("MM/dd/yyyy hh:mm:ss");
            
            /*Session *sess=Sessions[sid];
            Sessions.erase(Sessions.find(sid));
            delete sess;
            SessionStart[i]=0;
            SessionEnd[i]=0; */
        }
    }

    qDebug() << "Created" << SessionData.size() << "sessions";
    return SessionData.size();
}

////////////////////////////////////////////////////////////////////////////////////////
// Parse R.BIN for high resolution flow data
////////////////////////////////////////////////////////////////////////////////////////

bool load6HighResData () {

    RollingFile rf;
    Session *sess = nullptr;
    unsigned int rec_ts1, previousRecBegin = 0;
    bool inSession = false; // true if we are adding data to this session

    if (!rf.open("R.BIN")) {
        qWarning() << "DV6 Unable to open R.BIN";
        return false;
    }

    qDebug() << "R.BIN starting at record" << rf.recnum();

    sess = NULL;
    EventList * flow = NULL;
    EventList * pressure = NULL;
    EventList * FLG = NULL;
    EventList * snore = NULL;
//        EventList * leak = NULL;
/***
    EventList * OA  = NULL;
    EventList * HY  = NULL;
    EventList * NOA = NULL;
    EventList * EXP = NULL;
    EventList * FL  = NULL;
    EventList * PB  = NULL;
    EventList * VS  = NULL;
    EventList * LL  = NULL;
    EventList * RE  = NULL;
    bool inOA = false, inH = false, inCA = false, inExP = false, inVS = false, inFL = false, inPB = false, inRE = false, inLL = false;
    qint64 OAstart = 0, OAend = 0;
    qint64 Hstart = 0, Hend = 0;
    qint64 CAstart = 0, CAend = 0;
    qint64 ExPstart = 0, ExPend = 0;
    qint64 VSstart = 0, VSend = 0;
    qint64 FLstart = 0, FLend = 0;
    qint64 PBstart = 0, PBend = 0;
    qint64 REstart =0, REend = 0;
    qint64 LLstart =0, LLend = 0;
//    lastts1 = 0;
***/

    // sinfo is for walking through sessions when matching up with flow data records
    QMap<SessionID, DV6_SessionInfo>::iterator sinfo;
    sinfo = SessionData.begin();

    do {
        DV6_R_REC * R = (DV6_R_REC *) rf.get();
        if (R == nullptr)
            break;

        sess = sinfo->sess;

        // Get the timestamp from the record
        rec_ts1 = convertTime(R->timestamp);

        if (rec_ts1 < previousRecBegin) {
            qWarning() << "R.BIN - Corruption/Out of sequence data found, skipping record" << rf.recnum() << ", prev"
                       << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin
                       << "this"
                       << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1;
            continue;
        }

        // Look for a gap in DV6_R records.  They should be at two second intervals.
        // If there is a gap, we are probably in a new session
        if (inSession && ((rec_ts1 - previousRecBegin) > 2)) {
            if (sess) {
                sess->set_last(flow->last());
                if (sess->first() == 0)
                    qWarning() << "R.BIN first = 0 - 1320";
                EventDataType min = flow->Min();
                EventDataType max = flow->Max();
                sess->setMin(CPAP_FlowRate, min);
                sess->setMax(CPAP_FlowRate, max);
                sess->setPhysMax(CPAP_FlowRate, min); // not sure :/
                sess->setPhysMin(CPAP_FlowRate, max);
//                sess->really_set_last(flow->last());
            }
            sess = nullptr;
            flow = nullptr;
            pressure = nullptr;
            FLG = nullptr;
            snore = nullptr;
            inSession = false;
        }

        // Skip over sessions until we find one that this record is in
        while (rec_ts1 > sinfo->end) {
#ifdef DEBUG6
            qDebug() << "R.BIN - skipping session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss")
                     << "looking for" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
                     << "record" << rf.recnum();
#endif
            if (inSession && sess) {
                // update min and max
                // then add to machine
                if (sess->first() == 0)
                    qWarning() << "R.BIN first = 0 - 1284";
                EventDataType min = flow->Min();
                EventDataType max = flow->Max();
                sess->setMin(CPAP_FlowRate, min);
                sess->setMax(CPAP_FlowRate, max);
                sess->setPhysMax(CPAP_FlowRate, min); // not sure :/
                sess->setPhysMin(CPAP_FlowRate, max);

                sess = nullptr;
                flow = nullptr;
                pressure = nullptr;
                FLG = nullptr;
                snore = nullptr;
                inSession = false;
            }
            sinfo++;
            if (sinfo == SessionData.end())
                break;
        }

        previousRecBegin = rec_ts1;

        // If we have data beyond last session, we are in trouble (for unknown reasons)
        if (sinfo == SessionData.end()) {
            qWarning() << "DV6 R.BIN import ran out of sessions to match flow data, record" << rf.recnum();
            break;
        }

        // Check if record belongs in this session or a future session
        if (!inSession && rec_ts1 <= sinfo->end) {
            sess = sinfo->sess;         // this is the Session we want
            if (!inSession && sess) {
                inSession = true;
                flow = sess->AddEventList(CPAP_FlowRate, EVL_Waveform, 0.01f, 0.0f, 0.0f, 0.0f, double(2000) / double(50));
                pressure = sess->AddEventList(CPAP_Pressure, EVL_Waveform, 0.1f, 0.0f, 0.0f, 0.0f, double(2000) / double(2));
                FLG = sess->AddEventList(CPAP_FLG, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2));
                snore = sess->AddEventList(CPAP_Snore, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2));
                //                    sinfo->hasMaskPressure = true;
                //                    leak = R->sess->AddEventList(CPAP_Leak, EVL_Waveform, 1.0, 0.0, 0.0, 0.0, double(2000) / double(1));
                /***
                    OA = R->sess->AddEventList(CPAP_Obstructive, EVL_Event);
                    NOA = R->sess->AddEventList(CPAP_NRI, EVL_Event);
                    RE = R->sess->AddEventList(CPAP_RERA, EVL_Event);
                    VS = R->sess->AddEventList(CPAP_VSnore, EVL_Event);
                    HY = R->sess->AddEventList(CPAP_Hypopnea, EVL_Event);
                    EXP = R->sess->AddEventList(CPAP_ExP, EVL_Event);
                    FL = R->sess->AddEventList(CPAP_FlowLimit, EVL_Event);
                    PB = R->sess->AddEventList(CPAP_PB, EVL_Event);
                    LL = R->sess->AddEventList(CPAP_LargeLeak, EVL_Event);
***/
            }
        }
        if (inSession) {
            // Record breath and pressure waveforms
            qint64 ti = qint64(rec_ts1) * 1000;
            flow->AddWaveform(ti,R->breath,50,2000);
            pressure->AddWaveform(ti, &R->pressure1, 2, 2000);
            if (sinfo->firstHighRes == 0 || sinfo->firstHighRes > rec_ts1) sinfo->firstHighRes = rec_ts1;
            if (sinfo->lastHighRes == 0 || sinfo->lastHighRes < rec_ts1+2) sinfo->lastHighRes = rec_ts1+2;
//            sinfo->haveHighResData = true;
            if (sess->first() == 0)
                qWarning() << "first = 0 - 1442";


            //////////////////////////////////////////////////////////////////
            // Show Flow Limitation Events as a graph
            //////////////////////////////////////////////////////////////////
            qint16 severity = (R->flags1[0] >> 4) & 0x03;
            FLG->AddWaveform(ti, &severity, 1, 1000);
            severity = (R->flags2[0] >> 4) & 0x03;
            FLG->AddWaveform(ti+1000, &severity, 1, 1000);

            //////////////////////////////////////////////////////////////////
            // Show Snore Events as a graph
            //////////////////////////////////////////////////////////////////
            severity = R->flags1[0] & 0x03;
            snore->AddWaveform(ti, &severity, 1, 1000);
            severity = R->flags2[0] & 0x03;
            snore->AddWaveform(ti+1000, &severity, 1, 1000);

            /****
                // Fields data[107] && data[108] are bitfields default is 0x90, occasionally 0x98

                d[0] = data[107];
                d[1] = data[108];

                //leak->AddWaveform(ti+40000, d, 2, 2000);

                // Needs to track state to pull events out cleanly..

                //////////////////////////////////////////////////////////////////
                // High Leak
                //////////////////////////////////////////////////////////////////

                if (data[110] & 3) {  // LL state 1st second
                    if (!inLL) {
                        LLstart = ti;
                        inLL = true;
                    }
                    LLend = ti+1000L;
                } else {
                    if (inLL) {
                        inLL = false;
                        LL->AddEvent(LLstart,(LLend-LLstart) / 1000L);
                        LLstart = 0;
                    }
                }
                if (data[114] & 3) {
                    if (!inLL) {
                        LLstart = ti+1000L;
                        inLL = true;
                    }
                    LLend = ti+2000L;

                } else {
                    if (inLL) {
                        inLL = false;
                        LL->AddEvent(LLstart,(LLend-LLstart) / 1000L);
                        LLstart = 0;
                    }
                }


                //////////////////////////////////////////////////////////////////
                // Obstructive Apnea
                //////////////////////////////////////////////////////////////////

                if (data[110] & 12) {  // OA state 1st second
                    if (!inOA) {
                        OAstart = ti;
                        inOA = true;
                    }
                    OAend = ti+1000L;
                } else {
                    if (inOA) {
                        inOA = false;
                        OA->AddEvent(OAstart,(OAend-OAstart) / 1000L);
                        OAstart = 0;
                    }
                }
                if (data[114] & 12) {
                    if (!inOA) {
                        OAstart = ti+1000L;
                        inOA = true;
                    }
                    OAend = ti+2000L;

                } else {
                    if (inOA) {
                        inOA = false;
                        OA->AddEvent(OAstart,(OAend-OAstart) / 1000L);
                        OAstart = 0;
                    }
                }


                //////////////////////////////////////////////////////////////////
                // Hypopnea
                //////////////////////////////////////////////////////////////////

                if (data[110] & 192) {
                    if (!inH) {
                        Hstart = ti;
                        inH = true;
                    }
                    Hend = ti + 1000L;
                } else {
                    if (inH) {
                        inH = false;
                        HY->AddEvent(Hstart,(Hend-Hstart) / 1000L);
                        Hstart = 0;
                    }
                }

                if (data[114] & 192) {
                    if (!inH) {
                        Hstart = ti+1000L;
                        inH = true;
                    }
                    Hend = ti + 2000L;
                } else {
                    if (inH) {
                        inH = false;
                        HY->AddEvent(Hstart,(Hend-Hstart) / 1000L);
                        Hstart = 0;
                    }
                }

                //////////////////////////////////////////////////////////////////
                // Non Responding Apnea Event (Are these CA's???)
                //////////////////////////////////////////////////////////////////
                if (data[110] & 48) {  // OA state 1st second
                    if (!inCA) {
                        CAstart = ti;
                        inCA = true;
                    }
                    CAend = ti+1000L;
                } else {
                    if (inCA) {
                        inCA = false;
                        NOA->AddEvent(CAstart,(CAend-CAstart) / 1000L);
                        CAstart = 0;
                    }
                }
                if (data[114] & 48) {
                    if (!inCA) {
                        CAstart = ti+1000L;
                        inCA = true;
                    }
                    CAend = ti+2000L;

                } else {
                    if (inCA) {
                        inCA = false;
                        NOA->AddEvent(CAstart,(CAend-CAstart) / 1000L);
                        CAstart = 0;
                    }
                }

                //////////////////////////////////////////////////////////////////
                // VSnore Event
                //////////////////////////////////////////////////////////////////
                if (data[109] & 3) {  // OA state 1st second
                    if (!inVS) {
                        VSstart = ti;
                        inVS = true;
                    }
                    VSend = ti+1000L;
                } else {
                    if (inVS) {
                        inVS = false;
                        VS->AddEvent(VSstart,(VSend-VSstart) / 1000L);
                        VSstart = 0;
                    }
                }
                if (data[113] & 3) {
                    if (!inVS) {
                        VSstart = ti+1000L;
                        inVS = true;
                    }
                    VSend = ti+2000L;

                } else {
                    if (inVS) {
                        inVS = false;
                        VS->AddEvent(VSstart,(VSend-VSstart) / 1000L);
                        VSstart = 0;
                    }
                }

                //////////////////////////////////////////////////////////////////
                // Expiratory puff Event
                //////////////////////////////////////////////////////////////////
                if (data[109] & 12) {  // OA state 1st second
                    if (!inExP) {
                        ExPstart = ti;
                        inExP = true;
                    }
                    ExPend = ti+1000L;
                } else {
                    if (inExP) {
                        inExP = false;
                        EXP->AddEvent(ExPstart,(ExPend-ExPstart) / 1000L);
                        ExPstart = 0;
                    }
                }
                if (data[113] & 12) {
                    if (!inExP) {
                        ExPstart = ti+1000L;
                        inExP = true;
                    }
                    ExPend = ti+2000L;

                } else {
                    if (inExP) {
                        inExP = false;
                        EXP->AddEvent(ExPstart,(ExPend-ExPstart) / 1000L);
                        ExPstart = 0;
                    }
                }

                //////////////////////////////////////////////////////////////////
                // Flow Limitation Event
                //////////////////////////////////////////////////////////////////
                if (data[109] & 48) {  // OA state 1st second
                    if (!inFL) {
                        FLstart = ti;
                        inFL = true;
                    }
                    FLend = ti+1000L;
                } else {
                    if (inFL) {
                        inFL = false;
                        FL->AddEvent(FLstart,(FLend-FLstart) / 1000L);
                        FLstart = 0;
                    }
                }
                if (data[113] & 48) {
                    if (!inFL) {
                        FLstart = ti+1000L;
                        inFL = true;
                    }
                    FLend = ti+2000L;

                } else {
                    if (inFL) {
                        inFL = false;
                        FL->AddEvent(FLstart,(FLend-FLstart) / 1000L);
                        FLstart = 0;
                    }
                }

                //////////////////////////////////////////////////////////////////
                // Periodic Breathing Event
                //////////////////////////////////////////////////////////////////
                if (data[109] & 192) {  // OA state 1st second
                    if (!inPB) {
                        PBstart = ti;
                        inPB = true;
                    }
                    PBend = ti+1000L;
                } else {
                    if (inPB) {
                        inPB = false;
                        PB->AddEvent(PBstart,(PBend-PBstart) / 1000L);
                        PBstart = 0;
                    }
                }
                if (data[113] & 192) {
                    if (!inPB) {
                        PBstart = ti+1000L;
                        inPB = true;
                    }
                    PBend = ti+2000L;

                } else {
                    if (inPB) {
                        inPB = false;
                        PB->AddEvent(PBstart,(PBend-PBstart) / 1000L);
                        PBstart = 0;
                    }
                }

                //////////////////////////////////////////////////////////////////
                // Respiratory Effort Related Arousal Event
                //////////////////////////////////////////////////////////////////
                if (data[111] & 48) {  // OA state 1st second
                    if (!inRE) {
                        REstart = ti;
                        inRE = true;
                    }
                    REend = ti+1000L;
                } else {
                    if (inRE) {
                        inRE = false;
                        RE->AddEvent(REstart,(REend-REstart) / 1000L);
                        REstart = 0;
                    }
                }
                if (data[115] & 48) {
                    if (!inRE) {
                        REstart = ti+1000L;
                        inRE = true;
                    }
                    REend = ti+2000L;

                } else {
                    if (inRE) {
                        inRE = false;
                        RE->AddEvent(REstart,(REend-REstart) / 1000L);
                        REstart = 0;
                    }
                }
***/
        }

    } while (true);

    if (inSession && sess) {
        /***
            // Close event states if they are still open, and write event.
            if (inH) HY->AddEvent(Hstart,(Hend-Hstart) / 1000L);
            if (inOA) OA->AddEvent(OAstart,(OAend-OAstart) / 1000L);
            if (inCA) NOA->AddEvent(CAstart,(CAend-CAstart) / 1000L);
            if (inLL) LL->AddEvent(LLstart,(LLend-LLstart) / 1000L);
            if (inVS) HY->AddEvent(VSstart,(VSend-VSstart) / 1000L);
            if (inExP) EXP->AddEvent(ExPstart,(ExPend-ExPstart) / 1000L);
            if (inFL) FL->AddEvent(FLstart,(FLend-FLstart) / 1000L);
            if (inPB) PB->AddEvent(PBstart,(PBend-PBstart) / 1000L);
            if (inPB) RE->AddEvent(REstart,(REend-REstart) / 1000L);
***/
        // update min and max
        // then add to machine
        if (sess->first() == 0)
            qWarning() << "R.BIN first = 0 - 1665";
        EventDataType min = flow->Min();
        EventDataType max = flow->Max();
        sess->setMin(CPAP_FlowRate, min);
        sess->setMax(CPAP_FlowRate, max);

        sess->setPhysMax(CPAP_FlowRate, min); // TODO: not sure :/
        sess->setPhysMin(CPAP_FlowRate, max);
        sess->really_set_last(flow->last());

        sess = nullptr;
        inSession = false;
    }

    rf.close();
    qDebug() << "DV6 R.BIN processed" << rf.numread() << "records";
    return true;
}

////////////////////////////////////////////////////////////////////////////////////////
// Parse L.BIN for per minute data
////////////////////////////////////////////////////////////////////////////////////////

bool load6PerMinute () {

    RollingFile rf;
    Session *sess = nullptr;
    unsigned int rec_ts1, previousRecBegin = 0;
    bool inSession = false; // true if we are adding data to this session

    if (!rf.open("L.BIN")) {
        qWarning() << "DV6 Unable to open L.BIN";
        return false;
    }

    qDebug() << "L.BIN Minute Data starting at record" << rf.recnum();

    EventList * leak = NULL;
    EventList * maxleak = NULL;
    EventList * RR  = NULL;
    EventList * Pressure  = NULL;
    EventList * TV  = NULL;
    EventList * MV = NULL;

    QMap<SessionID, DV6_SessionInfo>::iterator sinfo;
    sinfo = SessionData.begin();

    // Walk through all the records
    do {
        DV6_L_REC * rec = (DV6_L_REC *) rf.get();
        if (rec == nullptr)
            break;

        sess = sinfo->sess;

        // Get the timestamp from the record
        rec_ts1 = convertTime(rec->timestamp);

        if (rec_ts1 < previousRecBegin) {
#ifdef DEBUG6
            qWarning() << "L.BIN - Corruption/Out of sequence data found, skipping record" << rf.recnum() << ", prev"
                       << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin
                       << "this"
                       << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1;
#endif
            continue;
        }
/****
        // Look for a gap in DV6_L records.  They should be at one minute intervals.
        // If there is a gap, we are probably in a new session
        if (inSession && ((rec_ts1 - previousRecBegin) > 60)) {
            qDebug() << "L.BIN record gap, current" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
                     << "previous" << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss");
            sess->set_last(maxleak->last());
            sess = nullptr;
            leak = maxleak = MV = TV = RR = Pressure = nullptr;
            inSession = false;
        }
****/
        // Skip over sessions until we find one that this record is in
        while (rec_ts1 > sinfo->end) {
#ifdef DEBUG6
            qDebug() << "L.BIN - skipping session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
#endif
            if (inSession && sess) {
                // Close the open session and update the min and max
                sess->set_last(maxleak->last());
                sess = nullptr;
                leak = maxleak = MV = TV = RR = Pressure = nullptr;
                inSession = false;
            }
            sinfo++;
            if (sinfo == SessionData.end())
                break;
        }

        previousRecBegin = rec_ts1;

        // If we have data beyond last session, we are in trouble (for unknown reasons)
        if (sinfo == SessionData.end()) {
            qWarning() << "DV6 L.BIN import ran out of sessions to match flow data";
            break;
        }

        if (rec_ts1 < previousRecBegin) {
            qWarning() << "L.BIN - Corruption/Out of sequence data found, stopping import, prev"
                       << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss")
                       << "this"
                       << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
            break;
        }

        // Check if record belongs in this session or a future session
        if (!inSession && rec_ts1 <= sinfo->end) {
            sess = sinfo->sess;         // this is the Session we want
            if (!inSession && sess) {
                leak = sess->AddEventList(CPAP_Leak, EVL_Event); // , 1.0, 0.0, 0.0, 0.0, double(60000) / double(1));
                maxleak = sess->AddEventList(CPAP_LeakTotal, EVL_Event);// , 1.0, 0.0, 0.0, 0.0, double(60000) / double(1));
                RR = sess->AddEventList(CPAP_RespRate, EVL_Event);
                MV = sess->AddEventList(CPAP_MinuteVent, EVL_Event);
                TV = sess->AddEventList(CPAP_TidalVolume, EVL_Event);

                if (sess->last()/1000 > sinfo->end)
                    sinfo->end = sess->last()/1000;

//                if (!sinfo->haveHighResData) {
                    // Don't use this pressure if we already have higher resolution data

                if (sinfo->mode == MODE_UNKNOWN) {
                    if (rec->pressureLimitLow != rec->pressureLimitHigh) {
                        sess->settings[CPAP_PressureMin] = rec->pressureLimitLow / 10.0f;
                        sess->settings[CPAP_PressureMax] = rec->pressureLimitHigh / 10.0f;
// if available         sess->settings[CPAP_PS) = ....
                        sess->settings[CPAP_Mode] = MODE_APAP;
                        sinfo->mode = MODE_APAP;
                    } else {
                        sess->settings[CPAP_Mode] = MODE_CPAP;
                        sess->settings[CPAP_Pressure] = rec->pressureLimitHigh / 10.0f;
                        sinfo->mode = MODE_CPAP;
                    }
                    inSession = true;
                }
            }
        }
        if (inSession) {
            // Record breath and pressure waveforms
            qint64 ti = qint64(rec_ts1) * 1000;
            maxleak->AddEvent(ti, rec->maxLeak);  //???
            leak->AddEvent(ti, rec->avgLeak);     //???
            RR->AddEvent(ti, rec->breathRate);

            if (   sinfo->firstHighRes ==  0        // No high res data
                || rec_ts1 < sinfo->firstHighRes    // Before high res data begins
                || ((rec_ts1 > (sinfo->lastHighRes+2)) && (sinfo->lastHighRes > 0))) // or after high res data ends
            {
                if (!Pressure)
                    Pressure = sess->AddEventList(CPAP_Pressure, EVL_Event, 0.1f);

//                if (sinfo->firstHighRes ==  0) {
                Pressure->AddEvent(ti, rec->avgPressure);  // average pressure for next minute
                Pressure->AddEvent(ti + 59998, rec->avgPressure);  // end of pressure block
//                } else {
//                    for (int i = 0; i < 60; i++) {
//                        Pressure->AddEvent(ti+i, rec->avgPressure);  // average pressure for next minute
//                    }
//                }
            }

/***
            if (Pressure)
                qDebug() << "Lowres pressure" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
                         << "rec_ts1" << rec_ts1 << "firstHighRes" << sinfo->firstHighRes << "last" << sinfo->lastHighRes
                         << "Pressure" << rec->avgPressure / 10.0f;
***/

            unsigned tv = rec->tidalVolume6 + (rec->tidalVolume7 << 8);
            MV->AddEvent(ti, rec->breathRate * tv / 1000.0 );
            TV->AddEvent(ti, tv);

            if (!sess->channelExists(CPAP_FlowRate)) {
                // No flow rate, so lets grab this data...
            }
        }

    } while (true);

    if (sess && inSession) {
        sess->set_last(maxleak->last());
    }

    rf.close();
    qDebug() << "DV6 L.BIN processed" << rf.numread() << "records";

    return true;
}

////////////////////////////////////////////////////////////////////////////////////////
// Parse E.BIN for event data
////////////////////////////////////////////////////////////////////////////////////////

bool load6EventData () {
    RollingFile rf;

    Session *sess = nullptr;
    unsigned int rec_ts1, rec_ts2, previousRecBegin;
    bool inSession = false; // true if we are adding data to this session

    EventList * OA = nullptr;
    EventList * CA = nullptr;
    EventList * H  = nullptr;
    EventList * RE = nullptr;
    EventList * PB = nullptr;
    EventList * LL = nullptr;
    EventList * EP = nullptr;
    EventList * SN = nullptr;
    EventList * FL = nullptr;
//    EventList * FLG = nullptr;

    if (!rf.open("E.BIN")) {
        qWarning() << "DV6 Unable to open E.BIN";
        return false;
    }

    qDebug() << "Processing E.BIN starting at record" << rf.recnum();

    QMap<SessionID, DV6_SessionInfo>::iterator sinfo;
    sinfo = SessionData.begin();

    // Walk through all the records
    do {
        DV6_E_REC * rec = (DV6_E_REC *) rf.get();
        if (rec == nullptr)
            break;
        sess = sinfo->sess;

        // Get the timestamp from the record
        rec_ts1 = convertTime(rec->begin);
        rec_ts2 = convertTime(rec->end);

        // Skip over sessions until we find one that this record is in
        while (rec_ts1 > sinfo->end) {
#ifdef DEBUG6
            qDebug() << "E.BIN - skipping session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
#endif
            if (inSession) {
                // Close the open session and update the min and max
                if (OA->last() > 0)
                    sess->set_last(OA->last());
                if (CA->last() > 0)
                    sess->set_last(CA->last());
                if (H->last() > 0)
                    sess->set_last(H->last());
                if (RE->last() > 0)
                    sess->set_last(RE->last());
                if (PB->last() > 0)
                    sess->set_last(PB->last());
                if (LL->last() > 0)
                    sess->set_last(LL->last());
                if (EP->last() > 0)
                    sess->set_last(EP->last());
                if (FL->last() > 0)
                    sess->set_last(FL->last());
                if (SN->last() > 0)
                    sess->set_last(SN->last());
/***
                if (FLG->last() > 0)
                    sess->set_last(FLG->last());
***/
                sess = nullptr;
                H = CA = RE = OA = PB = LL = EP = SN = FL = nullptr;
                inSession = false;
            }
            sinfo++;
            if (sinfo == SessionData.end())
                break;
        }

        previousRecBegin = rec_ts1;

        // If we have data beyond last session, we are in trouble (for unknown reasons)
        if (sinfo == SessionData.end()) {
            qWarning() << "DV6 E.BIN import ran out of sessions,"
                       << "event data begins"
                       << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
            break;
        }

        if (rec_ts1 < previousRecBegin) {
            qWarning() << "DV6 E.BIN - Out of sequence data found, skipping, prev"
                       << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss")
                       << "this event"
                       << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
            continue; // break;
        }

        // Check if record belongs in this session or a future session
        if (!inSession && rec_ts1 <= sinfo->end) {
            sess = sinfo->sess;         // this is the Session we want
            if (!inSession && sess) {
                OA = sess->AddEventList(CPAP_Obstructive, EVL_Event);
                H  = sess->AddEventList(CPAP_Hypopnea, EVL_Event);
                RE = sess->AddEventList(CPAP_RERA, EVL_Event);
                CA = sess->AddEventList(CPAP_ClearAirway, EVL_Event);
                PB = sess->AddEventList(CPAP_PB, EVL_Event);
                LL = sess->AddEventList(CPAP_LargeLeak, EVL_Event);
                EP = sess->AddEventList(CPAP_ExP, EVL_Event);
                SN = sess->AddEventList(INTP_SnoreFlag, EVL_Event);
                FL = sess->AddEventList(CPAP_FlowLimit, EVL_Event);

//                FLG = sess->AddEventList(CPAP_FLG, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2));
//                SN = sess->AddEventList(CPAP_Snore, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2));

                inSession = true;
            }
        }
        if (inSession) {
            qint64 duration = rec_ts2 - rec_ts1;
            // We make an ad hoc adjustment to the start time so that the event lines up better with the flow graph
            // TODO: We don't know what is really going on here. Is it sloppiness on the part of the DV6 in recording time stamps?
            qint64 ti = qint64(rec_ts1 - (duration/2)) * 1000L;
            if (duration < 0) {
                qDebug() << "E.BIN at" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
                         << "reports duration of" << duration
                         << "ending" <<  QDateTime::fromSecsSinceEpoch(rec_ts2).toString("MM/dd/yyyy hh:mm:ss");
            }
            int code = rec->event_type;
/***
            //////////////////////////////////////////////////////////////////
            // Show Snore Events as a graph
            //////////////////////////////////////////////////////////////////
            if (code == 9) {
                qint16 severity = rec->event_severity;
                SN->AddWaveform(ti, &severity, 1, duration*1000);
            }

            //////////////////////////////////////////////////////////////////
            // Show Flow Limit Events as a graph
            //////////////////////////////////////////////////////////////////
            if (code == 10) {
                qint16 severity = rec->event_severity;
                FLG->AddWaveform(ti, &severity, 1, duration*1000);
            }
***/
            if (rec->event_severity >= 3)
                switch (code) {
                case 1:
                    CA->AddEvent(ti, duration);
                    break;
                case 2:
                    OA->AddEvent(ti, duration);
//                    qDebug() << "E.BIN - OA" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
                    break;
                case 4:
                    H->AddEvent(ti, duration);
                    break;
                case 5:
                    RE->AddEvent(ti, duration);
//                    qDebug() << "E.BIN - RERA" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
                    break;
                case 8:     // snore
                    SN->AddEvent(ti, duration);
//                    qDebug() << "E.BIN - Snore" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
                    break;
                case 9:     // expiratory puff
                    EP->AddEvent(ti, duration);
//                    qDebug() << "E.BIN - exhale puff" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
                    break;
                case 10:    // flow limitation
                    FL->AddEvent(ti, duration);
//                    qDebug() << "E.BIN - flow limit" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
                    break;
                case 11:    // periodic breathing
                    PB->AddEvent(ti, duration);
                    break;
                case 12:    // large leaks
                    LL->AddEvent(ti, duration);
//                    qDebug() << "E.BIN - large leak" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
                    break;
                case 13:    // pressure change
                    break;
                case 14:    // start of session
                    break;
                default:
                    break;
                }
        }
    } while (true);

    rf.close();
    qDebug() << "DV6 E.BIN processed" << rf.numread() << "records";

    return true;
}

////////////////////////////////////////////////////////////////////////////////////////
// Finalize data and add to database
////////////////////////////////////////////////////////////////////////////////////////

int addSessions() {

    for (auto si=SessionData.begin(), end=SessionData.end(); si != end; ++si) {
        Session * sess = si.value().sess;

        if (sess) {
            if ( ! mach->AddSession(sess) ) {
                qWarning() << "Session" << sess->session() << "was not addded";
            }
#ifdef DEBUG6
            else
                qDebug() << "Added session" << sess->session()  << QDateTime::fromSecsSinceEpoch(sess->session()).toString("MM/dd/yyyy hh:mm:ss");;
#endif

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

            // Save is not threadsafe
            sess->Store(mach->getDataPath());

            // Unload them from memory
            sess->TrashEvents();
        } // else
          //  qWarning() << "addSessions: session pointer is null";
    }

    return SessionData.size();

}

////////////////////////////////////////////////////////////////////////////////////////
// Create backup of input files
// Create dated backup of settings file if changed
////////////////////////////////////////////////////////////////////////////////////////

bool backup6 (const QString & path)  {

    if (rebuild_from_backups || !create_backups)
        return true;

    QDir ipath(path);
    QDir cpath(card_path);
    QDir bpath(backup_path);
    QDir hpath(history_path);

    // Copy input data to backup location
    copyPath(ipath.absolutePath(), bpath.absolutePath(), true);

    // Create archive of settings file if needed (SET.BIN)
    bool backup_settings = true;

    QStringList filters;

    QFile settingsFile;
    QString inputFile = cpath.absolutePath() + "/SET.BIN";
    settingsFile.setFileName(inputFile);

    filters << "SET_*.BIN";
    hpath.setNameFilters(filters);
    hpath.setFilter(QDir::Files);
    hpath.setSorting(QDir::Name | QDir::Reversed);
    QStringList fileNames = hpath.entryList(); // Get list of files
    if (! fileNames.isEmpty()) {
        QString lastFile = fileNames.first();
        qDebug() << "last settings file is" << lastFile << "new file is" << settingsFile.fileName();
        QByteArray newMD5 = fileChecksum(settingsFile.fileName(), QCryptographicHash::Md5);
        QByteArray oldMD5 = fileChecksum(hpath.absolutePath()+"/"+lastFile, QCryptographicHash::Md5);
        if (newMD5 == oldMD5)
            backup_settings = false;
    }

    if (backup_settings && !DailySummaries.isEmpty()) {
        DV6_S_Data ds = DailySummaries.last();
        QString newFile = hpath.absolutePath() + "/SET_" + getNominalDate(ds.start_time).toString("yyyyMMdd") + ".BIN";
        if (!settingsFile.copy(inputFile, newFile)) {
            qWarning() << "DV6 backup could not copy" << inputFile << "to" << newFile << ", error code" << settingsFile.error() << settingsFile.errorString();
        }
    }

    // We're done!
    return true;
}

////////////////////////////////////////////////////////////////////////////////////////
// Initialize DV6 environment
////////////////////////////////////////////////////////////////////////////////////////

bool init6Environment (const QString & path) {

    // Create Machine database record if it doesn't exist already
    mach = p_profile->CreateMachine(info);
    if (mach == nullptr) {
        qWarning() << "Could not create DV6 Machine data structure";
        return false;
    }

    backup_path = mach->getBackupPath();
    history_path = backup_path + "/HISTORY";
    rebuild_path = backup_path + "/DV6";

    // Compare QDirs rather than QStrings because separators may be different, especially on Windows.
    QDir ipath(path);
    QDir bpath(backup_path);
    QDir hpath(history_path);

    if (ipath == bpath) {
        // Don't create backups if importing from backup folder
        rebuild_from_backups = true;
        create_backups = false;
    } else {
        rebuild_from_backups = false;
        create_backups = p_profile->session->backupCardData();

        if ( ! bpath.exists()) {
            if ( ! bpath.mkpath(backup_path) ) {
                qWarning() << "Could not create DV6 backup directory" << backup_path;
                return false;
            }
        }
        if ( ! hpath.exists()) {
            if ( ! hpath.mkpath(history_path) ) {
                qWarning() << "Could not create DV6 backup HISTORY directory" << history_path;
                return false;
            }
        }

    }

    return true;
}

////////////////////////////////////////////////////////////////////////////////////////
// Open a DV6 SD card, parse everything, add to OSCAR database
////////////////////////////////////////////////////////////////////////////////////////

int IntellipapLoader::OpenDV6(const QString & path)
{
    qDebug() << "DV6 loader started";
    card_path = path + DV6_DIR;

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

    // 1. Prime the machine database's info field with this machine
    info = newInfo();

    // 2. VER.BIN - Parse model number, serial, etc. into info structure
    if (!load6VersionInfo(card_path))
        return -1;

    // 3. Initialize rest of the DV6 loader environment
    if (!init6Environment (path))
        return -1;

    // 4. SET.BIN - Parse settings file (which is only the latest settings)
    if (!load6Settings(card_path))
        return -1;

    // 5. S.BIN - Open and parse day summary list and create a list of days
    if (!load6DailySummaries())
        return -1;

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

    // 6. Back up data files (must do after parsing VER.BIN, S.BIN, and creating Machine)
    if (!backup6(path))
        return -1;

    emit updateMessage(QObject::tr("Reading data files..."));
    QCoreApplication::processEvents();

    // 7. U.BIN - Open and parse session list and create a list of session times
    // (S.BIN must already be loaded)
    if (!load6Sessions())
        return -1;

    // Create OSCAR session list from session times and summary data
    if (create6Sessions() <= 0)
        return -1;

    // R.BIN - Open and parse flow data
    if (!load6HighResData())
        return -1;

    // L.BIN - Open and parse per minute data
    if (!load6PerMinute())
        return -1;

    // E.BIN - Open and parse event data
    if (!load6EventData())
        return -1;

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

    // Finalize input
    return addSessions();
}

int IntellipapLoader::Open(const QString & dirpath)
{
    // Check for SL directory
    // Check for DV5MFirm.bin?
    QString path(dirpath);
    path = path.replace("\\", "/");

    if (path.endsWith(SL_DIR)) {
        path.chop(3);
    } else if (path.endsWith(DV6_DIR)) {
        path.chop(4);
    }

    QDir dir;

    int r = -1;
    // Sometimes there can be an SL folder because SmartLink dumps an old DV5 firmware in it, so check it first
    if (dir.exists(path + SL_DIR))
        r = OpenDV5(path);

    if ((r<0) && dir.exists(path + DV6_DIR))
        r = OpenDV6(path);

    return r;
}

void IntellipapLoader::initChannels()
{
    using namespace schema;
    Channel * chan = nullptr;
    channel.add(GRP_CPAP, chan = new Channel(INTP_SmartFlexMode = 0x1165, SETTING,  MT_CPAP,  SESSION,
        "INTPSmartFlexMode", QObject::tr("SmartFlex Mode"),
        QObject::tr("Intellipap pressure relief mode."),
        QObject::tr("SmartFlex Mode"),
        "", DEFAULT, Qt::green));


    chan->addOption(0, STR_TR_Off);
    chan->addOption(1, QObject::tr("Ramp Only"));
    chan->addOption(2, QObject::tr("Full Time"));

    channel.add(GRP_CPAP, chan = new Channel(INTP_SmartFlexLevel = 0x1169, SETTING,  MT_CPAP,  SESSION,
        "INTPSmartFlexLevel", QObject::tr("SmartFlex Level"),
        QObject::tr("Intellipap pressure relief level."),
        QObject::tr("SmartFlex Level"),
        "", DEFAULT, Qt::green));

    channel.add(GRP_CPAP, new Channel(INTP_SnoreFlag = 0xe301, FLAG,  MT_CPAP,   SESSION,
        "INTP_SnoreFlag",
        QObject::tr("Snore"),
        QObject::tr("Snoring event."),
        QObject::tr("SN"),
        STR_UNIT_EventsPerHour,    DEFAULT,    QColor("#e20004")));

}

bool intellipap_initialized = false;
void IntellipapLoader::Register()
{
    if (!intellipap_initialized) {
        qDebug() << "Registering IntellipapLoader";
        RegisterLoader(new IntellipapLoader());
        //InitModelMap();
        intellipap_initialized = true;
    }
    return;
}
