40 files changed, 1878 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed613b6 --- a/dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/INSTALL +Makefile +Makefile.in +/aclocal.m4 +/autom4te.cache +/aux.d +/config.h +/config.h.in +/config.log +/config.status +/configure +/libtool +/stamp-h1 +/NEWS @@ -0,0 +1,8 @@ +Klever dissected: + Michael 'hacker' Krelin <hacker@klever.net> + Leonid Ivanov <kamel@klever.net> + +The credits also go to the unknown hacker called Joe for dissecting +sleeptracker data - +http://www.sleeptracker-tec.de/tools/phorum/read.php?2,19 +I hope I find the way to contact him and give him proper credit. @@ -0,0 +1,19 @@ +Copyright (c) 2008 Klever Group (http://www.klever.net/) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e69de29 --- a/dev/null +++ b/ChangeLog diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..c3bc31f --- a/dev/null +++ b/Makefile.am @@ -0,0 +1,15 @@ +SUBDIRS=include lib src test +EXTRA_DIST = NEWS NEWS.xml NEWS.xsl + +all-local: NEWS + +NEWS: NEWS.xsl NEWS.xml + ${XSLTPROC} -o $@ NEWS.xsl NEWS.xml + +ISSUEFILES = $$(find ${top_srcdir} -type f '(' \ + -name '*.cc' -or -name '*.h' -or -name '*.sql' \ + ')' ) \ + ${top_srcdir}/configure.ac +issues: todo fixme xxx +todo fixme xxx: + @grep --color=auto -in '$@:' ${ISSUEFILES} || true diff --git a/NEWS.xml b/NEWS.xml new file mode 100644 index 0000000..33907ee --- a/dev/null +++ b/NEWS.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="us-ascii"?> +<news> + <version version="0.0" date="April 5th, 2008"> + <ni>Initial release</ni> + </version> +</news> diff --git a/NEWS.xsl b/NEWS.xsl new file mode 100644 index 0000000..7c71307 --- a/dev/null +++ b/NEWS.xsl @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="us-ascii"?> +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + > + <xsl:output + method="text" + encoding="us-ascii" + media-type="text/plain" /> + + <xsl:template match="news"> + <xsl:apply-templates/> + </xsl:template> + <xsl:template match="version"> + <xsl:value-of select="concat(@version,' (',@date,')
')"/> + <xsl:apply-templates/> + </xsl:template> + <xsl:template match="ni"> + <xsl:text> - </xsl:text> + <xsl:apply-templates mode="text"/> + <xsl:text>
</xsl:text> + </xsl:template> + <xsl:template match="*|text()"/> + +</xsl:stylesheet> @@ -0,0 +1,15 @@ + +Napkin expects you to have sleeptracker port at /dev/sleeptracker + +On linux you'd need to have 'ftdi_sio' kernel module loaded (and usb support, +of course) and something along these lines in your udev rules: + +BUS=="usb", SYSFS{product}=="FT232R USB UART", SYSFS{serial}=="********", SYSFS{manufacturer}=="FTDI", KERNEL=="ttyUSB*", SYMLINK="sleeptracker", MODE="660", GROUP="usb" + +Note, that the I've wiped out the serial number, so that you don't try to use +mine. Perhaps it will differ, although I have only one device. + +You can also set the other port name using SLEEPTRACKER_PORT environment +variable, like + +env SLEEPTRACKER_PORT=/dev/ttyUSB0 napkin diff --git a/autogen.sh b/autogen.sh new file mode 100644 index 0000000..c3313be --- a/dev/null +++ b/autogen.sh @@ -0,0 +1,7 @@ +#!/bin/sh +test -d aux.d || mkdir aux.d + aclocal \ +&& autoheader \ +&& automake -a \ +&& autoconf \ +&& ./configure "$@" diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..3ca0b4e --- a/dev/null +++ b/configure.ac @@ -0,0 +1,66 @@ +AC_INIT([napkin], [0.0], [napkin-bugs@klever.net]) +AC_CONFIG_AUX_DIR([aux.d]) +AC_CONFIG_SRCDIR([src/napkin.cc]) +AC_CONFIG_HEADERS([config.h]) +AM_INIT_AUTOMAKE([dist-bzip2]) + +AC_PROG_INSTALL +AC_PROG_CXX +AC_PROG_CC +AC_PROG_LIBTOOL +PKG_PROG_PKG_CONFIG + +AC_HEADER_STDC + +AC_PATH_PROG([XSLTPROC],[xsltproc],[true]) + +PKG_CHECK_MODULES([MODULES],[gtkmm-2.4 sqlite3],,[ + AC_MSG_ERROR([not all dependencies could be satisfied]) +]) + +AC_MSG_CHECKING([whether to enable debugging code]) +ndebug=true +AC_ARG_ENABLE([debug], + AC_HELP_STRING([--enable-debug],[enable debugging/development code]), + [ test "$enableval" = "no" || ndebug=false ] +) +if $ndebug ; then + AC_MSG_RESULT([no]) + CPPFLAGS="${CPPFLAGS}-DNDEBUG" +else + AC_MSG_RESULT([yes]) +fi + +nitpick=false +AC_MSG_CHECKING([whether to enable compiler nitpicking]) +AC_ARG_ENABLE([nitpicking], + AC_HELP_STRING([--enable-nitpicking],[make compiler somewhat overly fastidious about the code it deals with]), + [ test "$enableval" = "no" || nitpick=true ] +) +if $nitpick ; then + AC_MSG_RESULT([yes]) + CPP_NITPICK="-pedantic -Wall -Wextra -Wundef -Wshadow \ + -Wunsafe-loop-optimizations -Wconversion -Wmissing-format-attribute \ + -Wredundant-decls -ansi" + # -Wlogical-op -Wmissing-noreturn + C_NITPICK="$CPP_NITPICK" + CXX_NITPICK="$C_NITPICK" + + CPPFLAGS="$CPPFLAGS $CPP_NITPICK" + CFLAGS="$CFLAGS $C_NITPICK" + CXXFLAGS="$CXXFLAGS $CXX_NITPICK" +else + AC_MSG_RESULT([no]) +fi + + +AC_CONFIG_FILES([ + Makefile + include/Makefile + lib/Makefile + src/Makefile + test/Makefile +]) +AC_OUTPUT + +dnl vim:set ft=m4 sw=1: diff --git a/include/Makefile.am b/include/Makefile.am new file mode 100644 index 0000000..f37e4d7 --- a/dev/null +++ b/include/Makefile.am @@ -0,0 +1,4 @@ +include_HEADERS = $(addprefix napkin/,\ + exception.h types.h util.h \ + st/decode.h st/download.h \ + ) diff --git a/include/napkin/exception.h b/include/napkin/exception.h new file mode 100644 index 0000000..b317886 --- a/dev/null +++ b/include/napkin/exception.h @@ -0,0 +1,36 @@ +#ifndef __NAPKIN_EXCEPTION_H +#define __NAPKIN_EXCEPTION_H + +#include <stdexcept> +#include <string> + +#define NAPKIN_E_SUBCLASS(derived,base) \ + class derived : public base { \ + public: \ + explicit derived(const string& w) \ + : base(w) { } \ + } + +namespace napkin { + using std::string; + + class exception : public std::runtime_error { + public: + explicit exception(const string& w) + : std::runtime_error(w) { } + ~exception() throw() { } + }; + + NAPKIN_E_SUBCLASS(exception_sleeptracker,exception); + NAPKIN_E_SUBCLASS(exception_st_port,exception_sleeptracker); + NAPKIN_E_SUBCLASS(exception_st_data,exception_sleeptracker); + NAPKIN_E_SUBCLASS(exception_st_data_envelope,exception_st_data); + NAPKIN_E_SUBCLASS(exception_st_data_integrity,exception_st_data_envelope); + + NAPKIN_E_SUBCLASS(exception_db,exception); + NAPKIN_E_SUBCLASS(exception_db_already,exception_db); +} + +#undef NAPKIN_E_SUBCLASS + +#endif /* __NAPKIN_EXCEPTION_H */ diff --git a/include/napkin/st/decode.h b/include/napkin/st/decode.h new file mode 100644 index 0000000..e1f1d07 --- a/dev/null +++ b/include/napkin/st/decode.h @@ -0,0 +1,15 @@ +#ifndef __NAPKIN_ST_DECODE_H +#define __NAPKIN_ST_DECODE_H + +#include <napkin/types.h> + +namespace napkin { + namespace sleeptracker { + + hypnodata_t& decode(hypnodata_t& rv,const void *data,size_t data_length); + hypnodata_ptr_t decode(const void *data,size_t data_length); + + } +} + +#endif /* __NAPKIN_ST_DECODE_H */ diff --git a/include/napkin/st/download.h b/include/napkin/st/download.h new file mode 100644 index 0000000..92d1d9d --- a/dev/null +++ b/include/napkin/st/download.h @@ -0,0 +1,20 @@ +#ifndef __NAPKIN_ST_DOWNLOAD_H +#define __NAPKIN_ST_DOWNLOAD_H + +#include <napkin/types.h> + +namespace napkin { + namespace sleeptracker { + + int download_initiate(const char *port=0); + size_t download_finish(int fd,void *buffer,size_t buffer_size); + + size_t download( + void *buffer,size_t buffer_size, + const char *port=0); + hypnodata_ptr_t download(const char *port=0); + + } +} + +#endif /* __NAPKIN_ST_DOWNLOAD_H */ diff --git a/include/napkin/types.h b/include/napkin/types.h new file mode 100644 index 0000000..2bc3a0a --- a/dev/null +++ b/include/napkin/types.h @@ -0,0 +1,46 @@ +#ifndef __NAPKIN_TYPES_H +#define __NAPKIN_TYPES_H + +#include <time.h> +#include <string> +#include <vector> +#include <tr1/memory> + +namespace napkin { + using std::vector; + using std::tr1::shared_ptr; + using std::string; + + class hypnodata_t { + public: + time_t to_bed; + time_t alarm; + int window; + vector<time_t> almost_awakes; + int data_a; + + void clear(); + + void set_to_bed(const string& w3c); + void set_alarm(const string& w3c); + void set_window(const string& str); + void set_data_a(const string& str); + void set_almost_awakes(const string& str); + + const string w3c_to_bed() const; + const string w3c_alarm() const; + const string w3c_almostawakes() const; + + const string str_to_bed() const; + const string str_alarm() const; + const string str_date() const; + const string str_data_a() const; + + time_t aligned_start() const; + }; + + typedef shared_ptr<hypnodata_t> hypnodata_ptr_t; + +} + +#endif /* __NAPKIN_TYPES_H */ diff --git a/include/napkin/util.h b/include/napkin/util.h new file mode 100644 index 0000000..bf7946d --- a/dev/null +++ b/include/napkin/util.h @@ -0,0 +1,14 @@ +#ifndef __NAPKIN_UTIL_H +#define __NAPKIN_UTIL_H + +#include <time.h> +#include <string> + +namespace napkin { + using std::string; + + string strftime(const char *fmt,time_t t); + +} + +#endif /* __NAPKIN_UTIL_H */ diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..b7b1941 --- a/dev/null +++ b/lib/.gitignore @@ -0,0 +1,11 @@ +/.deps +/.libs +/st-decode.lo +/st-decode.o +/libnapkin.la +/st-download.lo +/st-download.o +/hypnodata.lo +/hypnodata.o +/util.lo +/util.o diff --git a/lib/Makefile.am b/lib/Makefile.am new file mode 100644 index 0000000..bf55849 --- a/dev/null +++ b/lib/Makefile.am @@ -0,0 +1,11 @@ +lib_LTLIBRARIES = libnapkin.la + +INCLUDES = -I${top_builddir}/include/ -I${top_srcdir}/include/ \ + ${MODULES_CFLAGS} +LIBS = ${MODULES_CFLAGS} + +libnapkin_la_SOURCES = \ + st-decode.cc st-download.cc \ + hypnodata.cc \ + util.cc +libnapkin_la_LDFLAGS = -version-info 0:0:0 diff --git a/lib/hypnodata.cc b/lib/hypnodata.cc new file mode 100644 index 0000000..977fb76 --- a/dev/null +++ b/lib/hypnodata.cc @@ -0,0 +1,92 @@ +#include <napkin/exception.h> +#include <napkin/util.h> +#include <napkin/types.h> + +namespace napkin { + + void hypnodata_t::clear() { + to_bed = alarm = 0; + data_a = window = 0; + almost_awakes.clear(); + } + + static time_t from_minute_w3c(const string& w3c) { + struct tm t; memset(&t,0,sizeof(t)); t.tm_isdst=-1; + if(sscanf(w3c.c_str(),"%04d-%02d-%02dT%02d:%02d", + &t.tm_year,&t.tm_mon,&t.tm_mday,&t.tm_hour,&t.tm_min)!=5) + throw exception("failed to parse w3c time"); + --t.tm_mon;t.tm_year-=1900; + time_t rv = mktime(&t); + if(rv==(time_t)-1) + throw exception("failed to mktime()"); + return rv; + } + + void hypnodata_t::set_to_bed(const string& w3c) { + to_bed = from_minute_w3c(w3c); } + void hypnodata_t::set_alarm(const string& w3c) { + alarm = from_minute_w3c(w3c); } + void hypnodata_t::set_window(const string& str) { + window = strtol(str.c_str(),0,10); /* TODO: check for error */ + } + void hypnodata_t::set_data_a(const string& str) { + data_a = strtol(str.c_str(),0,10); /* TODO: check for error */ + } + void hypnodata_t::set_almost_awakes(const string& str) { + almost_awakes.clear(); + static const char *significants = "0123456789-T:Z"; + string::size_type p = str.find_first_of(significants); + struct tm t; memset(&t,0,sizeof(t)); t.tm_isdst=-1; + while(p!=string::npos) { + string::size_type ns = str.find_first_not_of(significants,p); + string w3c; + if(ns==string::npos) { + w3c = str.substr(p); + p = string::npos; + }else{ + w3c = str.substr(p,ns-p); + p = str.find_first_of(significants,ns); + } + if(w3c.empty()) continue; + if(sscanf(w3c.c_str(),"%04d-%02d-%02dT%02d:%02d:%02d", + &t.tm_year,&t.tm_mon,&t.tm_mday,&t.tm_hour,&t.tm_min,&t.tm_sec)!=6) + throw exception("failed to parse w3c time"); + --t.tm_mon;t.tm_year-=1900; + time_t aa = mktime(&t); + if(aa==(time_t)-1) + throw exception("failed to mktime()"); + almost_awakes.push_back(aa); + } + } + + const string hypnodata_t::w3c_to_bed() const { + return strftime("%Y-%m-%dT%H:%M",to_bed); } + const string hypnodata_t::w3c_alarm() const { + return strftime("%Y-%m-%dT%H:%M",alarm); } + const string hypnodata_t::w3c_almostawakes() const { + string rv; + for(vector<time_t>::const_iterator i=almost_awakes.begin();i!=almost_awakes.end();++i) { + if(!rv.empty()) + rv += ','; + rv += strftime("%Y-%m-%dT%H:%M:%S",*i); + } + return rv; + } + + const string hypnodata_t::str_to_bed() const { + return strftime("%H:%M",to_bed); } + const string hypnodata_t::str_alarm() const { + return strftime("%H:%M",alarm); } + const string hypnodata_t::str_date() const { + return strftime("%Y-%m-%d, %a",alarm); } + const string hypnodata_t::str_data_a() const { + char tmp[16]; + snprintf(tmp,sizeof(tmp),"%d:%02d:%02d", + data_a/3600, (data_a%3600)/60, + data_a % 60 ); + return tmp; } + + time_t hypnodata_t::aligned_start() const { + return alarm - (alarm % (24*60*60)) - 24*60*60; } + +} diff --git a/lib/st-decode.cc b/lib/st-decode.cc new file mode 100644 index 0000000..f8459ac --- a/dev/null +++ b/lib/st-decode.cc @@ -0,0 +1,121 @@ +#include <stdexcept> +#include <numeric> +#include <napkin/exception.h> +#include <napkin/st/decode.h> + +namespace napkin { + namespace sleeptracker { + using std::invalid_argument; + using std::runtime_error; + + struct st_time_t { + uint8_t hour; + uint8_t min; + }; + struct st_date_t { + uint8_t month; + uint8_t day; + }; + struct st_fulltime_t { + uint8_t hour; + uint8_t min; + uint8_t sec; + }; + struct st_data_header_t { + char magic; + st_date_t today; + uint8_t unknown; + uint8_t window; + st_time_t to_bed; + st_time_t alarm; + uint8_t nawakes; + }; + struct st_data_footer_t { + uint16_t data_a; + uint8_t checksum; + uint8_t eof_mark; + }; + + static void back_a_day(struct tm& t) { + time_t ts = mktime(&t); + if(ts==(time_t)-1) + throw exception_st_data("failed to make up time to step back a day"); + ts -= 60*60*24; + if(!localtime_r(&ts,&t)) + throw exception_st_data("failed to localtime_r() while stepping back a day"); + } + + hypnodata_t& decode(hypnodata_t& rv,const void *data,size_t data_length) { + if(data_length < (sizeof(st_data_header_t)+sizeof(st_data_footer_t))) + throw exception_st_data_envelope("not enough sleeptracker data to decode"); + st_data_header_t *h = (st_data_header_t*)data; + if(h->magic != 'V') + throw exception_st_data_envelope("invalid magic in the data"); + st_data_footer_t *f = (st_data_footer_t*)(static_cast<const char *>(data)+data_length-sizeof(st_data_footer_t)); + if( (std::accumulate((uint8_t*)&h->today,(uint8_t*)&f->checksum,0)&0xFF) != f->checksum ) + throw exception_st_data_integrity("checksum mismatch"); + st_fulltime_t *aawake = (st_fulltime_t*)&h[1]; + if((void*)&aawake[h->nawakes] != (void*)f) + throw exception_st_data_envelope("unbelievably screwed up data"); + rv.clear(); + time_t now = time(0); + struct tm t; + if(!localtime_r(&now,&t)) + throw exception_st_data("failed to localtime_r()"); + t.tm_mon = h->today.month-1; + t.tm_mday = h->today.day; + time_t mkt = mktime(&t); + if(mkt == (time_t)-1) + throw exception_st_data("failed to mktime() for a timestamp"); + if(mkt > now) { + --t.tm_year; + } + struct tm ta; + memmove(&ta,&t,sizeof(ta)); + ta.tm_sec = 0; + ta.tm_hour = h->alarm.hour; ta.tm_min = h->alarm.min; + rv.alarm = mktime(&ta); + if(rv.alarm == (time_t)-1) + throw exception_st_data("failed to mktime() for alarm"); + struct tm tb; + memmove(&tb,&ta,sizeof(tb)); + tb.tm_hour = h->to_bed.hour; tb.tm_min = h->to_bed.min; + rv.to_bed = mktime(&tb); + if(rv.to_bed == (time_t)-1) + throw exception_st_data("failed to mktime() for 'to bed'"); + if(rv.to_bed > rv.alarm) { + back_a_day(tb); + rv.to_bed -= 24*60*60; + } + struct tm taaw; + memmove(&taaw,&tb,sizeof(taaw)); + for(int rest=h->nawakes;rest;--rest,++aawake) { + if( + taaw.tm_mday!=ta.tm_mday + && ( + aawake->hour < tb.tm_hour + || ( + aawake->hour==tb.tm_hour + && aawake->min < tb.tm_min ) + ) ) + memmove(&taaw,&ta,sizeof(taaw)); + taaw.tm_hour = aawake->hour; + taaw.tm_min = aawake->min; + taaw.tm_sec = aawake->sec; + rv.almost_awakes.push_back( mktime(&taaw) ); + if(rv.almost_awakes.back() == (time_t)-1) + throw exception_st_data("failed to mktime() for almost awake moment"); + } + rv.window = h->window; + rv.data_a = f->data_a; + return rv; + } + + hypnodata_ptr_t decode(const void *data,size_t data_length) { + hypnodata_ptr_t rv( new hypnodata_t ); + decode(*rv,data,data_length); + return rv; + } + + } +} diff --git a/lib/st-download.cc b/lib/st-download.cc new file mode 100644 index 0000000..b56e52d --- a/dev/null +++ b/lib/st-download.cc @@ -0,0 +1,70 @@ +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <termios.h> +#include <stdexcept> +#include <napkin/exception.h> +#include <napkin/st/download.h> +#include <napkin/st/decode.h> + +namespace napkin { + namespace sleeptracker { + using std::runtime_error; + + int download_initiate(const char *port) { + int fd = open(port?port:"/dev/sleeptracker", + O_RDWR|O_NOCTTY|O_NONBLOCK); + if(fd<0) + throw exception_st_port("failed to open() sleeptracker port"); + + if(tcflush(fd,TCIOFLUSH)) { + close(fd); + throw exception_st_port("failed to tcflush()"); + } + struct termios ts; + ts.c_cflag = CS8|CREAD; + cfsetispeed(&ts,B2400); cfsetospeed(&ts,B2400); + ts.c_iflag = IGNPAR; + ts.c_oflag = ts.c_lflag = 0; + ts.c_cc[VMIN]=1; ts.c_cc[VTIME]=0; + if(tcsetattr(fd,TCSANOW,&ts)) { + close(fd); + throw exception_st_port("failed to tcsetattr()"); + } + + if(write(fd,"V",1)!=1) { + close(fd); + throw exception_st_port("failed to write() to sleeptracker"); + } + return fd; + } + size_t download_finish(int fd,void *buffer,size_t buffer_size) { + size_t rv = read(fd,buffer,buffer_size); + close(fd); + + if(rv==(size_t)-1) + throw exception_st_port("failed to read() from sleeptracker"); + return rv; + } + + size_t download( + void *buffer,size_t buffer_size, + const char *port) { + int fd = download_initiate(port); + /* this is not the best way to wait for data, but + * after all it's a sleeptracker! */ + sleep(1); + return download_finish(fd,buffer,buffer_size); + } + + hypnodata_ptr_t download(const char *port) { + char buffer[2048]; + size_t rb = download(buffer,sizeof(buffer),port); + hypnodata_ptr_t rv( new hypnodata_t ); + decode(*rv,buffer,rb); + return rv; + } + + } +} diff --git a/lib/util.cc b/lib/util.cc new file mode 100644 index 0000000..350cbac --- a/dev/null +++ b/lib/util.cc @@ -0,0 +1,13 @@ +#include <napkin/util.h> + +namespace napkin { + + string strftime(const char *fmt,time_t t) { + struct tm tt; + localtime_r(&t,&tt);// TODO: check res + char rv[1024]; + strftime(rv,sizeof(rv),fmt,&tt); // TODO: check res + return rv; + } + +} diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..1210d2d --- a/dev/null +++ b/src/.gitignore @@ -0,0 +1,13 @@ +/napkin +/napkin.o +/.deps +/.libs +/schema.cc +/schema.o +/db.o +/dialogs.o +/sleep_history.o +/sleep_timeline.o +/widgets.o +/COPYING.cc +/COPYING.o diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..1fdae31 --- a/dev/null +++ b/src/Makefile.am @@ -0,0 +1,32 @@ +bin_PROGRAMS = napkin + +AM_CXXFLAGS = ${MODULES_CFLAGS} -I${top_srcdir}/include/ -I${srcdir} +LIBS = ${MODULES_LIBS} \ + ${top_builddir}/lib/libnapkin.la + +noinst_HEADERS = sqlite.h db.h \ + sleep_timeline.h \ + widgets.h dialogs.h \ + sleep_history.h + +napkin_SOURCES = napkin.cc \ + db.cc \ + sleep_timeline.cc \ + widgets.cc dialogs.cc \ + sleep_history.cc \ + schema.cc COPYING.cc +napkin_DEPENDENCIES = \ + ${top_builddir}/lib/libnapkin.la + +EXTRA_DIST = schema.sql + +schema.cc: schema.sql + (\ + echo 'namespace napkin{const char *sql_bootstrap=' &&\ + sed -e 's/^\s\+//' -e 's/\s*--.*$$//' -e 's/^/"/' -e 's/$$/"/' $< &&\ + echo ';}'\ + ) >$@ +COPYING.cc: ${top_srcdir}/COPYING + echo "const char * COPYING =" >$@ || (rm $@;exit 1) + sed 's/"/\\"/g' $< | sed 's/^/\"/' | sed 's/$$/\\n\"/' >>$@ || (rm $@;exit 1) + echo ";" >>$@ || (rm $@;exit 1) diff --git a/src/db.cc b/src/db.cc new file mode 100644 index 0000000..d1e0a85 --- a/dev/null +++ b/src/db.cc @@ -0,0 +1,101 @@ +#include <unistd.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <cassert> +#include <napkin/exception.h> +#include "db.h" + +#include "config.h" + +namespace napkin { + + extern const char *sql_bootstrap; + + db_t::db_t() { + const char *h = getenv("HOME"); + if(h) { + datadir = h; + datadir += "/."PACKAGE_NAME"/"; + }else{ + char *cwd = get_current_dir_name(); + if(!cwd) + throw napkin::exception("failed to get_current_dir_name()"); + datadir = cwd; + free(cwd); + datadir += "/."PACKAGE_NAME"/"; + } + if(access(datadir.c_str(),R_OK|W_OK) + && mkdir(datadir.c_str(),0700)) + throw napkin::exception("no access to '"+datadir+"' directory"); + open((datadir+PACKAGE_NAME".db").c_str()); + assert(_D); + char **resp; int nr,nc; char *errm; + if(sqlite3_get_table( + _D, + "SELECT s_tobed FROM sleeps LIMIT 0", + &resp,&nr,&nc,&errm)!=SQLITE_OK) { + if(sqlite3_exec(_D,sql_bootstrap,NULL,NULL,&errm)!=SQLITE_OK) + throw napkin::exception(string("failed to bootstrap sqlite database: ")+errm); + }else + sqlite3_free_table(resp); + } + + void db_t::store(const hypnodata_t& hd) { + sqlite::mem_t<char*> S = sqlite3_mprintf( + "INSERT INTO sleeps (" + "s_tobed,s_alarm," + "s_window,s_data_a," + "s_almost_awakes," + "s_timezone" + ") VALUES (" + "%Q,%Q,%d,%d,%Q,%ld" + ")", + hd.w3c_to_bed().c_str(), + hd.w3c_alarm().c_str(), + hd.window,hd.data_a, + hd.w3c_almostawakes().c_str(), + timezone ); + try { + exec(S); + }catch(sqlite::exception& se) { + if(se.rcode==SQLITE_CONSTRAINT) + throw exception_db_already("The record seems to be already in the database"); + throw exception_db("Well, some error occured"); + } + } + + void db_t::remove(const hypnodata_t& hd) { + sqlite::mem_t<char*> S = sqlite3_mprintf( + "DELETE FROM sleeps" + " WHERE s_tobed=%Q AND s_alarm=%Q", + hd.w3c_to_bed().c_str(), + hd.w3c_alarm().c_str() ); + exec(S); + } + + void db_t::load(list<hypnodata_ptr_t>& rv, + const string& sql) { + sqlite::table_t T; + int nr,nc; + get_table( string( + "SELECT" + " s_tobed, s_alarm," + " s_window, s_data_a," + " s_almost_awakes" + " FROM sleeps" + " "+sql).c_str(),T,&nr,&nc ); + if(nr) { + assert(nc==5); + for(int r=1;r<=nr;++r) { + hypnodata_ptr_t hd(new hypnodata_t()); + hd->set_to_bed(T.get(r,0,nc)); + hd->set_alarm(T.get(r,1,nc)); + hd->set_window(T.get(r,2,nc)); + hd->set_data_a(T.get(r,3,nc)); + hd->set_almost_awakes(T.get(r,4,nc)); + rv.push_back(hd); + } + } + } + +} diff --git a/src/db.h b/src/db.h new file mode 100644 index 0000000..3794eae --- a/dev/null +++ b/src/db.h @@ -0,0 +1,28 @@ +#ifndef __N_DB_H +#define __N_DB_H + +#include <string> +#include <list> +#include <napkin/types.h> +#include "sqlite.h" + +namespace napkin { + using std::string; + using std::list; + + class db_t : public sqlite::db_t { + public: + string datadir; + + db_t(); + + void store(const hypnodata_t& hd); + void remove(const hypnodata_t& hd); + + void load(list<hypnodata_ptr_t>& rv, + const string& sql); + }; + +} + +#endif /* __N_DB_H */ diff --git a/src/dialogs.cc b/src/dialogs.cc new file mode 100644 index 0000000..efe0b36 --- a/dev/null +++ b/src/dialogs.cc @@ -0,0 +1,17 @@ +#include "dialogs.h" + +namespace napkin { + namespace gtk { + + hypnoinfo_dialog_t::hypnoinfo_dialog_t(Gtk::Window& w) + : Gtk::Dialog("Sleepy information",w,true/*modal*/,true/*use separator*/) + { + Gtk::VBox *vb = get_vbox(); + vb->set_spacing(2); + vb->add(w_hinfo); + vb->show_all(); + } + + + } +} diff --git a/src/dialogs.h b/src/dialogs.h new file mode 100644 index 0000000..0a7f1b0 --- a/dev/null +++ b/src/dialogs.h @@ -0,0 +1,25 @@ +#ifndef __N_DIALOGS_H +#define __N_DIALOGS_H + +#include <gtkmm/dialog.h> +#include <gtkmm/box.h> +#include "widgets.h" + +namespace napkin { + namespace gtk { + + class hypnoinfo_dialog_t : public Gtk::Dialog { + public: + hypnoinfo_t w_hinfo; + + hypnoinfo_dialog_t(Gtk::Window& w); + + inline void update_data(const hypnodata_ptr_t& hd) { + w_hinfo.update_data(hd); } + }; + + + } +} + +#endif /* __N_DIALOGS_H */ diff --git a/src/napkin.cc b/src/napkin.cc new file mode 100644 index 0000000..d9ba0c9 --- a/dev/null +++ b/src/napkin.cc @@ -0,0 +1,367 @@ +#include <fcntl.h> +#include <iostream> +using std::cerr; +using std::endl; +#include <fstream> +using std::ofstream; +#include <cstdlib> +using std::min; +#include <stdexcept> +using std::runtime_error; +#include <list> +using std::list; +#include <vector> +using std::vector; +#include <string> +using std::string; +#include <gtkmm/main.h> +#include <gtkmm/window.h> +#include <gtkmm/box.h> +#include <gtkmm/statusbar.h> +#include <gtkmm/uimanager.h> +#include <gtkmm/stock.h> +#include <gtkmm/toolbar.h> +#include <gtkmm/filechooserdialog.h> +#include <gtkmm/messagedialog.h> +#include <gtkmm/aboutdialog.h> +#include <napkin/exception.h> +#include <napkin/util.h> +#include <napkin/st/decode.h> +#include <napkin/st/download.h> + +#include "db.h" +#include "sleep_timeline.h" +#include "dialogs.h" +#include "sleep_history.h" + +#include "config.h" + +class napkin_ui : public Gtk::Window { + public: + Gtk::VBox w_outer_box; + Gtk::Statusbar w_status_bar; + napkin::gtk::sleep_history_t w_history; + Glib::RefPtr<Gtk::UIManager> uiman; + Glib::RefPtr<Gtk::ActionGroup> agroup; + napkin::db_t db; + Glib::RefPtr<Gtk::Action> a_remove; + + napkin_ui() + : w_history(db) + { + static char *ui_info = + "<ui>" + "<menubar name='menu_bar'>" + "<menu action='menu_sleep'>" +#ifndef NDEBUG + "<menu action='menu_sleep_add'>" +#endif + "<menuitem action='sleep_add_from_sleeptracker'/>" +#ifndef NDEBUG + "<menuitem action='sleep_add_from_datafile'/>" + "</menu>" +#endif + "<menuitem action='sleep_remove'/>" + "<menuitem action='exit'/>" + "</menu>" + "<menu action='menu_help'>" + "<menuitem action='help_about'/>" + "</menu>" + "</menubar>" + "<toolbar action='tool_bar'>" + "<toolitem action='sleep_add_from_sleeptracker'/>" + "<toolitem action='sleep_remove'/>" + "<separator expand='true'/>" +#ifndef NDEBUG + "<toolitem action='debug'/>" + "<separator/>" +#endif + "<toolitem action='exit'/>" + "</toolbar>" + "</ui>"; + agroup = Gtk::ActionGroup::create(); + agroup->add(Gtk::Action::create("menu_sleep","Sleep")); + agroup->add(Gtk::Action::create("menu_sleep_add","Add")); + agroup->add(Gtk::Action::create("sleep_add_from_sleeptracker",Gtk::Stock::CONNECT, + "from sleeptracker","import sleeptracker data from watch"), + Gtk::AccelKey("<Ctrl>d"), + sigc::mem_fun(*this,&napkin_ui::on_sleep_add_from_sleeptracker)); +#ifndef NDEBUG + agroup->add(Gtk::Action::create("sleep_add_from_datafile",Gtk::Stock::CONVERT, + "from data file","import sleeptracker data stored in a file"), + sigc::mem_fun(*this,&napkin_ui::on_sleep_add_from_datafile)); +#endif + agroup->add(a_remove=Gtk::Action::create("sleep_remove",Gtk::Stock::REMOVE, + "Remove","remove highlighted sleep event from the database"), + Gtk::AccelKey("delete"), + sigc::mem_fun(*this,&napkin_ui::on_remove)); + agroup->add(Gtk::Action::create("exit",Gtk::Stock::QUIT,"Exit","Exit "PACKAGE_NAME), + Gtk::AccelKey("<control>w"), + sigc::mem_fun(*this,&napkin_ui::on_quit)); + agroup->add(Gtk::Action::create("menu_help","Help")); + agroup->add(Gtk::Action::create("help_about",Gtk::Stock::ABOUT, + "About","About this program"), + sigc::mem_fun(*this,&napkin_ui::on_help_about)); +#ifndef NDEBUG + agroup->add(Gtk::Action::create("debug",Gtk::Stock::INFO,"Debug","debug action"), + sigc::mem_fun(*this,&napkin_ui::on_debug)); +#endif + uiman = Gtk::UIManager::create(); + uiman->insert_action_group(agroup); + add_accel_group(uiman->get_accel_group()); + uiman->add_ui_from_string(ui_info); + Gtk::Widget * mb = uiman->get_widget("/menu_bar"); + if(mb) + w_outer_box.pack_start(*mb,Gtk::PACK_SHRINK); + Gtk::Widget * tb = uiman->get_widget("/tool_bar"); + if(tb) { + static_cast<Gtk::Toolbar*>(tb)->set_toolbar_style(Gtk::TOOLBAR_ICONS); + w_outer_box.pack_start(*tb,Gtk::PACK_SHRINK); + } + w_outer_box.pack_start(w_history,true/*expand*/,true/*fill*/); + w_outer_box.pack_end(w_status_bar,false/*expand*/,false/*fill*/); + add(w_outer_box); + set_title(PACKAGE_STRING); + set_default_size(800,600); + show_all(); + w_status_bar.push(" "PACKAGE_STRING); + + refresh_data(); + + w_history.signal_cursor_changed().connect( + sigc::mem_fun(*this,&napkin_ui::on_history_cursor_changed)); + on_history_cursor_changed(); + w_history.signal_double_click().connect( + sigc::mem_fun(*this,&napkin_ui::on_history_double_click)); + } + + void on_help_about() { + Gtk::AboutDialog about; + about.set_authors(vector<string>(1,"Michael Krelin <hacker@klever.net>")); + about.set_copyright("© 2008 Klever Group"); + extern const char *COPYING; + about.set_license(COPYING); + about.set_program_name(PACKAGE_NAME); + about.set_version(VERSION); + about.set_website("http://kin.klever.net/"); + about.set_website_label("Klever Internet Nothings"); + about.set_comments("The Sleeptracker PRO watch support program"); + about.run(); + } + + void on_history_double_click() { + napkin::hypnodata_ptr_t hd = w_history.get_current(); + if(!hd) return; + napkin::gtk::hypnoinfo_dialog_t hid(*this); + hid.update_data(hd); + hid.add_button(Gtk::Stock::OK,Gtk::RESPONSE_OK); + hid.run(); + } + + void refresh_data() { + load_data("ORDER BY s_alarm DESC"); + } + + void load_data(const string& sql) { + list<napkin::hypnodata_ptr_t> hds; + db.load(hds,sql); + w_history.set_data(hds); + } + + void on_history_cursor_changed() { + a_remove->set_sensitive(w_history.get_current()); + } + + void on_remove() { + napkin::hypnodata_ptr_t hd = w_history.get_current(); + if(!hd) return; + napkin::gtk::hypnoinfo_dialog_t hid(*this); + hid.update_data(hd); + hid.add_button("Remove from the database",Gtk::RESPONSE_OK); + hid.add_button(Gtk::Stock::CANCEL,Gtk::RESPONSE_CANCEL); + if(hid.run() == Gtk::RESPONSE_OK) { + db.remove(*hd); // TODO: handle error + refresh_data(); + } + } + + void on_quit() { + hide(); + } + + void import_data(const napkin::hypnodata_ptr_t& hd) { + napkin::gtk::hypnoinfo_dialog_t hid(*this); + hid.update_data(hd); + hid.add_button("Add to the database",Gtk::RESPONSE_OK); + hid.add_button(Gtk::Stock::CANCEL,Gtk::RESPONSE_CANCEL); + if(hid.run() == Gtk::RESPONSE_OK) { + try { + db.store(*hd); + refresh_data(); + }catch(napkin::exception_db& nedb) { + Gtk::MessageDialog md(*this, + string("Failed to add data to the database... ")+nedb.what(), + false/*use_markup*/,Gtk::MESSAGE_ERROR,Gtk::BUTTONS_OK, + true/*modal*/); + md.run(); + } + } + } + + class st_download_t : public Gtk::Dialog { + public: + Gtk::Label hint, attempt, error; + int nattempt; + napkin::hypnodata_ptr_t rv; + + st_download_t(Gtk::Window& w) + : Gtk::Dialog("Importing data from watch",w,true/*modal*/,false/*use separator*/), + hint("\nImporting data from the sleeptracker...\n\n" + "Set your watch to the 'data' screen " + " and connect to the compuer, if you haven't yet.",0.5,0.5), + attempt("",1,0.5), error("",0,0.5), + nattempt(1), fd(-1) + { + Gtk::VBox *vb = get_vbox(); + vb->set_spacing(10); + hint.set_justify(Gtk::JUSTIFY_CENTER); + vb->pack_start(hint,Gtk::PACK_SHRINK,5); + vb->pack_start(attempt); + vb->pack_start(error); + add_button("Cancel",Gtk::RESPONSE_CANCEL); + vb->show_all(); + } + ~st_download_t() { + if(!(fd<0)) close(fd); + } + + void on_map() { + Gtk::Dialog::on_map(); + initiate_attempt(); + } + + void initiate_attempt() { + Glib::signal_timeout().connect_seconds( + sigc::mem_fun(*this,&st_download_t::try_watch), + 1); + } + void show_error(const napkin::exception& e) { + error.set_use_markup(true); + error.set_markup(string()+ + "<span color='red'>"+ + e.what()+"</span>"); + } + void next_attempt() { + char tmp[128]; + snprintf(tmp,sizeof(tmp),"Trying again, attempt #%d",++nattempt); + attempt.set_text(tmp); + } + + int fd; + char buffer[512]; + size_t rb; + + bool try_watch() { + try { + fd = napkin::sleeptracker::download_initiate(getenv("SLEEPTRACKER_PORT")); + Glib::signal_timeout().connect_seconds( + sigc::mem_fun(*this,&st_download_t::try_data), + 1); + return false; + }catch(napkin::exception_sleeptracker& nest) { + show_error(nest); + } + next_attempt(); + return true; + } + + bool try_data() { + try { + try { + rb = napkin::sleeptracker::download_finish(fd,buffer,sizeof(buffer)); + }catch(napkin::exception_st_port& nestp) { + fd = -1; + show_error(nestp); + next_attempt(); + initiate_attempt(); + return false; + } + rv = napkin::sleeptracker::decode(buffer,rb); + response(Gtk::RESPONSE_OK); + }catch(napkin::exception_st_data_envelope& neste) { + show_error(neste); + next_attempt(); + initiate_attempt(); + }catch(napkin::exception_sleeptracker& nest) { + show_error(nest); + } + return false; + } + }; + + void on_sleep_add_from_sleeptracker() { + st_download_t sd(*this); + if(sd.run()==Gtk::RESPONSE_OK && sd.rv ) { + sd.hide(); +#ifndef NDEBUG + { + ofstream dfile( + (db.datadir+"/raw-"+napkin::strftime("%Y-%m-%d.st",time(0))).c_str(), + std::ios::binary|std::ios::out|std::ios::trunc); + if(dfile) + dfile.write(sd.buffer,sd.rb); + dfile.close(); + } +#endif + import_data(sd.rv); + } + } + +#ifndef NDEBUG + void on_sleep_add_from_datafile() { + Gtk::FileChooserDialog d("Please select a file", + Gtk::FILE_CHOOSER_ACTION_OPEN); + d.set_transient_for(*this); + d.add_button(Gtk::Stock::CANCEL,Gtk::RESPONSE_CANCEL); + d.add_button(Gtk::Stock::OPEN,Gtk::RESPONSE_OK); + Gtk::FileFilter stfiles; + stfiles.set_name("Sleeptracker files"); + stfiles.add_pattern("*.st"); + d.add_filter(stfiles); + Gtk::FileFilter allfiles; + allfiles.set_name("All files"); + allfiles.add_pattern("*"); + d.add_filter(allfiles); + if(d.run()==Gtk::RESPONSE_OK) { + d.hide(); + + int fd = open(d.get_filename().c_str(),O_RDONLY); + if(fd<0) + throw napkin::exception("failed to open() data"); + unsigned char buffer[512]; + size_t rb = read(fd,buffer,sizeof(buffer)); + close(fd); + if( (rb==(size_t)-1) || rb==sizeof(buffer)) + throw napkin::exception("error reading datafile"); + napkin::hypnodata_ptr_t hd = napkin::sleeptracker::decode(buffer,rb); + import_data(hd); + } + } +#endif + +#ifndef NDEBUG + void on_debug() { + } +#endif +}; + +int main(int argc,char**argv) { + try { + Gtk::Main m(argc,argv); + napkin_ui hui; + m.run(hui); + return 0; + }catch(std::exception& e) { + cerr << "oops: " << e.what() << endl; + } +} diff --git a/src/schema.sql b/src/schema.sql new file mode 100644 index 0000000..f0fc4dd --- a/dev/null +++ b/src/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE sleeps ( + s_tobed text NOT NULL, -- w3cish timestamp with minute precision + s_alarm text NOT NULL PRIMARY KEY, -- w3cish timestamp with minute precision + s_window integer NOT NULL, -- number of minutes + s_data_a integer NOT NULL, -- number of seconds + s_almost_awakes text NOT NULL, -- [^0-9:TZ-]-separated list of w3cish + -- timestamps with second precision + s_timezone integer NOT NULL -- seconds west of GMT, TODO: make use of it +); diff --git a/src/sleep_history.cc b/src/sleep_history.cc new file mode 100644 index 0000000..1b5ce27 --- a/dev/null +++ b/src/sleep_history.cc @@ -0,0 +1,158 @@ +#include <gtkmm/main.h> +#include <napkin/util.h> + +#include "sleep_timeline.h" +#include "sleep_history.h" + +namespace napkin { + namespace gtk { + + sleep_history_t::basic_textrenderer::basic_textrenderer() { + property_family().set_value("monospace"); + property_single_paragraph_mode().set_value(true); + } + void sleep_history_t::basic_textrenderer::render_vfunc( + const Glib::RefPtr<Gdk::Drawable>& window, Gtk::Widget& widget, + const Gdk::Rectangle& background_area, const Gdk::Rectangle& cell_area, + const Gdk::Rectangle& expose_area, Gtk::CellRendererState flags) { + hypnodata_t *hd = (hypnodata_t*)property_user_data().get_value(); + property_text().set_value(hd?get_text(*hd).c_str():""); + Gtk::CellRendererText::render_vfunc(window,widget, + background_area,cell_area,expose_area, + flags); + } + + const string sleep_history_t::date_render_t::get_text(const hypnodata_t& hd) const { + return hd.str_date(); } + const string sleep_history_t::tobed_render_t::get_text(const hypnodata_t& hd) const { + return hd.str_to_bed(); } + const string sleep_history_t::alarm_render_t::get_text(const hypnodata_t& hd) const { + return hd.str_alarm(); } + sleep_history_t::window_render_t::window_render_t() { + property_xalign().set_value(1); } + const string sleep_history_t::window_render_t::get_text( + const hypnodata_t& hd) const { + char tmp[16]; + snprintf(tmp,sizeof(tmp),"%2d",hd.window); + return tmp; + } + sleep_history_t::nawakes_render_t::nawakes_render_t() { + property_xalign().set_value(1); } + const string sleep_history_t::nawakes_render_t::get_text( + const hypnodata_t& hd) const { + char tmp[16]; + snprintf(tmp,sizeof(tmp),"%2d",(int)hd.almost_awakes.size()); + return tmp; + } + const string sleep_history_t::data_a_render_t::get_text(const hypnodata_t& hd) const { + return hd.str_data_a(); } + + sleep_history_t::sleep_timeline_render_t::sleep_timeline_render_t( + const sleep_history_t& sh) : sleep_history(sh) + { + property_xpad().set_value(2); property_ypad().set_value(2); + } + void sleep_history_t::sleep_timeline_render_t::render_vfunc( + const Glib::RefPtr<Gdk::Drawable>& window, Gtk::Widget&/*widget*/, + const Gdk::Rectangle&/*background_area*/, const Gdk::Rectangle& cell_area, + const Gdk::Rectangle&/*expose_area*/, Gtk::CellRendererState/*flags*/) { + hypnodata_t *hd = (hypnodata_t*)property_user_data().get_value(); + if(!hd) return; + int xpad = property_xpad(), ypad = property_ypad(); + int x0 = cell_area.get_x()+xpad, y0 = cell_area.get_y()+ypad, + dx = cell_area.get_width()-2*xpad, dy = cell_area.get_height()-2*ypad; + time_t a = hd->aligned_start(); + gtk::render_sleep_timeline( + *hd, + window, + x0,y0, dx,dy, + a+sleep_history.min_tobed, + a+sleep_history.max_alarm ); + } + + sleep_history_t::sleep_history_t(db_t& d) + : store( Gtk::ListStore::create(cols) ), + r_sleep_timeline(*this), + min_tobed(24*60*60*2), max_alarm(0), + db(d) + { + w_tree.set_model(store); + add(w_tree); + append_c("Date",r_date); + append_c("To bed",r_to_bed); + (c_timeline=append_c("Sleep timeline",r_sleep_timeline))->set_expand(true); + append_c("Alarm",r_alarm); + append_c("Window",r_window); + append_c(" N",r_nawakes); + append_c("Data A",r_data_a); + + w_tree.signal_query_tooltip().connect( + sigc::mem_fun(*this,&sleep_history_t::on_query_tooltip)); + w_tree.set_has_tooltip(true); + w_tree.signal_button_press_event().connect( + sigc::mem_fun(*this,&sleep_history_t::on_button_press),false); + } + + Gtk::TreeView::Column *sleep_history_t::append_c(const string& title,Gtk::CellRenderer& renderer) { + Gtk::TreeView::Column *rv = w_tree.get_column(w_tree.append_column(title,renderer)-1); + rv->add_attribute(renderer.property_user_data(),cols.c_hypnodata_ptr); + rv->set_resizable(true); + return rv; + } + + bool sleep_history_t::on_button_press(GdkEventButton* geb) { + if(geb->type!=GDK_2BUTTON_PRESS) return false; + double_click_signal(); + return true; + } + + bool sleep_history_t::on_query_tooltip( + int x,int y,bool keyboard_tooltip, + const Glib::RefPtr<Gtk::Tooltip>& tooltip) { + if(keyboard_tooltip) return false; + int tx,ty; + w_tree.convert_widget_to_tree_coords(x,y,tx,ty); + Gtk::TreeModel::Path p; + Gtk::TreeViewColumn *c; + int cx, cy; + if(!w_tree.get_path_at_pos(tx,ty,p,c,cx,cy)) return false; + if(c != c_timeline) return false; + hypnodata_ptr_t hd = store->get_iter(p)->get_value(cols.c_hypnodata); + string mup = "Almost awake moments are:\n\n<tt>"; + for(vector<time_t>::const_iterator i=hd->almost_awakes.begin();i!=hd->almost_awakes.end();++i) + mup += strftime(" %H:%M:%S\n",*i); + mup += "</tt>"; + tooltip->set_markup( mup ); + return true; + } + + void sleep_history_t::set_data(list<napkin::hypnodata_ptr_t> data) { + store->clear(); + min_tobed = 24*60*60*2; max_alarm = 0; + for(std::list<napkin::hypnodata_ptr_t>::const_iterator + i=data.begin(); i!=data.end(); ++i) { + Gtk::TreeModel::Row r = *(store->append()); + r[cols.c_hypnodata] = *i; + r[cols.c_hypnodata_ptr] = i->get(); + time_t a = (*i)->aligned_start(); + time_t soff = (*i)->to_bed-a; + if(soff < min_tobed) min_tobed = soff; + time_t eoff = (*i)->alarm-a; + if(eoff > max_alarm) max_alarm = eoff; + } + while(Gtk::Main::events_pending()) Gtk::Main::iteration() ; + w_tree.columns_autosize(); + } + + const hypnodata_ptr_t sleep_history_t::get_current() { + Gtk::TreeModel::Path p; + Gtk::TreeViewColumn *c; + w_tree.get_cursor(p,c); + if( (!p.gobj()) || p.empty()) + return hypnodata_ptr_t(); /* XXX: or throw? */ + Gtk::ListStore::iterator i = store->get_iter(p); + return i->get_value(cols.c_hypnodata); + } + + } +} diff --git a/src/sleep_history.h b/src/sleep_history.h new file mode 100644 index 0000000..7837711 --- a/dev/null +++ b/src/sleep_history.h @@ -0,0 +1,113 @@ +#ifndef __N_SLEEP_HISTORY_H +#define __N_SLEEP_HISTORY_H + +#include <string> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treeview.h> +#include <gtkmm/liststore.h> +#include <napkin/types.h> +#include "db.h" + +namespace napkin { + namespace gtk { + using std::string; + + class sleep_history_t : public Gtk::ScrolledWindow { + public: + class basic_textrenderer : public Gtk::CellRendererText { + public: + basic_textrenderer(); + void render_vfunc( + const Glib::RefPtr<Gdk::Drawable>& window, Gtk::Widget& widget, + const Gdk::Rectangle& background_area, const Gdk::Rectangle& cell_area, + const Gdk::Rectangle& expose_area, Gtk::CellRendererState flags); + virtual const string get_text(const hypnodata_t& hd) const = 0; + }; + + class date_render_t : public basic_textrenderer { + public: + const string get_text(const hypnodata_t& hd) const; + }; + class tobed_render_t : public basic_textrenderer { + public: + const string get_text(const hypnodata_t& hd) const; + }; + class alarm_render_t : public basic_textrenderer { + public: + const string get_text(const hypnodata_t& hd) const; + }; + class window_render_t : public basic_textrenderer { + public: + window_render_t(); + const string get_text(const hypnodata_t& hd) const; + }; + class nawakes_render_t : public basic_textrenderer { + public: + nawakes_render_t(); + const string get_text(const hypnodata_t& hd) const; + }; + class data_a_render_t : public basic_textrenderer { + public: + const string get_text(const hypnodata_t& hd) const; + }; + + class sleep_timeline_render_t : public Gtk::CellRenderer { + public: + const sleep_history_t& sleep_history; + + sleep_timeline_render_t(const sleep_history_t& sh); + void render_vfunc(const Glib::RefPtr<Gdk::Drawable>& window, Gtk::Widget&/*widget*/, + const Gdk::Rectangle&/*background_area*/, const Gdk::Rectangle& cell_area, + const Gdk::Rectangle&/*expose_area*/, Gtk::CellRendererState/*flags*/); + }; + + class columns_t : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<hypnodata_ptr_t> c_hypnodata; + Gtk::TreeModelColumn<void*> c_hypnodata_ptr; + + columns_t() { + add(c_hypnodata); add(c_hypnodata_ptr); + } + }; + + columns_t cols; + Gtk::TreeView w_tree; + Glib::RefPtr<Gtk::ListStore> store; + date_render_t r_date; + tobed_render_t r_to_bed; + alarm_render_t r_alarm; + window_render_t r_window; + nawakes_render_t r_nawakes; + data_a_render_t r_data_a; + sleep_timeline_render_t r_sleep_timeline; + Gtk::TreeView::Column *c_timeline; + + sigc::signal<void> double_click_signal; + sigc::signal<void>& signal_double_click() { return double_click_signal; } + + time_t min_tobed, max_alarm; + + db_t& db; + + sleep_history_t(db_t& d); + + Gtk::TreeView::Column *append_c(const string& title,Gtk::CellRenderer& renderer); + + bool on_button_press(GdkEventButton* geb); + bool on_query_tooltip(int x,int y,bool keyboard_tooltip, const Glib::RefPtr<Gtk::Tooltip>& tooltip); + + + void set_data(list<napkin::hypnodata_ptr_t> data); + + Glib::SignalProxy0<void> signal_cursor_changed() { + return w_tree.signal_cursor_changed(); + } + + const hypnodata_ptr_t get_current(); + + }; + } +} + +#endif /* __N_SLEEP_HISTORY_H */ diff --git a/src/sleep_timeline.cc b/src/sleep_timeline.cc new file mode 100644 index 0000000..e5d4146 --- a/dev/null +++ b/src/sleep_timeline.cc @@ -0,0 +1,100 @@ +#include <cstdlib> +#include <vector> +#include <gtkmm/widget.h> + +#include "sleep_timeline.h" + +namespace napkin { + namespace gtk { + using std::vector; + using std::min; + + void render_sleep_timeline( + const hypnodata_t& hd, + const Glib::RefPtr<Gdk::Drawable>& w, + int x0,int y0,int dx,int dy, + time_t _t0,time_t _t1) { + static Gdk::Color c_tobed("darkgreen"), c_alarm("red"), + c_almostawake("maroon"), c_midnight("blue"), c_hour("#606060"), + c_timeline("#404040"), c_window("red"), + c_background("#ffffc0"), c_border("gray"); + static bool beenthere=false; + if(!beenthere) { + Glib::RefPtr<Gdk::Colormap> cm(Gtk::Widget::get_default_colormap()); + cm->alloc_color(c_tobed); cm->alloc_color(c_alarm); + cm->alloc_color(c_almostawake); + cm->alloc_color(c_midnight); cm->alloc_color(c_hour); + cm->alloc_color(c_timeline); cm->alloc_color(c_window); + cm->alloc_color(c_background); cm->alloc_color(c_border); + beenthere = true; + } + Glib::RefPtr<Gdk::GC> gc = Gdk::GC::create(w); + + gc->set_foreground(c_background); + w->draw_rectangle(gc,true, x0,y0, dx,dy+1 ); + gc->set_foreground(c_border); + w->draw_rectangle(gc,false, x0,y0, dx,dy+1 ); + x0+=3; dx-=6; + + time_t t0, t1; + if(_t0 && _t1 && _t0!=_t1 && _t0<=hd.to_bed && hd.alarm<=_t1) + t0 = _t0, t1 = _t1; + else + t0 = hd.to_bed, t1 = hd.alarm; + time_t dt = t1-t0; + + time_t tb = hd.to_bed; time_t ta = hd.alarm; + int xb = x0+dx*(tb-t0)/dt, + xa = x0+dx*(ta-t0)/dt; + int ym = y0+dy/2; + gc->set_line_attributes(1,Gdk::LINE_SOLID,Gdk::CAP_BUTT,Gdk::JOIN_MITER); + gc->set_foreground(c_timeline); + w->draw_line(gc, xb,ym, xa,ym ); + time_t ws = ta-hd.window*60; + int xws = x0+dx*(ws-t0)/dt; + gc->set_foreground(c_window); + w->draw_rectangle(gc, true, xws,ym-1, xa-xws,3 ); + gc->set_foreground(c_almostawake); + int tl2 = min(dy/2 - 3, 7); + int yt0 = ym-tl2, yt1 = ym+tl2+1; + for(vector<time_t>::const_iterator i=hd.almost_awakes.begin();i!=hd.almost_awakes.end();++i) { + int x = x0+dx*(*i-t0)/dt; + w->draw_line(gc, x,yt0, x,yt1 ); + } + tl2 = min(dy/5, 5); + yt0 = ym-tl2; yt1 = ym+tl2+1; + gc->set_foreground(c_hour); + time_t midnight = hd.aligned_start()+24*60*60; + for(time_t h = tb-tb%3600 + 3600; h<ta ; h+=3600) { + if(h==midnight) gc->set_foreground(c_midnight); + int x = x0+dx*(h-t0)/dt; + w->draw_line(gc, x,yt0, x,yt1 ); + if(h==midnight) gc->set_foreground(c_hour); + } + gc->set_line_attributes(2,Gdk::LINE_SOLID,Gdk::CAP_BUTT,Gdk::JOIN_MITER); + gc->set_foreground(c_tobed); + w->draw_line(gc, xb,yt0, xb,yt1 ); + gc->set_foreground(c_alarm); + w->draw_line(gc, xa,yt0, xa,yt1 ); + } + + + bool sleep_timeline_t::on_expose_event(GdkEventExpose*) { + if(!hd) return true; + Glib::RefPtr<Gdk::Window> w = get_window(); + int x0,y0,dx,dy,wd; + w->get_geometry(x0,y0,dx,dy,wd); + render_sleep_timeline( + *hd, + w, + 0,0, dx-2,dy-2 ); + return true; + } + + void sleep_timeline_t::set_data(const hypnodata_ptr_t& _hd) { + hd = _hd; + queue_draw(); + } + + } +} diff --git a/src/sleep_timeline.h b/src/sleep_timeline.h new file mode 100644 index 0000000..3264fd6 --- a/dev/null +++ b/src/sleep_timeline.h @@ -0,0 +1,29 @@ +#ifndef __N_SLEEP_TIMELINE_H +#define __N_SLEEP_TIMELINE_H + +#include <time.h> +#include <gtkmm/drawingarea.h> +#include <napkin/types.h> + +namespace napkin { + namespace gtk { + + void render_sleep_timeline( + const hypnodata_t& hd, + const Glib::RefPtr<Gdk::Drawable>& w, + int x0,int y0,int dx,int dy, + time_t _t0=0,time_t _t1=0); + + class sleep_timeline_t : public Gtk::DrawingArea { + public: + hypnodata_ptr_t hd; + + bool on_expose_event(GdkEventExpose*); + void set_data(const hypnodata_ptr_t& _hd); + }; + + + } +} + +#endif /* __N_SLEEP_TIMELINE_H */ diff --git a/src/sqlite.h b/src/sqlite.h new file mode 100644 index 0000000..ad276ee --- a/dev/null +++ b/src/sqlite.h @@ -0,0 +1,103 @@ +#ifndef __SQLITE_H +#define __SQLITE_H + +#include <cassert> +#include <stdexcept> +#include <string> +#include <sqlite3.h> + +namespace sqlite { + using std::string; + + class exception : public std::runtime_error { + public: + int rcode; + explicit exception(const string& w,int rc=-1) + : std::runtime_error(w), rcode(rc) { } + ~exception() throw() { } + }; + + class db_t { + public: + sqlite3 *_D; + + db_t() + : _D(0) { } + db_t(const char *f) + : _D(0) { open(f); } + ~db_t() { close(); } + + operator const sqlite3*(void) const { return _D; } + operator sqlite3*(void) { return _D; } + + void close() { + if(_D) { + sqlite3_close(_D); + _D = 0; + } + } + void open(const char *f) { + close(); + int r = sqlite3_open(f,&_D); + if(r!=SQLITE_OK) { + string msg = sqlite3_errmsg(_D); sqlite3_close(_D); + throw exception("Failed to open SQLite database: "+msg,r); + } + } + + void exec(const char *sql) { + assert(_D); + char *errm; + int r = sqlite3_exec(_D,sql,NULL,NULL,&errm); + if(r!=SQLITE_OK) + throw exception(string("Failed to sqlite3_exec():")+errm,r); + } + void get_table(const char *sql,char ***resp,int *nr,int *nc) { + assert(_D); + char *errm; + int r = sqlite3_get_table(_D,sql,resp,nr,nc,&errm); + if(r!=SQLITE_OK) + throw exception(string("Failed to sqlite3_get_table():")+errm,r); + } + }; + + template<typename T> + class mem_t { + public: + T _M; + + mem_t(T M) :_M(M) { } + ~mem_t() { if(_M) sqlite3_free(_M); } + + operator const T&(void) const { return _M; } + operator T&(void) { return _M; } + + mem_t operator=(T M) { + if(_M) sqlite3_free(_M); + _M = M; + } + }; + + class table_t { + public: + char **_T; + + table_t() : _T(0) { } + table_t(char **T) : _T(T) { } + ~table_t() { if(_T) sqlite3_free_table(_T); } + + operator char**&(void) { return _T; } + + operator char ***(void) { + if(_T) sqlite3_free_table(_T); + return &_T; } + + const char *get(int r,int c,int nc) { + assert(_T); + return _T[r*nc+c]; + } + }; + +} + +#endif /* __SQLITE_H */ diff --git a/src/widgets.cc b/src/widgets.cc new file mode 100644 index 0000000..ea85bc8 --- a/dev/null +++ b/src/widgets.cc @@ -0,0 +1,63 @@ +#include <napkin/util.h> +#include "widgets.h" + +namespace napkin { + namespace gtk { + + hypnoinfo_t::hypnoinfo_t() + : w_upper(4,3,false/*homogeneous*/), + lc_tobed("To bed:",0.5,0.5), + lc_timeline("Sleep timeline:",0.5,0.5), + lc_alarm("Alarm:",0.5,0.5), lc_window("Window:",0.5,0.5), + l_data_a("",0.9,0.5) + { + add(l_date); + add(l_hseparator); + w_upper.set_col_spacings(5); + w_upper.attach(lc_tobed,0,1,0,1, Gtk::SHRINK); + w_upper.attach(lc_timeline,1,2,0,1, Gtk::SHRINK); + w_upper.attach(lc_alarm,2,3,0,1, Gtk::SHRINK); + w_upper.attach(lf_tobed,0,1,1,4, Gtk::SHRINK); + w_upper.attach(st_timeline,1,2,1,4, + Gtk::FILL|Gtk::EXPAND,Gtk::FILL|Gtk::EXPAND,0,0); + w_upper.attach(lf_alarm,2,3,1,2, Gtk::SHRINK); + w_upper.attach(lc_window,2,3,2,3, Gtk::SHRINK); + w_upper.attach(lf_window,2,3,3,4, Gtk::SHRINK); + add(w_upper); + add(lc_almost_awakes); + add(lf_almost_awakes); + add(l_data_a); + show_all(); + } + + void hypnoinfo_t::update_data(const hypnodata_ptr_t& hd) { + l_date.set_use_markup(true); + l_date.set_markup("<b>"+hd->str_date()+"</b>"); + lf_tobed.set_use_markup(true); + lf_tobed.set_markup("<b>"+hd->str_to_bed()+"</b>"); + lf_alarm.set_use_markup(true); + lf_alarm.set_markup("<b>"+hd->str_alarm()+"</b>"); + char tmp[64]; + snprintf(tmp,sizeof(tmp),"<b>%d mins</b>",hd->window); + lf_window.set_use_markup(true); + lf_window.set_markup(tmp); + snprintf(tmp,sizeof(tmp),"<b>%d</b> almost awake moments:",(int)hd->almost_awakes.size()); + lc_almost_awakes.set_use_markup(true); + lc_almost_awakes.set_markup(tmp); + string awlist; + for(vector<time_t>::const_iterator i=hd->almost_awakes.begin();i!=hd->almost_awakes.end();++i) { + if(!awlist.empty()) + awlist += ", "; + awlist += strftime("<b>%H:%M:%S</b>",*i); + } + lf_almost_awakes.set_use_markup(true); + lf_almost_awakes.set_line_wrap(true); + lf_almost_awakes.set_line_wrap_mode(Pango::WRAP_WORD); + lf_almost_awakes.set_markup("<tt>"+awlist+"</tt>"); + l_data_a.set_use_markup(true); + l_data_a.set_markup("Data A is <b>"+hd->str_data_a()+"</b>"); + st_timeline.set_data(hd); + } + + } +} diff --git a/src/widgets.h b/src/widgets.h new file mode 100644 index 0000000..99936ff --- a/dev/null +++ b/src/widgets.h @@ -0,0 +1,33 @@ +#ifndef __N_WIDGETS_H +#define __N_WIDGETS_H + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/separator.h> +#include <gtkmm/table.h> +#include "sleep_timeline.h" + +namespace napkin { + namespace gtk { + + class hypnoinfo_t : public Gtk::VBox { + public: + Gtk::Label l_date; + Gtk::HSeparator l_hseparator; + Gtk::Table w_upper; + Gtk::Label lc_tobed, lc_timeline, lc_alarm, lc_window; + Gtk::Label lf_tobed, lf_alarm, lf_window; + sleep_timeline_t st_timeline; + Gtk::Label lc_almost_awakes; + Gtk::Label lf_almost_awakes; + Gtk::Label l_data_a; + + hypnoinfo_t(); + + void update_data(const hypnodata_ptr_t& hd); + }; + + } +} + +#endif /* __N_WIDGETS_H */ diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..579d0cc --- a/dev/null +++ b/test/.gitignore @@ -0,0 +1,4 @@ +/sleeptracker-decode +/sleeptracker-decode.o +/.libs +/.deps diff --git a/test/Makefile.am b/test/Makefile.am new file mode 100644 index 0000000..09c3ed7 --- a/dev/null +++ b/test/Makefile.am @@ -0,0 +1,7 @@ +noinst_PROGRAMS = sleeptracker-decode + +INCLUDES = -I${top_srcdir}/include/ ${MODULES_CFLAGS} +LIBS = ${top_builddir}/lib/libnapkin.la + +sleeptracker_decode_SOURCES = sleeptracker-decode.cc +sleeptracker_decode_DEPENDENCIES = ${LIBS} diff --git a/test/sleeptracker-decode.cc b/test/sleeptracker-decode.cc new file mode 100644 index 0000000..77d4fbd --- a/dev/null +++ b/test/sleeptracker-decode.cc @@ -0,0 +1,49 @@ +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <termios.h> +#include <iostream> +#include <stdexcept> +#include <algorithm> +#include <iterator> +using namespace std; +#include <napkin/st/decode.h> + +string str_f_time(const char *fmt,time_t t) { + struct tm tt; + localtime_r(&t,&tt); + char rv[1024]; + strftime(rv,sizeof(rv),fmt,&tt); + return rv; +} + +ostream& operator<<(ostream& o,const napkin::hypnodata_t& hd) { + o + << "Window is " << hd.window << endl + << "'To bed' time is " << str_f_time("%Y-%m-%d %H:%M",hd.to_bed) << endl + << "Alarm time is " << str_f_time("%Y-%m-%d %H:%M",hd.alarm) << endl + << "Data A is " << hd.data_a/60 << ":" << hd.data_a%60 << endl; + for(vector<time_t>::const_iterator i=hd.almost_awakes.begin();i!=hd.almost_awakes.end();++i) + o << " almost awake at " << str_f_time("%Y-%m-%d %H:%M:%S",*i) << endl; + return o; +} + +int main(int/*argc*/,char **argv) { + try { + int fd = open(argv[1],O_RDONLY); + if(fd<0) + throw runtime_error("failed to open() data"); + unsigned char buffer[1024]; + int rb = read(fd,buffer,sizeof(buffer)); + if(!(rb>0)) + throw runtime_error("failed to read() data"); + close(fd); + + napkin::hypnodata_t hd; + cout << napkin::sleeptracker::decode(hd,buffer,rb); + }catch(exception& e) { + cerr << "oops: " << e.what() << endl; + } +} + |