diff --git a/Building/Icons/OSCAR.icns b/Building/Icons/OSCAR.icns index ae6ad410..551d01cc 100644 Binary files a/Building/Icons/OSCAR.icns and b/Building/Icons/OSCAR.icns differ diff --git a/Building/Icons/Wave-32.png b/Building/Icons/Wave-32.png index b69b843e..3a8a6950 100644 Binary files a/Building/Icons/Wave-32.png and b/Building/Icons/Wave-32.png differ diff --git a/Building/Icons/logo.ico b/Building/Icons/logo.ico index 47e20f78..583fa8ef 100644 Binary files a/Building/Icons/logo.ico and b/Building/Icons/logo.ico differ diff --git a/oscar/SleepLib/common.cpp b/oscar/SleepLib/common.cpp index 0f287401..be4e249f 100644 --- a/oscar/SleepLib/common.cpp +++ b/oscar/SleepLib/common.cpp @@ -181,8 +181,12 @@ QString getBranchVersion() if (GIT_BRANCH != "master") { version += GIT_BRANCH+"-"; } - version += GIT_REVISION +" "; - version += getGraphicsEngine()+"]"; + version += GIT_REVISION; +#ifndef UNITTEST_MODE + // There is no graphics engine on the console. + version += QString(" ") + getGraphicsEngine(); +#endif + version += "]"; return version; } diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 5065bcff..7db293db 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -33,6 +33,9 @@ //const int PRS1_EVENT_FILE=2; //const int PRS1_WAVEFORM_FILE=5; +const int PRS1_HTYPE_NORMAL=0; +const int PRS1_HTYPE_INTERVAL=1; + //******************************************************************************************** /// IMPORTANT!!! @@ -43,61 +46,151 @@ QHash ModelMap; -#define PRS1_CRC_CHECK -#ifdef PRS1_CRC_CHECK -typedef quint16 crc_t; +// CRC-16/KERMIT, polynomial: 0x11021, bit reverse algorithm +// Table generated by crcmod (crc-kermit) -static const crc_t crc_table[256] = { - 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, - 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, - 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, - 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, - 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, - 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, - 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, - 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, - 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, - 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, - 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, - 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, - 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, - 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, - 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, - 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, - 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, - 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, - 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, - 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, - 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, - 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, - 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, - 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, - 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, - 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, - 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, - 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, - 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, - 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, - 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, - 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 -}; - -crc_t CRC16(const unsigned char *data, size_t data_len) +typedef quint16 crc16_t; +static crc16_t CRC16(unsigned char * data, size_t data_len, crc16_t crc=0) { - crc_t crc = 0; - unsigned int tbl_idx; - - while (data_len--) { - tbl_idx = (crc ^ *data) & 0xff; - crc = (crc_table[tbl_idx] ^ (crc >> 8)) & 0xffff; + static const crc16_t table[256] = { + 0x0000U, 0x1189U, 0x2312U, 0x329bU, 0x4624U, 0x57adU, 0x6536U, 0x74bfU, + 0x8c48U, 0x9dc1U, 0xaf5aU, 0xbed3U, 0xca6cU, 0xdbe5U, 0xe97eU, 0xf8f7U, + 0x1081U, 0x0108U, 0x3393U, 0x221aU, 0x56a5U, 0x472cU, 0x75b7U, 0x643eU, + 0x9cc9U, 0x8d40U, 0xbfdbU, 0xae52U, 0xdaedU, 0xcb64U, 0xf9ffU, 0xe876U, + 0x2102U, 0x308bU, 0x0210U, 0x1399U, 0x6726U, 0x76afU, 0x4434U, 0x55bdU, + 0xad4aU, 0xbcc3U, 0x8e58U, 0x9fd1U, 0xeb6eU, 0xfae7U, 0xc87cU, 0xd9f5U, + 0x3183U, 0x200aU, 0x1291U, 0x0318U, 0x77a7U, 0x662eU, 0x54b5U, 0x453cU, + 0xbdcbU, 0xac42U, 0x9ed9U, 0x8f50U, 0xfbefU, 0xea66U, 0xd8fdU, 0xc974U, + 0x4204U, 0x538dU, 0x6116U, 0x709fU, 0x0420U, 0x15a9U, 0x2732U, 0x36bbU, + 0xce4cU, 0xdfc5U, 0xed5eU, 0xfcd7U, 0x8868U, 0x99e1U, 0xab7aU, 0xbaf3U, + 0x5285U, 0x430cU, 0x7197U, 0x601eU, 0x14a1U, 0x0528U, 0x37b3U, 0x263aU, + 0xdecdU, 0xcf44U, 0xfddfU, 0xec56U, 0x98e9U, 0x8960U, 0xbbfbU, 0xaa72U, + 0x6306U, 0x728fU, 0x4014U, 0x519dU, 0x2522U, 0x34abU, 0x0630U, 0x17b9U, + 0xef4eU, 0xfec7U, 0xcc5cU, 0xddd5U, 0xa96aU, 0xb8e3U, 0x8a78U, 0x9bf1U, + 0x7387U, 0x620eU, 0x5095U, 0x411cU, 0x35a3U, 0x242aU, 0x16b1U, 0x0738U, + 0xffcfU, 0xee46U, 0xdcddU, 0xcd54U, 0xb9ebU, 0xa862U, 0x9af9U, 0x8b70U, + 0x8408U, 0x9581U, 0xa71aU, 0xb693U, 0xc22cU, 0xd3a5U, 0xe13eU, 0xf0b7U, + 0x0840U, 0x19c9U, 0x2b52U, 0x3adbU, 0x4e64U, 0x5fedU, 0x6d76U, 0x7cffU, + 0x9489U, 0x8500U, 0xb79bU, 0xa612U, 0xd2adU, 0xc324U, 0xf1bfU, 0xe036U, + 0x18c1U, 0x0948U, 0x3bd3U, 0x2a5aU, 0x5ee5U, 0x4f6cU, 0x7df7U, 0x6c7eU, + 0xa50aU, 0xb483U, 0x8618U, 0x9791U, 0xe32eU, 0xf2a7U, 0xc03cU, 0xd1b5U, + 0x2942U, 0x38cbU, 0x0a50U, 0x1bd9U, 0x6f66U, 0x7eefU, 0x4c74U, 0x5dfdU, + 0xb58bU, 0xa402U, 0x9699U, 0x8710U, 0xf3afU, 0xe226U, 0xd0bdU, 0xc134U, + 0x39c3U, 0x284aU, 0x1ad1U, 0x0b58U, 0x7fe7U, 0x6e6eU, 0x5cf5U, 0x4d7cU, + 0xc60cU, 0xd785U, 0xe51eU, 0xf497U, 0x8028U, 0x91a1U, 0xa33aU, 0xb2b3U, + 0x4a44U, 0x5bcdU, 0x6956U, 0x78dfU, 0x0c60U, 0x1de9U, 0x2f72U, 0x3efbU, + 0xd68dU, 0xc704U, 0xf59fU, 0xe416U, 0x90a9U, 0x8120U, 0xb3bbU, 0xa232U, + 0x5ac5U, 0x4b4cU, 0x79d7U, 0x685eU, 0x1ce1U, 0x0d68U, 0x3ff3U, 0x2e7aU, + 0xe70eU, 0xf687U, 0xc41cU, 0xd595U, 0xa12aU, 0xb0a3U, 0x8238U, 0x93b1U, + 0x6b46U, 0x7acfU, 0x4854U, 0x59ddU, 0x2d62U, 0x3cebU, 0x0e70U, 0x1ff9U, + 0xf78fU, 0xe606U, 0xd49dU, 0xc514U, 0xb1abU, 0xa022U, 0x92b9U, 0x8330U, + 0x7bc7U, 0x6a4eU, 0x58d5U, 0x495cU, 0x3de3U, 0x2c6aU, 0x1ef1U, 0x0f78U, + }; + for (size_t i=0; i < data_len; i++) { + crc = table[(*data ^ (unsigned char)crc) & 0xFF] ^ (crc >> 8); data++; } - - return crc & 0xffff; + return crc; } -#endif + + +// CRC-32/MPEG-2, polynomial: 0x104C11DB7 +// Table generated by crcmod (crc-32-mpeg) + +typedef quint32 crc32_t; +static crc32_t CRC32(const unsigned char *data, size_t data_len, crc32_t crc=0xffffffffU) +{ + static const crc32_t table[256] = { + 0x00000000U, 0x04c11db7U, 0x09823b6eU, 0x0d4326d9U, + 0x130476dcU, 0x17c56b6bU, 0x1a864db2U, 0x1e475005U, + 0x2608edb8U, 0x22c9f00fU, 0x2f8ad6d6U, 0x2b4bcb61U, + 0x350c9b64U, 0x31cd86d3U, 0x3c8ea00aU, 0x384fbdbdU, + 0x4c11db70U, 0x48d0c6c7U, 0x4593e01eU, 0x4152fda9U, + 0x5f15adacU, 0x5bd4b01bU, 0x569796c2U, 0x52568b75U, + 0x6a1936c8U, 0x6ed82b7fU, 0x639b0da6U, 0x675a1011U, + 0x791d4014U, 0x7ddc5da3U, 0x709f7b7aU, 0x745e66cdU, + 0x9823b6e0U, 0x9ce2ab57U, 0x91a18d8eU, 0x95609039U, + 0x8b27c03cU, 0x8fe6dd8bU, 0x82a5fb52U, 0x8664e6e5U, + 0xbe2b5b58U, 0xbaea46efU, 0xb7a96036U, 0xb3687d81U, + 0xad2f2d84U, 0xa9ee3033U, 0xa4ad16eaU, 0xa06c0b5dU, + 0xd4326d90U, 0xd0f37027U, 0xddb056feU, 0xd9714b49U, + 0xc7361b4cU, 0xc3f706fbU, 0xceb42022U, 0xca753d95U, + 0xf23a8028U, 0xf6fb9d9fU, 0xfbb8bb46U, 0xff79a6f1U, + 0xe13ef6f4U, 0xe5ffeb43U, 0xe8bccd9aU, 0xec7dd02dU, + 0x34867077U, 0x30476dc0U, 0x3d044b19U, 0x39c556aeU, + 0x278206abU, 0x23431b1cU, 0x2e003dc5U, 0x2ac12072U, + 0x128e9dcfU, 0x164f8078U, 0x1b0ca6a1U, 0x1fcdbb16U, + 0x018aeb13U, 0x054bf6a4U, 0x0808d07dU, 0x0cc9cdcaU, + 0x7897ab07U, 0x7c56b6b0U, 0x71159069U, 0x75d48ddeU, + 0x6b93dddbU, 0x6f52c06cU, 0x6211e6b5U, 0x66d0fb02U, + 0x5e9f46bfU, 0x5a5e5b08U, 0x571d7dd1U, 0x53dc6066U, + 0x4d9b3063U, 0x495a2dd4U, 0x44190b0dU, 0x40d816baU, + 0xaca5c697U, 0xa864db20U, 0xa527fdf9U, 0xa1e6e04eU, + 0xbfa1b04bU, 0xbb60adfcU, 0xb6238b25U, 0xb2e29692U, + 0x8aad2b2fU, 0x8e6c3698U, 0x832f1041U, 0x87ee0df6U, + 0x99a95df3U, 0x9d684044U, 0x902b669dU, 0x94ea7b2aU, + 0xe0b41de7U, 0xe4750050U, 0xe9362689U, 0xedf73b3eU, + 0xf3b06b3bU, 0xf771768cU, 0xfa325055U, 0xfef34de2U, + 0xc6bcf05fU, 0xc27dede8U, 0xcf3ecb31U, 0xcbffd686U, + 0xd5b88683U, 0xd1799b34U, 0xdc3abdedU, 0xd8fba05aU, + 0x690ce0eeU, 0x6dcdfd59U, 0x608edb80U, 0x644fc637U, + 0x7a089632U, 0x7ec98b85U, 0x738aad5cU, 0x774bb0ebU, + 0x4f040d56U, 0x4bc510e1U, 0x46863638U, 0x42472b8fU, + 0x5c007b8aU, 0x58c1663dU, 0x558240e4U, 0x51435d53U, + 0x251d3b9eU, 0x21dc2629U, 0x2c9f00f0U, 0x285e1d47U, + 0x36194d42U, 0x32d850f5U, 0x3f9b762cU, 0x3b5a6b9bU, + 0x0315d626U, 0x07d4cb91U, 0x0a97ed48U, 0x0e56f0ffU, + 0x1011a0faU, 0x14d0bd4dU, 0x19939b94U, 0x1d528623U, + 0xf12f560eU, 0xf5ee4bb9U, 0xf8ad6d60U, 0xfc6c70d7U, + 0xe22b20d2U, 0xe6ea3d65U, 0xeba91bbcU, 0xef68060bU, + 0xd727bbb6U, 0xd3e6a601U, 0xdea580d8U, 0xda649d6fU, + 0xc423cd6aU, 0xc0e2d0ddU, 0xcda1f604U, 0xc960ebb3U, + 0xbd3e8d7eU, 0xb9ff90c9U, 0xb4bcb610U, 0xb07daba7U, + 0xae3afba2U, 0xaafbe615U, 0xa7b8c0ccU, 0xa379dd7bU, + 0x9b3660c6U, 0x9ff77d71U, 0x92b45ba8U, 0x9675461fU, + 0x8832161aU, 0x8cf30badU, 0x81b02d74U, 0x857130c3U, + 0x5d8a9099U, 0x594b8d2eU, 0x5408abf7U, 0x50c9b640U, + 0x4e8ee645U, 0x4a4ffbf2U, 0x470cdd2bU, 0x43cdc09cU, + 0x7b827d21U, 0x7f436096U, 0x7200464fU, 0x76c15bf8U, + 0x68860bfdU, 0x6c47164aU, 0x61043093U, 0x65c52d24U, + 0x119b4be9U, 0x155a565eU, 0x18197087U, 0x1cd86d30U, + 0x029f3d35U, 0x065e2082U, 0x0b1d065bU, 0x0fdc1becU, + 0x3793a651U, 0x3352bbe6U, 0x3e119d3fU, 0x3ad08088U, + 0x2497d08dU, 0x2056cd3aU, 0x2d15ebe3U, 0x29d4f654U, + 0xc5a92679U, 0xc1683bceU, 0xcc2b1d17U, 0xc8ea00a0U, + 0xd6ad50a5U, 0xd26c4d12U, 0xdf2f6bcbU, 0xdbee767cU, + 0xe3a1cbc1U, 0xe760d676U, 0xea23f0afU, 0xeee2ed18U, + 0xf0a5bd1dU, 0xf464a0aaU, 0xf9278673U, 0xfde69bc4U, + 0x89b8fd09U, 0x8d79e0beU, 0x803ac667U, 0x84fbdbd0U, + 0x9abc8bd5U, 0x9e7d9662U, 0x933eb0bbU, 0x97ffad0cU, + 0xafb010b1U, 0xab710d06U, 0xa6322bdfU, 0xa2f33668U, + 0xbcb4666dU, 0xb8757bdaU, 0xb5365d03U, 0xb1f740b4U, + }; + + for (size_t i=0; i < data_len; i++) { + crc = table[(*data ^ (unsigned char)(crc >> 24)) & 0xFF] ^ (crc << 8); + data++; + } + return crc; +} + + +// Strangely, the PRS1 CRC32 appears to consider every byte a 32-bit wchar_t. +// Nothing like trying a bunch of encodings and CRC32 variants on PROP.TXT files +// until you find a winner. + +static crc32_t CRC32wchar(const unsigned char *data, size_t data_len, crc32_t crc=0xffffffffU) +{ + for (size_t i=0; i < data_len; i++) { + static unsigned char wch[4] = { 0, 0, 0, 0 }; + wch[3] = *data++; + crc = CRC32(wch, 4, crc); + } + return crc; +} + enum FlexMode { FLEX_None, FLEX_CFlex, FLEX_CFlexPlus, FLEX_AFlex, FLEX_RiseTime, FLEX_BiFlex, FLEX_Unknown }; @@ -112,6 +205,7 @@ PRS1::~PRS1() } +#if 0 // TODO: Remove: unused, superseded by PRS1Waveform /*! \struct WaveHeaderList \brief Used in PRS1 Waveform Parsing */ struct WaveHeaderList { @@ -119,6 +213,7 @@ struct WaveHeaderList { quint8 sample_format; WaveHeaderList(quint16 i, quint8 f) { interleave = i; sample_format = f; } }; +#endif PRS1Loader::PRS1Loader() @@ -701,20 +796,28 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin for (int p=0; p < size; ++p) { dir.setPath(paths.at(p)); - if (!dir.exists() || !dir.isReadable()) { continue; } + if (!dir.exists() || !dir.isReadable()) { + qWarning() << dir.canonicalPath() << "can't read directory"; + continue; + } QFileInfoList flist = dir.entryInfoList(); // Scan for individual session files for (int i = 0; i < flist.size(); i++) { - if (isAborted()) break; + if (isAborted()) { + qDebug() << "received abort signal"; + break; + } QFileInfo fi = flist.at(i); + QString path = fi.canonicalFilePath(); bool ok; QString ext_s = fi.fileName().section(".", -1); ext = ext_s.toInt(&ok); if (!ok) { // not a numerical extension + qWarning() << path << "unexpected filename"; continue; } @@ -722,6 +825,7 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin sid = session_s.toInt(&ok, sessionid_base); if (!ok) { // not a numerical session ID + qWarning() << path << "unexpected filename"; continue; } @@ -737,6 +841,7 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin if (m->SessionExists(sid)) { // Skip already imported session + qDebug() << path << "session already exists, skipping" << sid; continue; } @@ -766,10 +871,16 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin } // Parse the data chunks and read the files.. + if (fi.canonicalFilePath().isEmpty()) { + qWarning() << fi; + } QList Chunks = ParseFile(fi.canonicalFilePath()); for (int i=0; i < Chunks.size(); ++i) { - if (isAborted()) break; + if (isAborted()) { + qDebug() << "received abort signal 2"; + break; + } PRS1DataChunk * chunk = Chunks.at(i); @@ -777,16 +888,19 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin const unsigned char * data = (unsigned char *)chunk->m_data.constData(); if (data[0x00] != 0) { + // 5 length 5, 6 length 1, 7 length 3, 8 length 3 seen on 960P + qWarning() << path << "data doesn't start with 0, skipping:" << data[0x00] << chunk->m_data.size(); delete chunk; continue; } } SessionID chunk_sid = chunk->sessionid; - if (chunk_sid != sid && chunk_sid > 2000) { // log any really weird session IDs + if (i > 0 || chunk_sid != sid) { // log multiple chunks in non-waveform files and session ID mismatches qDebug() << fi.canonicalFilePath() << chunk_sid; } - if (m->SessionExists(sid)) { + if (m->SessionExists(sid)) { // BUG: this should presumably be chunk_sid, but any change needs to be tested. + qDebug() << path << "session already exists, skipping" << sid << chunk_sid; delete chunk; continue; } @@ -804,23 +918,39 @@ void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machin } switch (ext) { case 0: - if (task->compliance) continue; // (skipping to avoid duplicates) + if (task->compliance) { + qWarning() << path << "duplicate compliance?"; + delete chunk; + continue; // (skipping to avoid duplicates) + } task->compliance = chunk; break; case 1: - if (task->summary) continue; + if (task->summary) { + qWarning() << path << "duplicate summary?"; + delete chunk; + continue; + } task->summary = chunk; break; case 2: - if (task->event) continue; + if (task->event) { + qWarning() << path << "duplicate events?"; + delete chunk; + continue; + } task->event = chunk; break; default: + qWarning() << path << "unexpected file"; break; } } } - if (isAborted()) break; + if (isAborted()) { + qDebug() << "received abort signal 3"; + break; + } } } @@ -1656,6 +1786,12 @@ bool PRS1Import::ParseF3Events() int hy, oa, ca; qint64 div = 0; + // TODO: make sure the assumptions here agree with the header: + // size == number of intervals + // interval seconds = 120 + // interleave for each channel = 1 + // also warn on any remainder of data size % record size (but don't fail) + const qint64 block_duration = 120000; for (int x=0; x < size; x++) { @@ -3062,6 +3198,78 @@ bool PRS1Import::ParseEvents() return res; } + +QList PRS1Import::CoalesceWaveformChunks(QList & allchunks) +{ + QList coalesced; + PRS1DataChunk *chunk = nullptr, *lastchunk = nullptr; + int num; + + for (int i=0; i < allchunks.size(); ++i) { + chunk = allchunks.at(i); + + if (lastchunk != nullptr) { + // Waveform files shouldn't contain multiple sessions + if (lastchunk->sessionid != chunk->sessionid) { + qWarning() << "lastchunk->sessionid != chunk->sessionid in PRS1Loader::CoalesceWaveformChunks()"; + // Free any remaining chunks + for (int j=i; j < allchunks.size(); ++j) { + chunk = allchunks.at(j); + delete chunk; + } + break; + } + + // Check whether the data format is the same between the two chunks + bool same_format = (lastchunk->waveformInfo.size() == chunk->waveformInfo.size()); + if (same_format) { + num = chunk->waveformInfo.size(); + for (int n=0; n < num; n++) { + const PRS1Waveform &a = lastchunk->waveformInfo.at(n); + const PRS1Waveform &b = chunk->waveformInfo.at(n); + if (a.interleave != b.interleave) { + // We've never seen this before + qWarning() << chunk->m_path << "format change?" << a.interleave << b.interleave; + same_format = false; + break; + } + } + } else { + // We've never seen this before + qWarning() << chunk->m_path << "channels change?" << lastchunk->waveformInfo.size() << chunk->waveformInfo.size(); + } + + qint64 diff = (chunk->timestamp - lastchunk->timestamp) - lastchunk->duration; + if (same_format && diff == 0) { + // Same format and in sync, so append waveform data to previous chunk + lastchunk->m_data.append(chunk->m_data); + lastchunk->duration += chunk->duration; + delete chunk; + continue; + } + // else start a new chunk to resync + } + + // Report any formats we haven't seen before + num = chunk->waveformInfo.size(); + if (num > 2) { + qDebug() << chunk->m_path << num << "channels"; + } + for (int n=0; n < num; n++) { + int interleave = chunk->waveformInfo.at(n).interleave; + if (interleave != 5) { + qDebug() << chunk->m_path << "interleave?" << interleave; + } + } + + coalesced.append(chunk); + lastchunk = chunk; + } + + return coalesced; +} + + bool PRS1Import::ParseOximetery() { int size = oximetry.size(); @@ -3072,6 +3280,7 @@ bool PRS1Import::ParseOximetery() int size = oxi->m_data.size(); if (size == 0) { + qDebug() << oxi->sessionid << oxi->timestamp << "empty?"; continue; } quint64 ti = quint64(oxi->timestamp) * 1000L; @@ -3121,12 +3330,16 @@ bool PRS1Import::ParseWaveforms() int size = waveform->m_data.size(); if (size == 0) { + qDebug() << waveform->sessionid << waveform->timestamp << "empty?"; continue; } quint64 ti = quint64(waveform->timestamp) * 1000L; quint64 dur = qint64(waveform->duration) * 1000L; quint64 diff = ti - lastti; + if ((lastti != 0) && diff > 0) { + qDebug() << waveform->sessionid << waveform->timestamp << "BND?" << (diff / 1000L) << "=" << waveform->timestamp << "-" << (lastti / 1000L); + } if ((diff > 500) && (lastti != 0)) { if (!bnd) { bnd = session->AddEventList(PRS1_BND, EVL_Event); @@ -3194,6 +3407,7 @@ bool PRS1Import::ParseSession(void) // Parse .005 Waveform file waveforms = loader->ParseFile(wavefile); + waveforms = CoalesceWaveformChunks(waveforms); if (session->eventlist.contains(CPAP_FlowRate)) { if (waveforms.size() > 0) { // Delete anything called "Flow rate" picked up in the events file if real data is present @@ -3204,6 +3418,7 @@ bool PRS1Import::ParseSession(void) // Parse .006 Waveform file oximetry = loader->ParseFile(oxifile); + oximetry = CoalesceWaveformChunks(oximetry); ParseOximetery(); if (session->first() > 0) { @@ -3245,270 +3460,412 @@ QList PRS1Loader::ParseFile(const QString & path) { QList CHUNKS; - if (path.isEmpty()) + if (path.isEmpty()) { + // ParseSession passes empty filepaths for waveforms if none exist. + //qWarning() << path << "ParseFile given empty path"; return CHUNKS; + } QFile f(path); if (!f.exists()) { + qWarning() << path << "missing"; return CHUNKS; } if (!f.open(QIODevice::ReadOnly)) { + qWarning() << path << "can't open"; return CHUNKS; } PRS1DataChunk *chunk = nullptr, *lastchunk = nullptr; - quint8 fileVersion; - quint16 blocksize; - quint16 wvfm_signals=0; - - unsigned char * header; int cnt = 0; - //int lastheadersize = 0; - int lastblocksize = 0; - int cruft = 0; int firstsession = 0; - int htype,family,familyVersion,ext,header_size = 0; - quint8 achk=0; - quint32 sessionid=0, timestamp=0; - - int duration=0; - - QByteArray headerBA, headerB2, extra; - - QList waveformInfo; do { - headerBA = f.read(16); - if (headerBA.size() != 16) { + chunk = PRS1DataChunk::ParseNext(f); + if (chunk == nullptr) { break; } + chunk->SetIndex(cnt); // for logging/debugging purposes - header = (unsigned char *)headerBA.data(); + if (lastchunk != nullptr) { + // If there's any mismatch between header information, try and skip the block + // This probably isn't the best approach for dealing with block corruption :/ + if ((lastchunk->fileVersion != chunk->fileVersion) + || (lastchunk->ext != chunk->ext) + || (lastchunk->family != chunk->family) + || (lastchunk->familyVersion != chunk->familyVersion) + || (lastchunk->htype != chunk->htype)) { + qWarning() << path << "unexpected header data, skipping"; + + // TODO: Find a sample of this problem to see if the below approach has any + // value, or whether we should just drop the chunk. + QByteArray junk = f.read(lastchunk->blockSize - chunk->m_header.size()); - fileVersion = header[0]; // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed - blocksize = (header[2] << 8) | header[1]; - htype = header[3]; // 00 = normal, 01=waveform - family = header[4]; - familyVersion = header[5]; - ext = header[6]; - sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7]; - timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11]; - - if (blocksize == 0) - break; - - if (fileVersion < 2) { - qDebug() << "Never seen PRS1 header version < 2 before"; - break; - } - - header_size = 16; // most common header size, newer familyVersion 3 models are larger. - - int diff = 0; - - waveformInfo.clear(); - - bool hasHeaderDataBlock = (fileVersion == 3); - if (ext < 5) { // Not a waveform chunk - - // Check if this is a newer machine with a header data block - - if (hasHeaderDataBlock) { - // This is a new machine, byte 15 is header data block length - // followed by variable, data byte pairs - // then the 8bit Checksum - - int hdb_len = header[15]; - int hdb_size = hdb_len * 2; - - headerB2 = f.read(hdb_size+1); // add extra byte for checksum - if (headerB2.size() != hdb_size+1) { + Q_UNUSED(junk) + if (lastchunk->ext == 5) { + // The data is random crap + // lastchunk->m_data.append(junk.mid(lastheadersize-16)); + } + ++cruft; + // quit after 3 attempts + if (cruft > 3) { + qWarning() << path << "too many unexpected headers, bailing"; break; } - headerBA.append(headerB2); - header = (unsigned char *)headerBA.data(); // important because it's memory location could move - - header_size += hdb_size+1; - } else headerB2 = QByteArray(); - - } else { // Waveform Chunk - extra = f.read(4); - if (extra.size() != 4) { - break; - } - header_size += 4; - headerBA.append(extra); - // Get the header address again to be safe - header = (unsigned char *)headerBA.data(); - - duration = header[0x0f] | header[0x10] << 8; - wvfm_signals = header[0x12] | header[0x13] << 8; - - int ws_size = (fileVersion == 3) ? 4 : 3; - int sbsize = wvfm_signals * ws_size + 1; - - extra = f.read(sbsize); - if (extra.size() != sbsize) { - break; - } - headerBA.append(extra); - header = (unsigned char *)headerBA.data(); - header_size += sbsize; - - // Read the waveform information in reverse. - int pos = 0x14 + (wvfm_signals - 1) * ws_size; - for (int i = 0; i < wvfm_signals; ++i) { - quint16 interleave = header[pos] | header[pos + 1] << 8; // samples per block (Usually 05 00) - - if (fileVersion == 2) { - quint8 sample_format = header[pos + 2]; - waveformInfo.push_back(PRS1Waveform(interleave, sample_format)); - pos -= 3; - } else if (fileVersion == 3) { - //quint16 sample_size = header[pos + 2] | header[pos + 3] << 8; // size in bits?? (08 00) - // Possibly this is size in bits, and sign bit for the other byte? - waveformInfo.push_back(PRS1Waveform(interleave, 0)); - pos -= 4; - } - } - if (lastchunk != nullptr) { - diff = (timestamp - lastchunk->timestamp) - lastchunk->duration; - } - } - - // Calculate 8bit additive header checksum - achk=0; - for (int i=0; i < (header_size-1); i++) achk += header[i]; - - if (achk != header[header_size-1]) { // Header checksum mismatch? - break; - } - - - if (lastchunk != nullptr) { - // If there's any mismatch between header information, try and skip the block - // This probably isn't the best approach for dealing with block corruption :/ - if ((lastchunk->fileVersion != fileVersion) - || (lastchunk->ext != ext) - || (lastchunk->family != family) - || (lastchunk->familyVersion != familyVersion) - || (lastchunk->htype != htype)) { - QByteArray junk = f.read(lastblocksize - header_size); - - Q_UNUSED(junk) - if (lastchunk->ext == 5) { - // The data is random crap - // lastchunk->m_data.append(junk.mid(lastheadersize-16)); - } - ++cruft; - // quit after 3 attempts - if (cruft > 3) - break; - - continue; - // Corrupt header.. skip it. + cnt++; + delete chunk; + continue; + // Corrupt header.. skip it. } } - - chunk = new PRS1DataChunk(); - - chunk->sessionid = sessionid; - + if (!firstsession) { firstsession = chunk->sessionid; } - chunk->fileVersion = fileVersion; - chunk->htype = htype; - chunk->family = family; - chunk->familyVersion = familyVersion; - chunk->ext = ext; - chunk->timestamp = timestamp; - if (hasHeaderDataBlock) { - const unsigned char * hd = (unsigned char *)headerB2.constData(); - int pos = 0; - int recs = header[15]; - for (int i=0; ihblock[hd[pos]] = hd[pos+1]; - pos += 2; - } - } - chunk->m_headerblock = headerB2; - - lastblocksize = blocksize; - blocksize -= header_size; - - if (ext >= 5) { - chunk->duration = duration; - - // I don't trust deep copy, just being safe... - for (int i=0;iwaveformInfo.push_back(waveformInfo.at(i)); - } - } - - // Read data block - chunk->m_data = f.read(blocksize); - - if (chunk->m_data.size() < blocksize) { - delete chunk; - break; - } - - if (chunk->fileVersion==3) { - //int ds = chunk->m_data.size(); - //quint32 crc16 = chunk->m_data.at(ds-2) | chunk->m_data.at(ds-1) << 8; - chunk->m_data.chop(4); - } else { - // last two bytes contain crc16 checksum. - int ds = chunk->m_data.size(); - quint16 crc16 = chunk->m_data.at(ds-2) | chunk->m_data.at(ds-1) << 8; - chunk->m_data.chop(2); -#ifdef PRS1_CRC_CHECK - // This fails.. it needs to include the header! - quint16 calc16 = CRC16((unsigned char *)chunk->m_data.data(), chunk->m_data.size()); - if (calc16 != crc16) { - // corrupt data block.. bleh.. - // qDebug() << "CRC16 doesn't match for chunk" << chunk->sessionid << "for" << path; - } -#endif - } - - if ((chunk->ext == 5) || (chunk->ext == 6)) { // if Flow/MaskPressure Waveform or OXI Waveform file - if (lastchunk != nullptr) { - if (lastchunk->sessionid != chunk->sessionid) { - qWarning() << "lastchunk->sessionid != chunk->sessionid in PRS1Loader::ParseFile2()"; - break; - } - - if (diff == 0) { - // In sync, so append waveform data to previous chunk - lastchunk->m_data.append(chunk->m_data); - lastchunk->duration += chunk->duration; - delete chunk; - cnt++; - chunk = lastchunk; - continue; - } - // else start a new chunk to resync - } - } CHUNKS.append(chunk); lastchunk = chunk; cnt++; - } while (!f.atEnd()); return CHUNKS; } + +PRS1DataChunk::PRS1DataChunk(QFile & f) +{ + m_path = QFileInfo(f).canonicalFilePath(); +} + + +PRS1DataChunk* PRS1DataChunk::ParseNext(QFile & f) +{ + PRS1DataChunk* out_chunk = nullptr; + PRS1DataChunk* chunk = new PRS1DataChunk(f); + + do { + // Parse the header and calculate its checksum. + bool ok = chunk->ReadHeader(f); + if (!ok) { + break; + } + + // Make sure the calculated checksum matches the stored checksum. + if (chunk->calcChecksum != chunk->storedChecksum) { + qWarning() << chunk->m_path << "header checksum calc" << chunk->calcChecksum << "!= stored" << chunk->storedChecksum; + break; + } + + // Log mismatched waveform session IDs + if (chunk->htype == PRS1_HTYPE_INTERVAL) { + QFileInfo fi(f); + bool numeric; + int sessionid_base = (chunk->fileVersion == 2 ? 10 : 16); + if (chunk->family == 3 && chunk->familyVersion >= 3) sessionid_base = 16; + QString session_s = fi.fileName().section(".", 0, -2); + quint32 sid = session_s.toInt(&numeric, sessionid_base); + if (!numeric || sid != chunk->sessionid) { + qDebug() << chunk->m_path << chunk->sessionid; + } + } + + // Read the block's data and calculate the block CRC. + ok = chunk->ReadData(f); + if (!ok) { + break; + } + + // Make sure the calculated CRC over the entire chunk (header and data) matches the stored CRC. + if (chunk->calcCrc != chunk->storedCrc) { + // corrupt data block.. bleh.. + qDebug() << chunk->m_path << "@" << chunk->m_filepos << "block CRC calc" << hex << chunk->calcCrc << "!= stored" << hex << chunk->storedCrc; + //break; // don't break to avoid changing behavior (for now) + } + + // Only return the chunk if it has passed all tests above. + out_chunk = chunk; + } while (false); + + if (out_chunk == nullptr) delete chunk; + return out_chunk; +} + + +bool PRS1DataChunk::ReadHeader(QFile & f) +{ + bool ok = false; + do { + // Read common header fields. + this->m_filepos = f.pos(); + this->m_header = f.read(15); + if (this->m_header.size() != 15) { + qWarning() << this->m_path << "file too short?"; + break; + } + + unsigned char * header = (unsigned char *)this->m_header.data(); + this->fileVersion = header[0]; // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed + this->blockSize = (header[2] << 8) | header[1]; + this->htype = header[3]; // 00 = normal, 01=waveform + this->family = header[4]; + this->familyVersion = header[5]; + this->ext = header[6]; + this->sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7]; + this->timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11]; + + // Do a few early sanity checks before any variable-length header data. + if (this->blockSize == 0) { + qWarning() << this->m_path << "blocksize 0?"; + break; + } + if (this->fileVersion < 2 || this->fileVersion > 3) { + qWarning() << this->m_path << "@" << hex << this->m_filepos << "Never seen PRS1 header version < 2 or > 3 before"; + break; + } + if (this->htype != PRS1_HTYPE_NORMAL && this->htype != PRS1_HTYPE_INTERVAL) { + qWarning() << this->m_path << "unexpected htype:" << this->htype; + //break; // don't break to avoid changing behavior (for now) + } + + // Read format-specific variable-length header data. + bool hdr_ok = false; + if (this->htype != PRS1_HTYPE_INTERVAL) { // Not just waveforms: the 1160P uses this for its .002 events file. + // Not a waveform/interval chunk + switch (this->fileVersion) { + case 2: + hdr_ok = ReadNormalHeaderV2(f); + break; + case 3: + hdr_ok = ReadNormalHeaderV3(f); + break; + default: + //hdr_ok remains false, warning is above + break; + } + } else { + // Waveform/interval chunk + hdr_ok = ReadWaveformHeader(f); + } + if (!hdr_ok) { + break; + } + + // The 8bit checksum comes at the end. + QByteArray checksum = f.read(1); + if (checksum.size() < 1) { + qWarning() << this->m_path << "read error header checksum"; + break; + } + this->storedChecksum = checksum.data()[0]; + + // Calculate 8bit additive header checksum. + header = (unsigned char *)this->m_header.data(); // important because its memory location could move + int header_size = this->m_header.size(); + quint8 achk=0; + for (int i=0; i < header_size; i++) { + achk += header[i]; + } + this->calcChecksum = achk; + + // Append the stored checksum to the raw data *after* calculating the checksum on the preceding data. + this->m_header.append(checksum); + + ok = true; + } while (false); + + return ok; +} + + +bool PRS1DataChunk::ReadNormalHeaderV2(QFile & /*f*/) +{ + this->m_headerblock = QByteArray(); + return true; // always OK +} + + +bool PRS1DataChunk::ReadNormalHeaderV3(QFile & f) +{ + bool ok = false; + unsigned char * header; + QByteArray headerB2; + + // This is a new machine, byte 15 is header data block length + // followed by variable, data byte pairs + do { + QByteArray extra = f.read(1); + if (extra.size() < 1) { + qWarning() << this->m_path << "read error extended header"; + break; + } + this->m_header.append(extra); + header = (unsigned char *)this->m_header.data(); + + int hdb_len = header[15]; + int hdb_size = hdb_len * 2; + + headerB2 = f.read(hdb_size); + if (headerB2.size() != hdb_size) { + qWarning() << this->m_path << "read error in extended header"; + break; + } + this->m_headerblock = headerB2; + + this->m_header.append(headerB2); + header = (unsigned char *)this->m_header.data(); + const unsigned char * hd = (unsigned char *)headerB2.constData(); + int pos = 0; + int recs = header[15]; + for (int i=0; ihblock[hd[pos]] = hd[pos+1]; + pos += 2; + } + + ok = true; + } while (false); + + return ok; +} + + +bool PRS1DataChunk::ReadWaveformHeader(QFile & f) +{ + bool ok = false; + unsigned char * header; + do { + // Read the fixed-length waveform header. + QByteArray extra = f.read(4); + if (extra.size() != 4) { + qWarning() << this->m_path << "read error in waveform header"; + break; + } + this->m_header.append(extra); + header = (unsigned char *)this->m_header.data(); + + // Parse the fixed-length portion. + this->interval_count = header[0x0f] | header[0x10] << 8; + this->interval_seconds = header[0x11]; // not always 1 after all + this->duration = this->interval_count * this->interval_seconds; // ??? the last entry doesn't always seem to be a full interval? + quint8 wvfm_signals = header[0x12]; + + // Read the variable-length data + trailing byte. + int ws_size = (this->fileVersion == 3) ? 4 : 3; + int sbsize = wvfm_signals * ws_size + 1; + + extra = f.read(sbsize); + if (extra.size() != sbsize) { + qWarning() << this->m_path << "read error in waveform header 2"; + break; + } + this->m_header.append(extra); + header = (unsigned char *)this->m_header.data(); + + // Parse the variable-length waveform information. + int pos = 0x13; + for (int i = 0; i < wvfm_signals; ++i) { + quint8 kind = header[pos]; + if (kind != i) { // always seems to range from 0...wvfm_signals-1, alert if not + qWarning() << this->m_path << kind << "!=" << i << "waveform kind"; + //break; // don't break to avoid changing behavior (for now) + } + quint16 interleave = header[pos + 1] | header[pos + 2] << 8; // samples per interval + if (this->fileVersion == 2) { + this->waveformInfo.push_back(PRS1Waveform(interleave, kind)); + pos += 3; + } else if (this->fileVersion == 3) { + int always_8 = header[pos + 3]; // sample size in bits? + if (always_8 != 8) { + qWarning() << this->m_path << always_8 << "!= 8 in waveform header"; + //break; // don't break to avoid changing behavior (for now) + } + this->waveformInfo.push_back(PRS1Waveform(interleave, kind)); + pos += 4; + } + } + + // And the trailing byte, whatever it is. + int always_0 = header[pos]; + if (always_0 != 0) { + qWarning() << this->m_path << always_0 << "!= 0 in waveform header"; + //break; // don't break to avoid changing behavior (for now) + } + + ok = true; + } while (false); + + return ok; +} + + +bool PRS1DataChunk::ReadData(QFile & f) +{ + bool ok = false; + do { + // Read data block + int data_size = this->blockSize - this->m_header.size(); + if (data_size < 0) { + qWarning() << this->m_path << "chunk size smaller than header"; + break; + } + this->m_data = f.read(data_size); + if (this->m_data.size() < data_size) { + qWarning() << this->m_path << "less data in file than specified in header"; + break; + } + + // Extract the stored CRC from the data buffer and calculate the current CRC. + if (this->fileVersion==3) { + // The last 4 bytes contain a CRC32 checksum of the data. + if (!ExtractStoredCrc(4)) { + break; + } + this->calcCrc = CRC32wchar((unsigned char *)this->m_data.data(), this->m_data.size()); + } else { + // The last 2 bytes contain a CRC16 checksum of the data. + if (!ExtractStoredCrc(2)) { + break; + } + this->calcCrc = CRC16((unsigned char *)this->m_data.data(), this->m_data.size()); + } + + ok = true; + } while (false); + + return ok; +} + + +bool PRS1DataChunk::ExtractStoredCrc(int size) +{ + // Make sure there's enough data for the CRC. + int offset = this->m_data.size() - size; + if (offset < 0) { + qWarning() << this->m_path << "chunk truncated"; + return false; + } + + // Read the last 16- or 32-bit little-endian integer. + quint32 storedCrc = 0; + unsigned char* data = (unsigned char*)this->m_data.data(); + for (int i=0; i < size; i++) { + storedCrc |= data[offset+i] << (8*i); + } + this->storedCrc = storedCrc; + + // Drop the CRC from the data. + this->m_data.chop(size); + + return true; +} + + void InitModelMap() { ModelMap[0x34] = QObject::tr("RemStar Pro with C-Flex+"); // 450/460P diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index fd4bf620..17772769 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -62,35 +62,80 @@ class PRS1DataChunk friend class PRS1DataGroup; public: PRS1DataChunk() { - timestamp = 0; - ext = 255; - sessionid = 0; + fileVersion = 0; + blockSize = 0; htype = 0; family = 0; familyVersion = 0; + ext = 255; + sessionid = 0; + timestamp = 0; + duration = 0; + m_filepos = -1; + m_index = -1; } + PRS1DataChunk(class QFile & f); ~PRS1DataChunk() { } inline int size() const { return m_data.size(); } + QByteArray m_header; QByteArray m_data; QByteArray m_headerblock; - SessionID sessionid; + QString m_path; + qint64 m_filepos; // file offset + int m_index; // nth chunk in file + inline void SetIndex(int index) { m_index = index; } + // Common fields quint8 fileVersion; - quint8 ext; + quint16 blockSize; quint8 htype; quint8 family; quint8 familyVersion; + quint8 ext; + SessionID sessionid; quint32 timestamp; - quint16 duration; - + // Waveform-specific fields + quint16 interval_count; + quint8 interval_seconds; + int duration; QList waveformInfo; + + // V3 normal/non-waveform fields QMap hblock; + + // Trailing common fields + quint8 storedChecksum; // header checksum stored in file, last byte of m_header + quint8 calcChecksum; // header checksum as calculated when parsing + quint32 storedCrc; // header + data CRC stored in file, last 2-4 bytes of chunk + quint32 calcCrc; // header + data CRC as calculated when parsing + + //! \brief Parse and return the next chunk from a PRS1 file + static PRS1DataChunk* ParseNext(class QFile & f); + + //! \brief Read and parse the next chunk header from a PRS1 file + bool ReadHeader(class QFile & f); + + //! \brief Read the chunk's data from a PRS1 file and calculate its CRC, must be called after ReadHeader + bool ReadData(class QFile & f); + +protected: + //! \brief Read and parse the non-waveform header data from a V2 PRS1 file + bool ReadNormalHeaderV2(class QFile & f); + + //! \brief Read and parse the non-waveform header data from a V3 PRS1 file + bool ReadNormalHeaderV3(class QFile & f); + + //! \brief Read and parse the waveform-specific header data from a PRS1 file + bool ReadWaveformHeader(class QFile & f); + + //! \brief Extract the stored CRC from the end of the data of a PRS1 chunk + bool ExtractStoredCrc(int size); }; class PRS1Loader; @@ -138,6 +183,9 @@ public: //! \brief Figures out which Event Parser to call, based on machine family/version and calls it. bool ParseEvents(); + //! \brief Coalesce contiguous .005 or .006 waveform chunks from the file into larger chunks for import. + QList CoalesceWaveformChunks(QList & allchunks); + //! \brief Takes the parsed list of Flow/MaskPressure waveform chunks and adds them to the database bool ParseWaveforms(); diff --git a/oscar/daily.cpp b/oscar/daily.cpp index 84c0a51f..dec0dbea 100644 --- a/oscar/daily.cpp +++ b/oscar/daily.cpp @@ -1558,10 +1558,11 @@ void Daily::Load(QDate date) const int maxcolors=sizeof(cols)/sizeof(QColor); QList::iterator i; + sessionbar->clear(); // clear sessionbar as some days don't have sessions + if (cpap) { int c=0; - sessionbar->clear(); for (i=day->begin();i!=day->end();++i) { Session * s=*i; if ((*s).type() == MT_CPAP) diff --git a/oscar/icons/OSCAR.icns b/oscar/icons/OSCAR.icns index ae6ad410..947fc46d 100644 Binary files a/oscar/icons/OSCAR.icns and b/oscar/icons/OSCAR.icns differ diff --git a/oscar/icons/logo.ico b/oscar/icons/logo.ico index 47e20f78..2d464294 100644 Binary files a/oscar/icons/logo.ico and b/oscar/icons/logo.ico differ diff --git a/oscar/mainwindow.cpp b/oscar/mainwindow.cpp index 82800b70..5171873f 100644 --- a/oscar/mainwindow.cpp +++ b/oscar/mainwindow.cpp @@ -1345,37 +1345,25 @@ void MainWindow::on_actionCheck_for_Updates_triggered() bool toolbox_visible = false; void MainWindow::on_action_Screenshot_triggered() { - daily->hideSpaceHogs(); + if (daily) + daily->hideSpaceHogs(); toolbox_visible = ui->toolBox->isVisible(); ui->toolBox->hide(); QTimer::singleShot(250, this, SLOT(DelayedScreenshot())); } + void MainWindow::DelayedScreenshot() { // Make sure to scale for high resolution displays (like Retina) // qreal pr = devicePixelRatio(); - - QScreen * screen = QApplication::primaryScreen(); - - - int titleBarHeight = -QApplication::style()->pixelMetric(QStyle::PM_TitleBarHeight); -#ifdef Q_OS_WIN - titleBarHeight += 6; -#endif - - QPixmap pixmap = screen->grabWindow(winId(),0,titleBarHeight); - -/*#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_HAIKU) - // grab the whole screen - grab() - QPixmap desktop = QPixmap::grabWindow(QApplication::desktop()->winId()); - - QPixmap pixmap = desktop.copy(x() * pr, y() * pr, (width()+6) * pr, (height()+22) * pr); - -#elif defined(Q_OS_MAC) - QPixmap pixmap = QPixmap::grabWindow(this->winId(), x(), y(), width() / pr, (height() / pr) + 10); -#endif */ + auto screenshotRect = geometry(); + auto titleBarHeight = QApplication::style()->pixelMetric(QStyle::PM_TitleBarHeight); + auto pixmap = QApplication::primaryScreen()->grabWindow(QDesktopWidget().winId(), + screenshotRect.left(), + screenshotRect.top() - titleBarHeight, + screenshotRect.width(), + screenshotRect.height() + titleBarHeight); QString a = p_pref->Get("{home}/Screenshots"); QDir dir(a); @@ -1393,7 +1381,8 @@ void MainWindow::DelayedScreenshot() } else { Notify(tr("Screenshot saved to file \"%1\"").arg(QDir::toNativeSeparators(a))); } - daily->showSpaceHogs(); + if (daily) + daily->showSpaceHogs(); ui->toolBox->setVisible(toolbox_visible); } diff --git a/oscar/tests/prs1tests.cpp b/oscar/tests/prs1tests.cpp index f1cfd470..16ffb412 100644 --- a/oscar/tests/prs1tests.cpp +++ b/oscar/tests/prs1tests.cpp @@ -13,10 +13,13 @@ static PRS1Loader* s_loader = nullptr; static void iterateTestCards(const QString & root, void (*action)(const QString &)); +static QString prs1OutputPath(const QString & inpath, const QString & serial, const QString & basename, const QString & suffix); static QString prs1OutputPath(const QString & inpath, const QString & serial, int session, const QString & suffix); void PRS1Tests::initTestCase(void) { + initializeStrings(); + qDebug() << STR_TR_OSCAR + " " + getBranchVersion(); QString profile_path = TESTDATA_PATH "profile/"; Profiles::Create("test", &profile_path); @@ -74,9 +77,166 @@ void PRS1Tests::testSessionsToYaml() } +// ==================================================================================================== + +static QString ts(qint64 msecs) +{ + return QDateTime::fromMSecsSinceEpoch(msecs).toString(Qt::ISODate); +} + +static QString byteList(QByteArray data) +{ + QStringList l; + for (int i = 0; i < data.size(); i++) { + l.push_back(QString( "%1" ).arg((int) data[i] & 0xFF, 2, 16, QChar('0') ).toUpper()); + } + QString s = l.join(""); + return s; +} + +void ChunkToYaml(QFile & file, PRS1DataChunk* chunk) +{ + QTextStream out(&file); + + // chunk header + out << "chunk:" << endl; + out << " at: " << hex << chunk->m_filepos << endl; + out << " version: " << dec << chunk->fileVersion << endl; + out << " size: " << chunk->blockSize << endl; + out << " htype: " << chunk->htype << endl; + out << " family: " << chunk->family << endl; + out << " familyVersion: " << chunk->familyVersion << endl; + out << " ext: " << chunk->ext << endl; + out << " session: " << chunk->sessionid << endl; + out << " start: " << ts(chunk->timestamp * 1000L) << endl; + + // hblock for V3 non-waveform chunks + if (chunk->fileVersion == 3 && chunk->htype == 0) { + out << " hblock:" << endl; + QMapIterator i(chunk->hblock); + while (i.hasNext()) { + i.next(); + out << " " << (int) i.key() << ": " << i.value() << endl; + } + } + + // waveform chunks + if (chunk->htype == 1) { + out << " intervals: " << chunk->interval_count << endl; + out << " intervalSeconds: " << (int) chunk->interval_seconds << endl; + out << " interleave:" << endl; + for (int i=0; i < chunk->waveformInfo.size(); i++) { + const PRS1Waveform & w = chunk->waveformInfo.at(i); + out << " " << i << ": " << w.interleave << endl; + } + out << " end: " << ts((chunk->timestamp + chunk->duration) * 1000L) << endl; + } + + // header checksum + out << " checksum: " << hex << chunk->storedChecksum << endl; + if (chunk->storedChecksum != chunk->calcChecksum) { + out << " calcChecksum: " << hex << chunk->calcChecksum << endl; + } + + // data + out << " data: " << byteList(chunk->m_data) << endl; + + // data CRC + out << " crc: " << hex << chunk->storedCrc << endl; + if (chunk->storedCrc != chunk->calcCrc) { + out << " calcCrc: " << hex << chunk->calcCrc << endl; + } + out << endl; +} + +void parseAndEmitChunkYaml(const QString & path) +{ + qDebug() << path; + + QStringList paths; + QString propertyfile; + int sessionid_base; + sessionid_base = s_loader->FindSessionDirsAndProperties(path, paths, propertyfile); + + Machine *m = s_loader->CreateMachineFromProperties(propertyfile); + Q_ASSERT(m != nullptr); + + // This mirrors the functional bits of PRS1Loader::ScanFiles. + + QDir dir; + dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks); + dir.setSorting(QDir::Name); + + int size = paths.size(); + + // for each p0/p1/p2/etc... folder + for (int p=0; p < size; ++p) { + dir.setPath(paths.at(p)); + if (!dir.exists() || !dir.isReadable()) { + qWarning() << dir.canonicalPath() << "can't read directory"; + continue; + } + QFileInfoList flist = dir.entryInfoList(); + + // Scan for individual .00X files + for (int i = 0; i < flist.size(); i++) { + QFileInfo fi = flist.at(i); + QString inpath = fi.canonicalFilePath(); + bool ok; + + QString ext_s = fi.fileName().section(".", -1); + ext_s.toInt(&ok); + if (!ok) { + // not a numerical extension + qWarning() << inpath << "unexpected filename"; + continue; + } + + QString session_s = fi.fileName().section(".", 0, -2); + session_s.toInt(&ok, sessionid_base); + if (!ok) { + // not a numerical session ID + qWarning() << inpath << "unexpected filename"; + continue; + } + + // Create the YAML file. + QString outpath = prs1OutputPath(path, m->serial(), fi.fileName(), "-chunks.yml"); + QFile file(outpath); + if (!file.open(QFile::WriteOnly | QFile::Truncate)) { + qDebug() << outpath; + Q_ASSERT(false); + } + + // Parse the chunks in the file. + QList chunks = s_loader->ParseFile(inpath); + for (int i=0; i < chunks.size(); i++) { + // Emit the YAML. + PRS1DataChunk * chunk = chunks.at(i); + ChunkToYaml(file, chunk); + delete chunk; + } + + file.close(); + } + } +} + +void PRS1Tests::testChunksToYaml() +{ + iterateTestCards(TESTDATA_PATH "prs1/input/", parseAndEmitChunkYaml); +} + + // ==================================================================================================== QString prs1OutputPath(const QString & inpath, const QString & serial, int session, const QString & suffix) +{ + QString basename = QString("%1").arg(session, 8, 10, QChar('0')); + return prs1OutputPath(inpath, serial, basename, suffix); +} + +QString prs1OutputPath(const QString & inpath, const QString & serial, const QString & basename, const QString & suffix) { // Output to prs1/output/FOLDER/SERIAL-000000(-session.yml, etc.) QDir path(inpath); @@ -90,7 +250,7 @@ QString prs1OutputPath(const QString & inpath, const QString & serial, int sessi QString filename = QString("%1-%2%3") .arg(serial) - .arg(session, 6, 10, QChar('0')) + .arg(basename) .arg(suffix); return outdir.path() + QDir::separator() + filename; } diff --git a/oscar/tests/prs1tests.h b/oscar/tests/prs1tests.h index 592d7225..5c8f2992 100644 --- a/oscar/tests/prs1tests.h +++ b/oscar/tests/prs1tests.h @@ -18,6 +18,7 @@ class PRS1Tests : public QObject private slots: void initTestCase(); + void testChunksToYaml(); void testSessionsToYaml(); // void test2(); void cleanupTestCase(); diff --git a/oscar/tests/sessiontests.cpp b/oscar/tests/sessiontests.cpp index ac216a24..f5491d85 100644 --- a/oscar/tests/sessiontests.cpp +++ b/oscar/tests/sessiontests.cpp @@ -162,10 +162,6 @@ void SessionToYaml(QString filepath, Session* session) } QTextStream out(&file); - // TODO: We sometimes see invalid session IDs. Either memory is getting trampled or the file - // header has the wrong ID (or isn't getting parsed right). Track this down once we can test parsing. - if (session->session() > 2000) qDebug() << "memory trampled? session ID" << session->session(); - out << "session:" << endl; out << " id: " << session->session() << endl; out << " start: " << ts(session->first()) << endl;