/* SleepLib (DeVilbiss) Intellipap Loader Implementation * * Notes: Intellipap DV54 requires the SmartLink attachment to access this data. * * Copyright (c) 2011-2018 Mark Watkins * Copyright (c) 2019-2022 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 #include #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 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 set1; QHash::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::iterator i = set1.begin(); i != set1.end(); i++) { mach->info.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 SessionStart; QVector SessionEnd; QHash 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 rampstart; QHash 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::iterator rit; QHash::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 DailySummaries; QMap 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::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::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::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; }