diff --git a/history/README b/history/README new file mode 100644 index 00000000..5b28e6e3 --- /dev/null +++ b/history/README @@ -0,0 +1,9 @@ +This folder contains the python/GTK script I (jedimark) wrote that eventually turned into SleepyHead + +I can't honestly tell you if this is the latest version, because the computer I originally wrote this on died. + +I put it here for project history, reference (and humour at how sad it is.) + +It requires matplotlib and pygtk to run.. Probably pytz too, I honestly can't remember if it needed any other libs. + +I've mostly forgotten python since then. diff --git a/history/cpap.py b/history/cpap.py new file mode 100644 index 00000000..d844790d --- /dev/null +++ b/history/cpap.py @@ -0,0 +1,1783 @@ +#!/usr/bin/env python +''' + This python script is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This python script is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this script; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +''' + +# Author: Mark Watkins +# Date: 09/03/2011 +# Purpose: CPAP Support +# License: GPL + +#Attempt at faster CPAP Loader + +import sys +import os +from struct import * +from datetime import datetime as DT +from datetime import timedelta,date,time #,datetime,date,time +import time +from matplotlib.dates import drange +from matplotlib.figure import Figure +from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas +from pylab import * + +#from pytz import timezone +import pytz +import gobject +import gtk + +MYTIMEZONE="Australia/Queensland"; + + +localtz=pytz.timezone(MYTIMEZONE) +utc = pytz.utc +utcoff=time.timezone / -(60*60) + +Device_Types={ + "Unknown":0, + "PAP":1, + "Oximeter":2, + "ZEO":3 + } + +def LookupDeviceType(type): + for k,v in Device_Types.iteritems(): + if type.lower()==k.lower(): + return v + return 0 + + +class Event: + code=0 + time=None + data=[] + def __init__(self,time,code,data): + self.time=time + self.code=code + self.data=data + +class Waveform: + time=None + def __init__(self,time,waveform,size,duration,format,rate): + self.time=time + self.waveform=waveform + self.size=size + self.duration=duration + self.format=format + self.rate=rate + + +class Machine: + def __init__(self,brand,model,type): + self.brand=brand + self.model=model + self.type=LookupDeviceType(type) + + def Open(self): + print "in Machine.Open()"; + +class OxiMeter(Machine): + CodeTypes=['Error','Pulse','SpO2'] + + def __init__(self,brand,model): + Machine.__init__(self,brand,model,"Oximeter") + +import serial +class CMS50X(OxiMeter): + Baudrate=19200 + Timeout=5 + Home=os.path.expanduser('~') + + #LogDirectory=Home+os.sep+"CMS50" + #os.system("mkdir "+LogDirectory) + + def __init__(self): + OxiMeter.__init__(self,"Contec","CMS50X") + self.Device=None + self.devopen=False + # Borrowed from PySerial + if (os.name=="nt") or (sys.platform=="win32"): + self.ports = [] + for i in range(256): + try: + s = serial.Serial(i) + self.ports.append( (i, s.portstr)) + s.close() # explicit close 'cause of delayed GC in java + except serial.SerialException: + pass + self.Device=self.ports[5] + elif os.name=='posix': + import glob + + self.ports=glob.glob('/dev/ttyUSB*') + if (len(self.ports)>0): + self.Device=self.ports[0] + + def Open(self): + if not self.Device: + print "No serial device detected" + return False + + if self.devopen: + print "Device is already open" + return True + + try: + self.ser=serial.Serial(self.Device,self.Baudrate,timeout=self.Timeout) + except: + print "Couldn't open",self.Device + return False + + self.ser.flushInput() + + self.lastpulse=0 + self.lastspo2=0 + self.devopen=True + return True + + def Close(self): + if (self.devopen): + ser.close() + self.devopen=False + + def Read(self): + while self.devopen: + while self.devopen: + h=self.ser.read(1) + if len(h)>0: + if (ord(h[0]) & 0x80): # Sync Bit + break + else: + #print "Timeout!"; + self.devopen=False + return None + + c=self.ser.read(4) + if len(c)==4: + break + else: + #print "Sync error"; + self.devopen=False + return None + + + hdr=ord(h) + if (hdr & 0x10): + alarm=True + else: alarm=False + + if (hdr & 0x8):# or (hdr & 0x20): # (hdr & 0x10)==alarm + signal=True + else: signal=False + + wave1=ord(c[0]) + wave2=ord(c[1]) + pulse=ord(c[2]) + spo2=ord(c[3]) + + return [signal,alarm,wave1,wave2,pulse,spo2] + + def Save(self,time,code,value): + if (not self.start): return + + delta=time-self.lasttime + s=int(((delta.seconds*1000)+delta.microseconds/1000)) + + self.events[self.start].append(s>>8) + self.events[self.start].append(s&255) + self.events[self.start].append(code) + self.events[self.start].append(value) + self.evcnt+=1 + + self.lasttime=time + + def Record(self,path): + if self.devopen: return (None,None) + + self.starttime=DT.utcnow() + self.lasttime=self.starttime + lt=self.lasttime + + self.Open() + + lastpulse=0 + lastspo2=0 + lastalarm=True + lastsignal=True + self.evcnt=0 + + wave=[dict(),dict()] + wavestart=None + self.start=None + self.events=dict() + + while self.devopen: + D=self.Read() + if not D: continue + + t=DT.utcnow(); + d=t-lt + + if D[0]!=lastsignal or ((t-self.lasttime)>timedelta(seconds=64)): + self.Save(t,0,D[0]) + lastsignal=D[0] + + if D[1]!=lastalarm or ((t-self.lasttime)>timedelta(seconds=64)): + self.Save(t,1,D[1]) + lastalarm=D[1] + + if (not self.start or (d>timedelta(microseconds=30000))): #Lost serial sync for wavefom + self.start=t + print "Starting new event chunk",self.start + self.events[self.start]=bytearray() + + lt=t + + if D[1]: continue + + if (not wavestart or (d>timedelta(microseconds=30000))): #Lost serial sync for wavefom + wavestart=t + wave[0][wavestart]=bytearray() + wave[1][wavestart]=bytearray() + print "Starting new wave chunk",wavestart + + wave[0][wavestart].append(D[2]) + wave[1][wavestart].append(D[3]) + + if (D[4]!=lastpulse) or ((t-self.lasttime)>timedelta(seconds=64)): + self.Save(t,2,D[4]) + lastpulse=D[4] + + if (D[5]!=lastspo2) or ((t-self.lasttime)>timedelta(seconds=64)): + self.Save(t,3,D[5]) + lastspo2=D[5] + + + self.Close() + + if (path[-1]!=os.sep): path+=os.sep + + ed=sorted(self.events.keys()) + basename=path+ed[0].strftime("CMS50-%Y%m%d-%H%M%S") + efname=basename+".001" + + magic=0x35534d43 #CMS5 + f=open(efname,"wb"); + j=0 + for k,v in self.events.iteritems(): + header=bytearray(16) + timestamp=time.mktime(k.timetupple()) + l=len(v) + struct.pack_into('time[1]): continue + if (endtime[1]): continue + if (endself.sessiontimes[s][1]): + val+=len(self.session[s][type]); + + else: + for e in self.session[s][type]: + if (e.time>=start) and (e.time<=end): + val+=1 + return val + + def FirstLastEventTime(self,field,start,end): + sess=self.GetEvSessions(start,end) + + a1=self.sessiontimes[sess[0]][0] + a2=self.sessiontimes[sess[-1]][1] + #if (a1>=start): + # st=a1 + #else: + st=a1 + for e in self.session[sess[0]][field]: + if (e.time>=start): + st=e.time + break; + + #if (a2<=end): + #et=a2 + #else: + et=a2 + for e in self.session[sess[-1]][field]: + if (e.time>=end): + et=e.time + break; + + return (st,et) + + + + def GetTotalTime(self,start,end): + t=timedelta(seconds=0) + sess=self.GetEvSessions(start,end) + for s in sess: + #print self.sessiontimes[s] + a1=self.sessiontimes[s][0] + a2=self.sessiontimes[s][1] + + d=a2-a1 + + if a1end: + d-=a2-end + + #print s,a2-a1 + t+=d + + return t + + def GetEvents(self,type,start,end): + if type not in self.CodeTypes: + print "Unrecognized cpap code",field + return None + + sess=self.GetEvSessions(start,end) + E=[] + for s in sess: + for e in self.session[s][type]: + if e.time>=start and e.time<=end: + E.append(e) + return E + + def GetEventsPlot(self,type,start,end,dc=None,di=0,padsession=False): + if type not in self.CodeTypes: + print "Unrecognized cpap code",field + return None + + sess=self.GetEvSessions(start,end) + T=[] + D=[] + laste=None + firste=None + for s in sess: + for e in self.session[s][type]: + if e.time>=start and e.time<=end: + if not firste and padsession: + firste=e + D.append(0) + T.append(e.time) + T.append(e.time) + if dc: + D.append(dc) + else: + D.append(e.data[di]) + laste=e + + if padsession: + if laste: + D.append(0) + T.append(laste.time) + + return (T,D) + + def GetFlowPlots(self,start,end): + sess=self.GetFlowSessions(start,end) + T=[] + D=[] + for s in sess: + X=[] + Y=[] + for w in self.flowrate[s]: + d=timedelta(microseconds=w.rate*1000000.0) + t=w.time + for i in w.waveform: + if t>=start and t<=end: + Y.append(i) + X.append(t) + t+=d + T.append(X) + D.append(Y) + return (T,D) + + def ScanMachines(self,path): + print "Pure virtual function" + exit(1) + + def OpenSD(self): + self.machine=dict() + self.session=dict() + self.sessiontimes=dict() + self.flowrate=dict() + self.flowtimes=dict(); + + if os.name=="posix": + posix_mountpoints=["/media","/mnt"] + d=[] + for i in posix_mountpoints: + try: + a=os.listdir(i) + for j in range(0,len(a)): + a[j]=i+os.sep+a[j] + #print j + d.extend(a) + except: + 1 + + elif (os.name=="nt") or (sys.platform=="win32"): + #Meh.. i'll figure this out later. + d=['D:','E:','F:','G:','H:','I:','J:'] + #elif sys.platform=="darwin": #Darwin is posix aswell, but where? + # d=[] + + r=0 + if not len(d): + print "I've have no idea where for an SDCard on",os.name,sys.platform + return 0 + + print "Looking for CPAP data in",d + for i in d: + if self.ScanMachines(i): + r+=1 + + return r + + def GetDays(self,numdays=7,date=None): + DAYS=[] + if (not date): + dt=DT.now()#localtz.localize(DT.utcnow())-timedelta(hours=24); + else: + dt=date + + for i in range(0,numdays): + d=dt.date(); + + (sleep,wake)=cpap.GetBedtime(dt) + if sleep!=None: + ln=wake-sleep + b=cpap.GetTotalTime(sleep,wake) + DAYS.append([d,sleep,wake,ln,b]) + dt-=timedelta(hours=24) + return DAYS + + +class PRS1(CPAP): + codes=dict() + codes[0]=['UN1',[2,1]] + codes[1]=['UN2',[2,1]] + codes[2]=['PR',[2,1]] + codes[3]=['BP',[2,1,1]] + codes[4]=['PP',[2,1]] + codes[5]=['RE',[2,1]] + codes[6]=['OA',[2,1]] + codes[7]=['CA',[2,1]] + codes[0xa]=['H',[2,1]] + codes[0xb]=['UNB',[2,2]] + codes[0xc]=['FL',[2,1]] + codes[0xd]=['VS',[2]] + codes[0xe]=['UNE',[2,1,1,1]] + codes[0xf]=['CSR',[2,2,1]] + codes[0x10]=['UN10',[2,2,1]] + codes[0x11]=['LR',[2,1,1]] + codes[0x12]=['SUM',[1,1,2]] + + def __init__(self): + CPAP.__init__(self,"Philips Respironics","System One") + + def ScanMachines(self,path): + try: + d=os.listdir(path); + r=d.index("P-Series"); + except: + return False + + path+=os.sep+d[r]; + try: + d=os.listdir(path); + except: + print "Path",path,"unreadable" + return False + + prs1unit=[] + l=0 + for f in d: + if (f[0]!='P'): continue + if (f[1].isdigit()): + if f not in self.machine.keys(): + self.machine[f]=[] + self.machine[f].append(path+os.sep+f) + l+=1 + + if not l: + print "No",self.model,"machine data stored under",path + return False + + return True + + def OpenMachine(self,serial): + if serial not in self.machine.keys(): + print "Couldn't open device!" + return False + + self.session=dict() + self.sessiontimes=dict() + self.flowrate=dict() + self.flowtimes=dict(); + + for path in self.machine[serial]: + self.ReadMachineData(path,serial) + + + def ReadMachineData(self,path,serial): + try: + d=os.listdir(path); + r=d.index("p0"); + except: + print "Expected PRS1's p0 directory, and couldn't find it",path + return False + + path+=os.sep+"p0" + try: + df=os.listdir(path); + except: + print "Couldn't read directory" + return False + + + r=0 + for f in df: + filename=f + e2=f.rfind('.') + if (e2<0): continue + ext=int(f[e2+1:]) + seq=int(f[0:e2]) + + if (ext==2) and (not seq in self.session.keys()): + if self.Read002(path,filename): + r+=1 + elif (ext==5) and (not seq in self.flowrate.keys()): + if self.Read005(path,filename): + r+=1 + if (r>0): + print "Loaded",r,"files for",serial + return True + + def Read002(self,path,filename): + fn=path+os.sep+filename + try: + f=open(fn,'rb'); + except: + print "Couldn't Open File",fn + return False + + header=f.read(16) + if (len(header)<16): + print "Not enough header data in",filename + f.close() + return False + + sm=0 + for i in range(0,15): sm+=ord(header[i]) + sm&=0xff + + h1=ord(header[0]); + filesize,=unpack_from('0): #These events are also classed as vibratory snore + E=Event(td,c,fields) + self.session[sequence]['VS'].append(E); + + if (c==2) or (c==3): #CPAP Pressure + fields[0]/=10.0 + if c==3: fields[1]/=10.0 #Bipap + + E=Event(d,c,fields) + #print E.time.astimezone(localtz),self.CodeTypes[gc],fields + self.session[sequence][self.CodeTypes[gc]].append(E); + + self.sessiontimes[sequence]=[starttime,td] + return True + + def Read005(self,path,filename): + #print "Importing file",filename + fn=path+os.sep+filename + try: + f=open(fn,'rb'); + except: + print "Couldn't Open File",fn + return False + + done=0 + blocks=0; + + starts=None + while not done: + header=f.read(24) + if (len(header)<24): + if (blocks==0): + print "Not enough header data in",filename + f.close() + return False + done=1 + break; + + sm=0 + for i in range(0,23): sm+=ord(header[i]) + sm&=0xff + + h1=ord(header[0]); + blocksize,=unpack_from('self.xlimits[1]): #check start and end are within xlimits + print "Creating Highlights out of xlimit area is a sucky idea in matplotlib"; + + self.HL[index]=self.ax.axvspan(start,end,facecolor=color,alpha=0.5) + #self.ax.draw_patches(self.HL[index]) + self.ResetLimits() + + def SetXLim(self,start,end): + self.xlimits=[start,end] + self.ax.set_xlim(self.xlimits) + + def SetYLim(self,bottom,top): + self.ylimits=[bottom,top] + self.ax.set_ylim(self.ylimits) + + def ResetLimits(self): + self.ax.set_xlim(self.xlimits) + self.ax.set_ylim(self.ylimits) + + def SetDateTicks(self): + e=self.xlimits[1]-self.xlimits[0] + self.ax.xaxis.set_major_formatter(DateFormatter("%H:%M",tz=localtz)) + if e>=timedelta(hours=10): + self.ax.xaxis.set_major_locator(HourLocator(range(0,100,2),tz=localtz)) + self.ax.xaxis.set_minor_locator(MinuteLocator(range( 0,100,10),tz=localtz)) + elif e>=timedelta(hours=4): + self.ax.xaxis.set_major_locator(HourLocator(range(0,100,1),tz=localtz)) + self.ax.xaxis.set_minor_locator(MinuteLocator(range( 0,100,5),tz=localtz)) + elif e>=timedelta(seconds=3600): + self.ax.xaxis.set_major_locator(MinuteLocator(range(0,100,30),tz=localtz)) + self.ax.xaxis.set_minor_locator(MinuteLocator(range( 0,100,1),tz=localtz)) + elif e>=timedelta(seconds=1200): + self.ax.xaxis.set_major_locator(MinuteLocator(range(0,100,5),tz=localtz)) + self.ax.xaxis.set_minor_locator(SecondLocator(range( 0,100,15),tz=localtz)) + elif e>=timedelta(seconds=300): + self.ax.xaxis.set_major_locator(MinuteLocator(range(0,100,1),tz=localtz)) + self.ax.xaxis.set_minor_locator(SecondLocator(range(0,100,5),tz=localtz)) + else: + self.ax.xaxis.set_major_locator(SecondLocator(range(0,100,30),tz=localtz)) + self.ax.xaxis.set_minor_locator(SecondLocator(range(0,100,1),tz=localtz)) + self.ax.xaxis.set_major_formatter(DateFormatter("%H:%M:%S",tz=localtz)) + + +class LeaksGraph(Graph): + def __init__(self,cpap,xlim=[0,0],ylim=[0,129],grid=True): + Graph.__init__(self,"Leak Rate") + #self.ax=ax + self.machine=cpap + self.Create() + self.ylimits=ylim + self.grid=grid + self.xlimits=xlim + self.T=[] + self.D=[] + + def Update(self,start,end): + if self.xlimits: + if (start==self.xlimits[0]) and (end==self.xlimits[1]): + return + + #(start,end)=self.machine.FirstLastEventTime('LR',start,end) + + (self.T,self.D)=self.machine.GetEventsPlot('LR',start=start,end=end,padsession=True) + self.xlimits=[self.T[0],self.T[-1]] + + avg=sum(self.D)/len(self.D) + for i in range(0,len(self.D)): + self.D[i]-=avg; + + print "Average Leaks:",avg + + def Plot(self): + self.ax.cla() + self.SetTitle(self.name) + if (self.grid): self.ax.grid(True); + + + if len(self.T)>0: + self.ax.plot_date(self.T,self.D,'black',aa=True,tz=localtz) + self.ax.fill_between(self.T,self.D,0,color='gray') + + + self.SetDateTicks() + self.ax.yaxis.set_major_locator(MultipleLocator(20)) + self.ax.yaxis.set_minor_locator(MultipleLocator(5)) + + self.ResetLimits() + + +class PressureGraph(Graph): + def __init__(self,cpap,xlim=[0,0],ylim=[1,20],grid=True): + self.name="Pressure" + #self.ax=ax + self.machine=cpap + self.Create() + self.ylimits=ylim + self.grid=grid + self.xlimits=xlim + self.T=[] + self.D=[] + + def Update(self,start,end): + if self.xlimits: + if (start==self.xlimits[0]) and (end==self.xlimits[1]): + return + + #(start,end)=self.machine.FirstLastEventTime('LR',start,end) + self.xlimits=[start,end] + + (self.T,self.D)=self.machine.GetEventsPlot('PR',start=start,end=end) + (self.T1,self.D1)=self.machine.GetEventsPlot('BP',start=start,end=end,di=0) + (self.T2,self.D2)=self.machine.GetEventsPlot('BP',start=start,end=end,di=1) + + #for i in range(0,len(self.D)): + # self.D[i]/=10.0; + #avg=sum(self.D)/len(self.D) + + #print "Average Pressure:",avg + + def Plot(self): + self.ax.cla() + self.SetTitle(self.name) + if (self.grid): self.ax.grid(True); + + if len(self.T)>0: + self.ax.plot_date(self.T,self.D,'green',aa=True,tz=localtz) + if (len(self.T1)>0): + self.ax.plot_date(self.T1,self.D2,'orange',aa=True,tz=localtz) + if (len(self.T2)>0): + self.ax.plot_date(self.T2,self.D2,'purple',aa=True,tz=localtz) + + self.SetDateTicks() + self.ax.yaxis.set_major_locator(MultipleLocator(5)) + self.ax.yaxis.set_minor_locator(MultipleLocator(1)) + + self.ResetLimits() + + +class SleepFlagsGraph(Graph): + colours=['','y','r','k','b','c','m','g'] + flags=['','RE','VS','FL','H','OA','CA','CSR'] + barcolors=['w','#ffffd0','#ffdfdf','#efefef','#d0d0ff','#cfefff','#ebcdef','#dfffdf','w']; + + marker='.' + + def __init__(self,cpap,waveform,xlim=[0,0],ylim=[0,10],grid=True): + self.name="Sleep Flags" + self.waveform=waveform + #self.ax=ax + self.machine=cpap + self.Create(height=175) + self.ylimits=[0,len(self.flags)] + self.grid=grid + self.xlimits=xlim + self.T=dict() + self.D=dict() + self.canvas.mpl_connect('pick_event', self.onpick) + self.canvas.mpl_connect('button_press_event', self.on_press) + self.canvas.mpl_connect('button_release_event', self.on_release) + #self.canvas.mpl_connect('scroll_event', self.on_scroll) + self.lastscroll=DT.now() + self.scrollsteps=0 + + #self.canvas.mpl_connect('motion_notify_event', self.on_motion) + + def onpick(self,event): + N = len(event.ind) + if not N: return True + return True + thisline = event.artist + xdata, ydata = thisline.get_data() + ind = event.ind + wavedelta=timedelta(seconds=300) #self.waveform.xlimits[1]-self.waveform.xlimits[0] + + d=timedelta(seconds=wavedelta.seconds/2) + self.waveform.xlimits[0]=xdata[ind][0]-d + if (self.waveform.xlimits[0](self.xlimits[1]-wavedelta)): self.waveform.xlimits[0]=self.xlimits[1]-wavedelta + self.waveform.xlimits[1]=self.waveform.xlimits[0]+wavedelta; + + self.Highlight(self.waveform.xlimits[0],self.waveform.xlimits[1],'orange') + self.Redraw() + self.waveform.ResetLimits() + self.waveform.SetDateTicks() + self.waveform.Redraw() + + def do_scroll(self,steps,event): + ct=DT.now() + if (cttimedelta(seconds=3600)): wd=timedelta(seconds=300) + self.waveform.xlimits[0]=d1-timedelta(seconds=wd.seconds/2); + self.waveform.xlimits[1]=self.waveform.xlimits[0]+wd + else: + self.waveform.xlimits[0]=d1 + self.waveform.xlimits[1]=d2 + + if (self.waveform.xlimits[0]self.xlimits[1]): + self.waveform.xlimits[1]=self.xlimits[1] + self.waveform.xlimits[0]=self.xlimits[1]-wd + + self.Highlight(self.waveform.xlimits[0],self.waveform.xlimits[1],'orange') + self.Redraw() + + self.waveform.ResetLimits(); + self.waveform.SetDateTicks() + self.waveform.Redraw() + + def Update(self,start,end): + if self.xlimits: + if (start==self.xlimits[0]) and (end==self.xlimits[1]): + return + + #(start,end)=self.machine.FirstLastEventTime('LR',start,end) + self.xlimits=[start,end] + + #if self.waveform: + #self.waveform.xlimits[0]=start + #self.WaveDelta=timedelta(seconds=300) + #self.waveform.xlimits[1]=start+self.WaveDelta + + j=0 + for i in self.flags: + if (i=="CSR"): + self.T[i]=[] + E=self.machine.GetEvents(i,start=start,end=end) + for e in E: + r=e.time-timedelta(seconds=e.data[1])-timedelta(seconds=e.data[0]/2) + self.T[i].append(r) + self.D[i]=[j]*len(self.T[i]) + elif (i!=""): + (self.T[i],self.D[i])=self.machine.GetEventsPlot(i,start=start,end=end,dc=j) + + j+=1 + + def Plot(self): + self.ax.cla() + self.SetTitle(self.name) + if (self.grid): self.ax.grid(True); + + j=0; + for i in self.flags: + if (i!=""): + if (len(self.T[i])>0): + self.ax.plot_date(self.T[i],self.D[i],self.colours[j]+self.marker,picker=5,aa=False,tz=localtz,alpha=1) + j+=1 + + self.SetDateTicks() + self.ax.yaxis.set_major_locator(MultipleLocator(1)) + self.ax.set_yticklabels(self.flags) + + yTicks=[0] + yTicks.extend(self.ax.get_yticks()) + h=(yTicks[1]-yTicks[0]) + for i in range(1,len(yTicks)): + yTicks[i]-=h/2 + a1=date2num(self.xlimits[0]) + a2=date2num(self.xlimits[1]) + self.ax.barh(yTicks, [a2-a1]*len(yTicks), height=h, left=a1, color=self.barcolors,alpha=0.5) + + self.ResetLimits() + + +class WaveformGraph(Graph): + colours=['y','r','k','b','c','m','g'] + flags=['RE','VS','FL','H','OA','CA'] + + def __init__(self,cpap,xlim=[0,0],ylim=[-69,69],grid=True): + self.name="Flow Rate Waveform" + #self.ax=ax + self.machine=cpap + self.Create() + self.ylimits=ylim + self.grid=grid + self.xlimits=xlim + self.T=[] + self.D=[] + self.FT=dict() + self.FD=dict() + self.canvas.mpl_connect('button_press_event', self.on_press) + self.canvas.mpl_connect('button_release_event', self.on_release) + self.canvas.mpl_connect('scroll_event', self.on_scroll) + self.lastscroll=DT.now() + self.scrollsteps=0 + self.sg=None + + def set_sleepgraph(self,sg): + self.sg=sg + + def on_press(self,event): + if event.inaxes != self.ax: return + + contains, attrd = self.ax.patch.contains(event) + if not contains: return + #print 'event contains', self.ax.patch.xy + x0, y0 = self.ax.patch.xy + self.press = event.xdata, event.ydata + + def on_release(self,event): + if event.inaxes != self.ax: return + #if event.inaxes != self.ax: return + #minx=min(event.xdata,self.press[0]); + #maxx=max(event.xdata,self.press[0]); + d1=num2date(self.press[0],tz=localtz) + d2=num2date(event.xdata,tz=localtz) + d=d2-d1 + self.xlimits[0]-=d + self.xlimits[1]-=d + if self.xlimits[0]self.sg.xlimits[1]: + self.xlimits[1]=self.sg.xlimits[1] + self.xlimits[0]=self.xlimits[1]-d + + #update SleepGraph + if (self.sg): + self.sg.Highlight(self.xlimits[0],self.xlimits[1],'orange') + self.sg.Redraw() + + self.ResetLimits() + self.SetDateTicks() + self.Redraw() + + def do_scroll(self,steps,event): + ct=DT.now() + if (ctlastpressure: cod="PUP" + elif p0): + self.ax.plot_date(self.FT[i],self.FD[i],self.colours[j]+'d',aa=True,alpha=.8,tz=localtz) + self.ax.vlines(self.FT[i],50,-50,self.colours[j],lw=1,alpha=0.4) + j+=1 + + j=0 + + for i in range(0,len(self.T)): + self.ax.plot_date(self.T[i],self.D[i],'green',aa=True,tz=localtz,alpha=0.7) + + self.SetDateTicks() + self.ax.yaxis.set_major_locator(MultipleLocator(20)) + self.ax.yaxis.set_minor_locator(MultipleLocator(5)) + + if (len(self.FT['PUP'])): + self.ax.plot_date(self.FT['PUP'],self.FD['PUP'],'k^',aa=True,alpha=.8,tz=localtz) + if (len(self.FT['PDN'])): + self.ax.plot_date(self.FT['PDN'],self.FD['PDN'],'kv',aa=True,alpha=.8,tz=localtz) + + if (len(self.FT['PP'])): + self.ax.plot_date(self.FT['PP'],self.FD['PP'],'r.',aa=True,alpha=.8,tz=localtz) + + for E in self.FT['CSR']: + e=E.time-timedelta(seconds=E.data[1]); + s=e-timedelta(seconds=E.data[0]) + self.ax.axvspan(s,e,facecolor='#d0ffd0'); + #self.Highlight(s,e,color="#d0ffd0",index=j) + + self.ResetLimits() + +def AboutBox(a): + txt='''SleepyHead v0.02 + +Details: +Author: Mark Watkins (jedimark) +Homepage: http://sleepyhead.sourceforge.net +Please report any bugs on sourceforge. + +License: +This software is released under the GNU Public Licence. + +Disclaimer: +This is not medical software. Any output this program +produces should not be used to make medical decisions. + +Special Thanks: +Mike Hoolehan - Check out his awesome Onkor Project +Troy Schultz - For great technical advice +Mark Bruscke - For encouragement and advice + +and to the very awesome CPAPTalk Forum +''' + msg=gtk.MessageDialog(flags=gtk.DIALOG_MODAL,type=gtk.MESSAGE_INFO,buttons=gtk.BUTTONS_CLOSE) + msg.set_markup(txt) + + msg.run() + msg.destroy() + +def CreateMenu(): + file_menu = gtk.Menu() + open_item = gtk.MenuItem("_Backup SD Card") + save_item = gtk.MenuItem("_Print") + quit_item = gtk.MenuItem("E_xit") + file_menu.append(open_item) + file_menu.append(save_item) + file_menu.append(quit_item) + quit_item.connect_object ("activate", lambda x: gtk.main_quit(), "file.quit") + open_item.show() + save_item.show() + quit_item.show() + + help_menu = gtk.Menu() + about_item = gtk.MenuItem("_About") + about_item.connect_object("activate",AboutBox,"help.about") + help_menu.append(about_item) + about_item.show() + + file_item = gtk.MenuItem("_File") + file_item.show() + help_item = gtk.MenuItem("_Help") + help_item.show() + + menu_bar = gtk.MenuBar() + menu_bar.show() + file_item.set_submenu(file_menu) + menu_bar.append(file_item) + + help_item.set_submenu(help_menu) + menu_bar.append(help_item) + + return menu_bar + + +class DailyGraphs: + def __init__(self,cpap): + self.cpap=cpap + self.layout=gtk.ScrolledWindow() + self.layout.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) + self.vbox = gtk.VBox() + self.layout.add_with_viewport(self.vbox) + + self.graph=dict(); + self.graph['Waveform']=WaveformGraph(cpap) + self.graph['Leaks']=LeaksGraph(cpap) + self.graph['Pressure']=PressureGraph(cpap) + self.graph['SleepFlags']=SleepFlagsGraph(cpap,self.graph['Waveform']) + self.graph['Waveform'].set_sleepgraph(self.graph['SleepFlags']) + + self.vbox.pack_start(self.graph['SleepFlags'].canvas,expand=False) + self.vbox.pack_start(self.graph['Waveform'].canvas,expand=False) + self.vbox.pack_start(self.graph['Leaks'].canvas,expand=False) + self.vbox.pack_start(self.graph['Pressure'].canvas,expand=False) + + self.machines=gtk.combo_box_new_text() + + for mach in cpap.machine.keys(): + self.machines.append_text(mach) + + self.datesel=gtk.Calendar() + + self.textbox=gtk.TextView(buffer=None) + self.textbox.set_editable(False) + + self.datesel.connect('month_changed',self.cal_month_selected,cpap) + self.datesel.connect('day_selected',self.cal_day_selected) + + self.machines.connect("changed",self.select_machine,cpap) + + self.databox = gtk.VBox(homogeneous=False) + self.rescanbutton=gtk.Button("_Rescan Media") + self.rescanbutton.connect('pressed',self.pushed_rescan) + #self.zeobutton=gtk.Button("Load _ZEO Data") + #self.oxibutton=gtk.Button("Load _Oximeter Data") + self.databox.pack_start(self.machines,expand=False,padding=2) + self.databox.pack_start(self.datesel,expand=False,padding=2) + self.databox.pack_start(self.rescanbutton,expand=False,padding=0) + #self.databox.pack_start(self.zeobutton,expand=False,padding=0) + #self.databox.pack_start(self.oxibutton,expand=False,padding=0) + self.databox.pack_start(self.textbox,expand=True,padding=2) + + self.machines.set_active(0) + #self.cal_month_selected(self.datesel,cpap) + #self.cal_day_selected(self.datesel) + + def select_machine(self,combo,cpap): + msg=gtk.MessageDialog(type=gtk.MESSAGE_INFO,buttons=gtk.BUTTONS_NONE,message_format="Please wait, Loading CPAP Data") + msg.show_all() + gtk.gdk.window_process_all_updates() + + mach=combo.get_active_text(); + cpap.OpenMachine(mach) + msg.destroy() + self.cal_month_selected(self.datesel,self.cpap) + self.cal_day_selected(self.datesel) + + def pushed_rescan(self,event): + + self.cpap.OpenSD() + + mach=self.machines.get_active_text(); + self.machines.get_model().clear() + j=0 + cmi=-1 + for m in cpap.machine.keys(): + i=self.machines.insert_text(j,m) + if (m==mach): cmi=j + j+=1 + + if (cmi>=0): + self.machines.set_active(cmi) + else: + self.machines.set_active(0) + + + #cpap.OpenMachine(mach) + + self.cal_month_selected(self.datesel,self.cpap) + #self.cal_day_selected(self.datesel) + + def Draw(self): + for k,v in self.graph.iteritems(): + v.Redraw() + + def ShowGraphs(self,show): + if show: + vis=True + else: + vis=False + + for i in self.graph.keys(): + self.graph[i].canvas.set_visible(vis) + + def Update(self,start,end): + for k,v in self.graph.iteritems(): + v.Update(start,end) + sess=cpap.GetFlowSessions(start,end) + if (len(sess)>0): + wvis=True; + else: wvis=False; + self.graph['Waveform'].canvas.set_visible(wvis) + text="Date: "+start.astimezone(localtz).strftime("%Y-%m-%d")+"\n\n" + text+="Bedtime: "+start.astimezone(localtz).strftime("%H:%M:%S")+"\n" + text+="Waketime: "+end.astimezone(localtz).strftime("%H:%M:%S")+"\n\n" + + tt=cpap.GetTotalTime(start,end) + text+="Total Time: "+str(tt)+"\n\n" + + if not wvis: + text+="No Waveform Data Available\n\n" + oa=cpap.CountEvents('OA',start,end) + h=cpap.CountEvents('H',start,end) + ah=oa+h + ca=cpap.CountEvents('CA',start,end) + fl=cpap.CountEvents('FL',start,end) + vs=cpap.CountEvents('VS',start,end) + re=cpap.CountEvents('RE',start,end) + + PR=cpap.GetEvents('PR',start,end) + + if (len(PR)>0): + avgp=0 + laste=PR[0] + lastp=int(PR[0].data[0]*10) + lastt=PR[0].time + TPR=[timedelta(seconds=0)]*256 + don=False + totalptime=timedelta(0) + for e in PR[1:]: + p=int(e.data[0]*10) + TPR[lastp]+=(e.time-lastt) + totalptime+=(e.time-lastt) + lastt=e.time + lastp=p + + + #if (not don): + # TPR[lastp]+=lastt- + + np=timedelta(seconds=totalptime.seconds*.9) + npc=timedelta(seconds=0) + npp=0 + lastp=0 + for i in range(0,256): + lpc=npc + npc+=TPR[i] + if (npc>=np): + s2=1-(float(lpc.seconds)/float(npc.seconds)) + d=(i-lastp)/10.0 + npp=(lastp/10.0)+(s2*d) + break + + if TPR[i]>timedelta(seconds=0): + lastp=i + + avgp=0 + + sm1=0 + sm2=0 + sm3=0 + for i in range(0,256): + if TPR[i]>timedelta(seconds=0): + s=float(TPR[i].seconds)/float(totalptime.seconds) + sm1+=s*float(i) + sm2+=s + sm3=(float(i)/10.0)*TPR[i].seconds + + #avgp=sm3/totalptime.seconds + avgp=sm1/sm2/10.0 #Weighted Average + else: + avgp=0 + npp=0 + + LK=cpap.GetEvents('LR',start,end); + avgl=0 + for e in LK: avgl+=e.data[0]-19 + avgl/=len(LK) + + + + CSR=cpap.GetEvents('CSR',start,end); + dur=0 + for e in CSR: dur+=e.data[0]; + csr=(100.0/tt.seconds)*dur + + text+="Average Pressure=%(#)0.2f\n"%{'#':avgp} + text+="90%% Pressure=%(#)0.2f\n\n"%{'#':npp} + + text+="CSR %% of night=%(#)0.2f\n" % {"#":csr} + + s=tt.seconds/3600.0 + text+="OA=%(#)0.2f\n"%{'#':oa/s} + text+="H=%(#)0.2f\n"%{'#':h/s} + text+="CA=%(#)0.2f\n"%{'#':ca/s} + text+="FL=%(#)0.2f\n"%{'#':fl/s} + text+="VS=%(#)0.2f\n"%{'#':vs/s} + text+="RE=%(#)0.2f\n"%{'#':re/s} + text+="AHI=%(#)0.2f\n\n"%{'#':ah/s} + + text+="Leak=%(#)0.2f\n"%{'#':avgl} + + + buf=self.textbox.get_buffer() + buf.set_text(text) + #self.date.set_text()) +# self.bedtime.set_text("Bedtime: "+start.astimezone(localtz).strftime("%H:%M:%S")) +# self.waketime.set_text("Waketime: "+end.astimezone(localtz).strftime("%H:%M:%S")) + + def Plot(self): + for k,v in self.graph.iteritems(): + v.Plot() + self.graph['Waveform'].ResetLimits() + + def cal_month_selected(self,cal,cpap): + (y,m,d)=cal.get_date(); + + d=1 + m+=2 + + if (m>11): + y+=1 + m%=12 + + ldom=DT(y,m,d,0,0,0)-timedelta(hours=1) + #print "Getting",ldom.day,"days back from",ldom + D=cpap.GetDays(ldom.day,date=ldom) + cal.freeze() + for i in range(0,ldom.day-1): + cal.unmark_day(i) + + for i in D: + cal.mark_day(i[0].day) + + cal.thaw() + + def cal_day_selected(self,cal): + (y,m,d)=cal.get_date() + dat=DT(y,m+1,d,0,0,0) + (st,et)=cpap.GetBedtime(dat) + + if st: + msg=gtk.MessageDialog(type=gtk.MESSAGE_INFO,buttons=gtk.BUTTONS_NONE,message_format="Updating Plots - Please wait") + msg.show_all() + gtk.gdk.window_process_all_updates() + + self.ShowGraphs(True) + #print "Bedtime",st.astimezone(localtz),"Wakeup",et.astimezone(localtz) + self.Update(st,et) + self.Plot() + self.Draw() + msg.destroy() + else: + self.ShowGraphs(False) + text="No data available for selected date" + buf=self.textbox.get_buffer() + buf.set_text(text) + + +path="/home/mark/.sleepyhead/CMS50" +#cms50=CMS50X() +#(event,wave)=cms50.Record(path) + +#exit(1) + +cpap=PRS1() +cpap.OpenSD() + +win=gtk.Window() +win.connect("destroy", lambda x: gtk.main_quit()) +win.set_default_size(1200,680) +win.set_title("SleepyHead v0.02") + +mainbox=gtk.VBox() +mainbox.pack_start(CreateMenu(),expand=False) + +notebook=gtk.Notebook() +notebook.unset_flags(gtk.CAN_FOCUS) + +dailybox=gtk.HBox() + +spo2box=gtk.HBox() + + +mainbox.pack_start(notebook,expand=True) + +DG=DailyGraphs(cpap) +dailybox.pack_start(DG.databox,expand=False) +dailybox.pack_start(DG.layout,expand=True) + +page1=notebook.insert_page(dailybox,gtk.Label("Daily")) +#page2=notebook.insert_page(dailybox,gtk.Label("Overview")) +#page3=notebook.insert_page(spo2box,gtk.Label("SpO2")) +notebook.set_current_page(page1) + +win.add(mainbox) + + +win.show_all() +gtk.main() + + + diff --git a/sleepyhead/SleepLib/loader_plugins/cms50_loader.cpp b/sleepyhead/SleepLib/loader_plugins/cms50_loader.cpp index 8bff12a4..38560ae4 100644 --- a/sleepyhead/SleepLib/loader_plugins/cms50_loader.cpp +++ b/sleepyhead/SleepLib/loader_plugins/cms50_loader.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,7 @@ CMS50Loader::CMS50Loader() m_vendorID = 0x10c4; m_productID = 0xea60; + cms50dplus = false; oxirec = nullptr; @@ -124,7 +126,7 @@ int CMS50Loader::Open(QString path, Profile *profile) return 1; } QString ext = path.section(".",1); - if ((ext.compare("spo", Qt::CaseInsensitive)==0) || (ext.compare("spor", Qt::CaseInsensitive)==0)) { + if ((ext.compare("spo2", Qt::CaseInsensitive)==0) || (ext.compare("spo", Qt::CaseInsensitive)==0) || (ext.compare("spor", Qt::CaseInsensitive)==0)) { // try to read and process SpoR file.. return readSpoRFile(path) ? 1 : 0; } @@ -471,41 +473,88 @@ bool CMS50Loader::readSpoRFile(QString path) return false; } + bool spo2header = false; + QString ext = path.section('.', -1); + if (ext.compare("spo2",Qt::CaseInsensitive) == 0) { + spo2header = true; + } + QByteArray data; data = file.readAll(); - long size = data.size(); + QDataStream in(data); + in.setByteOrder(QDataStream::LittleEndian); + quint16 pos; + in >> pos; - // position data stream starts at - int pos = ((unsigned char)data.at(1) << 8) | (unsigned char)data.at(0); + in.skipRawData(pos - 2); - // next is 0x0002 - // followed by 16bit duration in seconds + //long size = data.size(); - // Read date and time (it's a 16bit charset) - char dchr[20]; - int j = 0; - for (int i = 0; i < 18 * 2; i += 2) { - dchr[j++] = data.at(8 + i); + if (!spo2header) { + // next is 0x0002 + // followed by 16bit duration in seconds + + // Read date and time (it's a 16bit charset) + + char dchr[20]; + int j = 0; + for (int i = 0; i < 18 * 2; i += 2) { + dchr[j++] = data.at(8 + i); + } + + dchr[j] = 0; + if (dchr[0]) { + QString dstr(dchr); + m_startTime = QDateTime::fromString(dstr, "MM/dd/yy HH:mm:ss"); + if (m_startTime.date().year() < 2000) { m_startTime = m_startTime.addYears(100); } + } else { + m_startTime = QDateTime(QDate::currentDate(), QTime(0,0,0)); + } + } else { // !spo2header + + quint32 samples = 0; // number of samples + + quint32 year, month, day; + quint32 hour, minute, second; + + if (data.at(pos) != 1) { + qWarning() << ".spo2 file" << path << "might be a different"; + } + + // Unknown cruft... + in.skipRawData(200); + + in >> year >> month >> day; + in >> hour >> minute >> second; + + m_startTime = QDateTime(QDate(year, month, day), QTime(hour, minute, second)); + + // ignoring it for now + pos += 0x1c + 200; + + in >> samples; } - dchr[j] = 0; - QString dstr(dchr); - m_startTime = QDateTime::fromString(dstr, "MM/dd/yy HH:mm:ss"); - if (m_startTime.date().year() < 2000) { m_startTime = m_startTime.addYears(100); } - oxirec = new QVector; oxisessions[m_startTime] = oxirec; unsigned char o2, pr; // Read all Pulse and SPO2 data - for (int i = pos; i < size - 2;) { - o2 = (unsigned char)(data.at(i + 1)); - pr = (unsigned char)(data.at(i + 0)); + do { + in >> o2; + in >> pr; oxirec->append(OxiRecord(pr, o2)); - i += 2; - } + } while (!in.atEnd()); + + +// for (int i = pos; i < size - 2;) { +// o2 = (unsigned char)(data.at(i + 1)); +// pr = (unsigned char)(data.at(i + 0)); +// oxirec->append(OxiRecord(pr, o2)); +// i += 2; +// } // processing gets done later return true; diff --git a/sleepyhead/SleepLib/schema.cpp b/sleepyhead/SleepLib/schema.cpp index db6645ed..58d79e0b 100644 --- a/sleepyhead/SleepLib/schema.cpp +++ b/sleepyhead/SleepLib/schema.cpp @@ -177,7 +177,7 @@ void init() QObject::tr("VS"), STR_UNIT_EventsPerHour, DEFAULT, QColor("red"))); schema::channel.add(GRP_CPAP, new Channel(CPAP_VSnore2 = 0x1008, FLAG, SESSION, "VSnore2", - QObject::tr("Vibratory Snore"), + QObject::tr("Vibratory Snore (VS2) "), QObject::tr("A vibratory snore as detcted by a System One machine"), QObject::tr("VS2"), STR_UNIT_EventsPerHour, DEFAULT, QColor("red"))); diff --git a/sleepyhead/oximeterimport.cpp b/sleepyhead/oximeterimport.cpp index 14dacd56..f2a8bb6d 100644 --- a/sleepyhead/oximeterimport.cpp +++ b/sleepyhead/oximeterimport.cpp @@ -271,7 +271,7 @@ void OximeterImport::on_fileImportButton_clicked() #endif - QString filename = QFileDialog::getOpenFileName(nullptr , tr("Select a valid oximetry data file"), documentsFolder, tr("Oximetry Files (*.spo *.spor *.dat)")); + QString filename = QFileDialog::getOpenFileName(nullptr , tr("Select a valid oximetry data file"), documentsFolder, tr("Oximetry Files (*.spo *.spor *.spo2 *.dat)")); if (filename.isEmpty()) return;