mirror of
https://gitlab.com/pholy/OSCAR-code.git
synced 2025-04-04 02:00:43 +00:00
605 lines
17 KiB
C++
605 lines
17 KiB
C++
/* SleepLib Weinmann SOMNOsoft/Balance Loader Implementation
|
|
*
|
|
* Copyright (c) 2011-2018 Mark Watkins
|
|
* Copyright (c) 2019-2024 The OSCAR Team
|
|
*
|
|
* This file is subject to the terms and conditions of the GNU General Public
|
|
* License. See the file COPYING in the main directory of the source code
|
|
* for more details. */
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QProgressBar>
|
|
#include <QDomDocument>
|
|
#include <QDomElement>
|
|
#include <QDomNode>
|
|
|
|
|
|
#include "weinmann_loader.h"
|
|
|
|
// The qt5.15 obsolescence of hex requires this change.
|
|
// this solution to QT's obsolescence is only used in debug statements
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
|
|
#define QTHEX Qt::hex
|
|
#define QTDEC Qt::dec
|
|
#else
|
|
#define QTHEX hex
|
|
#define QTDEC dec
|
|
#endif
|
|
|
|
|
|
Weinmann::Weinmann(Profile *profile, MachineID id)
|
|
: CPAP(profile, id)
|
|
{
|
|
}
|
|
|
|
Weinmann::~Weinmann()
|
|
{
|
|
}
|
|
|
|
WeinmannLoader::WeinmannLoader()
|
|
{
|
|
m_buffer = nullptr;
|
|
m_type = MT_CPAP;
|
|
}
|
|
|
|
WeinmannLoader::~WeinmannLoader()
|
|
{
|
|
}
|
|
|
|
bool WeinmannLoader::Detect(const QString & givenpath)
|
|
{
|
|
QDir dir(givenpath);
|
|
|
|
if (!dir.exists()) {
|
|
return false;
|
|
}
|
|
|
|
// Check for the settings file inside the .. folder
|
|
if (!dir.exists("WM_DATA.TDF")) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int WeinmannLoader::ParseIndex(QFile & wmdata)
|
|
{
|
|
QByteArray xml;
|
|
do {
|
|
xml += wmdata.readLine(250);
|
|
} while (!wmdata.atEnd());
|
|
|
|
QDomDocument index_xml("weinmann");
|
|
|
|
index_xml.setContent(xml);
|
|
QDomElement docElem = index_xml.documentElement();
|
|
|
|
QDomNode n = docElem.firstChild();
|
|
|
|
index.clear();
|
|
while (!n.isNull()) {
|
|
QDomElement e = n.toElement();
|
|
if (!e.isNull()) {
|
|
bool ok;
|
|
int val = e.attribute("val").toInt(&ok);
|
|
if (ok) {
|
|
index[e.attribute("name")] = val;
|
|
qDebug() << e.attribute("name") << "=" << QTHEX << val;
|
|
}
|
|
}
|
|
n = n.nextSibling();
|
|
}
|
|
return index.size();
|
|
}
|
|
|
|
const QString DayComplianceCount = "DayComplianceCount";
|
|
const QString CompOffset = "DayComplianceOffset";
|
|
const QString FlowOffset = "TID_Flow_Offset";
|
|
const QString StatusOffset = "TID_Status_Offset";
|
|
const QString PresOffset = "TID_P_Offset";
|
|
const QString AMVOffset = "TID_AMV_Offset";
|
|
const QString EventsOffset = "TID_Events_Offset";
|
|
|
|
|
|
void HighPass(char * data, int samples, float cutoff, float dt)
|
|
{
|
|
float *Y = new float [samples];
|
|
for (int i=0; i < samples; ++i) Y[i] = 0.0f;
|
|
|
|
Y[0] = ((unsigned char *)data)[0] ;
|
|
|
|
float RC = 1.0 / (cutoff * 2 * 3.1415926);
|
|
float alpha = RC / (RC + dt);
|
|
|
|
for (int i=1; i < samples; ++i) {
|
|
float x = ((unsigned char *)data)[i] ;
|
|
float x1 = ((unsigned char *)data)[i-1] ;
|
|
Y[i] = alpha * (Y[i-1] + x - x1);
|
|
}
|
|
|
|
for (int i=0; i< samples; ++i) {
|
|
data[i] = Y[i];
|
|
}
|
|
delete [] Y;
|
|
}
|
|
|
|
int WeinmannLoader::Open(const QString & dirpath)
|
|
{
|
|
QString path(dirpath);
|
|
path = path.replace("\\", "/");
|
|
|
|
QFile wmdata(path + "/WM_DATA.TDF");
|
|
if (!wmdata.open(QFile::ReadOnly)) {
|
|
return -1;
|
|
}
|
|
|
|
int res = ParseIndex(wmdata);
|
|
if (res < 0) return -1;
|
|
|
|
|
|
MachineInfo info = newInfo();
|
|
info.serial = "141819";
|
|
Machine * mach = p_profile->CreateMachine(info);
|
|
|
|
|
|
int WeekComplianceOffset = index["WeekComplianceOffset"];
|
|
int WCD_Pin_Offset = index["WCD_Pin_Offset"];
|
|
// int WCD_Pex_Offset = index["WCD_Pex_Offset"];
|
|
// int WCD_Snore_Offset = index["WCD_Snore_Offset"];
|
|
// int WCD_Lf_Offset = index["WCD_Lf_Offset"];
|
|
// int WCD_Events_Offset = index["WCD_Events_Offset"];
|
|
// int WCD_IO_Offset = index["WCD_IO_Offset"];
|
|
int comp_start = index[CompOffset];
|
|
|
|
int wccount = index["WeekComplianceCount"];
|
|
|
|
int size = WCD_Pin_Offset - WeekComplianceOffset;
|
|
|
|
quint8 * weekco = new quint8 [size];
|
|
memset(weekco, 0, size);
|
|
wmdata.seek(WeekComplianceOffset);
|
|
wmdata.read((char *)weekco, size);
|
|
|
|
unsigned char *p = weekco;
|
|
for (int c=0; c < wccount; ++c) {
|
|
int year = QString::asprintf("%02i%02i", p[0], p[1]).toInt();
|
|
int month = p[2];
|
|
int day = p[3];
|
|
int hour = p[5];
|
|
int minute = p[6];
|
|
int second = p[7];
|
|
QDateTime date = QDateTime(QDate(year,month,day), QTime(hour,minute,second));
|
|
quint32 ts = date.toTime_t();
|
|
if (!mach->SessionExists(ts)) {
|
|
qDebug() << date;
|
|
}
|
|
|
|
// stores used length of data at 0x46, in 16bit integers, for IPAP, EPAP, snore, leak,
|
|
// stores total length of data block at 0x66, in 16 bit integers for IPAP, EPAP, snore, leak
|
|
|
|
|
|
p+=0x84;
|
|
}
|
|
|
|
delete [] weekco;
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// Read Day Compliance Information....
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
int comp_end = index[FlowOffset];
|
|
int comp_size = comp_end - comp_start;
|
|
// TODO: This entire loader needs significant work. For now just
|
|
// hard-code values here to make sure we don't crash in the loop below.
|
|
if (comp_size < 5 * 0xd6) {
|
|
qWarning() << "Weinmann loader comp_size too short:" << comp_size;
|
|
return -1;
|
|
}
|
|
|
|
quint8 * comp = new quint8 [comp_size];
|
|
memset((char *)comp, 0, comp_size);
|
|
|
|
wmdata.seek(comp_start);
|
|
wmdata.read((char *)comp, comp_size);
|
|
|
|
p = comp;
|
|
|
|
QDateTime dt_epoch(QDate(2000,1,1), QTime(0,0,0));
|
|
//int epoch = dt_epoch.toTime_t();
|
|
//epoch = 0;
|
|
|
|
|
|
float flow_sample_duration = 1000.0 / 5;
|
|
float pressure_sample_duration = 1000.0 / 2;
|
|
//float amv_sample_duration = 200 * 10;
|
|
|
|
//int c = index[DayComplianceCount];
|
|
for (int i=0; i < 5; i++) {
|
|
int year = QString::asprintf("%02i%02i", p[0], p[1]).toInt();
|
|
int month = p[2];
|
|
int day = p[3];
|
|
int hour = p[5];
|
|
int minute = p[6];
|
|
int second = p[7];
|
|
QDateTime date = QDateTime(QDate(year,month,day), QTime(hour,minute,second));
|
|
quint32 ts = date.toTime_t();
|
|
|
|
if (mach->SessionExists(ts)) continue;
|
|
|
|
Session * sess = new Session(mach, ts);
|
|
sess->SetChanged(true);
|
|
|
|
|
|
// Flow Waveform
|
|
quint32 fs = p[8] | p[9] << 8 | p[10] << 16 | p[11] << 24;
|
|
quint32 fl = p[0x44] | p[0x45] << 8 | p[0x46] << 16 | p[0x47] << 24;
|
|
|
|
// Status
|
|
quint32 ss = p[12] | p[13] << 8 | p[14] << 16 | p[15] << 24;
|
|
quint32 sl = p[0x48] | p[0x49] << 8 | p[0x4a] << 16 | p[0x4b] << 24;
|
|
|
|
// Pressure
|
|
quint32 ps = p[16] | p[17] << 8 | p[18] << 16 | p[19] << 24;
|
|
quint32 pl = p[0x4c] | p[0x4d] << 8 | p[0x4e] << 16 | p[0x4f] << 24;
|
|
|
|
// AMV
|
|
quint32 ms = p[20] | p[21] << 8 | p[22] << 16 | p[23] << 24;
|
|
quint32 ml = p[0x50] | p[0x51] << 8 | p[0x52] << 16 | p[0x53] << 24;
|
|
|
|
// Events
|
|
quint32 es = p[24] | p[25] << 8 | p[26] << 16 | p[27] << 24;
|
|
quint32 er = p[0x54] | p[0x55] << 8 | p[0x56] << 16 | p[0x57] << 24; // number of records
|
|
|
|
|
|
compinfo.append(CompInfo(sess, date, fs, fl, ss, sl, ps, pl, ms, ml, es, er));
|
|
|
|
int dur = fl / 5;
|
|
|
|
sess->really_set_first(qint64(ts) * 1000L);
|
|
sess->really_set_last(qint64(ts+dur) * 1000L);
|
|
sessions[ts] = sess;
|
|
|
|
// qDebug() << date << ts << dur << QString::asprintf("%02i:%02i:%02i", dur / 3600, dur/60 % 60, dur % 60);
|
|
|
|
p += 0xd6;
|
|
}
|
|
|
|
delete [] comp;
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// Read Flow Waveform....
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
int flowstart = index[FlowOffset];
|
|
int flowend = index[StatusOffset];
|
|
|
|
wmdata.seek(flowstart);
|
|
|
|
int flowsize = flowend - flowstart;
|
|
char * data = new char [flowsize];
|
|
memset((char *)data, 0, flowsize);
|
|
wmdata.read((char *)data, flowsize);
|
|
|
|
float dt = 1.0 / (1000.0 / flow_sample_duration); // samples per second
|
|
|
|
// Centre Waveform using High Pass Filter
|
|
HighPass(data, flowsize, 0.1f, dt);
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// Read Status....
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
int st_start = index[StatusOffset];
|
|
int st_end = index[PresOffset];
|
|
int st_size = st_end - st_start;
|
|
|
|
char * st = new char [st_size];
|
|
memset(st, 0, st_size);
|
|
|
|
wmdata.seek(st_start);
|
|
wmdata.read(st, st_size);
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// Read Mask Pressure....
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
int pr_start = index[PresOffset];
|
|
int pr_end = index[AMVOffset];
|
|
int pr_size = pr_end - pr_start;
|
|
|
|
char * pres = new char [pr_size];
|
|
memset(pres, 0, pr_size);
|
|
|
|
wmdata.seek(pr_start);
|
|
wmdata.read(pres, pr_size);
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// Read AMV....
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
int mv_start = index[AMVOffset];
|
|
int mv_end = index[EventsOffset];
|
|
int mv_size = mv_end - mv_start;
|
|
|
|
char * mv = new char [mv_size];
|
|
memset(mv, 0, mv_size);
|
|
|
|
wmdata.seek(mv_start);
|
|
wmdata.read(mv, mv_size);
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// Read Events....
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
int ev_start = index[EventsOffset];
|
|
int ev_end = wmdata.size();
|
|
int ev_size = ev_end - ev_start;
|
|
|
|
quint8 * ev = new quint8 [ev_size];
|
|
memset((char *) ev, 0, ev_size);
|
|
|
|
wmdata.seek(ev_start);
|
|
wmdata.read((char *) ev, ev_size);
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// Process sessions
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
for (int i=0; i< compinfo.size(); ++i) {
|
|
const CompInfo & ci = compinfo.at(i);
|
|
Session * sess = ci.session;
|
|
|
|
qint64 ti = sess->first();
|
|
|
|
EventList * flow = sess->AddEventList(CPAP_FlowRate, EVL_Waveform, 1.0, 0.0, 0.0, 0.0, flow_sample_duration);
|
|
flow->AddWaveform(ti, &data[ci.flow_start], ci.flow_size, (ci.flow_size/(1000.0/flow_sample_duration)) * 1000.0);
|
|
|
|
EventList * PR = sess->AddEventList(CPAP_MaskPressure, EVL_Waveform, 0.1f, 0.0, 0.0, 0.0, pressure_sample_duration);
|
|
PR->AddWaveform(ti, (unsigned char *)&pres[ci.pres_start], ci.pres_size, (ci.pres_size/(1000.0/pressure_sample_duration)) * 1000.0);
|
|
|
|
// Weinmann's MV graph is pretty dodgy... commenting this out and using my flow calced ones instead (the code below is mapped to snore for comparison purposes)
|
|
//EventList * MV = sess->AddEventList(CPAP_Snore, EVL_Waveform, 1.0f, 0.0, 0.0, 0.0, amv_sample_duration);
|
|
//MV->AddWaveform(ti, (unsigned char *)&mv[ci.amv_start], ci.amv_size, (ci.amv_size/(1000/amv_sample_duration)) * 1000L);
|
|
|
|
// EventList * L = sess->AddEventList(CPAP_Leak, EVL_Event);
|
|
// EventList * S = sess->AddEventList(CPAP_Snore, EVL_Event);
|
|
EventList * OA = sess->AddEventList(CPAP_Obstructive, EVL_Event);
|
|
EventList * A = sess->AddEventList(CPAP_Apnea, EVL_Event);
|
|
EventList * H = sess->AddEventList(CPAP_Hypopnea, EVL_Event);
|
|
EventList * FL = sess->AddEventList(CPAP_FlowLimit, EVL_Event);
|
|
// EventList * VS = sess->AddEventList(CPAP_VSnore, EVL_Event);
|
|
quint64 tt = ti;
|
|
Q_UNUSED (tt);
|
|
quint64 step = sess->length() / ci.event_recs;
|
|
unsigned char *p = &ev[ci.event_start];
|
|
for (quint32 j=0; j < ci.event_recs; ++j) {
|
|
QDate evdate = ci.time.date();
|
|
QTime evtime(p[1], p[2], p[3]);
|
|
if (evtime < ci.time.time()) {
|
|
evdate = evdate.addDays(1);
|
|
}
|
|
quint64 ts = QDateTime(evdate, evtime).toMSecsSinceEpoch();
|
|
|
|
// I think p[0] is amount of flow restriction..
|
|
|
|
unsigned char evcode = p[0];
|
|
EventStoreType data = p[4] | p[5] << 8;
|
|
|
|
if (evcode == '@') {
|
|
OA->AddEvent(ts,data/10.0);
|
|
} else if (evcode =='A') {
|
|
A->AddEvent(ts,data/10.0);
|
|
} else if (evcode == 'F') {
|
|
FL->AddEvent(ts,data/10.0);
|
|
} else if (evcode == '*') {
|
|
H->AddEvent(ts,data/10.0);
|
|
}
|
|
/* switch (evcode) {
|
|
case 0x03:
|
|
break;
|
|
case 0x04:
|
|
break;
|
|
case 0x08:
|
|
break;
|
|
case 0x09:
|
|
break;
|
|
case 0x0a:
|
|
break;
|
|
case 0x0b:
|
|
break;
|
|
case 0x0c:
|
|
break;
|
|
case 0x10:
|
|
break;
|
|
case 0x11:
|
|
break;
|
|
case 0x12:
|
|
break;
|
|
case 0x13:
|
|
S->AddEvent(ts, data);
|
|
break;
|
|
case 0x22:
|
|
// VS->AddEvent(ts, data/10.0);
|
|
|
|
break;
|
|
case 0x28:
|
|
VS->AddEvent(ts, data/10.0);
|
|
|
|
break;
|
|
case 'F':
|
|
FL->AddEvent(ts, data/10.0);
|
|
break;
|
|
case '@':
|
|
OA->AddEvent(ts, data/10.0);
|
|
break;
|
|
case '\'':
|
|
//A->AddEvent(ts, data/10.0);
|
|
break;
|
|
case 'a':
|
|
A->AddEvent(ts, data/10.0);
|
|
break;
|
|
case 'A':
|
|
// A->AddEvent(ts, data/10.0);
|
|
break;
|
|
case '*':
|
|
H->AddEvent(ts, data/10.0);
|
|
break;
|
|
case 'd':
|
|
break;
|
|
case 0x91:
|
|
break;
|
|
case 0x96:
|
|
break;
|
|
case 0x84:
|
|
break;
|
|
default:
|
|
qDebug() << (int)evcode << endl;
|
|
}*/
|
|
|
|
// S->AddEvent(ts, p[5]);
|
|
|
|
|
|
|
|
// p[5] == 0 corresponds to peak events
|
|
// p[5] == 1 corresponds to hypopnea/bstructive events
|
|
|
|
//if (p[5] == 2) OA->AddEvent(ts, p[4]);
|
|
|
|
|
|
// This is ugggggly...
|
|
|
|
|
|
tt += step;
|
|
p += 6;
|
|
}
|
|
|
|
sess->UpdateSummaries();
|
|
mach->AddSession(sess);
|
|
}
|
|
delete [] data;
|
|
delete [] st;
|
|
delete [] pres;
|
|
delete [] mv;
|
|
delete [] ev;
|
|
|
|
mach->Save();
|
|
|
|
return 1;
|
|
|
|
/*
|
|
|
|
// Center the waveform
|
|
HighPass(data, flowsize, 0.6, dt);
|
|
|
|
EventList * flow = sess->AddEventList(CPAP_FlowRate, EVL_Waveform, 1.0, 0.0, 0.0, 0.0, sample_duration);
|
|
flow->AddWaveform(tt, (char *)data, flowsize, (flowsize/(1000/sample_duration)) * 1000L);
|
|
|
|
|
|
qint64 ti = tt;
|
|
for (int i=0; i < pr_size; ++i) {
|
|
EventStoreType c = ((unsigned char *)pres)[i];
|
|
PR->AddEvent(ti, c);
|
|
ti += sample_duration * 2.5; //46296296296296;
|
|
}
|
|
// Their calcs is uglier than mine!
|
|
EventList * MV = sess->AddEventList(CPAP_Snore, EVL_Event, 1.0);
|
|
|
|
|
|
ti = tt;
|
|
for (int i=0; i < mv_size; ++i) {
|
|
EventStoreType c = ((unsigned char *)mv)[i];
|
|
MV->AddEvent(ti, c);
|
|
ti += sample_duration * 9;
|
|
}
|
|
|
|
// Their calcs is uglier than mine!
|
|
EventList * ST = sess->AddEventList(CPAP_Leak, EVL_Event, 1.0);
|
|
int st_start = index[StatusOffset];
|
|
int st_end = index[PresOffset];
|
|
int st_size = st_end - st_start;
|
|
|
|
char st[st_size];
|
|
memset(st, 0, st_size);
|
|
|
|
wmdata.seek(st_start);
|
|
wmdata.read(st, st_size);
|
|
|
|
ti = tt;
|
|
for (int i=0; i < st_size; ++i) {
|
|
EventStoreType c = ((unsigned char *)st)[i];
|
|
// if (c & 0x80) {
|
|
ST->AddEvent(ti, c & 0x10);
|
|
// }
|
|
ti += sample_duration*4; // *9
|
|
}
|
|
|
|
|
|
// EventList * LEAK = sess->AddEventList(CPAP_Leak, EVL_Event);
|
|
// EventList * SNORE = sess->AddEventList(CPAP_Snore, EVL_Event);
|
|
|
|
// int ev_start = index[EventsOffset];
|
|
// int ev_end = wmdata.size();
|
|
// int ev_size = ev_end - ev_start;
|
|
// int recs = ev_size / 0x12;
|
|
|
|
// unsigned char ev[ev_size];
|
|
// memset((char *) ev, 0, ev_size);
|
|
|
|
// wmdata.seek(ev_start);
|
|
// wmdata.read((char *) ev, ev_size);
|
|
|
|
sess->really_set_last(flow->last());
|
|
|
|
// int pos = 0;
|
|
// ti = tt;
|
|
|
|
// // 6 byte repeating structure.. No Leaks :(
|
|
// do {
|
|
// //EventStoreType c = ((unsigned char*)ev)[pos+0]; // TV?
|
|
// //c = ((unsigned char*)ev)[pos+6]; // MV?
|
|
|
|
// EventStoreType c = ((EventStoreType*)ev)[pos+0];
|
|
// LEAK->AddEvent(ti, c);
|
|
// SNORE->AddEvent(ti, ((unsigned char*)ev)[pos+2]);
|
|
// pos += 0x6;
|
|
// ti += 30000;
|
|
// if (ti > sess->last())
|
|
// break;
|
|
// } while (pos < (ev_size - 0x12));
|
|
|
|
|
|
|
|
m->AddSession(sess);
|
|
sess->UpdateSummaries();
|
|
|
|
return 1;*/
|
|
}
|
|
|
|
void WeinmannLoader::initChannels()
|
|
{
|
|
//using namespace schema;
|
|
//Channel * chan = nullptr;
|
|
// channel.add(GRP_CPAP, chan = new Channel(INTP_SmartFlex = 0x1165, SETTING, SESSION,
|
|
// "INTPSmartFlex", QObject::tr("SmartFlex"),
|
|
// QObject::tr("Weinmann pressure relief setting."),
|
|
// QObject::tr("SmartFlex"),
|
|
// "", DEFAULT, Qt::green));
|
|
|
|
|
|
// chan->addOption(1, STR_TR_None);
|
|
}
|
|
|
|
bool weinmann_initialized = false;
|
|
void WeinmannLoader::Register()
|
|
{
|
|
if (weinmann_initialized) { return; }
|
|
|
|
qDebug() << "Registering WeinmannLoader";
|
|
RegisterLoader(new WeinmannLoader());
|
|
//InitModelMap();
|
|
weinmann_initialized = true;
|
|
}
|