/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * vim: set ts=8 sts=4 et sw=4 tw=99: * * SleepLib Fisher & Paykel Icon Loader Implementation * * Copyright (c) 2011 Mark Watkins <jedimark@users.sourceforge.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 Linux * distribution for more details. */ #include <QDir> #include <QProgressBar> #include <QMessageBox> #include <QDataStream> #include <QTextStream> #include <cmath> #include "icon_loader.h" extern QProgressBar *qprogress; FPIcon::FPIcon(Profile *p,MachineID id) :CPAP(p,id) { m_class=fpicon_class_name; } FPIcon::~FPIcon() { } FPIconLoader::FPIconLoader() { m_buffer=NULL; } FPIconLoader::~FPIconLoader() { } int FPIconLoader::Open(QString & path,Profile *profile) { QString newpath; path=path.replace("\\","/"); if (path.endsWith("/")) path.chop(1); QString dirtag="FPHCARE"; if (path.endsWith("/"+dirtag)) { newpath=path; } else { newpath=path+"/"+dirtag; } newpath+="/ICON/"; QString filename; QDir dir(newpath); if ((!dir.exists() || !dir.isReadable())) return 0; dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks); dir.setSorting(QDir::Name); QFileInfoList flist=dir.entryInfoList(); QStringList SerialNumbers; bool ok; for (int i=0;i<flist.size();i++) { QFileInfo fi=flist.at(i); QString filename=fi.fileName(); filename.toInt(&ok); if (ok) { SerialNumbers.push_back(filename); } } Machine *m; QString npath; for (int i=0;i<SerialNumbers.size();i++) { QString & sn=SerialNumbers[i]; m=CreateMachine(sn,profile); npath=newpath+"/"+sn; try { if (m) OpenMachine(m,npath,profile); } catch(OneTypePerDay e) { profile->DelMachine(m); MachList.erase(MachList.find(sn)); QMessageBox::warning(NULL,"Import Error","This Machine Record cannot be imported in this profile.\nThe Day records overlap with already existing content.",QMessageBox::Ok); delete m; } } return MachList.size(); } struct FPWaveChunk { FPWaveChunk(){ st=0; duration=0; flow=NULL; pressure=NULL; leak=NULL; file=0; } FPWaveChunk(qint64 start, qint64 dur,int f) { st=start; duration=dur; file=f, flow=NULL; leak=NULL; pressure=NULL; } FPWaveChunk(const FPWaveChunk & copy) { st=copy.st; duration=copy.duration; flow=copy.flow; leak=copy.leak; pressure=copy.pressure; file=copy.file; } qint64 st; qint64 duration; int file; EventList * flow; EventList * leak; EventList * pressure; }; bool operator<(const FPWaveChunk & a, const FPWaveChunk & b) { return (a.st < b.st); } int FPIconLoader::OpenMachine(Machine *mach, QString & path, Profile * profile) { qDebug() << "Opening FPIcon " << path; QDir dir(path); if (!dir.exists() || (!dir.isReadable())) return false; dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks); dir.setSorting(QDir::Name); QFileInfoList flist=dir.entryInfoList(); QString filename,fpath; if (qprogress) qprogress->setValue(0); QStringList summary, log, flw, det; for (int i=0;i<flist.size();i++) { QFileInfo fi=flist.at(i); filename=fi.fileName(); fpath=path+"/"+filename; if (filename.left(3).toUpper()=="SUM") { summary.push_back(fpath); OpenSummary(mach,fpath,profile); } else if (filename.left(3).toUpper()=="DET") { det.push_back(fpath); } else if (filename.left(3).toUpper()=="FLW") { flw.push_back(fpath); } else if (filename.left(3).toUpper()=="LOG") { log.push_back(fpath); } } for (int i=0;i<det.size();i++) { OpenDetail(mach,det[i],profile); } for (int i=0;i<flw.size();i++) { OpenFLW(mach,flw[i],profile); } SessionID sid;//,st; float hours,mins; qDebug() << "Last 20 Sessions"; int cnt=0; QDateTime dt; QString a; 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); if (sess->channelDataExists(CPAP_FlowRate)) a="(flow)"; else a=""; 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() : ""); // } // } // qSort(chunks); // 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() : ""); // } mach->Save(); return true; } QDateTime FPIconLoader::readFPDateTime(quint8 * data) { quint32 ts=(data[3] << 24) | (data[2] << 16) | ((data[1] << 8) | data[0]);// ^ 0xc00000; // 0x20a41b18 quint8 day=ts & 0x1f; // 0X18 24 ts >>= 5; // 10520D8 quint8 month=ts & 0x0f; // 0X08 8 ts >>= 4; // 10520D quint8 year=ts & 0x3f; // 0X0D 13 ts >>= 6; // 4148 quint8 second=ts & 0x3f;// 0X08 8 ts >>= 6; // 20A quint8 minute=ts & 0x3f;// 0A 10 ts >>= 6; // 10 quint8 hour=ts & 0x1f; // 10 16 QDate date=QDate(2000+year,month,day); QTime time=QTime(hour,minute,second); QDateTime datetime=QDateTime(date,time); return datetime; } /* *in >> a1; in >> a2; t1=a2 << 8 | a1; if (t1==0xfafe) break; day=t1 & 0x1f; month=(t1 >> 5) & 0x0f; year=2000+((t1 >> 9) & 0x3f); in >> a1; in >> a2; ts=((a2 << 8) | a1) << 1; ts|=(t1 >> 15) & 1; second=(ts & 0x3f); minute=(ts >> 6) & 0x3f; hour=(ts >> 12) & 0x1f; */ // FLW Header Structure // 0x0000-0x01fe // newline (0x0d) seperated list of machine information strings. // magic? 0201 // version 1.5.0 // serial number 12 digits // Machine Series "ICON" // Machine Model "Auto" // Remainder of header is 0 filled... // 0x01ff 8 bit additive sum checksum byte of previous header bytes // 0x0200-0x0203 32bit timestamp in bool FPIconLoader::OpenFLW(Machine * mach,QString filename, Profile * profile) { Q_UNUSED(mach); Q_UNUSED(profile); QByteArray data; quint16 t1; quint32 ts; double ti; EventList * flow=NULL, * pressure=NULL, *leak=NULL; QDateTime datetime; unsigned char * buf, *endbuf; qDebug() << filename; QByteArray header; QFile file(filename); if (!file.open(QFile::ReadOnly)) { qDebug() << "Couldn't open" << filename; return false; } header=file.read(0x200); if (header.size()!=0x200) { qDebug() << "Short file" << filename; return false; } unsigned char hsum=0xff; for (int i=0;i<0x1ff;i++) { hsum+=header[i]; } if (hsum!=header[0x1ff]) { qDebug() << "Header checksum mismatch" << filename; } QTextStream htxt(&header); QString h1,version,fname,serial,model,type; htxt >> h1; htxt >> version; htxt >> fname; htxt >> serial; htxt >> model; htxt >> type; if (mach->properties[STR_PROP_Model].isEmpty()) { mach->properties[STR_PROP_Model]=model+" "+type; } fname.chop(4); QString num=fname.right(4); int filenum=num.toInt(); data=file.readAll(); buf=(unsigned char *)data.data(); endbuf=buf+data.size(); t1=buf[1] << 8 | buf[0]; if (t1==0xfafe) // End of file marker.. { qDebug() << "FaFE observed in" << filename; return false; } datetime=readFPDateTime(buf); buf+=4; QDate date; QTime time; if (!datetime.isValid()) { qDebug() << "DateTime invalid in OpenFLW:" << filename; return false; } else { date=datetime.date(); time=datetime.time(); ts=datetime.toTime_t(); } ti=qint64(ts)*1000L; EventStoreType pbuf[256]; QMap<SessionID, Session *>::iterator sit=Sessions.find(ts); Session *sess; bool newsess=false; if (sit!=Sessions.end()) { sess=sit.value(); qDebug() << filenum << ":" << date << sess->session() << ":" << sess->hours()*60.0; } else { qint64 k=-1; Session * s1=NULL; sess=NULL; for (sit=Sessions.begin();sit!=Sessions.end();sit++) { s1=sit.value(); qint64 z=qAbs(s1->first()-ti); if (z<3600000) { if ((k<0) || (k>z)) { k=z; sess=s1; } } } if (sess) { sess->set_first(ti); sess->setFirst(CPAP_FlowRate,ti); sess->setFirst(CPAP_MaskPressure,ti); } else { sess=new Session(mach,ts); sess->set_first(ti); sess->setFirst(CPAP_FlowRate,ti); sess->setFirst(CPAP_MaskPressure,ti); newsess=true; qDebug() << filenum << ":" << date << "couldn't find matching session for" << ts; } } const int samples_per_block=50; const double rate=1000.0/double(samples_per_block); // F&P Overwrites this file, not appends to it. flow=new EventList(EVL_Waveform,1.0,0,0,0,rate); //leak=new EventList(EVL_Event,1.0,0,0,0,rate*double(samples_per_block)); // 1 per second pressure=new EventList(EVL_Event,0.01,0,0,0,rate*double(samples_per_block)); // 1 per second flow->setFirst(ti); //leak->setFirst(ti); pressure->setFirst(ti); qint16 pr; quint16 lkaj; EventDataType val; qint16 tmp; do { quint8 * p=buf; // Scan ahead looking for end of block, marked by ff ff do { p++; if (p>=endbuf) { delete flow; delete leak; delete pressure; return false; } } while (!((p[0]==0xff) && (p[1]==0xff))); // The Pressure and lkaj codes are before the end of block marker p-=3; pr=p[1] << 8 | p[0]; lkaj=p[2]; int i=0; pressure->AddEvent(ti,pr); //leak->AddEvent(ti,lkaj); do { tmp=buf[1] << 8 | buf[0]; val=(EventDataType(tmp)/100.0)-lkaj; if (val<-128) val=-128; else if (val>128) val=128; buf+=2; pbuf[i++]=val; } while (buf<p); flow->AddWaveform(ti,pbuf,i,rate); ti+=i*rate; buf=p+5; if (buf>=endbuf) break; } while (!((buf[0]==0xff) && (buf[1]==0x7f))); if (sess) { sess->setLast(CPAP_FlowRate,ti); sess->setLast(CPAP_MaskPressure,ti); sess->eventlist[CPAP_FlowRate].push_back(flow); // sess->eventlist[CPAP_Leak].push_back(leak); sess->eventlist[CPAP_MaskPressure].push_back(pressure); } if (newsess) mach->AddSession(sess,profile); return true; } //////////////////////////////////////////////////////////////////////////////////////////// // Open Summary file //////////////////////////////////////////////////////////////////////////////////////////// bool FPIconLoader::OpenSummary(Machine * mach,QString filename, Profile * profile) { qDebug() << filename; QByteArray header; QFile file(filename); if (!file.open(QFile::ReadOnly)) { qDebug() << "Couldn't open" << filename; return false; } header=file.read(0x200); if (header.size()!=0x200) { qDebug() << "Short file" << filename; return false; } unsigned char hsum=0xff; for (int i=0;i<0x1ff;i++) { hsum+=header[i]; } if (hsum!=header[0x1ff]) { qDebug() << "Header checksum mismatch" << filename; } QTextStream htxt(&header); QString h1,version,fname,serial,model,type; htxt >> h1; htxt >> version; htxt >> fname; htxt >> serial; htxt >> model; htxt >> type; mach->properties[STR_PROP_Model]=model+" "+type; QByteArray data; data=file.readAll(); //long size=data.size(),pos=0; QDataStream in(data); in.setVersion(QDataStream::Qt_4_6); in.setByteOrder(QDataStream::LittleEndian); quint16 t1;//,t2; quint32 ts; //QByteArray line; unsigned char a1,a2, a3,a4, a5, p1, p2, p3, p4, p5, j1, j2, j3 ,j4,j5,j6,j7, x1, x2; quint16 d1,d2,d3; QDateTime datetime; int runtime,usage; int day,month,year,hour,minute,second; QDate date; do { in >> a1; in >> a2; t1=a2 << 8 | a1; if (t1==0xfafe) break; day=t1 & 0x1f; month=(t1 >> 5) & 0x0f; year=2000+((t1 >> 9) & 0x3f); in >> a1; in >> a2; ts=((a2 << 8) | a1) << 1; ts|=(t1 >> 15) & 1; second=(ts & 0x3f); minute=(ts >> 6) & 0x3f; hour=(ts >> 12) & 0x1f; datetime=QDateTime(QDate(year,month,day),QTime(hour,minute,second)); date=datetime.date(); ts=datetime.toTime_t(); // the following two quite often match in value in >> a1; // 0x04 Run Time in >> a2; // 0x05 Usage Time runtime=a1 * 360; // durations are in tenth of an hour intervals usage=a2 * 360; in >> a3; // 0x06 // Ramps??? in >> a4; // 0x07 // a pressure value? in >> a5; // 0x08 // ?? varies.. always less than 90% leak.. in >> d1; // 0x09 in >> d2; // 0x0b in >> d3; // 0x0d // 90% Leak value.. in >> p1; // 0x0f in >> p2; // 0x10 in >> j1; // 0x11 in >> j2; // 0x12 // Apnea Events in >> j3; // 0x13 // Hypopnea events in >> j4; // 0x14 // Flow Limitation events in >> j5; // 0x15 in >> j6; // 0x16 in >> j7; // 0x17 in >> p3; // 0x18 in >> p4; // 0x19 in >> p5; // 0x1a in >> x1; // 0x1b in >> x2; // 0x1c // humidifier setting 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); sess->setCount(CPAP_Obstructive, j2); sess->setCount(CPAP_Hypopnea, j3); SessDate.insert(date,sess); // sess->setCount(CPAP_Obstructive,j1); // sess->setCount(CPAP_Hypopnea,j2); // sess->setCount(CPAP_ClearAirway,j3); // sess->setCount(CPAP_Apnea,j4); //sess->setCount(CPAP_,j5); if (p1!=p2) { sess->settings[CPAP_Mode]=(int)MODE_APAP; sess->settings[CPAP_PressureMin]=p3/10.0; sess->settings[CPAP_PressureMax]=p4/10.0; } else { sess->settings[CPAP_Mode]=(int)MODE_CPAP; sess->settings[CPAP_Pressure]=p1/10.0; } sess->settings[CPAP_HumidSetting]=x2; //sess->settings[CPAP_PresReliefType]=PR_SENSAWAKE; Sessions[ts]=sess; mach->AddSession(sess,profile); } } while (!in.atEnd()); return true; } bool FPIconLoader::OpenDetail(Machine * mach, QString filename, Profile * profile) { Q_UNUSED(mach); Q_UNUSED(profile); qDebug() << filename; QByteArray header; QFile file(filename); if (!file.open(QFile::ReadOnly)) { qDebug() << "Couldn't open" << filename; return false; } header=file.read(0x200); if (header.size()!=0x200) { qDebug() << "Short file" << filename; return false; } unsigned char hsum=0; for (int i=0;i<0x1ff;i++) { hsum+=header[i]; } if (hsum!=header[0x1ff]) { qDebug() << "Header checksum mismatch" << filename; } QByteArray index; index=file.read(0x800); //long size=index.size(),pos=0; QDataStream in(index); in.setVersion(QDataStream::Qt_4_6); in.setByteOrder(QDataStream::LittleEndian); quint32 ts; QDateTime datetime; QDate date; QTime time; //FPDetIdx *idx=(FPDetIdx *)index.data(); QVector<quint32> times; QVector<quint16> start; QVector<quint8> records; quint16 t1; quint16 strt; quint8 recs,z1,z2; int day,month,year,hour,minute,second; int totalrecs=0; do { in >> z1; in >> z2; t1=z2 << 8 | z1; if (t1==0xfafe) break; day=t1 & 0x1f; month=(t1 >> 5) & 0x0f; year=2000+((t1 >> 9) & 0x3f); in >> z1; in >> z2; // ts=((z2 << 8) | z1) << 1; ts|=(t1 >> 15) & 1; // second=(ts & 0x3f); minute=(ts >> 6) & 0x3f; hour=(ts >> 12) & 0x1f; datetime=QDateTime(QDate(year,month,day),QTime(hour,minute,second)); //datetime=datetime.toTimeSpec(Qt::UTC); ts=datetime.toTime_t(); date=datetime.date(); time=datetime.time(); in >> strt; in >> recs; totalrecs+=recs; if (Sessions.contains(ts)) { times.push_back(ts); start.push_back(strt); records.push_back(recs); } } while (!in.atEnd()); QByteArray databytes=file.readAll(); in.setVersion(QDataStream::Qt_4_6); in.setByteOrder(QDataStream::BigEndian); // 5 byte repeating patterns quint8 * data=(quint8 *)databytes.data(); qint64 ti; quint8 pressure,leak, a1,a2,a3; 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.1); EventList * FLG=sess->AddEventList(CPAP_FLG,EVL_Event); EventList * OA=sess->AddEventList(CPAP_Obstructive,EVL_Event); EventList * H=sess->AddEventList(CPAP_Hypopnea,EVL_Event); EventList * FL=sess->AddEventList(CPAP_FlowLimit,EVL_Event); unsigned stidx=start[r]; int rec=records[r]; idx=stidx*15; for (int i=0;i<rec;i++) { for (int j=0;j<3;j++) { pressure=data[idx]; leak=data[idx+1]; a1=data[idx+2]; a2=data[idx+3]; a3=data[idx+4]; PR->AddEvent(ti,pressure); LK->AddEvent(ti,leak); if (a1>0) OA->AddEvent(ti,a1); if (a2>0) H->AddEvent(ti,a2); if (a3>0) FL->AddEvent(ti,a3); FLG->AddEvent(ti,a3); ti+=120000L; idx+=5; } } // sess->really_set_last(ti-360000L); // sess->SetChanged(true); // mach->AddSession(sess,profile); } return 1; } Machine *FPIconLoader::CreateMachine(QString serial,Profile *profile) { if (!profile) return NULL; qDebug() << "Create Machine " << serial; QList<Machine *> ml=profile->GetMachines(MT_CPAP); bool found=false; QList<Machine *>::iterator i; Machine *m; for (i=ml.begin(); i!=ml.end(); i++) { if (((*i)->GetClass()==fpicon_class_name) && ((*i)->properties[STR_PROP_Serial]==serial)) { MachList[serial]=*i; found=true; m=*i; break; } } if (!found) { m=new FPIcon(profile,0); } m->properties[STR_PROP_Brand]="Fisher & Paykel"; m->properties[STR_PROP_Series]=STR_MACH_FPIcon; m->properties[STR_PROP_Model]=STR_MACH_FPIcon; if (found) return m; MachList[serial]=m; profile->AddMachine(m); m->properties[STR_PROP_Serial]=serial; m->properties[STR_PROP_DataVersion]=QString::number(fpicon_data_version); QString path="{"+STR_GEN_DataFolder+"}/"+m->GetClass()+"_"+serial+"/"; m->properties[STR_PROP_Path]=path; m->properties[STR_PROP_BackupPath]=path+"Backup/"; return m; } bool fpicon_initialized=false; void FPIconLoader::Register() { if (fpicon_initialized) return; qDebug() << "Registering F&P Icon Loader"; RegisterLoader(new FPIconLoader()); //InitModelMap(); fpicon_initialized=true; }