mirror of
https://gitlab.com/pholy/OSCAR-code.git
synced 2025-04-22 05:30:44 +00:00
Merge branch 'master' into json-11
This commit is contained in:
commit
f69e22f876
@ -12,6 +12,8 @@ CONFIG -= debug_and_release
|
||||
|
||||
QT += core widgets
|
||||
|
||||
DEFINES+=DUMPSTR
|
||||
|
||||
TARGET = anotDump
|
||||
|
||||
TEMPLATE = app
|
||||
@ -27,6 +29,6 @@ SOURCES += \
|
||||
dumpSTR/edfparser.cpp \
|
||||
|
||||
HEADERS += \
|
||||
dumpSTR/common.h \
|
||||
dumpSTR/SleepLib/common.h \
|
||||
dumpSTR/edfparser.h \
|
||||
|
||||
|
@ -81,9 +81,14 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
EDFInfo edf;
|
||||
QByteArray * buffer = edf.Open(filename);
|
||||
if ( ! edf.Parse(buffer) )
|
||||
if ( ! edf.Open(filename) ) {
|
||||
qDebug() << "Failed to open" << filename;
|
||||
exit(-1);
|
||||
}
|
||||
if ( ! edf.Parse() ) {
|
||||
qDebug() << "Parsing failed on" << filename;
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
QDate d2 = edf.edfHdr.startdate_orig.date();
|
||||
if (d2.year() < 2000) {
|
||||
@ -120,4 +125,5 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
@ -193,4 +193,6 @@ int main(int argc, char *argv[]) {
|
||||
// delete &str;
|
||||
QThread::sleep(1);
|
||||
qDebug() << "Done";
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
@ -480,6 +480,7 @@ void FlowParser::calc(bool calcResp, bool calcTv, bool calcTi, bool calcTe, bool
|
||||
quint32 *tv_tptr = nullptr;
|
||||
EventStoreType *tv_dptr = nullptr;
|
||||
int tv_count = 0;
|
||||
double tvlast, tvlast2, tvlast3;
|
||||
|
||||
if (calcTv) {
|
||||
TV = m_session->AddEventList(CPAP_TidalVolume, EVL_Event);
|
||||
@ -598,8 +599,15 @@ void FlowParser::calc(bool calcResp, bool calcTv, bool calcTi, bool calcTe, bool
|
||||
//double x=sqrt(q)*2;
|
||||
//val2=x;
|
||||
|
||||
if (tv < mintv) { mintv = tv; }
|
||||
// Average TV over last three data points
|
||||
if (tv_count == 0)
|
||||
tvlast = tvlast2 = tvlast3 = tv;
|
||||
tv = (tvlast + tvlast2 + tvlast3 + tv*2)/5;
|
||||
tvlast3 = tvlast2;
|
||||
tvlast2 = tvlast;
|
||||
tvlast = tv;
|
||||
|
||||
if (tv < mintv) { mintv = tv; }
|
||||
if (tv > maxtv) { maxtv = tv; }
|
||||
|
||||
*tv_tptr++ = timeval;
|
||||
@ -889,8 +897,9 @@ void calcRespRate(Session *session, FlowParser *flowparser)
|
||||
bool calcTe = !session->eventlist.contains(CPAP_Te);
|
||||
bool calcMv = !session->eventlist.contains(CPAP_MinuteVent);
|
||||
|
||||
|
||||
int z = (calcResp ? 1 : 0) + (calcTv ? 1 : 0) + (calcMv ? 1 : 0);
|
||||
// Force calculation for testing calculation vs CPAP data
|
||||
// z = 1;
|
||||
|
||||
// If any of these three missing, remove all, and switch all on
|
||||
if (z > 0 && z < 3) {
|
||||
|
@ -415,7 +415,7 @@ bool removeDir(const QString &path)
|
||||
return result;
|
||||
}
|
||||
|
||||
void copyPath(QString src, QString dst)
|
||||
void copyPath(QString src, QString dst, bool overwrite)
|
||||
{
|
||||
QDir dir(src);
|
||||
if (!dir.exists())
|
||||
@ -425,7 +425,7 @@ void copyPath(QString src, QString dst)
|
||||
foreach (QString d, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
QString dst_path = dst + QDir::separator() + d;
|
||||
dir.mkpath(dst_path);
|
||||
copyPath(src + QDir::separator() + d, dst_path);
|
||||
copyPath(src + QDir::separator() + d, dst_path, overwrite);
|
||||
}
|
||||
|
||||
// Files
|
||||
@ -433,6 +433,9 @@ void copyPath(QString src, QString dst)
|
||||
QString srcFile = src + QDir::separator() + f;
|
||||
QString destFile = dst + QDir::separator() + f;
|
||||
|
||||
if (overwrite && QFile::exists(destFile)) {
|
||||
QFile::remove(destFile);
|
||||
}
|
||||
if (!QFile::exists(destFile)) {
|
||||
if (!QFile::copy(srcFile, destFile)) {
|
||||
qWarning() << "copyPath: could not copy" << srcFile << "to" << destFile;
|
||||
|
@ -74,7 +74,7 @@ struct ValueCount {
|
||||
|
||||
extern int idealThreads();
|
||||
|
||||
void copyPath(QString src, QString dst);
|
||||
void copyPath(QString src, QString dst, bool overwrite=false);
|
||||
|
||||
|
||||
// Primarily sort by value
|
||||
@ -157,6 +157,7 @@ const QString STR_MACH_Journal = "Journal";
|
||||
const QString STR_MACH_Intellipap = "Intellipap";
|
||||
const QString STR_MACH_Weinmann= "Weinmann";
|
||||
const QString STR_MACH_FPIcon = "FPIcon";
|
||||
const QString STR_MACH_SleepStyle = "SleepStyle";
|
||||
const QString STR_MACH_MSeries = "MSeries";
|
||||
const QString STR_MACH_CMS50 = "CMS50";
|
||||
const QString STR_MACH_ZEO = "Zeo";
|
||||
|
@ -58,6 +58,12 @@ EDFInfo::~EDFInfo()
|
||||
// delete a;
|
||||
}
|
||||
|
||||
// Set timezone to UTC
|
||||
void EDFInfo::setTimeZoneUTC () {
|
||||
TZ_offset = 0;
|
||||
EDFInfo::localNoDST = QTimeZone(TZ_offset);
|
||||
}
|
||||
|
||||
bool EDFInfo::Open(const QString & name)
|
||||
{
|
||||
if (hdrPtr != nullptr) {
|
||||
|
@ -142,6 +142,9 @@ class EDFInfo
|
||||
|
||||
static QDateTime getStartDT(const QString str); //! \brief Returns the start time using noLocalDST
|
||||
|
||||
static void setTimeZoneUTC(); //! \brief Sets noLocalDST to UTC (for EDF files using UTC time)
|
||||
|
||||
|
||||
// The data members follow
|
||||
|
||||
static int TZ_offset;
|
||||
|
@ -10,6 +10,7 @@
|
||||
* for more details. */
|
||||
|
||||
#include <QDir>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include "intellipap_loader.h"
|
||||
|
||||
@ -357,7 +358,7 @@ int IntellipapLoader::OpenDV5(const QString & path)
|
||||
sess->AddEventList(CPAP_Snore, EVL_Event);
|
||||
|
||||
sess->AddEventList(CPAP_Obstructive, EVL_Event);
|
||||
sess->AddEventList(CPAP_VSnore, 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);
|
||||
@ -375,7 +376,7 @@ int IntellipapLoader::OpenDV5(const QString & path)
|
||||
}
|
||||
}
|
||||
|
||||
QDateTime d = QDateTime::fromTime_t(sid);
|
||||
QDateTime d = QDateTime::fromSecsSinceEpoch(sid);
|
||||
qDebug() << sid << "has double ups" << d;
|
||||
/*Session *sess=Sessions[sid];
|
||||
Sessions.erase(Sessions.find(sid));
|
||||
@ -483,7 +484,7 @@ int IntellipapLoader::OpenDV5(const QString & path)
|
||||
sess->eventlist[CPAP_Snore][0]->AddEvent(time, m_buffer[pos + 0x4]); //4/5??
|
||||
|
||||
if (m_buffer[pos+0x4] > 0) {
|
||||
sess->eventlist[CPAP_VSnore][0]->AddEvent(time, m_buffer[pos + 0x5]);
|
||||
sess->eventlist[INTP_SnoreFlag][0]->AddEvent(time, m_buffer[pos + 0x5]);
|
||||
}
|
||||
|
||||
// 0x0f == Leak Event
|
||||
@ -720,7 +721,7 @@ PACK (struct DV6_S_REC{
|
||||
unsigned char checksum; //54
|
||||
});
|
||||
|
||||
// DV6 SET.BIN - structure of the entire file
|
||||
// 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
|
||||
@ -868,13 +869,16 @@ struct DV6_SessionInfo {
|
||||
unsigned int begin;
|
||||
unsigned int end;
|
||||
unsigned int written;
|
||||
bool haveHighResData;
|
||||
// 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;
|
||||
@ -882,6 +886,8 @@ Machine * mach = nullptr;
|
||||
bool rebuild_from_backups = false;
|
||||
bool create_backups = false;
|
||||
|
||||
QStringList inputFilePaths;
|
||||
|
||||
QMap<SessionID, DV6_S_Data> DailySummaries;
|
||||
QMap<SessionID, DV6_SessionInfo> SessionData;
|
||||
SET_BIN_REC * settings;
|
||||
@ -909,84 +915,119 @@ public:
|
||||
~RollingBackup () {
|
||||
}
|
||||
|
||||
bool open (const QString filetype, DV6_HEADER * newhdr); // Open the file
|
||||
bool open (const QString filetype, DV6_HEADER * newhdr, QByteArray * startTime); // Open the file
|
||||
bool close(); // close the file
|
||||
bool save(QByteArray dataBA); // save the next record in the file
|
||||
bool save(const QByteArray &dataBA); // save the next record in the file
|
||||
|
||||
private:
|
||||
//DV6_HEADER hdr; // file header
|
||||
DV6_HEADER hdr; // file header
|
||||
QString filetype;
|
||||
QFile hFile;
|
||||
QFile histfile;
|
||||
|
||||
//int record_length; // Length of record block in incoming file
|
||||
//const int maxHistFileSize = 20*10e6; // Maximum size of file before we create a new file
|
||||
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)
|
||||
|
||||
//int numWritten; // Number of records written
|
||||
//quint32 lastTimestamp;
|
||||
//unsigned int wrap_record;
|
||||
unsigned int lastTimeInFile; // Timestamp of last data record in history file
|
||||
int numWritten; // Number of records written
|
||||
};
|
||||
|
||||
bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) {
|
||||
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;
|
||||
|
||||
#ifdef ROLLBACKUP
|
||||
QDir hpath(history_path);
|
||||
QString historypath = hpath.absolutePath() + "/";
|
||||
int histfilesize = 0;
|
||||
|
||||
this->filetype = filetype;
|
||||
|
||||
QDir hpath(history_path);
|
||||
QStringList filters;
|
||||
|
||||
bool needNewFile = false;
|
||||
memcpy (&hdr, inputhdr, sizeof(DV6_HEADER));
|
||||
numWritten = 0;
|
||||
|
||||
filters.append(filetype);
|
||||
filters[0].insert(1, "_*");
|
||||
hpath.setNameFilters(filters);
|
||||
hpath.setFilter(QDir::Files);
|
||||
hpath.setSorting(QDir::Name | QDir::Reversed);
|
||||
|
||||
QStringList fileNames = hpath.entryList(); // Get list of files
|
||||
QFile histfile(fileNames.first());
|
||||
|
||||
// bool needNewFile = false;
|
||||
QStringList fileNames = getHistoryFileNames(filetype, true);
|
||||
|
||||
// Handle first time a history file is being created
|
||||
if (fileNames.isEmpty()) {
|
||||
memcpy (&hdr, newhdr, sizeof(DV6_HEADER));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
hdr.recordStart[i] = 0;
|
||||
hdr.lasttime[i] = 0;
|
||||
}
|
||||
record_length = hdr.recordLength;
|
||||
lastTimeInFile = 0;
|
||||
histfile.setFileName(historypath + getNewFileName (filetype, startTimeOfBackup));
|
||||
needNewFile= true;
|
||||
}
|
||||
|
||||
// We have an existing history record
|
||||
if (! fileNames.isEmpty()) {
|
||||
// See if this file is large enough that we want to create a new file
|
||||
if (histfile.size() > maxHistFileSize) {
|
||||
memcpy (&hdr, newhdr, sizeof(DV6_HEADER));
|
||||
for (int i = 0; i < 4; i++)
|
||||
hdr.recordStart[i] = 0;
|
||||
histfile.setFileName(historypath + fileNames.first()); // File names are in reverse order, so latest is first
|
||||
|
||||
if (!histfile.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "DV6 RollingBackup could not open" << fileNames.first() << "for reading, error code" << histfile.error() << histfile.errorString();
|
||||
// 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;
|
||||
}
|
||||
record_length = hdr.recordLength;
|
||||
histfilesize = histfile.size();
|
||||
QByteArray dataBA = histfile.read(sizeof(DV6_HEADER));
|
||||
memcpy (&hdr, dataBA.data(), sizeof(DV6_HEADER));
|
||||
lastTimeInFile = convertTime(hdr.lasttime);
|
||||
|
||||
wrap_record = convertNum(hdr.recordStart);
|
||||
if (!histfile.seek(sizeof(DV6_HEADER) + (wrap_record-1) * record_length)) {
|
||||
qWarning() << "DV6 RollingBackup unable to make initial seek to record" << wrap_record
|
||||
<< "in" + histfile.fileName() << histfile.error() << histfile.errorString();
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(filetype)
|
||||
Q_UNUSED(newhdr)
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -994,13 +1035,62 @@ bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) {
|
||||
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(QByteArray dataBA) {
|
||||
Q_UNUSED(dataBA)
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1018,7 +1108,7 @@ public:
|
||||
hdr = nullptr;
|
||||
}
|
||||
|
||||
bool open (QString fn); // Open the file
|
||||
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
|
||||
|
||||
@ -1043,13 +1133,33 @@ private:
|
||||
unsigned char * data = nullptr; // record pointer
|
||||
};
|
||||
|
||||
bool RollingFile::open(QString filetype) {
|
||||
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 RollingFile could not open" << filename << "for reading, error code" << file.error() << file.errorString();
|
||||
qWarning() << "DV6 rf(open) could not open" << filename << "for reading, error code" << file.error() << file.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1068,30 +1178,48 @@ bool RollingFile::open(QString filetype) {
|
||||
// Create buffer to hold each record as it is read
|
||||
data = new unsigned char[record_length];
|
||||
|
||||
// Seek to first data record in file
|
||||
if (!file.seek(sizeof(DV6_HEADER) + wrap_record * record_length)) {
|
||||
qWarning() << "DV6 RollingFile unable to make initial seek to record" << wrap_record << "in" + filename << file.error() << file.errorString();
|
||||
// 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;
|
||||
}
|
||||
#ifdef ROLLBACKUP
|
||||
if (!rb.open(filetype, hdr)) {
|
||||
qWarning() << "DV6 RollingBackup failed";
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
qDebug() << "DV6 rf(open)" << filetype << "positioning to oldest record at pos" << seekpos << "after seek" << file.pos();
|
||||
|
||||
qDebug() << "DV6 RollingFile opening" << filename << "at wrap record" << wrap_record;
|
||||
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();
|
||||
|
||||
#ifdef ROLLBACKUP
|
||||
rb.close();
|
||||
#endif
|
||||
|
||||
if (data)
|
||||
delete [] data;
|
||||
@ -1105,44 +1233,54 @@ bool RollingFile::close() {
|
||||
|
||||
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))
|
||||
return nullptr;
|
||||
|
||||
// Hare we reached end of file and need to wrap around to beginning?
|
||||
if (file.atEnd()) {
|
||||
if (wrapping) {
|
||||
qDebug() << "DV6 RollingFile wrap - second time through";
|
||||
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;
|
||||
}
|
||||
qDebug() << "DV6 RollingFile wrapping to beginning of data in" << filename << "record number is" << record_number-1 << "records read" << number_read;
|
||||
record_number = 0;
|
||||
}
|
||||
|
||||
// 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 RollingFile unable to seek to first data record in file";
|
||||
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 RollingFile record" << record_number << "wrong length";
|
||||
qWarning() << "DV6 rf(get) #" << record_number << "wrong length";
|
||||
file.close();
|
||||
return nullptr;
|
||||
}
|
||||
#ifdef ROLLBACKUP
|
||||
|
||||
if (!rb.save(dataBA)) {
|
||||
qWarning() << "DV6 RollingBackup failed";
|
||||
qWarning() << "DV6 rf(get) failed";
|
||||
}
|
||||
#endif
|
||||
|
||||
number_read++;
|
||||
|
||||
// qDebug() << "RollingFile read" << filename << "record number" << record_number << "of length" << record_length << "number read so far" << 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;
|
||||
}
|
||||
@ -1163,15 +1301,6 @@ QByteArray fileChecksum(const QString &fileName,
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
/***
|
||||
// Return the OSCAR date that the last data was written.
|
||||
// This will be considered to be the last day for which we have any data.
|
||||
// Adjust to get the correct date for sessions starting after midnight.
|
||||
QDate getLastDate () {
|
||||
return QDate();
|
||||
}
|
||||
***/
|
||||
|
||||
// Return date used within OSCAR, assuming day ends at split time in preferences (usually noon)
|
||||
QDate getNominalDate (QDateTime dt) {
|
||||
QDate d = dt.date();
|
||||
@ -1213,15 +1342,17 @@ bool load6Sessions () {
|
||||
// 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::fromTime_t(ts1).toString("MM/dd/yyyy hh:mm:ss") << ts1 << "to" << QDateTime::fromTime_t(ts2).toString("MM/dd/yyyy hh:mm:ss") << ts2;
|
||||
#endif
|
||||
//#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.haveHighResData = false;
|
||||
sinfo.firstHighRes = 0;
|
||||
sinfo.lastHighRes = 0;
|
||||
|
||||
SessionData[ts1] = sinfo;
|
||||
} while (true);
|
||||
@ -1241,6 +1372,8 @@ 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
|
||||
@ -1361,6 +1494,8 @@ bool load6VersionInfo(const QString & path) {
|
||||
QByteArray str;
|
||||
|
||||
QFile f(path+"/VER.BIN");
|
||||
if (rebuild_from_backups)
|
||||
f.setFileName(rebuild_path+"/VER.BIN");
|
||||
info.series = "DV6";
|
||||
info.brand = "DeVilbiss";
|
||||
|
||||
@ -1434,19 +1569,21 @@ int create6Sessions() {
|
||||
|
||||
if (mach->SessionExists(sid)) {
|
||||
// skip already imported sessions..
|
||||
qDebug() << "Session already exists" << QDateTime::fromTime_t(sid).toString("MM/dd/yyyy hh:mm:ss");
|
||||
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::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "to" << QDateTime::fromTime_t(sinfo->end).toString("MM/dd/yyyy hh:mm:ss");
|
||||
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->haveHighResData = false;
|
||||
sinfo->firstHighRes = 0;
|
||||
sinfo->lastHighRes = 0;
|
||||
|
||||
sess->really_set_first(quint64(sinfo->begin) * 1000L);
|
||||
sess->really_set_last(quint64(sinfo->end) * 1000L);
|
||||
@ -1464,9 +1601,9 @@ int create6Sessions() {
|
||||
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_VSnore, EVL_Event);
|
||||
sess->AddEventList(CPAP_Hypopnea, EVL_Event);
|
||||
sess->AddEventList(CPAP_NRI, EVL_Event);
|
||||
// sess->AddEventList(CPAP_LeakFlag, EVL_Event);
|
||||
@ -1483,7 +1620,7 @@ int create6Sessions() {
|
||||
//?? SessionEnd[z] = 0;
|
||||
//?? break;
|
||||
//?? }
|
||||
qDebug() << sid << "has double ups" << QDateTime::fromTime_t(sid).toString("MM/dd/yyyy hh:mm:ss");
|
||||
qDebug() << sid << "has double ups" << QDateTime::fromSecsSinceEpoch(sid).toString("MM/dd/yyyy hh:mm:ss");
|
||||
|
||||
/*Session *sess=Sessions[sid];
|
||||
Sessions.erase(Sessions.find(sid));
|
||||
@ -1560,9 +1697,9 @@ bool load6HighResData () {
|
||||
|
||||
if (rec_ts1 < previousRecBegin) {
|
||||
qWarning() << "R.BIN - Corruption/Out of sequence data found, skipping record" << rf.recnum() << ", prev"
|
||||
<< QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin
|
||||
<< QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin
|
||||
<< "this"
|
||||
<< QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1;
|
||||
<< QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1592,8 +1729,8 @@ bool load6HighResData () {
|
||||
// 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::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss")
|
||||
<< "looking for" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
|
||||
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) {
|
||||
@ -1657,10 +1794,13 @@ bool load6HighResData () {
|
||||
qint64 ti = qint64(rec_ts1) * 1000;
|
||||
flow->AddWaveform(ti,R->breath,50,2000);
|
||||
pressure->AddWaveform(ti, &R->pressure1, 2, 2000);
|
||||
sinfo->haveHighResData = true;
|
||||
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
|
||||
//////////////////////////////////////////////////////////////////
|
||||
@ -2050,18 +2190,20 @@ bool load6PerMinute () {
|
||||
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::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin
|
||||
<< QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin
|
||||
<< "this"
|
||||
<< QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1;
|
||||
<< 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::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
|
||||
<< "previous" << QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss");
|
||||
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;
|
||||
@ -2071,7 +2213,7 @@ bool load6PerMinute () {
|
||||
// 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::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
|
||||
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
|
||||
@ -2095,9 +2237,9 @@ bool load6PerMinute () {
|
||||
|
||||
if (rec_ts1 < previousRecBegin) {
|
||||
qWarning() << "L.BIN - Corruption/Out of sequence data found, stopping import, prev"
|
||||
<< QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss")
|
||||
<< QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss")
|
||||
<< "this"
|
||||
<< QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
|
||||
<< QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
|
||||
break;
|
||||
}
|
||||
|
||||
@ -2114,10 +2256,9 @@ bool load6PerMinute () {
|
||||
if (sess->last()/1000 > sinfo->end)
|
||||
sinfo->end = sess->last()/1000;
|
||||
|
||||
if (!sinfo->haveHighResData) {
|
||||
// if (!sinfo->haveHighResData) {
|
||||
// Don't use this pressure if we already have higher resolution data
|
||||
Pressure = sess->AddEventList(CPAP_Pressure, EVL_Event);
|
||||
}
|
||||
|
||||
if (sinfo->mode == MODE_UNKNOWN) {
|
||||
if (rec->pressureLimitLow != rec->pressureLimitHigh) {
|
||||
sess->settings[CPAP_PressureMin] = rec->pressureLimitLow / 10.0f;
|
||||
@ -2141,7 +2282,29 @@ bool load6PerMinute () {
|
||||
leak->AddEvent(ti, rec->avgLeak); //???
|
||||
RR->AddEvent(ti, rec->breathRate);
|
||||
|
||||
if (Pressure) Pressure->AddEvent(ti, rec->avgPressure / 10.0f); // average pressure
|
||||
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 );
|
||||
@ -2184,6 +2347,7 @@ bool load6EventData () {
|
||||
EventList * EP = nullptr;
|
||||
EventList * SN = nullptr;
|
||||
EventList * FL = nullptr;
|
||||
// EventList * FLG = nullptr;
|
||||
|
||||
if (!rf.open("E.BIN")) {
|
||||
qWarning() << "DV6 Unable to open E.BIN";
|
||||
@ -2209,7 +2373,7 @@ bool load6EventData () {
|
||||
// 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::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
|
||||
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
|
||||
@ -2227,11 +2391,14 @@ bool load6EventData () {
|
||||
sess->set_last(LL->last());
|
||||
if (EP->last() > 0)
|
||||
sess->set_last(EP->last());
|
||||
if (SN->last() > 0)
|
||||
sess->set_last(SN->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;
|
||||
@ -2245,16 +2412,18 @@ bool load6EventData () {
|
||||
|
||||
// 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 to match flow data";
|
||||
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() << "E.BIN - Corruption/Out of sequence data found, stopping import, prev"
|
||||
<< QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss")
|
||||
<< "this"
|
||||
<< QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
|
||||
break;
|
||||
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
|
||||
@ -2268,10 +2437,12 @@ bool load6EventData () {
|
||||
PB = sess->AddEventList(CPAP_PB, EVL_Event);
|
||||
LL = sess->AddEventList(CPAP_LargeLeak, EVL_Event);
|
||||
EP = sess->AddEventList(CPAP_ExP, EVL_Event);
|
||||
// SN = sess->AddEventList(CPAP_VSnore, EVL_Event);
|
||||
SN = sess->AddEventList(INTP_SnoreFlag, EVL_Event);
|
||||
FL = sess->AddEventList(CPAP_FlowLimit, EVL_Event);
|
||||
|
||||
SN = sess->AddEventList(CPAP_Snore, EVL_Waveform, 1.0f, 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));
|
||||
// SN = sess->AddEventList(CPAP_Snore, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2));
|
||||
|
||||
inSession = true;
|
||||
}
|
||||
}
|
||||
@ -2281,11 +2452,12 @@ bool load6EventData () {
|
||||
// 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::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
|
||||
qDebug() << "E.BIN at" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
|
||||
<< "reports duration of" << duration
|
||||
<< "ending" << QDateTime::fromTime_t(rec_ts2).toString("MM/dd/yyyy hh:mm:ss");
|
||||
<< "ending" << QDateTime::fromSecsSinceEpoch(rec_ts2).toString("MM/dd/yyyy hh:mm:ss");
|
||||
}
|
||||
int code = rec->event_type;
|
||||
/***
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// Show Snore Events as a graph
|
||||
//////////////////////////////////////////////////////////////////
|
||||
@ -2293,6 +2465,15 @@ bool load6EventData () {
|
||||
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:
|
||||
@ -2300,33 +2481,33 @@ bool load6EventData () {
|
||||
break;
|
||||
case 2:
|
||||
OA->AddEvent(ti, duration);
|
||||
// qDebug() << "E.BIN - OA" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
|
||||
// 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::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
|
||||
// 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::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
|
||||
// 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::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
|
||||
// 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::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
|
||||
// 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::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
|
||||
// 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;
|
||||
@ -2359,7 +2540,7 @@ int addSessions() {
|
||||
}
|
||||
#ifdef DEBUG6
|
||||
else
|
||||
qDebug() << "Added session" << sess->session() << QDateTime::fromTime_t(sess->session()).toString("MM/dd/yyyy hh:mm:ss");;
|
||||
qDebug() << "Added session" << sess->session() << QDateTime::fromSecsSinceEpoch(sess->session()).toString("MM/dd/yyyy hh:mm:ss");;
|
||||
#endif
|
||||
|
||||
// Update indexes, process waveform and perform flagging
|
||||
@ -2370,8 +2551,8 @@ int addSessions() {
|
||||
|
||||
// Unload them from memory
|
||||
sess->TrashEvents();
|
||||
} else
|
||||
qWarning() << "addSessions: session pointer is null";
|
||||
} // else
|
||||
// qWarning() << "addSessions: session pointer is null";
|
||||
}
|
||||
|
||||
return SessionData.size();
|
||||
@ -2391,24 +2572,10 @@ bool backup6 (const QString & path) {
|
||||
QDir ipath(path);
|
||||
QDir cpath(card_path);
|
||||
QDir bpath(backup_path);
|
||||
|
||||
if ( ! bpath.exists()) {
|
||||
if ( ! bpath.mkpath(backup_path) ) {
|
||||
qWarning() << "Could not create DV6 backup directory" << backup_path;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
QDir hpath(history_path);
|
||||
|
||||
// Copy input data to backup location
|
||||
copyPath(ipath.absolutePath(), bpath.absolutePath());
|
||||
|
||||
// Create history directory for dated backups
|
||||
QDir hpath(history_path);
|
||||
if ( ! hpath.exists())
|
||||
if ( ! hpath.mkpath(history_path)) {
|
||||
qWarning() << "Could not create DV6 archive directory" << history_path;
|
||||
return false;
|
||||
}
|
||||
copyPath(ipath.absolutePath(), bpath.absolutePath(), true);
|
||||
|
||||
// Create archive of settings file if needed (SET.BIN)
|
||||
bool backup_settings = true;
|
||||
@ -2460,10 +2627,12 @@ bool init6Environment (const QString & path) {
|
||||
|
||||
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
|
||||
@ -2472,6 +2641,20 @@ bool init6Environment (const QString & path) {
|
||||
} 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;
|
||||
@ -2486,6 +2669,10 @@ 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();
|
||||
|
||||
@ -2505,10 +2692,16 @@ int IntellipapLoader::OpenDV6(const QString & path)
|
||||
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())
|
||||
@ -2530,6 +2723,9 @@ int IntellipapLoader::OpenDV6(const QString & path)
|
||||
if (!load6EventData())
|
||||
return -1;
|
||||
|
||||
emit updateMessage(QObject::tr("Finishing up..."));
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
// Finalize input
|
||||
return addSessions();
|
||||
}
|
||||
@ -2580,6 +2776,14 @@ void IntellipapLoader::initChannels()
|
||||
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;
|
||||
|
111
oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.cpp
Normal file
111
oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.cpp
Normal file
@ -0,0 +1,111 @@
|
||||
/* SleepLib SleepStyle Loader Implementation
|
||||
*
|
||||
* Copyright (c) 2020 The OSCAR Team
|
||||
* Copyright (c) 2011-2018 Mark Watkins <mark@jedimark.net>
|
||||
*
|
||||
* This file is subject to the terms and conditions of the GNU General Public
|
||||
* License. See the file COPYING in the main directory of the source code
|
||||
* for more details. */
|
||||
|
||||
#include <QApplication>
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QTextStream>
|
||||
#include <QDebug>
|
||||
#include <QStringList>
|
||||
#include <cmath>
|
||||
|
||||
#include "SleepLib/session.h"
|
||||
#include "SleepLib/calcs.h"
|
||||
|
||||
#include "SleepLib/loader_plugins/sleepstyle_EDFinfo.h"
|
||||
|
||||
|
||||
SleepStyleEDFInfo::SleepStyleEDFInfo() : EDFInfo() {
|
||||
setTimeZoneUTC(); // Ask EDF Parser to assume data is in UTC, not in local time
|
||||
}
|
||||
SleepStyleEDFInfo::~SleepStyleEDFInfo() { }
|
||||
|
||||
bool SleepStyleEDFInfo::Parse( ) // overrides and calls the super's Parse
|
||||
{
|
||||
if ( ! EDFInfo::Parse( ) ) {
|
||||
qWarning() << "sleepStyle EDFInfo::Parse failed!";
|
||||
// sleep(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now massage some stuff into OSCAR's layout
|
||||
// Extract the serial number from header string
|
||||
QStringList parts = edfHdr.recordingident.split(' ');
|
||||
serialnumber = parts[6];
|
||||
|
||||
if (!edfHdr.startdate_orig.isValid()) {
|
||||
qDebug() << "sleepStyle EDFInfo::Parse Invalid date time retreieved parsing EDF File" << filename;
|
||||
// sleep(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
startdate = qint64(edfHdr.startdate_orig.toTime_t()) * 1000L;
|
||||
//startdate-=timezoneOffset();
|
||||
if (startdate == 0) {
|
||||
qDebug() << "sleepStyle EDFInfo::Parse Invalid startdate = 0 in EDF File" << filename;
|
||||
// sleep(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
dur_data_record = (edfHdr.duration_Seconds * 1000.0L);
|
||||
|
||||
enddate = startdate + dur_data_record * qint64(edfHdr.num_data_records);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
extern QHash<ChannelID, QStringList> resmed_codes;
|
||||
|
||||
// Looks up foreign language Signal names that match this channelID
|
||||
EDFSignal *SleepStyleEDFInfo::lookupSignal(ChannelID ch)
|
||||
{
|
||||
// Get list of all known foreign language names for this channel
|
||||
auto channames = resmed_codes.find(ch);
|
||||
if (channames == resmed_codes.end()) {
|
||||
// no alternatives strings found for this channel
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// This is bad, because ResMed thinks it was a cool idea to use two channels with the same name.
|
||||
|
||||
// Scan through EDF's list of signals to see if any match
|
||||
for (auto & name : channames.value()) {
|
||||
EDFSignal *sig = lookupLabel(name);
|
||||
if (sig)
|
||||
return sig;
|
||||
}
|
||||
|
||||
// Failed
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QDateTime SleepStyleEDFInfo::getStartDT( QString dateTimeStr )
|
||||
{
|
||||
// edfHdr.startdate_orig = QDateTime::fromString(QString::fromLatin1(hdrPtr->datetime, 16), "dd.MM.yyHH.mm.ss");
|
||||
// QString dateTimeStr; // , dateStr, timeStr;
|
||||
QDate qDate;
|
||||
QTime qTime;
|
||||
// dateTimeStr = QString::fromLatin1(hdrPtr->datetime, 16);
|
||||
// dateStr = dateTimeStr.left(8);
|
||||
// timeStr = dateTimeStr.right(8);
|
||||
qDate = QDate::fromString(dateTimeStr.left(8), "dd.MM.yy");
|
||||
qTime = QTime::fromString(dateTimeStr.right(8), "HH.mm.ss");
|
||||
return QDateTime(qDate, qTime, Qt::UTC);
|
||||
}
|
||||
|
||||
|
||||
void dumpEDFduration( ssEDFduration dur )
|
||||
{
|
||||
qDebug() << "Fullpath" << dur.path << "Filename" << dur.filename << "Start" << dur.start << "End" << dur.end;
|
||||
}
|
||||
|
64
oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.h
Normal file
64
oscar/SleepLib/loader_plugins/sleepstyle_EDFinfo.h
Normal file
@ -0,0 +1,64 @@
|
||||
/* SleepLib SleepStyle EDFinfo Header
|
||||
*
|
||||
* Copyright (C) 2011-2018 Mark Watkins <mark@jedimark.net>
|
||||
*
|
||||
* This file is subject to the terms and conditions of the GNU General Public
|
||||
* License. See the file COPYING in the main directory of the source code
|
||||
* for more details. */
|
||||
|
||||
#ifndef SLEEPSTYLE_EDFINFO_H
|
||||
#define SLEEPSTYLE_EDFINFO_H
|
||||
|
||||
#include <QVector>
|
||||
#include "SleepLib/machine.h" // Base class: MachineLoader
|
||||
#include "SleepLib/machine_loader.h"
|
||||
#include "SleepLib/profiles.h"
|
||||
#include "SleepLib/loader_plugins/edfparser.h"
|
||||
|
||||
//enum EDFType { EDF_UNKNOWN, EDF_BRP, EDF_PLD, EDF_SAD, EDF_EVE, EDF_CSL, EDF_AEV };
|
||||
enum EDFType { EDF_UNKNOWN, EDF_RT };
|
||||
|
||||
// EDFType lookupEDFType(const QString & filename);
|
||||
|
||||
const QString SLEEPSTYLE_class_name = STR_MACH_ResMed;
|
||||
|
||||
//class STRFile; // forward
|
||||
|
||||
class SleepStyleEDFInfo : public EDFInfo
|
||||
{
|
||||
public:
|
||||
SleepStyleEDFInfo();
|
||||
~SleepStyleEDFInfo();
|
||||
|
||||
virtual bool Parse() override; // overrides and calls the super's Parse
|
||||
|
||||
virtual qint64 GetDurationMillis() { return dur_data_record; } // overrides the super
|
||||
|
||||
EDFSignal *lookupSignal(ChannelID ch);
|
||||
|
||||
QDateTime getStartDT( QString dateTimeStr );
|
||||
|
||||
//! \brief The following are computed from the edfHdr data
|
||||
QString serialnumber;
|
||||
qint64 dur_data_record;
|
||||
qint64 startdate;
|
||||
qint64 enddate;
|
||||
};
|
||||
|
||||
class ssEDFduration
|
||||
{
|
||||
public:
|
||||
ssEDFduration() { start = end = 0; type = EDF_UNKNOWN; }
|
||||
ssEDFduration(quint32 start, quint32 end, QString path) :
|
||||
start(start), end(end), path(path) {}
|
||||
|
||||
quint32 start;
|
||||
quint32 end;
|
||||
QString path;
|
||||
QString filename;
|
||||
EDFType type;
|
||||
};
|
||||
|
||||
void dumpEDFduration( ssEDFduration dur );
|
||||
|
||||
#endif // SLEEPSTYLE_EDFINFO_H
|
936
oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp
Normal file
936
oscar/SleepLib/loader_plugins/sleepstyle_loader.cpp
Normal file
@ -0,0 +1,936 @@
|
||||
/* SleepLib Fisher & Paykel SleepStyle Loader Implementation
|
||||
*
|
||||
* Copyright (c) 2020 The Oscar Team
|
||||
*
|
||||
* Derived from icon_loader.cpp
|
||||
* Copyright (c) 2011-2018 Mark Watkins <mark@jedimark.net>
|
||||
*
|
||||
* This file is subject to the terms and conditions of the GNU General Public
|
||||
* License. See the file COPYING in the main directory of the source code
|
||||
* for more details. */
|
||||
|
||||
#include <QDir>
|
||||
#include <QMessageBox>
|
||||
#include <QDataStream>
|
||||
#include <QTextStream>
|
||||
#include <QCoreApplication>
|
||||
#include <cmath>
|
||||
|
||||
#include "sleepstyle_loader.h"
|
||||
#include "sleepstyle_EDFinfo.h"
|
||||
|
||||
const QString FPHCARE = "FPHCARE";
|
||||
|
||||
SleepStyle::SleepStyle(Profile *profile, MachineID id)
|
||||
: CPAP(profile, id)
|
||||
{
|
||||
}
|
||||
|
||||
SleepStyle::~SleepStyle()
|
||||
{
|
||||
}
|
||||
|
||||
SleepStyleLoader::SleepStyleLoader()
|
||||
{
|
||||
m_buffer = nullptr;
|
||||
m_type = MT_CPAP;
|
||||
}
|
||||
|
||||
SleepStyleLoader::~SleepStyleLoader()
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
* getIconDir - returns the path to the ICON directory
|
||||
*/
|
||||
QString getIconDir (QString givenpath) {
|
||||
|
||||
QString path = givenpath;
|
||||
|
||||
path = path.replace("\\", "/");
|
||||
|
||||
if (path.endsWith("/")) {
|
||||
path.chop(1);
|
||||
}
|
||||
|
||||
if (path.endsWith("/" + FPHCARE)) {
|
||||
path = path.section("/",0,-2);
|
||||
}
|
||||
|
||||
QDir dir(path);
|
||||
|
||||
if (!dir.exists()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// If this is a backup directory, higher level directories have been
|
||||
// omitted.
|
||||
if (path.endsWith("/Backup/", Qt::CaseInsensitive))
|
||||
return path;
|
||||
|
||||
// F&P Icon have a folder called FPHCARE in the root directory
|
||||
if (!dir.exists(FPHCARE)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// CHECKME: I can't access F&P ICON data right now
|
||||
if (!dir.exists("FPHCARE/ICON")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return dir.filePath("FPHCARE/ICON");
|
||||
}
|
||||
|
||||
/*
|
||||
* getSleepStyleMachines returns a list of all SleepStyle machine folders in the ICON directory
|
||||
*/
|
||||
QStringList getSleepStyleMachines (QString iconPath) {
|
||||
QStringList ssMachines;
|
||||
|
||||
QDir iconDir (iconPath);
|
||||
|
||||
// SleepStyle are mixed alpha and numeric; ICON serial numbers (directory names) are all digits
|
||||
iconDir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
|
||||
iconDir.setSorting(QDir::Name);
|
||||
|
||||
QFileInfoList flist = iconDir.entryInfoList(); // List of Icon subdirectories
|
||||
|
||||
bool isIconFilename;
|
||||
|
||||
// Walk though directory list and save those that appear to be for SleepStyle machins.
|
||||
for (int i = 0; i < flist.size(); i++) {
|
||||
QFileInfo fi = flist.at(i);
|
||||
QString filename = fi.fileName();
|
||||
filename.toInt(&isIconFilename);
|
||||
if (isIconFilename) // Ignore this directory if named as used for older F&P Icon machine
|
||||
continue;
|
||||
if (filename.length() < 8) // F&P machine names are 8 characters long, but we allow more just in case...
|
||||
continue;
|
||||
|
||||
// directory is serial number and must not be all digits (which would make it an ICON directory)
|
||||
// and it must have *.FPH files within it to be a SleepStyle folder
|
||||
|
||||
QDir machineDir (iconPath + "/" + filename);
|
||||
machineDir.setFilter(QDir::NoDotAndDotDot | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
|
||||
machineDir.setSorting(QDir::Name);
|
||||
QStringList filters;
|
||||
filters << "*.fph";
|
||||
machineDir.setNameFilters(filters);
|
||||
QFileInfoList flist = machineDir.entryInfoList();
|
||||
if (flist.size() <= 0) {
|
||||
continue;
|
||||
}
|
||||
ssMachines.push_back(filename);
|
||||
}
|
||||
|
||||
return ssMachines;
|
||||
}
|
||||
|
||||
bool SleepStyleLoader::Detect(const QString & givenpath)
|
||||
{
|
||||
QString iconPath = getIconDir(givenpath);
|
||||
if (iconPath.isEmpty())
|
||||
return false;
|
||||
|
||||
QStringList machines = getSleepStyleMachines(iconPath);
|
||||
if (machines.length() <= 0)
|
||||
// Did not find any SleepStyle machine directories
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SleepStyleLoader::backupData (Machine * mach, const QString & path) {
|
||||
|
||||
QDir ipath(path);
|
||||
QDir bpath(mach->getBackupPath());
|
||||
|
||||
// Compare QDirs rather than QStrings because separators may be different, especially on Windows.
|
||||
|
||||
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 (rebuild_from_backups || !create_backups)
|
||||
return true;
|
||||
|
||||
// Copy input data to backup location
|
||||
copyPath(ipath.absolutePath(), bpath.absolutePath());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
int SleepStyleLoader::Open(const QString & path)
|
||||
{
|
||||
QString iconPath = getIconDir(path);
|
||||
if (iconPath.isEmpty())
|
||||
return false;
|
||||
|
||||
QStringList serialNumbers = getSleepStyleMachines(iconPath);
|
||||
if (serialNumbers.length() <= 0)
|
||||
// Did not find any SleepStyle machine directories
|
||||
return false;
|
||||
|
||||
Machine *m;
|
||||
|
||||
int c = 0;
|
||||
for (int i = 0; i < serialNumbers.size(); i++) {
|
||||
MachineInfo info = newInfo();
|
||||
info.serial = serialNumbers[i];
|
||||
m = p_profile->CreateMachine(info);
|
||||
|
||||
setSerialPath(iconPath + "/" + info.serial);
|
||||
|
||||
try {
|
||||
if (m) {
|
||||
c+=OpenMachine(m, path, serialPath);
|
||||
}
|
||||
} catch (OneTypePerDay& e) {
|
||||
Q_UNUSED(e)
|
||||
p_profile->DelMachine(m);
|
||||
MachList.erase(MachList.find(info.serial));
|
||||
QMessageBox::warning(nullptr, tr("Import Error"),
|
||||
tr("This Machine Record cannot be imported in this profile.")+"\n\n"+tr("The Day records overlap with already existing content."),
|
||||
QMessageBox::Ok);
|
||||
delete m;
|
||||
}
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
int SleepStyleLoader::OpenMachine(Machine *mach, const QString & path, const QString & ssPath)
|
||||
{
|
||||
emit updateMessage(QObject::tr("Getting Ready..."));
|
||||
emit setProgressValue(0);
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
QDir dir(ssPath);
|
||||
|
||||
if (!dir.exists() || (!dir.isReadable())) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
backupData(mach, path);
|
||||
|
||||
qDebug() << "Opening F&P SleepStyle" << ssPath;
|
||||
|
||||
dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
|
||||
dir.setSorting(QDir::Name);
|
||||
QFileInfoList flist = dir.entryInfoList();
|
||||
|
||||
QString filename, fpath;
|
||||
|
||||
emit updateMessage(QObject::tr("Reading data files..."));
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
QStringList summary, det, his;
|
||||
Sessions.clear();
|
||||
|
||||
for (int i = 0; i < flist.size(); i++) {
|
||||
QFileInfo fi = flist.at(i);
|
||||
filename = fi.fileName();
|
||||
fpath = ssPath + "/" + filename;
|
||||
|
||||
if (filename.left(3).toUpper() == "SUM") {
|
||||
summary.push_back(fpath);
|
||||
OpenSummary(mach, fpath);
|
||||
} else if (filename.left(3).toUpper() == "DET") {
|
||||
det.push_back(fpath);
|
||||
} else if (filename.left(3).toUpper() == "HIS") {
|
||||
his.push_back(fpath);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < det.size(); i++) {
|
||||
OpenDetail(mach, det[i]);
|
||||
}
|
||||
|
||||
// Process REALTIME files
|
||||
dir.cd("REALTIME");
|
||||
QFileInfoList rtlist = dir.entryInfoList();
|
||||
for (int i = 0; i < rtlist.size(); i++) {
|
||||
QFileInfo fi = rtlist.at(i);
|
||||
filename = fi.fileName();
|
||||
fpath = ssPath + "/REALTIME/" + filename;
|
||||
if (filename.left(3).toUpper() == "HRD"
|
||||
&& filename.right(3).toUpper() == "EDF" ) {
|
||||
OpenRealTime (mach, filename, fpath);
|
||||
}
|
||||
}
|
||||
|
||||
// LOG files were not processed by icon_loader
|
||||
// So we don't need to do anything
|
||||
|
||||
SessionID sid;//,st;
|
||||
float hours, mins;
|
||||
|
||||
// For diagnostics, print summary of last 20 session or one week
|
||||
qDebug() << "SS Loader - last 20 Sessions:";
|
||||
|
||||
int cnt = 0;
|
||||
QDateTime dt;
|
||||
QString a = "";
|
||||
|
||||
if (Sessions.size() > 0) {
|
||||
|
||||
QMap<SessionID, Session *>::iterator it = Sessions.end();
|
||||
it--;
|
||||
|
||||
dt = QDateTime::fromTime_t(qint64(it.value()->first()) / 1000L);
|
||||
QDate date = dt.date().addDays(-7);
|
||||
it++;
|
||||
|
||||
do {
|
||||
it--;
|
||||
Session *sess = it.value();
|
||||
sid = sess->session();
|
||||
hours = sess->hours();
|
||||
mins = hours * 60;
|
||||
dt = QDateTime::fromTime_t(sid);
|
||||
|
||||
qDebug() << cnt << ":" << dt << "session" << sid << "," << mins << "minutes" << a;
|
||||
|
||||
if (dt.date() < date) {
|
||||
break;
|
||||
}
|
||||
|
||||
++cnt;
|
||||
|
||||
} while (it != Sessions.begin());
|
||||
|
||||
}
|
||||
|
||||
// qDebug() << "Unmatched Sessions";
|
||||
// QList<FPWaveChunk> chunks;
|
||||
// for (QMap<int,QDate>::iterator dit=FLWDate.begin();dit!=FLWDate.end();dit++) {
|
||||
// int k=dit.key();
|
||||
// //QDate date=dit.value();
|
||||
//// QList<Session *> values = SessDate.values(date);
|
||||
// for (int j=0;j<FLWTS[k].size();j++) {
|
||||
|
||||
// FPWaveChunk chunk(FLWTS[k].at(j),FLWDuration[k].at(j),k);
|
||||
// chunk.flow=FLWMapFlow[k].at(j);
|
||||
// chunk.leak=FLWMapLeak[k].at(j);
|
||||
// chunk.pressure=FLWMapPres[k].at(j);
|
||||
|
||||
// chunks.push_back(chunk);
|
||||
|
||||
// zz=FLWTS[k].at(j)/1000;
|
||||
// dur=double(FLWDuration[k].at(j))/60000.0;
|
||||
// bool b,c=false;
|
||||
// if (Sessions.contains(zz)) b=true; else b=false;
|
||||
// if (b) {
|
||||
// if (Sessions[zz]->channelDataExists(CPAP_FlowRate)) c=true;
|
||||
// }
|
||||
// qDebug() << k << "-" <<j << ":" << zz << qRound(dur) << "minutes" << (b ? "*" : "") << (c ? QDateTime::fromTime_t(zz).toString() : "");
|
||||
// }
|
||||
// }
|
||||
// std::sort(chunks.begin(), chunks.end());
|
||||
// bool b,c;
|
||||
// for (int i=0;i<chunks.size();i++) {
|
||||
// const FPWaveChunk & chunk=chunks.at(i);
|
||||
// zz=chunk.st/1000;
|
||||
// dur=double(chunk.duration)/60000.0;
|
||||
// if (Sessions.contains(zz)) b=true; else b=false;
|
||||
// if (b) {
|
||||
// if (Sessions[zz]->channelDataExists(CPAP_FlowRate)) c=true;
|
||||
// }
|
||||
// qDebug() << chunk.file << ":" << i << zz << dur << "minutes" << (b ? "*" : "") << (c ? QDateTime::fromTime_t(zz).toString() : "");
|
||||
// }
|
||||
|
||||
int c = Sessions.size();
|
||||
qDebug() << "SS Loader found" << c << "sessions";
|
||||
|
||||
emit updateMessage(QObject::tr("Finishing up..."));
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
finishAddingSessions();
|
||||
|
||||
mach->Save();
|
||||
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
// !\brief Convert F&P 32bit date format to 32bit UNIX Timestamp
|
||||
quint32 ssconvertDate(quint32 timestamp)
|
||||
{
|
||||
quint16 day, month,hour=0, minute=0, second=0;
|
||||
quint16 year;
|
||||
|
||||
|
||||
day = timestamp & 0x1f;
|
||||
month = (timestamp >> 5) & 0x0f;
|
||||
year = 2000 + ((timestamp >> 9) & 0x3f);
|
||||
quint32 ts2 = timestamp >> 15;
|
||||
second = ts2 & 0x3f;
|
||||
minute = (ts2 >> 6) & 0x3f;
|
||||
hour = (ts2 >> 12);
|
||||
|
||||
QDateTime dt = QDateTime(QDate(year, month, day), QTime(hour, minute, second), Qt::UTC);
|
||||
|
||||
qDebug().noquote() << "SS timestamp" << timestamp << year << month << day << dt << hour << minute << second;
|
||||
|
||||
// Q NO!!! _ASSERT(dt.isValid());
|
||||
// if ((year == 2013) && (month == 9) && (day == 18)) {
|
||||
// // this is for testing.. set a breakpoint on here and
|
||||
// int i=5;
|
||||
// }
|
||||
|
||||
|
||||
// From Rudd's data set compared to times reported from his F&P software's report (just the time bits left over)
|
||||
// 90514 = 00:06:18 WET 23:06:18 UTC 09:06:18 AEST
|
||||
// 94360 = 01:02:24 WET
|
||||
// 91596 = 00:23:12 WET
|
||||
// 19790 = 23:23:50 WET
|
||||
|
||||
return dt.addSecs(-54).toTime_t(); // Huh? Why do this?
|
||||
}
|
||||
|
||||
// SessionID is in seconds, not msec
|
||||
SessionID SleepStyleLoader::findSession (SessionID sid) {
|
||||
for(auto sessKey : Sessions.keys())
|
||||
{
|
||||
Session * sess = Sessions.value(sessKey);
|
||||
if (sid >= (sess->realFirst() / 1000L) && sid <= (sess->realLast() / 1000L))
|
||||
return sessKey;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SleepStyleLoader::OpenRealTime(Machine *mach, const QString & fname, const QString & filepath)
|
||||
{
|
||||
// Q_UNUSED(filepath)
|
||||
Q_UNUSED(mach)
|
||||
Q_UNUSED(fname)
|
||||
|
||||
SleepStyleEDFInfo edf;
|
||||
|
||||
// Open the EDF file and read contents into edf object
|
||||
if (!edf.Open(filepath)) {
|
||||
qWarning() << "SS Realtime failed to open" << filepath;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!edf.Parse()) {
|
||||
qWarning() << "SS Realtime Parse failed to open" << filepath;
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug().noquote() << "SS ORT timestamp" << edf.startdate / 1000L << QDateTime::fromSecsSinceEpoch(edf.startdate / 1000L).toString("MM/dd/yyyy hh:mm:ss");
|
||||
SessionID sessKey = findSession(edf.startdate / 1000L);
|
||||
if (sessKey == 0) {
|
||||
qWarning() << "SS ORT session not found";
|
||||
return true;
|
||||
}
|
||||
|
||||
Session * sess = Sessions.value(sessKey);
|
||||
|
||||
if (sess == nullptr) {
|
||||
qWarning() << "SS ORT session not found - nullptr";
|
||||
return true;
|
||||
}
|
||||
|
||||
sess->updateFirst(edf.startdate);
|
||||
|
||||
qint64 duration = edf.GetNumDataRecords() * edf.GetDurationMillis();
|
||||
sess->updateLast(edf.startdate + duration);
|
||||
|
||||
// Find the leak signal and data
|
||||
long leakrecs = 0;
|
||||
EDFSignal leakSignal;
|
||||
for (auto & esleak : edf.edfsignals) {
|
||||
leakrecs = esleak.sampleCnt * edf.GetNumDataRecords();
|
||||
if (leakrecs < 0)
|
||||
continue;
|
||||
if (esleak.label == "Leak") {
|
||||
leakSignal = esleak;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk through all signals, ignoring leaks
|
||||
for (auto & es : edf.edfsignals) {
|
||||
long recs = es.sampleCnt * edf.GetNumDataRecords();
|
||||
if (recs < 0)
|
||||
continue;
|
||||
ChannelID code = 0;
|
||||
|
||||
if (es.label == "Flow") {
|
||||
// Flow data appears to include total leaks, which are also reported in the edf file.
|
||||
// We subtract the leak from the flow data to get flow data that is centered around zero.
|
||||
// This is needed for other derived graphs (tidal volume, insp and exp times, etc.) to be reasonable
|
||||
code = CPAP_FlowRate;
|
||||
bool done = false;
|
||||
if (leakrecs > 0) {
|
||||
for (int ileak = 0; ileak < leakrecs && !done; ileak++) {
|
||||
for (int iflow = 0; iflow < 25 && !done; iflow++) {
|
||||
if (ileak*25 + iflow >= recs) {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
es.dataArray[ileak*25 + iflow] -= leakSignal.dataArray[ileak] - 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (es.label == "Pressure") {
|
||||
code = CPAP_MaskPressure;
|
||||
|
||||
} else
|
||||
continue;
|
||||
|
||||
if (code) {
|
||||
double rate = double(duration) / double(recs);
|
||||
EventList *a = sess->AddEventList(code, EVL_Waveform, es.gain, es.offset, 0, 0, rate);
|
||||
a->setDimension(es.physical_dimension);
|
||||
a->AddWaveform(edf.startdate, es.dataArray, recs, duration);
|
||||
|
||||
EventDataType min = a->Min();
|
||||
EventDataType max = a->Max();
|
||||
|
||||
// Cap to physical dimensions, because there can be ram glitches/whatever that throw really big outliers.
|
||||
if (min < es.physical_minimum)
|
||||
min = es.physical_minimum;
|
||||
if (max > es.physical_maximum)
|
||||
max = es.physical_maximum;
|
||||
|
||||
sess->updateMin(code, min);
|
||||
sess->updateMax(code, max);
|
||||
sess->setPhysMin(code, es.physical_minimum);
|
||||
sess->setPhysMax(code, es.physical_maximum);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Open Summary file, create list of sessions and session summary data
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
bool SleepStyleLoader::OpenSummary(Machine *mach, const QString & filename)
|
||||
{
|
||||
qDebug() << filename;
|
||||
QByteArray header;
|
||||
QFile file(filename);
|
||||
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
qDebug() << "SS SUM Couldn't open" << filename;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read header of summary file
|
||||
header = file.read(0x200);
|
||||
|
||||
if (header.size() != 0x200) {
|
||||
qDebug() << "SS SUM Short file" << filename;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Header is terminated by ';' at 0x1ff
|
||||
unsigned char hterm = 0x3b;
|
||||
|
||||
if (hterm != header[0x1ff]) {
|
||||
qWarning() << "SS SUM Header missing ';' terminator" << filename;
|
||||
}
|
||||
|
||||
QTextStream htxt(&header);
|
||||
QString h1, version, fname, serial, model, type, unknownident;
|
||||
htxt >> h1;
|
||||
htxt >> version;
|
||||
htxt >> fname;
|
||||
htxt >> serial;
|
||||
htxt >> model; //TODO: Should become Series in machine info???
|
||||
htxt >> type; // SPSAAN etc with 4th character being A (Auto) or C (CPAP)
|
||||
htxt >> unknownident; // Constant, but has different value when version number is different.
|
||||
|
||||
qDebug() << "SS SUM header" << h1 << version << fname << serial << model << type << unknownident;
|
||||
|
||||
if (type.length() > 4)
|
||||
type = (type.at(3) == 'C' ? "CPAP" : "Auto");
|
||||
mach->setModel(model + " " + type);
|
||||
|
||||
// Read remainder of summary file
|
||||
QByteArray data;
|
||||
data = file.readAll();
|
||||
file.close();
|
||||
|
||||
QDataStream in(data);
|
||||
in.setVersion(QDataStream::Qt_4_8);
|
||||
in.setByteOrder(QDataStream::LittleEndian);
|
||||
|
||||
quint32 ts;
|
||||
//QByteArray line;
|
||||
unsigned char p1, p2, p3, j1, x1, x2;
|
||||
|
||||
unsigned char runTime, useTime, minPressSet, maxPressSet, minPressSeen, pct95PressSeen, maxPressSeen;
|
||||
unsigned char senseAwakeLevel, humidityLevel, smartFlexLevel;
|
||||
|
||||
quint16 c1, c2, c3, c4;
|
||||
// quint16 d1, d2, d3;
|
||||
unsigned char d1, d2, d3, d4, d5, d6;
|
||||
|
||||
int usage; //,runtime;
|
||||
|
||||
QDate date;
|
||||
|
||||
int nblock = 0;
|
||||
|
||||
// Go through blocks of data until end marker is found
|
||||
do {
|
||||
nblock++;
|
||||
|
||||
in >> ts;
|
||||
if (ts == 0xffffffff) {
|
||||
qDebug() << "SS SUM 0xffffffff terminator found at block" << nblock;
|
||||
break;
|
||||
}
|
||||
if ((ts & 0xffff) == 0xfafe) {
|
||||
qDebug() << "SS SUM 0xfafa terminator found at block" << nblock;
|
||||
break;
|
||||
}
|
||||
|
||||
ts = ssconvertDate(ts);
|
||||
|
||||
qDebug() << "\nSS SUM Session" << nblock << "with timestamp" << ts << QDateTime::fromSecsSinceEpoch(ts).toString("MM/dd/yyyy hh:mm:ss");
|
||||
|
||||
// the following two quite often match in value
|
||||
in >> runTime; // 0x04
|
||||
in >> useTime; // 0x05
|
||||
usage = useTime * 360; // Convert to seconds (durations are in .1 hour intervals)
|
||||
|
||||
in >> minPressSeen; // 0x06
|
||||
in >> pct95PressSeen; // 0x07
|
||||
in >> maxPressSeen; // 0x08
|
||||
|
||||
in >> d1; // 0x09
|
||||
in >> d2; // 0x0a
|
||||
in >> d3; // 0x0b
|
||||
in >> d4; // 0x0c
|
||||
in >> d5; // 0x0d
|
||||
in >> d6; // 0x0e
|
||||
|
||||
in >> c1; // 0x0f
|
||||
in >> c2; // 0x11
|
||||
in >> c3; // 0x13
|
||||
in >> c4; // 0x15
|
||||
|
||||
in >> j1; // 0x17
|
||||
|
||||
in >> p1; // 0x18
|
||||
in >> p2; // 0x19
|
||||
in >> p3; // 0x1a
|
||||
|
||||
in >> x1; // 0x1b
|
||||
in >> x2; // 0x1c
|
||||
|
||||
in >> minPressSet;
|
||||
in >> maxPressSet;
|
||||
in >> senseAwakeLevel;
|
||||
in >> humidityLevel;
|
||||
in >> smartFlexLevel;
|
||||
|
||||
// soak up unknown stuff to apparent end of data for the day
|
||||
unsigned char s [6];
|
||||
for (unsigned int i=0; i < sizeof(s); i++)
|
||||
in >> s[i];
|
||||
|
||||
qDebug() << "SS SUM block" << nblock
|
||||
<< "a:" <<"Pressure Min"<<minPressSeen<<"95%"<<pct95PressSeen<<"Max"<<maxPressSeen
|
||||
<< "\nd:" <<d1<<d2<<d3<<d4<<d5<<d6
|
||||
<< "\nj:" <<j1 << " c:" << c1 << c2 << c3 << c4
|
||||
<< "\np:" <<p1<<p2<<p3
|
||||
<< "\nx:" <<x1<<x2
|
||||
<< "\ns:" <<"Min set" <<minPressSet<<"Max set"<<maxPressSet<<"SA"<<senseAwakeLevel<<"Humid"<<humidityLevel<<"SmartFlex"<<smartFlexLevel<<s[0]<<s[1]<<s[2]<<s[3]<<s[4]<<s[5];
|
||||
|
||||
if (runTime != useTime) {
|
||||
qDebug() << "SS SUM run time" << runTime << "!= use time" << useTime << "-" << nblock << QDateTime::fromSecsSinceEpoch(ts).toString("MM/dd/yyyy hh:mm:ss");
|
||||
}
|
||||
|
||||
if (!mach->SessionExists(ts)) {
|
||||
Session *sess = new Session(mach, ts);
|
||||
sess->really_set_first(qint64(ts) * 1000L);
|
||||
sess->really_set_last(qint64(ts + usage) * 1000L);
|
||||
sess->SetChanged(true);
|
||||
/****
|
||||
// TODO: None of the apnea numbers have been confirmed
|
||||
sess->setCount(CPAP_Obstructive, c3);
|
||||
// sess->setCph(CPAP_Obstructive, c3 / (float(usage)/3600.00));
|
||||
|
||||
sess->setCount(CPAP_Hypopnea, c4);
|
||||
// sess->setCph(CPAP_Hypopnea, c4 / (float(usage)/3600.00));
|
||||
|
||||
sess->setCount(CPAP_ClearAirway, c1);
|
||||
// sess->setCph(CPAP_ClearAirway, c1 / (float(usage)/3600.00));
|
||||
|
||||
sess->setCount(CPAP_FlowLimit, c2);
|
||||
// sess->setCph(CPAP_FlowLimit, c2 / (float(usage)/3600.00));
|
||||
****/
|
||||
SessDate.insert(date, sess);
|
||||
|
||||
if (minPressSet != maxPressSet) {
|
||||
sess->settings[CPAP_Mode] = (int)MODE_APAP;
|
||||
sess->settings[CPAP_PressureMin] = minPressSet / 10.0;
|
||||
sess->settings[CPAP_PressureMax] = maxPressSet / 10.0;
|
||||
} else {
|
||||
sess->settings[CPAP_Mode] = (int)MODE_CPAP;
|
||||
sess->settings[CPAP_Pressure] = minPressSet / 10.0;
|
||||
}
|
||||
|
||||
sess->settings[CPAP_HumidSetting] = humidityLevel;
|
||||
sess->settings[SS_SenseAwakeLevel] = senseAwakeLevel / 10.0;
|
||||
sess->settings[CPAP_PresReliefMode] = PR_SMARTFLEX;
|
||||
sess->settings[SS_SmartFlexLevel] = smartFlexLevel / 1.0;
|
||||
|
||||
Sessions[ts] = sess;
|
||||
|
||||
addSession(sess);
|
||||
}
|
||||
} while (!in.atEnd());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Open Detail record contains list of sessions and pressure, leak, and event flags
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
bool SleepStyleLoader::OpenDetail(Machine *mach, const QString & filename)
|
||||
{
|
||||
Q_UNUSED(mach);
|
||||
|
||||
qDebug() << "SS DET Opening Detail" << filename;
|
||||
QByteArray header;
|
||||
QFile file(filename);
|
||||
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
qDebug() << "SS DET Couldn't open" << filename;
|
||||
return false;
|
||||
}
|
||||
|
||||
header = file.read(0x200);
|
||||
|
||||
if (header.size() != 0x200) {
|
||||
qDebug() << "SS DET short file" << filename;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Header is terminated by ';' at 0x1ff
|
||||
unsigned char hterm = 0x3b;
|
||||
|
||||
if (hterm != header[0x1ff]) {
|
||||
file.close();
|
||||
qWarning() << "SS DET Header missing ';' terminator" << filename;
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream htxt(&header);
|
||||
QString h1, version, fname, serial, model, type, unknownident;
|
||||
htxt >> h1;
|
||||
htxt >> version;
|
||||
htxt >> fname;
|
||||
htxt >> serial;
|
||||
htxt >> model; //TODO: Should become Series in machine info???
|
||||
htxt >> type; // SPSAAN etc with 4th character being A (Auto) or C (CPAP)
|
||||
htxt >> unknownident; // Constant, but has different value when version number is different.
|
||||
|
||||
qDebug() << "SS DET file header" << h1 << version << fname << serial << model << type << unknownident;
|
||||
|
||||
// Read session indices
|
||||
QByteArray index = file.read(0x800);
|
||||
if (index.size()!=0x800) {
|
||||
// faulty file..
|
||||
qWarning() << "SS DET file short index block";
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
QDataStream in(index);
|
||||
quint32 ts;
|
||||
|
||||
in.setVersion(QDataStream::Qt_4_6);
|
||||
in.setByteOrder(QDataStream::LittleEndian);
|
||||
|
||||
QVector<quint32> times;
|
||||
QVector<quint16> start;
|
||||
QVector<quint8> records;
|
||||
|
||||
quint16 strt;
|
||||
quint8 recs;
|
||||
quint16 unknownIndex;
|
||||
|
||||
int totalrecs = 0;
|
||||
|
||||
do {
|
||||
// Read timestamp for session and check for end of data signal
|
||||
in >> ts;
|
||||
if (ts == 0xffffffff) break;
|
||||
if ((ts & 0xffff) == 0xfafe) break;
|
||||
|
||||
ts = ssconvertDate(ts);
|
||||
|
||||
in >> strt;
|
||||
in >> recs;
|
||||
in >> unknownIndex;
|
||||
totalrecs += recs; // Number of data records for this session
|
||||
|
||||
qDebug().noquote() << "SS DET block timestamp" << ts << QDateTime::fromSecsSinceEpoch(ts).toString("MM/dd/yyyy hh:mm:ss") << "start" << strt << "records" << recs << "unknown" << unknownIndex;
|
||||
|
||||
if (Sessions.contains(ts)) {
|
||||
times.push_back(ts);
|
||||
start.push_back(strt);
|
||||
records.push_back(recs);
|
||||
}
|
||||
else
|
||||
qDebug() << "SS DET session not found" << ts;
|
||||
} while (!in.atEnd());
|
||||
|
||||
QByteArray databytes = file.readAll();
|
||||
file.close();
|
||||
|
||||
in.setVersion(QDataStream::Qt_4_6);
|
||||
in.setByteOrder(QDataStream::BigEndian);
|
||||
|
||||
// 7 (was 5) byte repeating patterns
|
||||
|
||||
quint8 *data = (quint8 *)databytes.data();
|
||||
|
||||
qint64 ti;
|
||||
quint8 pressure, leak, a1, a2, a3, a4, a5, a6, a7;
|
||||
// quint8 sa1, sa2; // The two sense awake bits per 2 minutes
|
||||
SessionID sessid;
|
||||
Session *sess;
|
||||
int idx;
|
||||
|
||||
for (int r = 0; r < start.size(); r++) {
|
||||
sessid = times[r];
|
||||
sess = Sessions[sessid];
|
||||
ti = qint64(sessid) * 1000L;
|
||||
sess->really_set_first(ti);
|
||||
|
||||
EventList *LK = sess->AddEventList(CPAP_LeakTotal, EVL_Event, 1);
|
||||
EventList *PR = sess->AddEventList(CPAP_Pressure, EVL_Event, 0.1F);
|
||||
// EventList *OA = sess->AddEventList(CPAP_Obstructive, EVL_Event);
|
||||
EventList *H = sess->AddEventList(CPAP_Hypopnea, EVL_Event);
|
||||
EventList *FL = sess->AddEventList(CPAP_FlowLimit, EVL_Event);
|
||||
EventList *SA = sess->AddEventList(CPAP_SensAwake, EVL_Event);
|
||||
// EventList *CA = sess->AddEventList(CPAP_ClearAirway, EVL_Event);
|
||||
EventList *UA = sess->AddEventList(CPAP_Apnea, EVL_Event);
|
||||
// For testing to determine which bit is for which event type:
|
||||
// EventList *UF1 = sess->AddEventList(CPAP_UserFlag1, EVL_Event);
|
||||
// EventList *UF2 = sess->AddEventList(CPAP_UserFlag2, EVL_Event);
|
||||
|
||||
unsigned stidx = start[r];
|
||||
int rec = records[r];
|
||||
|
||||
idx = stidx * 21; // Each record has three blocks of 7 bytes for 21 bytes total
|
||||
|
||||
quint8 bitmask;
|
||||
for (int i = 0; i < rec; ++i) {
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
pressure = data[idx];
|
||||
PR->AddEvent(ti/*+120000*/, pressure);
|
||||
|
||||
leak = data[idx + 1];
|
||||
LK->AddEvent(ti/*+120000*/, leak);
|
||||
|
||||
// Comments below from MW. Appear not to be accurate
|
||||
a1 = data[idx + 2]; // [0..5] Obstructive flag, [6..7] Unknown
|
||||
a2 = data[idx + 3]; // [0..5] Hypopnea, [6..7] Unknown
|
||||
a3 = data[idx + 4]; // [0..5] Flow Limitation, [6..7] Unknown
|
||||
a4 = data[idx + 5]; // [0..5] UF1, [6..7] Unknown
|
||||
a5 = data[idx + 6]; // [0..5] UF2, [6..7] Unknown
|
||||
|
||||
// Sure there isn't 6 SenseAwake bits?
|
||||
a6 = (a3 >> 6) << 4 | ((a4 >> 6) << 2) | (a5 >> 6);
|
||||
|
||||
// this does the same thing as behaviour
|
||||
// a6 = (a3 >> 7) << 3 | ((a3 >> 6) & 1);
|
||||
a7 = (a1 >> 6) | (a2 >> 6); // Are these bits used?
|
||||
|
||||
bitmask = 1;
|
||||
for (int k = 0; k < 6; k++) { // There are 6 flag sets per 2 minutes
|
||||
if (a1 & bitmask) { UA->AddEvent(ti+60000, 0); }
|
||||
if (a2 & bitmask) { UA->AddEvent(ti+60000, 0); } // may be CA?
|
||||
if (a3 & bitmask) { H->AddEvent(ti+60000, 0); }
|
||||
if (a4 & bitmask) { H->AddEvent(ti+60000, 0); } // may be OA?
|
||||
if (a5 & bitmask) { FL->AddEvent(ti+60000, 0); }
|
||||
if (a6 & bitmask) { SA->AddEvent(ti+60000, 0); }
|
||||
|
||||
bitmask <<= 1;
|
||||
ti += 20000L; // Increment 20 seconds
|
||||
}
|
||||
|
||||
// Debug print non-zero flags
|
||||
if (a1 != 0 || a2 != 0 || a3 != 0 || a4 != 0 || a5 != 0 || a6 != 0 || a7 != 0) {
|
||||
qDebug() << "SS DET events" << QDateTime::fromSecsSinceEpoch(ti/1000).toString("MM/dd/yyyy hh:mm:ss")
|
||||
<< "pressure" << pressure
|
||||
<< "leak" << leak
|
||||
<< "flags" << a1 << a2 << a3 << a4 << a5 << a6 << "unknown" << a7;
|
||||
}
|
||||
|
||||
idx += 7; //was 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Update indexes, process waveform and perform flagging
|
||||
sess->UpdateSummaries();
|
||||
|
||||
// sess->really_set_last(ti-360000L);
|
||||
// sess->SetChanged(true);
|
||||
// addSession(sess,profile);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void SleepStyleLoader::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("Pressure relief mode."),
|
||||
QObject::tr("SmartFlex Mode"),
|
||||
"", DEFAULT, Qt::green));
|
||||
|
||||
chan->addOption(0, STR_TR_Off);
|
||||
****/
|
||||
|
||||
channel.add(GRP_CPAP, chan = new Channel(SS_SmartFlexLevel = 0xf304, SETTING, MT_CPAP, SESSION,
|
||||
"SSSmartFlexLevel", QObject::tr("SmartFlex Level"),
|
||||
QObject::tr("Exhalation pressure relief level."),
|
||||
QObject::tr("SmartFlex"),
|
||||
"", LOOKUP, Qt::green));
|
||||
chan->addOption(0, STR_TR_Off);
|
||||
|
||||
channel.add(GRP_CPAP, new Channel(SS_SenseAwakeLevel = 0xf305, SETTING, MT_CPAP, SESSION,
|
||||
"SS_SenseAwakeLevel",
|
||||
QObject::tr("SenseAwake level"),
|
||||
QObject::tr("SenseAwake level"),
|
||||
QObject::tr("SenseAwake"),
|
||||
STR_UNIT_CMH2O, LOOKUP, Qt::black));
|
||||
|
||||
}
|
||||
|
||||
bool sleepstyle_initialized = false;
|
||||
void SleepStyleLoader::Register()
|
||||
{
|
||||
if (sleepstyle_initialized) { return; }
|
||||
|
||||
qDebug() << "Registering F&P Sleepstyle Loader";
|
||||
RegisterLoader(new SleepStyleLoader());
|
||||
//InitModelMap();
|
||||
sleepstyle_initialized = true;
|
||||
}
|
129
oscar/SleepLib/loader_plugins/sleepstyle_loader.h
Normal file
129
oscar/SleepLib/loader_plugins/sleepstyle_loader.h
Normal file
@ -0,0 +1,129 @@
|
||||
/* SleepLib Fisher & Paykel SleepStyle Loader Implementation
|
||||
*
|
||||
* Copyright (c) 2020 The Oscar Team (info@oscar-team.org)
|
||||
* Copyright (C) 2011-2018 Mark Watkins <mark@jedimark.net>
|
||||
*
|
||||
* This file is subject to the terms and conditions of the GNU General Public
|
||||
* License. See the file COPYING in the main directory of the source code
|
||||
* for more details. */
|
||||
|
||||
#ifndef SLEEPSTYLE_LOADER_H
|
||||
#define SLEEPSTYLE_LOADER_H
|
||||
|
||||
#include <QMultiMap>
|
||||
#include "SleepLib/machine.h"
|
||||
#include "SleepLib/machine_loader.h"
|
||||
#include "SleepLib/profiles.h"
|
||||
|
||||
|
||||
//********************************************************************************************
|
||||
/// IMPORTANT!!!
|
||||
//********************************************************************************************
|
||||
// Please INCREMENT the following value when making changes to this loaders implementation.
|
||||
//
|
||||
const int sleepstyle_data_version = 1;
|
||||
//
|
||||
//********************************************************************************************
|
||||
|
||||
/*! \class SleepStyle
|
||||
\brief F&P SleepStyle customized machine object
|
||||
*/
|
||||
class SleepStyle: public CPAP
|
||||
{
|
||||
public:
|
||||
SleepStyle(Profile *, MachineID id = 0);
|
||||
virtual ~SleepStyle();
|
||||
};
|
||||
|
||||
|
||||
const int sleepstyle_load_buffer_size = 1024 * 1024;
|
||||
|
||||
extern ChannelID INTP_SmartFlexMode;
|
||||
extern ChannelID SS_SmartFlexLevel;
|
||||
extern ChannelID SS_SenseAwakeLevel;
|
||||
|
||||
const QString sleepstyle_class_name = STR_MACH_SleepStyle;
|
||||
|
||||
/*! \class SleepStyleLoader
|
||||
\brief Loader for Fisher & Paykel SleepStyle data
|
||||
This is only relatively recent addition and still needs more work
|
||||
*/
|
||||
|
||||
class SleepStyleLoader : public CPAPLoader
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
SleepStyleLoader();
|
||||
virtual ~SleepStyleLoader();
|
||||
|
||||
//! \brief Detect if the given path contains a valid Folder structure
|
||||
virtual bool Detect(const QString & path);
|
||||
|
||||
//! \brief Scans path for F&P SleepStyle data signature, and Loads any new data
|
||||
virtual int Open(const QString & path);
|
||||
|
||||
int OpenMachine(Machine *mach, const QString & path, const QString & ssPath);
|
||||
|
||||
bool OpenSummary(Machine *mach, const QString & path);
|
||||
bool OpenDetail(Machine *mach, const QString & path);
|
||||
// bool OpenFLW(Machine *mach, const QString & filename);
|
||||
bool OpenRealTime(Machine *mach, const QString & fname, const QString & filename);
|
||||
|
||||
//! \brief Returns SleepLib database version of this F&P SleepStyle loader
|
||||
virtual int Version() { return sleepstyle_data_version; }
|
||||
|
||||
//! \brief Returns the machine class name of this CPAP machine, "SleepStyle"
|
||||
virtual const QString & loaderName() { return sleepstyle_class_name; }
|
||||
|
||||
// ! \brief Creates a machine object, indexed by serial number
|
||||
//Machine *CreateMachine(QString serial);
|
||||
|
||||
QString getSerialPath () {return serialPath;}
|
||||
void setSerialPath (QString sp) {serialPath = sp;}
|
||||
bool backupData (Machine * mach, const QString & path);
|
||||
|
||||
SessionID findSession (SessionID sid);
|
||||
|
||||
void initChannels();
|
||||
|
||||
virtual MachineInfo newInfo() {
|
||||
return MachineInfo(MT_CPAP, 0, sleepstyle_class_name, QObject::tr("Fisher & Paykel"), QString(), QString(), QString(), QObject::tr("SleepStyle"), QDateTime::currentDateTime(), sleepstyle_data_version);
|
||||
}
|
||||
|
||||
|
||||
//! \brief Registers this MachineLoader with the master list, so F&P Icon data can load
|
||||
static void Register();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Now for some CPAPLoader overrides
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
virtual QString presRelType() { return QObject::tr(""); } // might not need this one
|
||||
|
||||
virtual ChannelID presRelSet() { return NoChannel; }
|
||||
virtual ChannelID presRelLevel() { return NoChannel; }
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
protected:
|
||||
// QDateTime readFPDateTime(quint8 *data);
|
||||
|
||||
QString last;
|
||||
QHash<QString, Machine *> MachList;
|
||||
QMap<SessionID, Session *> Sessions;
|
||||
QMultiMap<QDate, Session *> SessDate;
|
||||
//QMap<int,QList<EventList *> > FLWMapFlow;
|
||||
//QMap<int,QList<EventList *> > FLWMapLeak;
|
||||
//QMap<int,QList<EventList *> > FLWMapPres;
|
||||
//QMap<int,QList<qint64> > FLWDuration;
|
||||
//QMap<int,QList<qint64> > FLWTS;
|
||||
//QMap<int,QDate> FLWDate;
|
||||
|
||||
QString serialPath; // fully qualified path to the input data, ...SDCard.../FPHCARE/ICON/serial
|
||||
// QString serial; // Serial number
|
||||
bool rebuild_from_backups = false;
|
||||
bool create_backups = true;
|
||||
|
||||
unsigned char *m_buffer;
|
||||
};
|
||||
|
||||
#endif // SLEEPSTYLE_LOADER_H
|
@ -28,12 +28,14 @@ ChannelID CPAP_IPAP, CPAP_IPAPLo, CPAP_IPAPHi, CPAP_EPAP, CPAP_EPAPLo, CPAP_EPAP
|
||||
|
||||
|
||||
ChannelID RMS9_E01, RMS9_E02, RMS9_SetPressure, RMS9_MaskOnTime;
|
||||
ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2;
|
||||
ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2, INTP_SnoreFlag;
|
||||
|
||||
ChannelID CPAP_LargeLeak,
|
||||
PRS1_BND, PRS1_FlexMode, PRS1_FlexLevel, PRS1_HumidStatus, PRS1_HumidLevel, PRS1_HumidTargetTime, PRS1_MaskResistLock,
|
||||
PRS1_MaskResistSet, PRS1_HoseDiam, PRS1_AutoOn, PRS1_AutoOff, PRS1_MaskAlert, PRS1_ShowAHI;
|
||||
|
||||
ChannelID SS_SenseAwakeLevel, SS_SmartFlexLevel;
|
||||
|
||||
ChannelID OXI_Pulse, OXI_SPO2, OXI_Perf, OXI_PulseChange, OXI_SPO2Drop, OXI_Plethy;
|
||||
|
||||
ChannelID Journal_Notes, Journal_Weight, Journal_BMI, Journal_ZombieMeter, LastUpdated,
|
||||
|
@ -164,7 +164,9 @@ extern ChannelID CPAP_LargeLeak, PRS1_BND,
|
||||
CPAP_HumidSetting,
|
||||
PRS1_MaskResistSet, PRS1_HoseDiam, PRS1_AutoOn, PRS1_AutoOff, PRS1_MaskAlert, PRS1_ShowAHI;
|
||||
|
||||
extern ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2;
|
||||
extern ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2, INTP_SnoreFlag;
|
||||
|
||||
extern ChannelID SS_SenseAwakeLevel, SS_SmartFlexLevel;
|
||||
|
||||
extern ChannelID OXI_Pulse, OXI_SPO2, OXI_Perf, OXI_PulseChange, OXI_SPO2Drop, OXI_Plethy;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Update the string below to set OSCAR's version and release status.
|
||||
// See https://semver.org/spec/v2.0.0.html for details on format.
|
||||
|
||||
#define VERSION "1.2.1-alpha.0"
|
||||
#define VERSION "1.2.1-alpha.1"
|
||||
|
@ -43,6 +43,7 @@
|
||||
#include "SleepLib/loader_plugins/resmed_loader.h"
|
||||
#include "SleepLib/loader_plugins/intellipap_loader.h"
|
||||
#include "SleepLib/loader_plugins/icon_loader.h"
|
||||
#include "SleepLib/loader_plugins/sleepstyle_loader.h"
|
||||
#include "SleepLib/loader_plugins/weinmann_loader.h"
|
||||
#include "SleepLib/loader_plugins/viatom_loader.h"
|
||||
|
||||
@ -666,6 +667,7 @@ int main(int argc, char *argv[]) {
|
||||
ResmedLoader::Register();
|
||||
IntellipapLoader::Register();
|
||||
FPIconLoader::Register();
|
||||
SleepStyleLoader::Register();
|
||||
WeinmannLoader::Register();
|
||||
CMS50Loader::Register();
|
||||
CMS50F37Loader::Register();
|
||||
|
@ -1080,6 +1080,7 @@ QList<ImportPath> MainWindow::selectCPAPDataCards(const QString & prompt)
|
||||
w.setDirectory(folder);
|
||||
w.setFileMode(QFileDialog::Directory);
|
||||
w.setOption(QFileDialog::ShowDirsOnly, true);
|
||||
w.setWindowTitle(tr("Find your CPAP data card"));
|
||||
|
||||
// This doesn't work on WinXP
|
||||
|
||||
|
@ -295,6 +295,8 @@ SOURCES += \
|
||||
SleepLib/loader_plugins/cms50_loader.cpp \
|
||||
SleepLib/loader_plugins/dreem_loader.cpp \
|
||||
SleepLib/loader_plugins/icon_loader.cpp \
|
||||
SleepLib/loader_plugins/sleepstyle_loader.cpp \
|
||||
SleepLib/loader_plugins/sleepstyle_EDFinfo.cpp \
|
||||
SleepLib/loader_plugins/intellipap_loader.cpp \
|
||||
SleepLib/loader_plugins/mseries_loader.cpp \
|
||||
SleepLib/loader_plugins/prs1_loader.cpp \
|
||||
@ -380,6 +382,8 @@ HEADERS += \
|
||||
SleepLib/loader_plugins/cms50_loader.h \
|
||||
SleepLib/loader_plugins/dreem_loader.h \
|
||||
SleepLib/loader_plugins/icon_loader.h \
|
||||
SleepLib/loader_plugins/sleepstyle_loader.h \
|
||||
SleepLib/loader_plugins/sleepstyle_EDFinfo.h \
|
||||
SleepLib/loader_plugins/intellipap_loader.h \
|
||||
SleepLib/loader_plugins/mseries_loader.h \
|
||||
SleepLib/loader_plugins/prs1_loader.h \
|
||||
|
Loading…
Reference in New Issue
Block a user