From 62c03805fc5f68f1ee52ba1b5d4e8a8d2c5b854a Mon Sep 17 00:00:00 2001 From: Randy Sargent Date: Wed, 2 Feb 2011 06:15:22 -0500 Subject: [PATCH] Initial commit. Works --- .gitignore | 6 + Makefile | 15 ++ TODO | 4 + ZeoRawData-2.0/PKG-INFO | 10 ++ ZeoRawData-2.0/README.txt | 15 ++ ZeoRawData-2.0/ZeoRawData/BaseLink.py | 185 +++++++++++++++++++++++ ZeoRawData-2.0/ZeoRawData/BaseLink.pyc | Bin 0 -> 5548 bytes ZeoRawData-2.0/ZeoRawData/BaseLink.py~ | 184 ++++++++++++++++++++++ ZeoRawData-2.0/ZeoRawData/Parser.py | 118 +++++++++++++++ ZeoRawData-2.0/ZeoRawData/Parser.pyc | Bin 0 -> 4084 bytes ZeoRawData-2.0/ZeoRawData/Utility.py | 175 +++++++++++++++++++++ ZeoRawData-2.0/ZeoRawData/Utility.pyc | Bin 0 -> 3354 bytes ZeoRawData-2.0/ZeoRawData/__init__.py | 10 ++ ZeoRawData-2.0/ZeoRawData/__init__.pyc | Bin 0 -> 364 bytes ZeoRawData-2.0/setup.py | 12 ++ hex2binary.c | 14 ++ plot_hypnogram.pl | 15 ++ raw2csv.py | 201 +++++++++++++++++++++++++ 18 files changed, 964 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 TODO create mode 100644 ZeoRawData-2.0/PKG-INFO create mode 100644 ZeoRawData-2.0/README.txt create mode 100644 ZeoRawData-2.0/ZeoRawData/BaseLink.py create mode 100644 ZeoRawData-2.0/ZeoRawData/BaseLink.pyc create mode 100644 ZeoRawData-2.0/ZeoRawData/BaseLink.py~ create mode 100644 ZeoRawData-2.0/ZeoRawData/Parser.py create mode 100644 ZeoRawData-2.0/ZeoRawData/Parser.pyc create mode 100644 ZeoRawData-2.0/ZeoRawData/Utility.py create mode 100644 ZeoRawData-2.0/ZeoRawData/Utility.pyc create mode 100644 ZeoRawData-2.0/ZeoRawData/__init__.py create mode 100644 ZeoRawData-2.0/ZeoRawData/__init__.pyc create mode 100644 ZeoRawData-2.0/setup.py create mode 100644 hex2binary.c create mode 100755 plot_hypnogram.pl create mode 100755 raw2csv.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed1a652 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.zeo +*.csv +*.plot +*~ +hex2binary +junkyard diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b045e3a --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +all: hex2binary anne-110126.zeo anne-110127.zeo anne-110128.zeo anne-110129.zeo anne-110130.zeo anne-110131.zeo anne-110201.zeo + +%.zeo: hex/%.zeo + ./hex2binary <$^ >$@ + +sync: + rsync -av john-2.local:/Users/anne/education/bodytrack/zeologger/raw-data/\*.zeo . + rm *.csv + -./raw2csv.py + +plot: + ./plot_hypnogram.pl hypnogram.csv + +hex2binary: hex2binary.c + gcc -Wall hex2binary.c -o hex2binary diff --git a/TODO b/TODO new file mode 100644 index 0000000..0793bd9 --- /dev/null +++ b/TODO @@ -0,0 +1,4 @@ +Git setup +Figure out how to remotely cat +Commandline arg to select file +Commandline arg to block until new data, optionally diff --git a/ZeoRawData-2.0/PKG-INFO b/ZeoRawData-2.0/PKG-INFO new file mode 100644 index 0000000..63efbf3 --- /dev/null +++ b/ZeoRawData-2.0/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: ZeoRawData +Version: 2.0 +Summary: Zeo Raw Data Library +Home-page: http://www.myzeo.com +Author: Zeo Inc. +Author-email: customersupport@myZeo.com +License: http://developers.myzeo.com/terms-and-conditions/ +Description: Library allowing real-time interaction with a Zeo running the 2.6.3R firmware. +Platform: UNKNOWN diff --git a/ZeoRawData-2.0/README.txt b/ZeoRawData-2.0/README.txt new file mode 100644 index 0000000..c00c2e0 --- /dev/null +++ b/ZeoRawData-2.0/README.txt @@ -0,0 +1,15 @@ +Zeo Raw Data Library + +==================== + +Copyright 2010 by Zeo Inc. All Rights Reserved + + + +By using this library you agree to the terms and conditions located at: + http://developers.myzeo.com/terms-and-conditions/ + + + +Installation: + Run python setup.py install diff --git a/ZeoRawData-2.0/ZeoRawData/BaseLink.py b/ZeoRawData-2.0/ZeoRawData/BaseLink.py new file mode 100644 index 0000000..dbb65f0 --- /dev/null +++ b/ZeoRawData-2.0/ZeoRawData/BaseLink.py @@ -0,0 +1,185 @@ +""" +BaseLink +-------- + +Listens to the data coming from the serial port connected to Zeo. + +The serial port is set at baud 38400, no parity, one stop bit. +Data is sent Least Significant Byte first. + +The serial protocol is: + * AncllLLTttsid + + * A is a character starting the message + * n is the protocol "version", ie "4" + * c is a one byte checksum formed by summing the identifier byte and all the data bytes + * ll is a two byte message length sent LSB first. This length includes the size of the data block plus the identifier. + * LL is the inverse of ll sent for redundancy. If ll does not match ~LL, we can start looking for the start of the next block immediately, instead of reading some arbitrary number of bytes, based on a bad length. + * T is the lower 8 bits of Zeo's unix time. + * tt is the 16-bit sub-second (runs through 0xFFFF in 1second), LSB first. + * s is an 8-bit sequence number. + * i is the datatype + * d is the array of binary data + +The incoming data is cleaned up into packets containing a timestamp, +the raw data output version, and the associated data. + +External code can be sent new data as it arrives by adding +themselves to the callback list using the addCallBack function. +It is suggested, however, that external code use the ZeoParser to +organize the data into events and slices of data. + +""" + +#System Libraries +import threading +#from serial import Serial + +#Zeo Libraries +from Utility import * + +class BaseLink(threading.Thread): + """ + Runs on a seperate thread and handles the raw serial communications + and parsing required to communicate with Zeo. Each time data is + successfully received, it is sent out to all of the callbacks. + """ + + def __init__(self, port): + """ + Create a new class with default serial port settings. + Parameters: + port - the name of the serialport to open + i.e. COM1 or /dev/ttyUSB0 + """ + + threading.Thread.__init__(self) + self.setDaemon(True) + + self.terminateEvent = threading.Event() + + self.callbacks = [] + + self.ser = port + + def addCallback(self, callback): + """Add a callback that will be passed valid data.""" + self.callbacks.append(callback) + + def error(self, msg): + """Function for error printing.""" + print('Capture error: %s' % msg) + + def run(self): + """Begins thread execution and raw serial parsing.""" + + zeoTime = None + version = None + ring = ' ' + fatal_error_ring = ' ' + while not self.terminateEvent.isSet(): + if ring != 'A4': + c = self.ser.read(1) + if len(c) == 1: + ring = ring[1:] + c + fatal_error_ring = fatal_error_ring[1:] + c + if fatal_error_ring == 'FATAL_ERROR_': + print 'A fatal error has occured.' + self.terminateEvent.set() + else: + self.error('Read timeout in sync.') + ring = ' ' + self.ser.flushInput() + else: + + ring = ' ' + + raw = self.ser.read(1) + if len(raw) != 1: + self.error('Failed to read checksum.') + continue + + cksum = ord(raw) + + raw = self.ser.read(2) + if len(raw) != 2: + self.error('Failed to read length.') + continue + + datalen = getUInt16(raw) + + raw = self.ser.read(2) + if len(raw) != 2: + self.error('Failed to read second length.') + continue + + datalen2 = getUInt16(raw) ^ 0xffff + + if datalen != datalen2: + self.error('Mismatched lengths.') + continue + + raw = self.ser.read(3) + if len(raw) != 3: + self.error('Failed to read timestamp.') + continue + timestamp_lowbyte = ord(raw[0]) + timestamp_subsec = getUInt16(raw[1:3])/65535.0 + timestamp = 0 + + raw = self.ser.read(1) + if len(raw) != 1: + self.error('Failed to read sequence number.') + continue + + seqnum = ord(raw) + + raw = self.ser.read(datalen) + if len(raw) != datalen: + self.error('Failed to read data.') + continue + + data = raw + + if sum(map(ord, data)) % 256 != cksum: + self.error('bad checksum') + continue + + + # Check the datatype is supported + if dataTypes.keys().count(ord(data[0])) == 0: + self.error('Bad datatype 0x%02X.' % ord(data[0])) + continue + + datatype = dataTypes[ord(data[0])] + + # Check if this packet is an RTC time or version number + if datatype == 'ZeoTimestamp': + zeoTime = getUInt32(data[1:]) + continue + elif datatype == 'Version': + version = getUInt32(data[1:]) + continue + + # Don't pass the timestamp or version data since we send that + # information along with the other data + if zeoTime == None or version == None: + continue + + # Construct the full timestamp from the most recently received RTC + # value in seconds, and the lower 8 bits of the RTC value as of + # when this object was sent. + if (zeoTime & 0xff == timestamp_lowbyte): + timestamp = zeoTime + elif ((zeoTime - 1) & 0xff == timestamp_lowbyte): + timestamp = zeoTime - 1 + elif ((zeoTime + 1) & 0xff == timestamp_lowbyte): + timestamp = zeoTime + 1 + else: + # Something doesn't line up. Maybe unit was reset. + timestamp = zeoTime + + for callback in self.callbacks: + callback(timestamp, timestamp_subsec, version, data) + self.ser.close() + diff --git a/ZeoRawData-2.0/ZeoRawData/BaseLink.pyc b/ZeoRawData-2.0/ZeoRawData/BaseLink.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb211eed552b2ae594091aa945eb6f0a010aa615 GIT binary patch literal 5548 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 O29~=0a&fLyDf|zUlxU3r literal 0 HcmV?d00001 diff --git a/ZeoRawData-2.0/ZeoRawData/BaseLink.py~ b/ZeoRawData-2.0/ZeoRawData/BaseLink.py~ new file mode 100644 index 0000000..e5426e0 --- /dev/null +++ b/ZeoRawData-2.0/ZeoRawData/BaseLink.py~ @@ -0,0 +1,184 @@ +""" +BaseLink +-------- + +Listens to the data coming from the serial port connected to Zeo. + +The serial port is set at baud 38400, no parity, one stop bit. +Data is sent Least Significant Byte first. + +The serial protocol is: + * AncllLLTttsid + + * A is a character starting the message + * n is the protocol "version", ie "4" + * c is a one byte checksum formed by summing the identifier byte and all the data bytes + * ll is a two byte message length sent LSB first. This length includes the size of the data block plus the identifier. + * LL is the inverse of ll sent for redundancy. If ll does not match ~LL, we can start looking for the start of the next block immediately, instead of reading some arbitrary number of bytes, based on a bad length. + * T is the lower 8 bits of Zeo's unix time. + * tt is the 16-bit sub-second (runs through 0xFFFF in 1second), LSB first. + * s is an 8-bit sequence number. + * i is the datatype + * d is the array of binary data + +The incoming data is cleaned up into packets containing a timestamp, +the raw data output version, and the associated data. + +External code can be sent new data as it arrives by adding +themselves to the callback list using the addCallBack function. +It is suggested, however, that external code use the ZeoParser to +organize the data into events and slices of data. + +""" + +#System Libraries +import threading +from serial import Serial + +#Zeo Libraries +from Utility import * + +class BaseLink(threading.Thread): + """ + Runs on a seperate thread and handles the raw serial communications + and parsing required to communicate with Zeo. Each time data is + successfully received, it is sent out to all of the callbacks. + """ + + def __init__(self, port): + """ + Create a new class with default serial port settings. + Parameters: + port - the name of the serialport to open + i.e. COM1 or /dev/ttyUSB0 + """ + threading.Thread.__init__(self) + self.setDaemon(True) + + self.terminateEvent = threading.Event() + + self.callbacks = [] + + self.ser = Serial(port, 38400, timeout = 5) + self.ser.flushInput() + + def addCallback(self, callback): + """Add a callback that will be passed valid data.""" + self.callbacks.append(callback) + + def error(self, msg): + """Function for error printing.""" + print('Capture error: %s' % msg) + + def run(self): + """Begins thread execution and raw serial parsing.""" + zeoTime = None + version = None + ring = ' ' + fatal_error_ring = ' ' + while not self.terminateEvent.isSet(): + if ring != 'A4': + c = self.ser.read(1) + if len(c) == 1: + ring = ring[1:] + c + fatal_error_ring = fatal_error_ring[1:] + c + if fatal_error_ring == 'FATAL_ERROR_': + print 'A fatal error has occured.' + self.terminateEvent.set() + else: + self.error('Read timeout in sync.') + ring = ' ' + self.ser.flushInput() + else: + + ring = ' ' + + raw = self.ser.read(1) + if len(raw) != 1: + self.error('Failed to read checksum.') + continue + + cksum = ord(raw) + + raw = self.ser.read(2) + if len(raw) != 2: + self.error('Failed to read length.') + continue + + datalen = getUInt16(raw) + + raw = self.ser.read(2) + if len(raw) != 2: + self.error('Failed to read second length.') + continue + + datalen2 = getUInt16(raw) ^ 0xffff + + if datalen != datalen2: + self.error('Mismatched lengths.') + continue + + raw = self.ser.read(3) + if len(raw) != 3: + self.error('Failed to read timestamp.') + continue + timestamp_lowbyte = ord(raw[0]) + timestamp_subsec = getUInt16(raw[1:3])/65535.0 + timestamp = 0 + + raw = self.ser.read(1) + if len(raw) != 1: + self.error('Failed to read sequence number.') + continue + + seqnum = ord(raw) + + raw = self.ser.read(datalen) + if len(raw) != datalen: + self.error('Failed to read data.') + continue + + data = raw + + if sum(map(ord, data)) % 256 != cksum: + self.error('bad checksum') + continue + + + # Check the datatype is supported + if dataTypes.keys().count(ord(data[0])) == 0: + self.error('Bad datatype 0x%02X.' % ord(data[0])) + continue + + datatype = dataTypes[ord(data[0])] + + # Check if this packet is an RTC time or version number + if datatype == 'ZeoTimestamp': + zeoTime = getUInt32(data[1:]) + continue + elif datatype == 'Version': + version = getUInt32(data[1:]) + continue + + # Don't pass the timestamp or version data since we send that + # information along with the other data + if zeoTime == None or version == None: + continue + + # Construct the full timestamp from the most recently received RTC + # value in seconds, and the lower 8 bits of the RTC value as of + # when this object was sent. + if (zeoTime & 0xff == timestamp_lowbyte): + timestamp = zeoTime + elif ((zeoTime - 1) & 0xff == timestamp_lowbyte): + timestamp = zeoTime - 1 + elif ((zeoTime + 1) & 0xff == timestamp_lowbyte): + timestamp = zeoTime + 1 + else: + # Something doesn't line up. Maybe unit was reset. + timestamp = zeoTime + + for callback in self.callbacks: + callback(timestamp, timestamp_subsec, version, data) + self.ser.close() + diff --git a/ZeoRawData-2.0/ZeoRawData/Parser.py b/ZeoRawData-2.0/ZeoRawData/Parser.py new file mode 100644 index 0000000..3856ec5 --- /dev/null +++ b/ZeoRawData-2.0/ZeoRawData/Parser.py @@ -0,0 +1,118 @@ +""" +Parser +------ + +This module parses data from the BaseCapture module and assembles them +into slices that encompass a range of data representative of +Zeo's current status. + +There are two different callbacks. One for slice callbacks and one that +the module will pass events to. + +""" + +#System Libraries +from math import sqrt +import time + +#Zeo Libraries +from Utility import * + +class Parser: + """ + Interprets the incoming Zeo data and encapsulates it into an easy to use dictionary. + """ + def clearSlice(self): + """Resets the current Slice""" + self.Slice = {'ZeoTimestamp' : None, # String %m/%d/%Y %H:%M:%S + 'Version' : None, # Integer value + 'SQI' : None, # Integer value (0-30) + 'Impedance' : None, # Integer value as read by the ADC + # Unfortunately left raw/unitless due to + # nonlinearity in the readings. + 'Waveform' : [], # Array of signed ints + 'FrequencyBins' : {}, # Dictionary of frequency bins which are relative to the 2-30hz power + 'BadSignal' : None, # Boolean + 'SleepStage' : None # String + } + + def __init__(self): + """Creates a new parser object.""" + self.EventCallbacks = [] + self.SliceCallbacks = [] + self.WaveBuffer = [0]*128 + + self.clearSlice() + + def addEventCallback(self, callback): + """Add a function to call when an Event has occured.""" + self.EventCallbacks.append(callback) + + def addSliceCallback(self, callback): + """Add a function to call when a Slice of data is completed.""" + self.SliceCallbacks.append(callback) + + def update(self, timestamp, timestamp_subsec, version, data): + """ + Update the current Slice with new data from Zeo. + This function is setup to be easily added to the + BaseLink's callbacks. + """ + + if version != 3: + print 'Unsupport raw data output version: %i' % version + return + + datatype = dataTypes[ord(data[0])] + + if datatype == 'Event': + for callback in self.EventCallbacks: + callback(time.strftime('%m/%d/%Y %H:%M:%S', time.gmtime(timestamp)), + version, eventTypes[getUInt32(data[1:5])])#for some reason 5 long when did 1: + + elif datatype == 'SliceEnd': + self.Slice['ZeoTimestamp'] = time.strftime('%m/%d/%Y %H:%M:%S', time.gmtime(timestamp)) + self.Slice['Version'] = version + for callback in self.SliceCallbacks: + callback(self.Slice) + self.clearSlice() + + elif datatype == 'Waveform': + wave = [] + for i in range(1,256,2): + value = getInt16(data[i:i+2])# Raw value + value = float(value*315)/0x8000 # convert to uV value FIX + wave.append(value) + + filtered = filter60hz(self.WaveBuffer + wave) + + self.Slice[datatype] = filtered[90:218] # grab the valid middle as the current second + self.WaveBuffer = wave # store this second for processing the next second + + # NOTE: it is possible to mess this up easily for the first second. + # A second could be stored, headband docked, then undocked and it would + # use the old data as the previous second. This is considered ok since it + # will only be bad for the first portion of the first second of data. + + elif datatype == 'FrequencyBins': + for bin in range(7): + value = float(getUInt16(data[(bin*2+1):(bin*2+3)]))/0x8000 + self.Slice[datatype][frequencyBins[bin]] = value + + elif datatype == 'BadSignal': + self.Slice[datatype] = (getUInt32(data[1:])>0) + + elif datatype == 'SleepStage': + self.Slice[datatype] = sleepStages[getUInt32(data[1:])] + + elif datatype == 'Impedance': + impedance = getUInt32(data[1:]) + impi = (impedance & 0x0000FFFF) - 0x8000 # In Phase Component + impq = ((impedance & 0xFFFF0000) >> 16) - 0x8000 # Quadrature Component + if not impi == 0x7FFF: # 32767 indicates the impedance is bad + impSquared = (impi * impi) + (impq * impq) + self.Slice[datatype] = sqrt(impSquared) + + elif datatype == 'SQI': + self.Slice[datatype] = getUInt32(data[1:]) + diff --git a/ZeoRawData-2.0/ZeoRawData/Parser.pyc b/ZeoRawData-2.0/ZeoRawData/Parser.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a902539a00741f733bbc505717e5c50e7018305b GIT binary patch literal 4084 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 HB+lXsO~++s literal 0 HcmV?d00001 diff --git a/ZeoRawData-2.0/setup.py b/ZeoRawData-2.0/setup.py new file mode 100644 index 0000000..5128094 --- /dev/null +++ b/ZeoRawData-2.0/setup.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup(name='ZeoRawData', + version='2.0', + description='Zeo Raw Data Library', + author='Zeo Inc.', + author_email='customersupport@myZeo.com', + url='http://www.myzeo.com', + packages=['ZeoRawData'], + ) diff --git a/hex2binary.c b/hex2binary.c new file mode 100644 index 0000000..1fb506f --- /dev/null +++ b/hex2binary.c @@ -0,0 +1,14 @@ +#include + +int main(int argc, char **argv) +{ + char buf[1000]; + int i; + for (i=0; i<5; i++) fgets(buf, sizeof(buf), stdin); + while (1) { + int n; + if (1 != fscanf(stdin, "%x", &n)) break; + putchar(n); + } + return 0; +} diff --git a/plot_hypnogram.pl b/plot_hypnogram.pl new file mode 100755 index 0000000..9d1abea --- /dev/null +++ b/plot_hypnogram.pl @@ -0,0 +1,15 @@ +#!/usr/bin/perl -w +<>; +$time=0; + +open(PLOT, ">hypnogram.plot"); + +while (<>) { + chomp; + @f = split ','; + print PLOT "$time $f[5]\n"; + $time += .5; +} + +system 'echo "plot \'hypnogram.plot\'" | gnuplot -p -'; + diff --git a/raw2csv.py b/raw2csv.py new file mode 100755 index 0000000..1c89c9b --- /dev/null +++ b/raw2csv.py @@ -0,0 +1,201 @@ +#!/usr/bin/python -d +# -*- coding: utf-8 -*- +# DataRecorder.pyw +# +# Streams the real-time data coming from a Zeo unit into .csv files. +import sys +import time +import csv +import os + +# from serial import * + +from glob import glob + +sys.path.append("/Users/rsargent/projects/bodytrack/zeo/raw-data/raw2csv/ZeoRawData-2.0"); + +from ZeoRawData import BaseLink, Parser + +def scanPorts(): + portList = [] + + # Helps find USB>Serial converters on linux + for p in glob('/dev/ttyUSB*'): + portList.append(p) + #Linux and Windows + for i in range(256): + try: + ser = Serial(i) + if ser.isOpen(): + #Check that the port is actually valid + #Otherwise invalid /dev/ttyS* ports may be added + portList.append(ser.portstr) + ser.close() + except SerialException: + pass + return portList + + +class HexFileReader: + def __init__(self, filename): + self.file = open(filename, 'r') + # skip 5 lines + for i in range(5): + self.file.readline() + + def read(self, num): + ret = "" + for i in range(num): + ret += self.read1() + return ret + + def read1(self): + c = self.file.read(1) + if (c == ' '): + c = self.file.read(1) + c += self.file.read(1) + if (len(c) < 2): + print "DONE" + sys.exit(0) + return chr(int(c,16)) + + def flushInput(self): + pass + +class FileReader: + def __init__(self, filename): + self.file = open(filename, 'r') + # skip 5 lines + for i in range(5): + self.file.readline() + + def read(self, num): + return self.file.read(num) + + def flushInput(self): + pass + + +class ZeoToCSV: + def __init__(self, parent=None): + samplesFileName = 'raw_samples.csv' + sgramFileName = 'spectrogram.csv' + hgramFileName = 'hypnogram.csv' + eventsFileName = 'events.csv' + + self.hypToHeight = {'Undefined' : 0, + 'Deep' : 1, + 'Light' : 2, + 'REM' : 3, + 'Awake' : 4} + + # Only create headers when the files are being created for the first time. + # After that, all new data should be appended to the existing files. + samplesNeedHeader = False + sgramNeedHeader = False + hgramNeedHeader = False + eventsNeedHeader = False + + if not os.path.isfile(samplesFileName): + samplesNeedHeader = True + + if not os.path.isfile(sgramFileName): + sgramNeedHeader = True + + if not os.path.isfile(hgramFileName): + hgramNeedHeader = True + + if not os.path.isfile(eventsFileName): + eventsNeedHeader = True + + self.rawSamples = csv.writer(open(samplesFileName, 'a+b'), delimiter=',', + quotechar='"', quoting=csv.QUOTE_MINIMAL) + self.spectrogram = csv.writer(open(sgramFileName, 'a+b'), delimiter=',', + quotechar='"', quoting=csv.QUOTE_MINIMAL) + self.hypnogram = csv.writer(open(hgramFileName, 'a+b'), delimiter=',', + quotechar='"', quoting=csv.QUOTE_MINIMAL) + self.eventsOut = csv.writer(open(eventsFileName, 'a+b'), delimiter=',', + quotechar='"', quoting=csv.QUOTE_MINIMAL) + + if samplesNeedHeader: + self.rawSamples.writerow(["Time Stamp","Version","SQI","Impedance","Bad Signal (Y/N)","Voltage (uV)"]) + + if sgramNeedHeader: + self.spectrogram.writerow(["Time Stamp","Version","SQI","Impedance","Bad Signal (Y/N)", + "2-4 Hz","4-8 Hz","8-13 Hz","11-14 Hz","13-18 Hz","18-21 Hz","30-50 Hz"]) + + if hgramNeedHeader: + self.hypnogram.writerow(["Time Stamp","Version","SQI","Impedance","Bad Signal (Y/N)","State (0-4)","State (named)"]) + + if eventsNeedHeader: + self.eventsOut.writerow(["Time Stamp","Version","Event"]) + + def updateSlice(self, slice): + + timestamp = slice['ZeoTimestamp'] + ver = slice['Version'] + + if not slice['SQI'] == None: + sqi = str(slice['SQI']) + else: + sqi = '–' + + if not slice['Impedance'] == None: + imp = str(int(slice['Impedance'])) + else: + imp = '–' + if slice['BadSignal']: + badSignal = 'Y' + else: + badSignal = 'N' + if not slice['Waveform'] == []: + self.rawSamples.writerow([timestamp,ver,sqi,imp,badSignal] + slice['Waveform']) + if len(slice['FrequencyBins'].values()) == 7: + f = slice['FrequencyBins'] + bins = [f['2-4'],f['4-8'],f['8-13'],f['11-14'],f['13-18'],f['18-21'],f['30-50']] + self.spectrogram.writerow([timestamp,ver,sqi,imp,badSignal] + bins) + if not slice['SleepStage'] == None: + stage = slice['SleepStage'] + self.hypnogram.writerow([timestamp,ver,sqi,imp,badSignal] + + [self.hypToHeight[stage],str(stage)]) + + def updateEvent(self, timestamp, version, event): + self.eventsOut.writerow([timestamp,version,event]) + +if __name__ == '__main__': + # Find ports. + # TODO: offer a command line option for selecting ports. + +# ports = scanPorts() +# if len(ports) > 0 : +# print "Found the following ports:" +# for port in ports: +# print port +# print "" +# print "Using port "+ports[0] +# print "" +# portStr = ports[0] +# else: +# sys.exit("No serial ports found.") + + # Initialize + output = ZeoToCSV() + + reader = FileReader("anne-110202.zeo") + + link = BaseLink.BaseLink(reader) + + parser = Parser.Parser() + # Add callbacks + link.addCallback(parser.update) + parser.addEventCallback(output.updateEvent) + parser.addSliceCallback(output.updateSlice) + # Start Link + link.start() + + # TODO: perhaps use a more forgiving key? This would require polling the keyboard without blocking. + print "Hit ctrl-C at any time to stop." + while True: + time.sleep(5) + + sys.exit()