commit aae0d3c1166e9549d5f8b501e347b60ba024ce31 Author: Sean C. Rhea Date: Wed Sep 6 23:59:42 2006 +0000 initial import diff --git a/COPYING b/COPYING new file mode 100644 index 000000000..f90922eea --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..64b14ecd8 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +# +# $Id: Makefile,v 1.11 2006/09/06 23:23:03 srhea Exp $ +# +# Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +CC=gcc +CFLAGS=-g -W -Wall -Werror -ansi -pedantic +CXXFLAGS=-g -W -Wall -Werror + +all: ptdl ptpk ptunpk cpint subdirs +.PHONY: all clean subdirs + +clean: + rm -f *.o ptdl ptpk ptunpk cpint + $(MAKE) -wC gui clean + +subdirs: + @if test -d gui; \ + then \ + if ! test -f gui/Makefile; then cd gui; qmake; cd -; fi; \ + $(MAKE) -wC gui; \ + fi + +cpint: cpint-cmd.o cpint.o pt.o + $(CXX) -o $@ $^ + +ptdl: ptdl.o pt.o + $(CC) -o $@ $^ + +ptunpk: ptunpk.o pt.o + $(CC) -o $@ $^ + +ptpk: ptpk.o pt.o + $(CC) -o $@ $^ + +ptdl.o: pt.h +ptunpk.o: pt.h + diff --git a/compare_rows.pl b/compare_rows.pl new file mode 100755 index 000000000..6c6684fe4 --- /dev/null +++ b/compare_rows.pl @@ -0,0 +1,83 @@ +#!/usr/bin/perl -w +# +# $Id: compare_rows.pl,v 1.1 2006/05/12 21:50:51 srhea Exp $ +# +# Copyright (c) 2006 Russ Cox (rsc@swtch.com) and Sean Rhea (srhea@srhea.net) +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +use strict; + +if ($#ARGV < 1) { + print STDERR "usage: compare_rows.pl [column]\n"; + exit 1; +} + +my $file1 = $ARGV[0]; +my $file2 = $ARGV[1]; +my $start_col = 0; +my $stop_col = 8; +if ($#ARGV >= 2) { + my $stop_col = $ARGV[2]; + my $start_col = $stop_col - 1; +} + +open(FILE1, $file1) or die "Couldn't open $file1\n"; +open(FILE2, $file2) or die "Couldn't open $file2\n"; + +my $row = 0; +while() { + ++$row; + $_ =~ s/^\s+//; + my @cols1 = split /\s+/, $_; + my $row2 = ; + if ($row2) { + my @cols2 = split /\s+/, $row2; + my $col; + for ($col = $start_col; $col < $stop_col; ++$col) { + if (($cols1[$col] =~ m/[0-9.]+/) && ($cols2[$col] =~ m/[0-9.]+/)) { + if ($col == 0) { + # Times are sometimes off by a bit; just make sure they're + # within a tenth of a percent. + if (($cols1[$col] * 0.999 > $cols2[$col]) + || ($cols1[$col] * 1.001 < $cols2[$col])) { + print "$file1: @cols1\n"; + print "$file2: @cols2\n"; + } + } + else { + # All other columns should match exactly. + if ($cols1[$col] != $cols2[$col]) { + print "$file1: @cols1\n"; + print "$file2: @cols2\n"; + } + } + } + elsif (!($cols1[$col] eq $cols2[$col])) { + print "$file1: @cols1\n"; + print "$file2: @cols2\n"; + } + } + } + else { + print "$file1 is longer.\n"; + } +} + +if () { + print "$file2 is longer.\n"; +} + diff --git a/cpint-cmd.c b/cpint-cmd.c new file mode 100644 index 000000000..260338b90 --- /dev/null +++ b/cpint-cmd.c @@ -0,0 +1,55 @@ +/* + * $Id: cpint-cmd.c,v 1.1 2006/08/11 19:53:07 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "cpint.h" + +static void +one_done_cb(const char *longdate) +{ + fprintf(stderr, "Compiling data for ride on %s...", longdate); +} + +int +main(int argc, char *argv[]) +{ + int i; + double *bests; + int bestlen; + char *dir = "."; + if (argc > 1) + dir = argv[1]; + update_cpi_files(dir, one_done_cb); + combine_cpi_files(dir, &bests, &bestlen); + for (i = 0; i < bestlen; ++i) { + if (bests[i] != 0) + printf("%6.3f %3.0f\n", i * 0.021, round(bests[i])); + } + return 0; +} + diff --git a/cpint.c b/cpint.c new file mode 100644 index 000000000..1f412fd32 --- /dev/null +++ b/cpint.c @@ -0,0 +1,251 @@ +/* + * $Id: cpint.c,v 1.4 2006/08/11 19:53:07 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "pt.h" + +struct point +{ + double secs; + int watts; + struct point *next; +}; + +static struct point *head, *tail; +time_t start_since_epoch; +static double last_secs; +static int rec_int_ms = 1.26 * 1000.0; + +static int +secs_to_interval(double secs) +{ + if (rec_int_ms == 0) { + fprintf(stderr, "Missing recording interval.\n"); + exit(1); + } + return (int) round(secs * 1000.0 / rec_int_ms); +} + +static void +add_point(double secs, double watts) +{ + struct point *p = (struct point*) malloc(sizeof(struct point)); + p->secs = secs; + p->watts = watts; + p->next = NULL; + if (tail) tail->next = p; else head = p; + tail = p; +} + +static void +time_cb(struct tm *time, time_t since_epoch, void *context) +{ + double secs; + context = NULL; + time = NULL; + if (start_since_epoch == 0) + start_since_epoch = since_epoch; + secs = since_epoch - start_since_epoch; + /* Be conservative: a sleep interval counts as all zeros. */ + add_point(secs, 0.0); + last_secs = secs; +} + +static void +data_cb(double secs, double nm, double mph, double watts, double miles, + unsigned cad, unsigned hr, unsigned interval, void *context) +{ + context = NULL; + nm = 0.0; miles = 0.0; cad = 0; hr = 0; interval = 0; + /* Be conservative: count NaN's as zeros. */ + add_point(secs, (mph == -1.0) ? 0.0 : watts); + last_secs = secs; +} + +static void +error_cb(const char *msg, void *context) +{ + context = NULL; + fprintf(stderr, "%s\n", msg); + exit(1); +} + +void +one_file(FILE *in, FILE *out) +{ + double start_secs, prev_secs, dur_secs, avg, sum; + int dur_ints, i, total_intervals; + double *bests; + struct point *p, *q; + + if (head) { + p = head; + while (p) { q = p; p = p->next; free(q); } + head = tail = NULL; + } + start_since_epoch = 0; + last_secs = 0.0; + + pt_read_raw(in, 0 /* not compat */, NULL, NULL, + &time_cb, &data_cb, &error_cb); + + total_intervals = secs_to_interval(tail->secs); + bests = (double*) calloc(total_intervals + 1, sizeof(double)); + + start_secs = prev_secs = 0.0; + for (p = head; p; p = p->next) { + sum = 0.0; + for (q = p; q; q = q->next) { + sum += (q->secs - prev_secs) * q->watts; + dur_secs = q->secs - start_secs; + dur_ints = secs_to_interval(dur_secs); + avg = sum / dur_secs; + if (bests[dur_ints] < avg) + bests[dur_ints] = avg; + prev_secs = q->secs; + } + start_secs = prev_secs = p->secs; + } + + for (i = 0; i <= total_intervals; ++i) { + if (bests[i] != 0) + fprintf(out, "%6.3f %3.0f\n", i * rec_int_ms / 1000.0 / 60.0, + round(bests[i])); + } +} + +void +update_cpi_files(const char *dir, void (*one_done_cb)(const char *date)) +{ + DIR *dirp; + struct dirent *dp; + regex_t reg; + struct stat sbi, sbo; + FILE *in, *out; + char *outname; + char year[5], mon[3], day[3], hour[3], min[3], sec[3]; + char longdate[26]; + struct tm time; + time_t t; + + int nmatch = 7; + regmatch_t *pmatch = (regmatch_t*) calloc(nmatch, sizeof(regmatch_t)); + + if (regcomp(®, "^([0-9][0-9][0-9][0-9])_([0-9][0-9])_([0-9][0-9])" + "_([0-9][0-9])_([0-9][0-9])_([0-9][0-9])\\.raw$", REG_EXTENDED)) + assert(0); + + outname = malloc(strlen(dir) + 25); + dirp = opendir(dir); + while ((dp = readdir(dirp)) != NULL) { + if (regexec(®, dp->d_name, nmatch, pmatch, 0) == 0) { + if (stat(dp->d_name, &sbi)) + assert(0); + sprintf(outname, "%s/%s", dir, dp->d_name); + strcpy(outname + strlen(outname) - 4, ".cpi"); + if ((stat(outname, &sbo)) || (sbo.st_mtime < sbi.st_mtime)) { + strncpy(year, dp->d_name + pmatch[1].rm_so, 4); year[4] = '\0'; + strncpy(mon, dp->d_name + pmatch[2].rm_so, 2); mon[2] = '\0'; + strncpy(day, dp->d_name + pmatch[3].rm_so, 2); day[2] = '\0'; + strncpy(hour, dp->d_name + pmatch[4].rm_so, 2); hour[2] = '\0'; + strncpy(min, dp->d_name + pmatch[5].rm_so, 2); min[2] = '\0'; + strncpy(sec, dp->d_name + pmatch[6].rm_so, 2); sec[2] = '\0'; + memset(&time, 0, sizeof(time)); + time.tm_year = atoi(year) - 1900; + time.tm_mon = atoi(mon) - 1; + time.tm_mday = atoi(day); + time.tm_hour = atoi(hour); + time.tm_min = atoi(min); + time.tm_sec = atoi(sec); + time.tm_isdst = -1; + t = mktime(&time); + assert(t != -1); + ctime_r(&t, longdate); + longdate[24] = '\0'; /* get rid of newline */ + one_done_cb(longdate); + fflush(stderr); + in = fopen(dp->d_name, "r"); + assert(in); + out = fopen(outname, "w"); + assert(out); + one_file(in, out); + fclose(in); + fclose(out); + fprintf(stderr, "done.\n"); + } + } + } + closedir(dirp); + free(pmatch); +} + +void +combine_cpi_files(const char *dir, double *bests[], int *bestlen) +{ + DIR *dirp; + struct dirent *dp; + FILE *in; + char line[40]; + int lineno; + double mins; + int watts; + double *tmp; + int interval; + + *bestlen = 1000; + + *bests = calloc(*bestlen, sizeof(double)); + dirp = opendir(dir); + while ((dp = readdir(dirp)) != NULL) { + if (strcmp(".cpi", dp->d_name + dp->d_namlen - 4) == 0) { + in = fopen(dp->d_name, "r"); + assert(in); + lineno = 1; + while (fgets(line, sizeof(line), in) != NULL) { + if (sscanf(line, "%lf %d\n", &mins, &watts) != 2) { + fprintf(stderr, "Bad match on line %d: %s", lineno, line); + exit(1); + } + interval = secs_to_interval(mins * 60.0); + while (interval >= *bestlen) { + tmp = calloc(*bestlen * 2, sizeof(double)); + memcpy(tmp, *bests, *bestlen * sizeof(double)); + free(*bests); + *bests = tmp; + *bestlen *= 2; + } + if ((*bests)[interval] < watts) + (*bests)[interval] = watts; + ++lineno; + } + fclose(in); + } + } + closedir(dirp); +} + + diff --git a/cpint.h b/cpint.h new file mode 100644 index 000000000..330412f8e --- /dev/null +++ b/cpint.h @@ -0,0 +1,29 @@ +/* + * $Id: cpint.h,v 1.1 2006/08/11 19:53:07 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __cpint_h +#define __cpint_h 1 + +extern void update_cpi_files(const char *dir, + void (*one_done_cb)(const char *date)); +extern void combine_cpi_files(const char *dir, double *bests[], int *bestlen); + +#endif /* __cpint_h */ + diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 000000000..0514ec7bf --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,42 @@ + +CONTENT=$(wildcard *.content) +HTML=$(subst .content,.html,$(CONTENT)) +TARBALLS=$(wildcard gc_*.tgz) +OTHER=logo.png sample.gp sample.png cpint.gp cpint.png gui-preview.png + +all: $(HTML) +.PHONY: all clean install + +clean: + rm -f $(HTML) + +install: + rsync -avz -e ssh $(HTML) $(TARBALLS) $(OTHER) \ + srhea.net:public_html/goldencheetah/ + +# ftp -u ftp://srhea@ftp.goldencheetah.org/ $(HTML) $(TARBALLS) $(OTHER) + +contact.html: contact.content genpage.pl + ./genpage.pl "Contact Us" $< > $@ + +contrib.html: contrib.content genpage.pl + ./genpage.pl "Contributors" $< > $@ + +download.html: download.content genpage.pl + ./genpage.pl "Download" $< > $@ + +faq.html: faq.content genpage.pl + ./genpage.pl "Frequently Asked Questions" $< > $@ + +index.html: index.content genpage.pl + ./genpage.pl "Introduction" $< > $@ + +license.html: license.content genpage.pl + ./genpage.pl "License" $< > $@ + +search.html: search.content genpage.pl + ./genpage.pl "Search" $< > $@ + +users-guide.html: users-guide.content genpage.pl + ./genpage.pl "User's Guide" $< > $@ + diff --git a/doc/contact.content b/doc/contact.content new file mode 100644 index 000000000..6cd3d04f7 --- /dev/null +++ b/doc/contact.content @@ -0,0 +1,5 @@ + + +Please send all correspondence to +info@goldencheetah.org. + diff --git a/doc/contrib.content b/doc/contrib.content new file mode 100644 index 000000000..939e82fda --- /dev/null +++ b/doc/contrib.content @@ -0,0 +1,18 @@ + + +Sean Rhea bought a PowerTap Pro on April 20, 2006, and immediately set to +figuring out how to use it from his Mac Powerbook without using Virtual PC. +Within a week, he was able to download the raw data. Shortly thereafter, Russ +Cox asked what he was up to, and the two worked together to figure out the +packing format used. By May 4, they could reproduce the numbers given by the +PowerTap software except for minor discrepancies in the time values. David +Easter then pointed out how the checksum bytes in the download protocol were +used, and Sean Rhea coded up their combined discoveries into the two +utilities, ptdl and ptunpk. + +

+Rob Carlsen helped get the serial port version of the PowerTap Pro working +with the Keyspan USB-to-serial adaptor. Scott Overfield helped me figure out +that we should be using the /dev/cu.* devices instead of the +/dev/tty.* ones. + diff --git a/doc/cpint.gp b/doc/cpint.gp new file mode 100644 index 000000000..bb62814a9 --- /dev/null +++ b/doc/cpint.gp @@ -0,0 +1,12 @@ +set title "Critical Power Output" +set xlabel "Interval Duration (MM:SS)" +set ylabel "Average Power (Watts)" +set mxtics 5 +set mytics 2 +set yrange [0:] +set xrange [0.021:120] +set logscale x +set xtics ("0:01.26" 0.021, "0:05" 0.08333, "0:12" 0.2, "1:00" 1, "5:00" 5, \ + "12:00" 12, "30:00" 30, "60:00" 60, "120:00" 120) +plot 'cpint.out' noti with li lt 1 +pause -1 diff --git a/doc/cpint.png b/doc/cpint.png new file mode 100644 index 000000000..b65f28c19 Binary files /dev/null and b/doc/cpint.png differ diff --git a/doc/download.content b/doc/download.content new file mode 100644 index 000000000..6c42f173c --- /dev/null +++ b/doc/download.content @@ -0,0 +1,43 @@ + + +Right now we're only distributing tarballs of the Golden Cheetah source. + +

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
DateSourceDescription
Aug 11, 2006gc_2006-08-11.tgzptdl now works with Keyspan USB-to-serial adaptor, after +debugging help from Rob Carlsen. +
May 27, 2006gc_2006-05-27.tgzAdds the cpint program for computing critical +power intervals and the ptpk program for converting from +PowerTuned data files (see the User's +Guide).
May 16, 2006gc_2006-05-16.tgzThe first code release, containing ptdl and +ptunpk.
+
+ +

+We hope to have anonymous CVS access set up soon. + diff --git a/doc/faq.content b/doc/faq.content new file mode 100644 index 000000000..4a292ed8f --- /dev/null +++ b/doc/faq.content @@ -0,0 +1,62 @@ + + +

+Why does Golden Cheetah only include command-line utilities? Where's the +GUI? + +

+Because this project is only a hobby of ours, and we haven't had time to +finish it yet. That said, we are working on it. Here's a teaser screenshot +to keep you interested: + +

+GUI Screen Shot +
+ +

+Does the output of ptunpk exactly match that of the software +included with the PowerTap? + +

+Almost. If you run it in compatibility mode, using the -c +option, it matches the PowerTap software's output exactly on everything but +the time values, at least for the five sample rides we've tried it with. +The times are a little off, but not by more than 0.1%, so we don't consider it +a big deal. + +

+That said, the PowerTap software does some weird things, like converting from +kilometers to miles by multiplying by 0.62, but then reporting the miles +values with five digits after the decimal place. If you run +ptunpk without the -c option, it will unpack the +data in the way we think it should. The results mostly match up with the +official ones, and are almost certainly identical within the range of accuracy +of the device. + +

+I've downloaded and unpacked the data. Now what do I do with it? + +

+We highly recommend that you buy and read both Joe Friel's The +Cyclist's Training Bible and Allen and Coggan's Training and +Racing with a Power Meter. The former is the definitive book about all +aspects of cycling training (although we think he's off his rocker in the +chapter about diet), and the latter is the definitive book about power-based +training. + +

+Also, if you buy either of these books through the links below, we'll get a +referral fee in return, helping us to support this web site. Thanks! + +

+ + + +
+ diff --git a/doc/gc_2006-05-16.tgz b/doc/gc_2006-05-16.tgz new file mode 100644 index 000000000..bc2db38f0 Binary files /dev/null and b/doc/gc_2006-05-16.tgz differ diff --git a/doc/gc_2006-05-25.tgz b/doc/gc_2006-05-25.tgz new file mode 100644 index 000000000..eaecfae2c Binary files /dev/null and b/doc/gc_2006-05-25.tgz differ diff --git a/doc/gc_2006-05-27.tgz b/doc/gc_2006-05-27.tgz new file mode 100644 index 000000000..0d25ff19c Binary files /dev/null and b/doc/gc_2006-05-27.tgz differ diff --git a/doc/gc_2006-08-11.tgz b/doc/gc_2006-08-11.tgz new file mode 100644 index 000000000..f04302098 Binary files /dev/null and b/doc/gc_2006-08-11.tgz differ diff --git a/doc/genpage.pl b/doc/genpage.pl new file mode 100755 index 000000000..5ed0b9bc7 --- /dev/null +++ b/doc/genpage.pl @@ -0,0 +1,131 @@ +#!/usr/bin/perl -w +# +# Copyright (c) 2001-2003 Regents of the University of California. +# All rights reserved. +# +# See the file LICENSE included in this distribution for details. +# +# $Id: genpage.pl,v 1.3 2006/07/05 16:59:56 srhea Exp $ + +use strict; + +my $title = shift; +my $content_file = shift; +open (FILE, "$content_file") or die "Could not open $content_file"; + +print< + + + +Golden Cheetah: Power for the Masses + + + + + + + + + + + + + + + + +
+Picture of Cheetah + +

Introduction +
User's Guide +
FAQ +
License +
Download +
Contributors +
Search +
Mailing List + +

+ + + + +

+ + + + + + +
+ +

+

+Golden +Cheetah +
+ +Power for the Masses + +

+

+ +$title + +
+EOF + +my $match = "\\\$" . "Id:.* (\\d\\d\\d\\d\\/\\d\\d\\/\\d\\d " + . "\\d\\d:\\d\\d:\\d\\d) .*\\\$"; + +my $last_mod; +while () { + + if (m/$match/) { + $last_mod = $1; + } + print; +} +close (FILE); + +if (defined $last_mod) { + print "


Last modified $last_mod.\n"; +} + +print<
+
+ + + +EOF + diff --git a/doc/gui-preview.png b/doc/gui-preview.png new file mode 100644 index 000000000..79464f8e2 Binary files /dev/null and b/doc/gui-preview.png differ diff --git a/doc/index.content b/doc/index.content new file mode 100644 index 000000000..169e135a5 --- /dev/null +++ b/doc/index.content @@ -0,0 +1,26 @@ + + +

+The goal of the Golden Cheetah project is to develop a software package that: +

+ +

+In short, we believe that cyclists should be able to download their power data +to the computer of their choice, analyze it in whatever way they see fit, and +share their methods of analysis with others. + diff --git a/doc/license.content b/doc/license.content new file mode 100644 index 000000000..29469b01c --- /dev/null +++ b/doc/license.content @@ -0,0 +1,68 @@ + + +Golden Cheetah is licensed under the +GNU General Public License, +the preamble of which states: + +

+ The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + +

+

+ When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + +

+

+ To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + +

+

+ For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + +

+

+ We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + +

+

+ Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + +

+

+ Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + +

+ +For the full text of the license, please click the link above. + diff --git a/doc/logo.jpg b/doc/logo.jpg new file mode 100644 index 000000000..d5947b50c Binary files /dev/null and b/doc/logo.jpg differ diff --git a/doc/logo.png b/doc/logo.png new file mode 100644 index 000000000..0787910f6 Binary files /dev/null and b/doc/logo.png differ diff --git a/doc/sample.gp b/doc/sample.gp new file mode 100644 index 000000000..b46ef782b --- /dev/null +++ b/doc/sample.gp @@ -0,0 +1,8 @@ +set title "May 3, 2006 16:24:04" +set xlabel "Ride Time (minutes)" +plot \ + "< awk '/^[^#]/ {print $1, $4}' 2006_05_03_16_24_04.dat | ./smooth.pl 10" ti 'Watts' wi li, \ + "< awk '/^[^#]/ {print $1, $7}' 2006_05_03_16_24_04.dat | ./smooth.pl 10" ti 'Heartrate' wi li, \ + "< awk '/^[^#]/ {print $1, $6}' 2006_05_03_16_24_04.dat | ./smooth.pl 10" ti 'Cadence' wi li, \ + "< awk '/^[^#]/ {print $1, $3}' 2006_05_03_16_24_04.dat | ./smooth.pl 10" ti 'MPH' wi li +pause -1 diff --git a/doc/sample.png b/doc/sample.png new file mode 100644 index 000000000..98209d69b Binary files /dev/null and b/doc/sample.png differ diff --git a/doc/search.content b/doc/search.content new file mode 100644 index 000000000..d9222a2f9 --- /dev/null +++ b/doc/search.content @@ -0,0 +1,42 @@ + + +
+ +
+ + + +
+ +Google +
+ + + +
+ + + + + +
+ +Web + + +goldencheetah.org +
+ + + + + + + +
+
+ +
+ diff --git a/doc/users-guide.content b/doc/users-guide.content new file mode 100644 index 000000000..dab33ad4f --- /dev/null +++ b/doc/users-guide.content @@ -0,0 +1,222 @@ + +Currently, Golden Cheetah consists of several command line utilities: +ptdl, which downloads ride data from a PowerTap Pro version 2.21 +cycling computer, ptunpk, which unpacks the raw bytes downloaded +by ptdl and outputs more human-friendly ride information, and +cpint, which computes your critical power (see below). All three +are written in simple C code but have only been tested on Mac OS X so far. +We've also written several Perl scripts to help you graph and summarize the +data. + +

+ +Extracting the Data + +

+To use ptdl, you'll first need to install +the drivers for the +FTDI chip the PowerTap Pro USB Downloader uses. Once these are installed, you +should be able to just run ptdl without arguments: + +

+$ ./ptdl 
+Reading from /dev/tty.usbserial-3B1.
+Reading version information...done.
+Reading ride time...done.
+Writing to 2006_05_15_11_34_03.raw.
+Reading ride data..............done.
+$ head -5 2006_05_15_11_34_03.raw
+57 56 55 64 02 15
+60 06 05 0f 6b 22
+40 08 30 00 00 00
+86 0e 74 99 00 55
+81 06 77 a8 40 55
+
+ +

+If everything goes well, ptdl will automatically detect the +device (/dev/tty.usbserial-3B1 in the example above), read the +ride data from it, and write to a file named by the date and time at which the +ride started (2006_05_15_11_34_03.raw in the example; the format +is YYYY_MM_DD_hh_mm_ss.raw). + +

+ +Unpacking the Data + +

As shown by the head command above, the data in this +.raw file is just the raw bytes that represent your ride. To +unpack those bytes and display them in a more human-friendly format, use +ptunpk: + +

+$ ./ptunpk 2006_05_15_11_34_03.raw
+$ head -5 2006_05_15_11_34_03.dat 
+# Time Torq MPH Watts Miles Cad HR Int
+# 2006/5/15 11:34:03 1147707243
+# wheel size=2096 mm, interval=0, rec int=1
+0.021 13.1 2.450 43 0.00781 0 85 0
+0.042 13.4 5.374 97 0.00912 64 85 0
+
+ +ptunpk takes a .raw file for input and writes a +.dat file as output. Lines that start with an ampersand ("#") in +this file are comments; the other lines represent measured samples. As shown +by the first comment in the file, the columns are: time in minutes, torque in +Newton-meters, speed in miles per hour, power in watts, distance in miles, +cadence, heart rate, and interval number. + +

+ +Summarizing the Data + +

+We hope to have a graphical interface to these programs soon, but until then, +the only summarization tools we have are command-line programs. The script +intervals.pl summarizes the intervals performed in a workout: + + +

+$ ./intervals.pl 2006_05_03_16_24_04.dat 
+                        Power           Heart Rate      Cadence         Speed
+Int      Dur    Dist     Avg     Max    Avg     Max     Avg     Max     Avg     Max
+ 0      77:10    19.3    213     693    134     167      82     141     16.0    27.8
+ 1       4:03     0.9    433     728    175     203      84     122     13.0    18.8
+ 2       7:23     1.0     86     502    135     179      71     141     16.0    28.2
+ 3       4:27     0.9    390     628    170     181      70     100     12.0    17.6
+ 4       8:04     0.9     60     203    130     178      50     120     18.0    30.1
+ 5       4:30     0.9    384     682    170     179      79     113     11.0    18.6
+ 6       8:51     1.1     53     245    125     176      70     141     8.0     26.6
+ 7       2:48     0.4    400     614    164     178      62      91     8.0     13.6
+ 8       7:01     1.1     46     268    128     170      71     141     12.0    28.8
+ 9       4:30     0.9    379     560    168     180      81     170     11.0    18.3
+10      28:46     6.5    120     409    128     179      79     141     15.0    31.0
+
+ + +

+In the example above, a rider performed five hill intervals, four of which +climbed a medium size hill that took about 4-5 minutes to climb (intervals +1, 3, 5, and 9), and one on a shorter hill that took just under 3 minutes to +climb (interval 7). + +

+ +Graphing the Data + +

+For graphing the data in the ride, we use smooth.pl and the +gnuplot program. You can use sample.gp +to graph the power, heart rate, cadence, and speed for the hill workout above: + +

+$ gnuplot sample.gp
+
+ +Sample Plot + +

+ +Finding Your "Critical Power" + +

+Joe Friel calls the maximum average power a rider can sustain over an interval +the rider's "critical power" for that duration. The cpint +program automatically computes your critical power over all interval lengths +using the data from all your past rides. This program looks at all the +.raw files in a directory, calculating your maximum power over +every subinterval length and storing them in a corresponding .cpi +file. It then combines the data in all of the .cpi files to find +your critical power over all subintervals of all your rides. + +

+$ ls *.raw
+2006_04_28_10_48_33.raw 2006_05_10_17_08_30.raw 2006_05_18_16_32_53.raw
+2006_05_03_16_24_04.raw 2006_05_13_10_29_12.raw 2006_05_21_12_25_07.raw
+2006_05_05_10_52_05.raw 2006_05_15_11_34_03.raw 2006_05_22_18_28_47.raw
+...
+2006_05_09_09_54_29.raw 2006_05_17_16_44_35.raw
+$ ./cpint 
+Compiling data for ride on Fri Apr 28 10:48:33 2006...done.
+Compiling data for ride on Sat Apr 29 10:07:48 2006...done.
+Compiling data for ride on Sun Apr 30 14:00:17 2006...done.
+ ...
+Compiling data for ride on Mon May 22 18:28:47 2006...done.
+ 0.021 1264
+ 0.042 1221
+ 0.063 1216
+ ...
+ 5.019 391
+ ...
+171.885 163
+
+ +

+Over this set of rides, the rider's maximum power is 1264 watts, achieved over +an interval of 0.021 minutes (1.26 seconds). Over all five-minute +subintervals, he has achieved a maximum average power of 391 watts. The +longest ride in this set was 171.885 minutes long, and he averaged 163 watts +over it. + +

+We can graph the output of cpint using gnuplot with +cpint.gp: + +

+$ ./cpint > cpint.out
+$ gnuplot cpint.gp
+
+ + + +

+The first time you run cpint it will take a while, as it has to +analyze all your past rides. On subsequent runs, however, it will only +analyze new files. + +

Training and Racing with a Power Meter (see the FAQ) contains a table of critical powers of Cat 5 cyclists +up through international pros at interval lengths of 5 seconds, 1 minute, 5 +minutes, and 60 minutes. Using this table and the cpint program, +you can determine whether you're stronger than others in your racing category +at each interval length and adapt your training program accordingly. + +

+ +Converting Old Data + + +

+If you've used the PowerTuned software that comes with the PowerTap you may +have lots of old ride data in that program that you'd like to include in your +critical power graph. You can convert the .xml files that +PowerTuned produces to .raw files using the ptpk +program: + +

+

+$ ./ptpk 2006_04_27_00_23_28.xml 
+$ head -5 2006_04_27_00_23_28.raw 
+57 56 55 64 02 15
+60 06 04 7b 80 17
+40 08 30 00 00 00
+84 04 00 24 00 ff
+83 03 00 d7 00 ff
+
+ +

+ptpk assumes the input .xml file was generated with +a wheel size of 2,096 mm and a recording interval of 1. If this is not the +case, you should specify the correct values with the -w and +-r options. + +

+Note that the PowerTuned software computes the output speed in miles per hour +by multiplying the measured speed in kilometers per hour by 0.62, and the +miles per hour values in a .xml file are thus only accurate to +two significant figures, even though they're printed out to three decimal +places. Because of this limitation, the sequence ptpk, +ptunpk is not quite the identity function; in particular, the +wattage values from ptpk may only be accurate to two significant +digits. + diff --git a/gui/AllPlot.cpp b/gui/AllPlot.cpp new file mode 100644 index 000000000..ddb3ce539 --- /dev/null +++ b/gui/AllPlot.cpp @@ -0,0 +1,176 @@ +/* + * $Id: AllPlot.cpp,v 1.2 2006/07/12 02:13:57 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "AllPlot.h" +#include "RawFile.h" +#include "Settings.h" + +#include +#include +#include +#include + +static const inline double +max(double a, double b) { if (a > b) return a; else return b; } + +AllPlot::AllPlot() : + hrArray(NULL), wattsArray(NULL), timeArray(NULL), smooth(30) +{ + insertLegend(new QwtLegend(), QwtPlot::BottomLegend); + setCanvasBackground(Qt::white); + + setAxisTitle(xBottom, "Time (minutes)"); + setAxisTitle(yLeft, "Power / HR"); + + wattsCurve = new QwtPlotCurve("Power"); + wattsCurve->setRenderHint(QwtPlotItem::RenderAntialiased); + wattsCurve->setPen(QPen(Qt::red)); + wattsCurve->attach(this); + + hrCurve = new QwtPlotCurve("Heart Rate"); + hrCurve->setRenderHint(QwtPlotItem::RenderAntialiased); + hrCurve->setPen(QPen(Qt::blue)); + hrCurve->attach(this); + + grid = new QwtPlotGrid(); + grid->enableX(false); + QPen gridPen; + gridPen.setStyle(Qt::DotLine); + grid->setPen(gridPen); + grid->attach(this); +} + +struct DataPoint { + double time, hr, watts; + DataPoint(double t, double h, double w) : time(t), hr(h), watts(w) {} +}; + +void +AllPlot::recalc() +{ + int rideTimeSecs = (int) ceil(timeArray[arrayLength - 1]); + double ymax = 0.0; + double totalWatts = 0.0; + double totalHr = 0.0; + QList list; + int i = 0; + double *smoothWatts = new double[rideTimeSecs + 1]; + double *smoothHr = new double[rideTimeSecs + 1]; + double *smoothTime = new double[rideTimeSecs + 1]; + for (int secs = 0; secs < smooth; ++secs) { + smoothWatts[secs] = 0.0; + smoothHr[secs] = 0.0; + smoothTime[secs] = secs / 60.0; + } + for (int secs = smooth; secs <= rideTimeSecs; ++secs) { + while ((i < arrayLength) && (timeArray[i] <= secs)) { + DataPoint *dp = + new DataPoint(timeArray[i], hrArray[i], wattsArray[i]); + totalWatts += wattsArray[i]; + totalHr += hrArray[i]; + list.append(dp); + ++i; + } + while (!list.empty() && (list.front()->time < secs - smooth)) { + DataPoint *dp = list.front(); + list.removeFirst(); + totalWatts -= dp->watts; + totalHr -= dp->hr; + delete dp; + } + // TODO: this is wrong. We should do a weighted average over the + // seconds represented by each point... + if (list.empty()) { + smoothWatts[secs] = 0.0; + smoothHr[secs] = 0.0; + } + else { + smoothWatts[secs] = totalWatts / list.size(); + if (smoothWatts[secs] > ymax) + ymax = smoothWatts[secs]; + smoothHr[secs] = totalHr / list.size(); + if (smoothHr[secs] > ymax) + ymax = smoothHr[secs]; + } + smoothTime[secs] = secs / 60.0; + } + wattsCurve->setData(smoothTime, smoothWatts, rideTimeSecs + 1); + hrCurve->setData(smoothTime, smoothHr, rideTimeSecs + 1); + setAxisScale(xBottom, 0.0, smoothTime[rideTimeSecs]); + setAxisScale(yLeft, 0.0, ymax + 30); + replot(); + delete [] smoothWatts; + delete [] smoothHr; + delete [] smoothTime; +} + +void +AllPlot::setData(RawFile *raw) +{ + delete [] hrArray; + delete [] wattsArray; + delete [] timeArray; + setTitle(raw->startTime.toString(GC_DATETIME_FORMAT)); + hrArray = new double[raw->points.size()]; + wattsArray = new double[raw->points.size()]; + timeArray = new double[raw->points.size()]; + arrayLength = 0; + QListIterator i(raw->points); + while (i.hasNext()) { + RawFilePoint *point = i.next(); + timeArray[arrayLength] = point->secs; + hrArray[arrayLength] = max(0, point->hr); + wattsArray[arrayLength] = max(0, point->watts); + ++arrayLength; + } + recalc(); +} + +void +AllPlot::showPower(int state) +{ + assert(state != Qt::PartiallyChecked); + wattsCurve->setVisible(state == Qt::Checked); + replot(); +} + +void +AllPlot::showHr(int state) +{ + assert(state != Qt::PartiallyChecked); + hrCurve->setVisible(state == Qt::Checked); + replot(); +} + +void +AllPlot::showGrid(int state) +{ + assert(state != Qt::PartiallyChecked); + grid->setVisible(state == Qt::Checked); + replot(); +} + +void +AllPlot::setSmoothing(int value) +{ + smooth = value; + recalc(); +} + diff --git a/gui/AllPlot.h b/gui/AllPlot.h new file mode 100644 index 000000000..831eb08e3 --- /dev/null +++ b/gui/AllPlot.h @@ -0,0 +1,67 @@ +/* + * $Id: AllPlot.h,v 1.2 2006/07/12 02:13:57 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _GC_AllPlot_h +#define _GC_AllPlot_h 1 + +#include + +class QwtPlotCurve; +class QwtPlotGrid; +class RawFile; + +class AllPlot : public QwtPlot +{ + Q_OBJECT + + public: + + QwtPlotCurve *wattsCurve; + QwtPlotCurve *hrCurve; + + AllPlot(); + + int smoothing() const { return smooth; } + + void setData(RawFile *raw); + + public slots: + + void showPower(int state); + void showHr(int state); + void showGrid(int state); + void setSmoothing(int value); + + protected: + + QwtPlotGrid *grid; + + double *hrArray; + double *wattsArray; + double *timeArray; + int arrayLength; + + int smooth; + + void recalc(); +}; + +#endif // _GC_AllPlot_h + diff --git a/gui/ChooseCyclistDialog.cpp b/gui/ChooseCyclistDialog.cpp new file mode 100644 index 000000000..194d32e1d --- /dev/null +++ b/gui/ChooseCyclistDialog.cpp @@ -0,0 +1,112 @@ +/* + * $Id: ChooseCyclistDialog.cpp,v 1.3 2006/07/04 12:55:40 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "ChooseCyclistDialog.h" +#include + +ChooseCyclistDialog::ChooseCyclistDialog(const QDir &home, bool allowNew) : + home(home) +{ + setWindowTitle("Choose a Cyclist"); + + listWidget = new QListWidget(this); + listWidget->setSelectionMode(QAbstractItemView::SingleSelection); + + QStringListIterator i(home.entryList(QDir::Dirs | QDir::NoDotAndDotDot)); + while (i.hasNext()) { + QString name = i.next(); + new QListWidgetItem(name, listWidget); + } + + if (allowNew) + newButton = new QPushButton(tr("&New..."), this); + okButton = new QPushButton(tr("&Open"), this); + cancelButton = new QPushButton(tr("&Cancel"), this); + + okButton->setEnabled(false); + + connect(okButton, SIGNAL(clicked()), this, SLOT(accept())); + if (allowNew) + connect(newButton, SIGNAL(clicked()), this, SLOT(newClicked())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked())); + connect(listWidget, + SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), + this, SLOT(enableOk(QListWidgetItem*))); + connect(listWidget, SIGNAL(itemDoubleClicked(QListWidgetItem*)), + this, SLOT(accept())); + + QHBoxLayout *buttonLayout = new QHBoxLayout; + buttonLayout->addWidget(okButton); + if (allowNew) + buttonLayout->addWidget(newButton); + buttonLayout->addWidget(cancelButton); + + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->addWidget(listWidget); + mainLayout->addLayout(buttonLayout); +} + +QString +ChooseCyclistDialog::choice() +{ + return listWidget->currentItem()->text(); +} + +void +ChooseCyclistDialog::enableOk(QListWidgetItem *item) +{ + okButton->setEnabled(item != NULL); +} + +void +ChooseCyclistDialog::cancelClicked() +{ + reject(); +} + +QString +ChooseCyclistDialog::newCyclistDialog(QDir &homeDir, QWidget *parent) +{ + QDir home(homeDir); + bool ok; + QString name = QInputDialog::getText(parent, tr("Create New Cyclist"), + tr("Enter New Cyclist's Name"), + QLineEdit::Normal, "", &ok); + if (ok && !name.isEmpty()) { + fprintf(stderr, "New name: %s\n", name.toAscii().data()); + if (!home.exists(name)) { + if (home.mkdir(name)) + return name; + QMessageBox::critical(0, tr("Fatal Error"), + tr("Can't create new directory ") + + home.path() + "/" + name, "OK"); + } + } + return QString(); +} + +void +ChooseCyclistDialog::newClicked() +{ + QString name = newCyclistDialog(home, this); + if (!name.isEmpty()) + new QListWidgetItem(name, listWidget); +} + diff --git a/gui/ChooseCyclistDialog.h b/gui/ChooseCyclistDialog.h new file mode 100644 index 000000000..900c0c1e9 --- /dev/null +++ b/gui/ChooseCyclistDialog.h @@ -0,0 +1,53 @@ +/* + * $Id: ChooseCyclistDialog.h,v 1.3 2006/07/04 12:55:40 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _GC_ChooseCyclistDialog_h +#define _GC_ChooseCyclistDialog_h 1 + +#include +#include + +class QListWidget; +class QListWidgetItem; +class QPushButton; + +class ChooseCyclistDialog : public QDialog +{ + Q_OBJECT + + public: + ChooseCyclistDialog(const QDir &home, bool allowNew); + QString choice(); + static QString newCyclistDialog(QDir &homeDir, QWidget *parent); + + private slots: + void newClicked(); + void cancelClicked(); + void enableOk(QListWidgetItem *item); + + private: + + QDir home; + QListWidget *listWidget; + QPushButton *okButton, *newButton, *cancelButton; +}; + +#endif // _GC_ChooseCyclistDialog_h + diff --git a/gui/DownloadRideDialog.cpp b/gui/DownloadRideDialog.cpp new file mode 100644 index 000000000..96640e131 --- /dev/null +++ b/gui/DownloadRideDialog.cpp @@ -0,0 +1,312 @@ +/* + * $Id: DownloadRideDialog.cpp,v 1.4 2006/08/11 20:02:13 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "DownloadRideDialog.h" +#include "MainWindow.h" +#include +#include +#include + +#define MAX_DEVICES 10 + +DownloadRideDialog::DownloadRideDialog(MainWindow *mainWindow, + const QDir &home) : + mainWindow(mainWindow), home(home), fd(-1), out(NULL), device(NULL), + notifier(NULL), timer(NULL), blockCount(0) +{ + setAttribute(Qt::WA_DeleteOnClose); + setWindowTitle("Download Ride Data"); + + listWidget = new QListWidget(this); + listWidget->setSelectionMode(QAbstractItemView::SingleSelection); + + QLabel *availLabel = new QLabel(tr("Available devices:"), this); + QLabel *instructLabel = new QLabel(tr("Instructions:"), this); + label = new QLabel(this); + label->setIndent(10); + + downloadButton = new QPushButton(tr("&Download"), this); + rescanButton = new QPushButton(tr("&Rescan"), this); + cancelButton = new QPushButton(tr("&Cancel"), this); + + connect(downloadButton, SIGNAL(clicked()), this, SLOT(downloadClicked())); + connect(rescanButton, SIGNAL(clicked()), this, SLOT(scanDevices())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked())); + connect(listWidget, + SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), + this, SLOT(setReadyInstruct())); + connect(listWidget, SIGNAL(itemDoubleClicked(QListWidgetItem*)), + this, SLOT(downloadClicked())); + + QHBoxLayout *buttonLayout = new QHBoxLayout; + buttonLayout->addWidget(downloadButton); + buttonLayout->addWidget(rescanButton); + buttonLayout->addWidget(cancelButton); + + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->addWidget(availLabel); + mainLayout->addWidget(listWidget); + mainLayout->addWidget(instructLabel); + mainLayout->addWidget(label); + mainLayout->addLayout(buttonLayout); + + scanDevices(); +} + +DownloadRideDialog::~DownloadRideDialog() +{ + if (device) + free(device); + if (fd >= 0) + ::close(fd); + if (out) + fclose(out); +} + +void +DownloadRideDialog::setReadyInstruct() +{ + label->setText(tr("Make sure the PowerTap unit is turned on,\n" + "and that the screen display says, \"Host\",\n" + "then click Download to begin downloading.")); +} + +void +DownloadRideDialog::scanDevices() +{ + listWidget->clear(); + char *devices[MAX_DEVICES]; + int devcnt = pt_find_device(devices, MAX_DEVICES); + for (int i = 0; i < devcnt; ++i) { + new QListWidgetItem(devices[i], listWidget); + free(devices[i]); + } + if (listWidget->count() == 1) { + listWidget->setCurrentRow(0); + setReadyInstruct(); + downloadButton->setEnabled(true); + downloadButton->setFocus(); + } + else { + downloadButton->setEnabled(false); + if (listWidget->count() > 1) { + label->setText(tr("Select the device from the above list from\n" + "which you would like to download a ride.")); + } + else { + label->setText(tr("No devices found. Make sure the PowerTap\n" + "unit is plugged into the computer's USB port,\n" + "then click \"Rescan\" to check again.")); + } + } +} + +static void +time_cb(struct tm *time, void *self) +{ + ((DownloadRideDialog*) self)->time_cb(time); +} + +static void +record_cb(unsigned char *buf, void *self) +{ + ((DownloadRideDialog*) self)->record_cb(buf); +} + +void +DownloadRideDialog::time_cb(struct tm *time) +{ + timer->stop(); + if (!out) { + if (!time) { + QMessageBox::critical(this, tr("Read error"), + tr("Can't find ride time")); + reject(); + } + sprintf(outname, "%04d_%02d_%02d_%02d_%02d_%02d.raw", + time->tm_year + 1900, time->tm_mon + 1, time->tm_mday, + time->tm_hour, time->tm_min, time->tm_sec); + assert(strlen(outname) == sizeof(outname) - 1); + label->setText(label->text() + tr("done.\nWriting to ") + + outname + "."); + std::string path = home.absolutePath().toStdString() + "/" + outname; + if ((out = fopen(path.c_str(), "r")) != NULL) { + if (QMessageBox::warning(this, + tr("Ride Already Downloaded"), + tr("This ride appears to have already ") + + tr("been downloaded. Do you want to ") + + tr("download it again and overwrite ") + + tr("the previous download?"), + tr("&Overwrite"), tr("&Cancel"), + QString(), 1, 1) == 1) { + reject(); + } + } + if ((out = fopen(path.c_str(), "w")) == NULL) { + QMessageBox::critical(this, tr("Write error"), + tr("Can't open ") + path.c_str() + + tr(" for writing: ") + strerror(errno)); + reject(); + } + label->setText(label->text() + tr("\nReading ride data...")); + } + timer->start(5000); +} + +void +DownloadRideDialog::record_cb(unsigned char *buf) +{ + timer->stop(); + if (!out) { + QMessageBox::critical(this, tr("Read error"), + tr("Can't find ride time.")); + reject(); + } + for (int i = 0; i < 6; ++i) + fprintf(out, "%02x%s", buf[i], (i == 5) ? "\n" : " "); + if ((++blockCount % 256) == 0) { + label->setText(label->text() + "."); + } + timer->start(5000); +} + +void +DownloadRideDialog::readVersion() +{ + if (notifier) + notifier->setEnabled(false); + int r = pt_read_version(&vstate, fd, hwecho); + if (r == PT_DONE) { + if (notifier) { + delete notifier; + notifier = NULL; + } + if (timer) { + delete timer; + timer = NULL; + } + label->setText(label->text() + tr("done.")); + ::close(fd); + fd = open(device, O_RDWR | O_NOCTTY); + if (fd < 0) { + QMessageBox::critical(this, tr("Read error"), + tr("Could not open device, ") + device + + + ": " + strerror(errno)); + reject(); + } + pt_make_async(fd); + label->setText(label->text() + tr("\nReading ride time...")); + memset(&dstate, 0, sizeof(dstate)); + readData(); + } + else { + assert(r == PT_NEED_READ); + if (notifier) + notifier->setEnabled(true); + else { + notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); + connect(notifier, SIGNAL(activated(int)), this, SLOT(readVersion())); + } + if (!timer) { + timer = new QTimer(this); + connect(timer, SIGNAL(timeout()), this, SLOT(versionTimeout())); + timer->start(5000); + } + } +} + +void +DownloadRideDialog::versionTimeout() +{ + timer->stop(); + label->setText(label->text() + tr("timeout.")); + QMessageBox::critical(this, tr("Read Error"), + tr("Timed out reading device ") + device + + tr(". Check that the device is plugged in and ") + + tr("try again.")); + reject(); +} + +void +DownloadRideDialog::readData() +{ + if (notifier) + notifier->setEnabled(false); + int r = pt_read_data(&dstate, fd, hwecho, ::time_cb, ::record_cb, this); + if (r == PT_DONE) { + if (notifier) { + delete notifier; + notifier = NULL; + } + if (timer) { + delete timer; + timer = NULL; + } + label->setText(label->text() + tr("done.")); + QMessageBox::information(this, tr("Success"), tr("Download complete.")); + fclose(out); + out = NULL; + mainWindow->addRide(outname); + accept(); + } + else { + assert(r == PT_NEED_READ); + if (notifier) + notifier->setEnabled(true); + else { + notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); + connect(notifier, SIGNAL(activated(int)), this, SLOT(readData())); + } + if (!timer) { + timer = new QTimer(this); + connect(timer, SIGNAL(timeout()), this, SLOT(versionTimeout())); + timer->start(5000); + } + } +} + +void +DownloadRideDialog::downloadClicked() +{ + downloadButton->setEnabled(false); + rescanButton->setEnabled(false); + if (device) + free(device); + device = strdup(listWidget->currentItem()->text().toAscii().data()); + hwecho = pt_hwecho(device); + fd = open(device, O_RDWR | O_NOCTTY); + if (fd < 0) { + QMessageBox::critical(this, tr("Read error"), + tr("Could not open device, ") + device + + + ": " + strerror(errno)); + reject(); + } + pt_make_async(fd); + label->setText(tr("Reading version information...")); + memset(&vstate, 0, sizeof(vstate)); + readVersion(); +} + +void +DownloadRideDialog::cancelClicked() +{ + reject(); +} + diff --git a/gui/DownloadRideDialog.h b/gui/DownloadRideDialog.h new file mode 100644 index 000000000..3b8d892a5 --- /dev/null +++ b/gui/DownloadRideDialog.h @@ -0,0 +1,72 @@ +/* + * $Id: DownloadRideDialog.h,v 1.4 2006/08/11 20:02:13 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _GC_DownloadRideDialog_h +#define _GC_DownloadRideDialog_h 1 + +#include +extern "C" { +#include "../pt.h" +} + +class MainWindow; + +class DownloadRideDialog : public QDialog +{ + Q_OBJECT + + public: + DownloadRideDialog(MainWindow *mainWindow, const QDir &home); + ~DownloadRideDialog(); + + void time_cb(struct tm *time); + void record_cb(unsigned char *buf); + + private slots: + void downloadClicked(); + void cancelClicked(); + void setReadyInstruct(); + void scanDevices(); + void readVersion(); + void readData(); + void versionTimeout(); + + private: + + MainWindow *mainWindow; + QDir home; + QListWidget *listWidget; + QPushButton *downloadButton, *rescanButton, *cancelButton; + QLabel *label; + int fd; + FILE *out; + char outname[24]; + + char *device; + struct pt_read_version_state vstate; + struct pt_read_data_state dstate; + QSocketNotifier *notifier; + QTimer *timer; + int blockCount; + int hwecho; +}; + +#endif // _GC_DownloadRideDialog_h + diff --git a/gui/GoldenCheetah.pro b/gui/GoldenCheetah.pro new file mode 100644 index 000000000..408976efc --- /dev/null +++ b/gui/GoldenCheetah.pro @@ -0,0 +1,30 @@ +###################################################################### +# Automatically generated by qmake (2.00a) Sun May 28 19:43:48 2006 +###################################################################### + +TEMPLATE = app +TARGET += +DEPENDPATH += . +INCLUDEPATH += ../qwt-20060130/include +CONFIG += static +OBJECTS += ../pt.o +LIBS += /usr/local/lib/libqwt.a +LIBS += -lz -framework Carbon + +# Input +HEADERS += \ + AllPlot.h \ + ChooseCyclistDialog.h \ + DownloadRideDialog.h \ + MainWindow.h \ + RawFile.h \ + RideItem.h +SOURCES += \ + AllPlot.cpp \ + ChooseCyclistDialog.cpp \ + DownloadRideDialog.cpp \ + MainWindow.cpp \ + RawFile.cpp \ + RideItem.cpp \ + \ + main.cpp diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp new file mode 100644 index 000000000..0affa5e92 --- /dev/null +++ b/gui/MainWindow.cpp @@ -0,0 +1,279 @@ +/* + * $Id: MainWindow.cpp,v 1.7 2006/07/12 02:13:57 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "MainWindow.h" +#include "AllPlot.h" +#include "ChooseCyclistDialog.h" +#include "DownloadRideDialog.h" +#include "RawFile.h" +#include "RideItem.h" +#include "Settings.h" +#include +#include +#include + +#define FOLDER_TYPE 0 +#define RIDE_TYPE 1 + +static char *rideFileRegExp = ("^(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)" + "_(\\d\\d)_(\\d\\d)_(\\d\\d)\\.raw$"); + +MainWindow::MainWindow(const QDir &home) : + home(home), settings(GC_SETTINGS_CO, GC_SETTINGS_APP) +{ + setWindowTitle(home.dirName()); + settings.setValue(GC_SETTINGS_LAST, home.dirName()); + + QVariant geom = settings.value(GC_SETTINGS_MAIN_GEOM); + if (geom == QVariant()) + resize(640, 480); + else + setGeometry(geom.toRect()); + + splitter = new QSplitter(this); + setCentralWidget(splitter); + + treeWidget = new QTreeWidget; + treeWidget->setColumnCount(3); + treeWidget->setSelectionMode(QAbstractItemView::SingleSelection); + // TODO: + // treeWidget->header()->resizeSection(0,80); + treeWidget->header()->hide(); + + allRides = new QTreeWidgetItem(treeWidget, FOLDER_TYPE); + allRides->setText(0, tr("All Rides")); + treeWidget->expandItem(allRides); + splitter->addWidget(treeWidget); + + QRegExp rx(rideFileRegExp); + QStringList filters; + filters << "*.raw"; + QTreeWidgetItem *last = NULL; + QStringListIterator i(home.entryList(filters, QDir::Files)); + while (i.hasNext()) { + QString name = i.next(); + if (rx.exactMatch(name)) { + assert(rx.numCaptures() == 6); + QDate date(rx.cap(1).toInt(), rx.cap(2).toInt(),rx.cap(3).toInt()); + QTime time(rx.cap(4).toInt(), rx.cap(5).toInt(),rx.cap(6).toInt()); + QDateTime dt(date, time); + last = new RideItem(allRides, RIDE_TYPE, home.path(), name, dt); + } + } + + tabWidget = new QTabWidget; + rideSummary = new QTextEdit; + rideSummary->setReadOnly(true); + tabWidget->addTab(rideSummary, "Ride Summary"); + + QWidget *window = new QWidget; + QVBoxLayout *vlayout = new QVBoxLayout; + + QHBoxLayout *showLayout = new QHBoxLayout; + QLabel *showLabel = new QLabel("Show:", this); + showLayout->addWidget(showLabel); + + QCheckBox *showGrid = new QCheckBox("Grid", this); + showGrid->setCheckState(Qt::Checked); + showLayout->addWidget(showGrid); + + QCheckBox *showPower = new QCheckBox("Power", this); + showPower->setCheckState(Qt::Checked); + showLayout->addWidget(showPower); + + QCheckBox *showHr = new QCheckBox("Heart Rate", this); + showHr->setCheckState(Qt::Checked); + showLayout->addWidget(showHr); + + QHBoxLayout *smoothLayout = new QHBoxLayout; + QLabel *smoothLabel = new QLabel(tr("Smoothing (secs)"), this); + smoothLineEdit = new QLineEdit(this); + smoothLineEdit->setFixedWidth(30); + + smoothLayout->addWidget(smoothLabel); + smoothLayout->addWidget(smoothLineEdit); + smoothSlider = new QSlider(Qt::Horizontal); + smoothSlider->setTickPosition(QSlider::TicksBelow); + smoothSlider->setTickInterval(1); + smoothSlider->setMinimum(2); + smoothSlider->setMaximum(60); + smoothLineEdit->setValidator(new QIntValidator(smoothSlider->minimum(), + smoothSlider->maximum(), + smoothLineEdit)); + smoothLayout->addWidget(smoothSlider); + allPlot = new AllPlot; + smoothSlider->setValue(allPlot->smoothing()); + smoothLineEdit->setText(QString("%1").arg(allPlot->smoothing())); + vlayout->addWidget(allPlot); + vlayout->addLayout(showLayout); + vlayout->addLayout(smoothLayout); + window->setLayout(vlayout); + window->show(); + + tabWidget->addTab(window, "All-in-One Graph"); + splitter->addWidget(tabWidget); + + QVariant splitterSizes = settings.value(GC_SETTINGS_SPLITTER_SIZES); + if (splitterSizes != QVariant()) + splitter->restoreState(splitterSizes.toByteArray()); + else { + QList sizes; + sizes.append(250); + sizes.append(390); + splitter->setSizes(sizes); + } + + connect(treeWidget, SIGNAL(itemSelectionChanged()), + this, SLOT(rideSelected())); + connect(splitter, SIGNAL(splitterMoved(int,int)), + this, SLOT(splitterMoved())); + connect(showPower, SIGNAL(stateChanged(int)), + allPlot, SLOT(showPower(int))); + connect(showHr, SIGNAL(stateChanged(int)), + allPlot, SLOT(showHr(int))); + connect(showGrid, SIGNAL(stateChanged(int)), + allPlot, SLOT(showGrid(int))); + connect(smoothSlider, SIGNAL(valueChanged(int)), + this, SLOT(setSmoothingFromSlider())); + connect(smoothLineEdit, SIGNAL(returnPressed()), + this, SLOT(setSmoothingFromLineEdit())); + + QMenu *fileMenu = new QMenu(tr("&File"), this); + fileMenu->addAction(tr("&New..."), this, + SLOT(newCyclist()), tr("Ctrl+N")); + fileMenu->addAction(tr("&Open..."), this, + SLOT(openCyclist()), tr("Ctrl+O")); + fileMenu->addAction(tr("&Download ride..."), this, + SLOT(downloadRide()), tr("Ctrl+D")); + + QMenuBar *menuBar = new QMenuBar(this); + menuBar->addMenu(fileMenu); + + if (last != NULL) { + treeWidget->setCurrentItem(last); + rideSelected(); + } +} + +void +MainWindow::addRide(QString name) +{ + QRegExp rx(rideFileRegExp); + if (!rx.exactMatch(name)) + assert(false); + assert(rx.numCaptures() == 6); + QDate date(rx.cap(1).toInt(), rx.cap(2).toInt(),rx.cap(3).toInt()); + QTime time(rx.cap(4).toInt(), rx.cap(5).toInt(),rx.cap(6).toInt()); + QDateTime dt(date, time); + RideItem *last = new RideItem(allRides, RIDE_TYPE, home.path(), name, dt); + treeWidget->setCurrentItem(last); +} + +void +MainWindow::newCyclist() +{ + QDir newHome = home; + newHome.cdUp(); + QString name = ChooseCyclistDialog::newCyclistDialog(newHome, this); + if (!name.isEmpty()) { + newHome.cd(name); + if (!newHome.exists()) + assert(false); + MainWindow *main = new MainWindow(newHome); + main->show(); + } +} + +void +MainWindow::openCyclist() +{ + QDir newHome = home; + newHome.cdUp(); + ChooseCyclistDialog d(newHome, false); + d.setModal(true); + if (d.exec() == QDialog::Accepted) { + newHome.cd(d.choice()); + if (!newHome.exists()) + assert(false); + MainWindow *main = new MainWindow(newHome); + main->show(); + } +} + +void +MainWindow::downloadRide() +{ + (new DownloadRideDialog(this, home))->show(); +} + +void +MainWindow::rideSelected() +{ + assert(treeWidget->selectedItems().size() <= 1); + if (treeWidget->selectedItems().size() == 1) { + QTreeWidgetItem *which = treeWidget->selectedItems().first(); + if (which->type() == RIDE_TYPE) { + RideItem *ride = (RideItem*) which; + rideSummary->setHtml(ride->htmlSummary()); + rideSummary->setAlignment(Qt::AlignCenter); + allPlot->setData(ride->raw); + return; + } + } + rideSummary->clear(); +} + +void +MainWindow::resizeEvent(QResizeEvent*) +{ + settings.setValue(GC_SETTINGS_MAIN_GEOM, geometry()); +} + +void +MainWindow::moveEvent(QMoveEvent*) +{ + settings.setValue(GC_SETTINGS_MAIN_GEOM, geometry()); +} + +void +MainWindow::splitterMoved() +{ + settings.setValue(GC_SETTINGS_SPLITTER_SIZES, splitter->saveState()); +} + +void +MainWindow::setSmoothingFromSlider() +{ + if (allPlot->smoothing() != smoothSlider->value()) { + allPlot->setSmoothing(smoothSlider->value()); + smoothLineEdit->setText(QString("%1").arg(allPlot->smoothing())); + } +} + +void +MainWindow::setSmoothingFromLineEdit() +{ + int value = smoothLineEdit->text().toInt(); + if (value != allPlot->smoothing()) { + allPlot->setSmoothing(value); + smoothSlider->setValue(value); + } +} + diff --git a/gui/MainWindow.h b/gui/MainWindow.h new file mode 100644 index 000000000..d408f7cb7 --- /dev/null +++ b/gui/MainWindow.h @@ -0,0 +1,66 @@ +/* + * $Id: MainWindow.h,v 1.6 2006/07/11 21:20:21 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _GC_MainWindow_h +#define _GC_MainWindow_h 1 + +#include +#include + +class AllPlot; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + + public: + MainWindow(const QDir &home); + void addRide(QString name); + + protected: + virtual void resizeEvent(QResizeEvent*); + virtual void moveEvent(QMoveEvent*); + + private slots: + void rideSelected(); + void splitterMoved(); + void newCyclist(); + void openCyclist(); + void downloadRide(); + void setSmoothingFromSlider(); + void setSmoothingFromLineEdit(); + + private: + + QDir home; + QSettings settings; + + QSplitter *splitter; + QTreeWidget *treeWidget; + QTabWidget *tabWidget; + QTextEdit *rideSummary; + AllPlot *allPlot; + QSlider *smoothSlider; + QLineEdit *smoothLineEdit; + QTreeWidgetItem *allRides; +}; + +#endif // _GC_MainWindow_h + diff --git a/gui/RawFile.cpp b/gui/RawFile.cpp new file mode 100644 index 000000000..b03d077ad --- /dev/null +++ b/gui/RawFile.cpp @@ -0,0 +1,100 @@ +/* + * $Id: RawFile.cpp,v 1.2 2006/08/11 19:58:07 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "RawFile.h" +#include +extern "C" { +#include "../pt.h" +} + +struct RawFileReadState +{ + QList &points; + QStringList &errors; + double last_secs, last_miles; + unsigned last_interval; + time_t start_since_epoch; + unsigned rec_int; + RawFileReadState(QList &points, QStringList &errors) : + points(points), errors(errors), last_secs(0.0), last_miles(0.0), + last_interval(0), start_since_epoch(0), rec_int(0) {} +}; + +static void +config_cb(unsigned /*interval*/, unsigned rec_int, + unsigned /*wheel_sz_mm*/, void *context) +{ + RawFileReadState *state = (RawFileReadState*) context; + // Assume once set, rec_int should never change. + assert((state->rec_int == 0) || (state->rec_int == rec_int)); + state->rec_int = rec_int; +} + +static void +time_cb(struct tm *, time_t since_epoch, void *context) +{ + RawFileReadState *state = (RawFileReadState*) context; + if (state->start_since_epoch == 0) + state->start_since_epoch = since_epoch; + double secs = since_epoch - state->start_since_epoch; + RawFilePoint *point = new RawFilePoint(secs, -1.0, -1.0, -1.0, + state->last_miles, 0, 0, + state->last_interval); + state->points.append(point); + state->last_secs = secs; +} + +static void +data_cb(double secs, double nm, double mph, double watts, double miles, + unsigned cad, unsigned hr, unsigned interval, void *context) +{ + RawFileReadState *state = (RawFileReadState*) context; + RawFilePoint *point = new RawFilePoint(secs, nm, mph, watts, miles, + cad, hr, interval); + state->points.append(point); + state->last_secs = secs; + state->last_miles = miles; + state->last_interval = interval; +} + +static void +error_cb(const char *msg, void *context) +{ + RawFileReadState *state = (RawFileReadState*) context; + state->errors.append(QString(msg)); +} + +RawFile *RawFile::readFile(const QFile &file, QStringList &errors) +{ + RawFile *result = new RawFile(file.fileName()); + if (!result->file.open(QIODevice::ReadOnly)) { + delete result; + return NULL; + } + FILE *f = fdopen(result->file.handle(), "r"); + assert(f); + RawFileReadState state(result->points, errors); + pt_read_raw(f, 0 /* not compat */, &state, config_cb, + time_cb, data_cb, error_cb); + result->rec_int = state.rec_int; + return result; +} + + diff --git a/gui/RawFile.h b/gui/RawFile.h new file mode 100644 index 000000000..7d7fa821e --- /dev/null +++ b/gui/RawFile.h @@ -0,0 +1,58 @@ +/* + * $Id: RawFile.h,v 1.3 2006/08/11 19:58:07 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _GC_RawFile_h +#define _GC_RawFile_h 1 + +#include +#include +#include +#include + +struct RawFilePoint +{ + double secs, nm, mph, watts, miles; + unsigned cad, hr, interval; + RawFilePoint(double secs, double nm, double mph, double watts, + double miles, unsigned cad, unsigned hr, unsigned interval) : + secs(secs), nm(nm), mph(mph), watts(watts), miles(miles), + cad(cad), hr(hr), interval(interval) {} +}; + +class RawFile +{ + private: + + QFile file; + + RawFile(QString fileName) { + file.setFileName(fileName); + } + + public: + + QDateTime startTime; + int rec_int; + QList points; + static RawFile *readFile(const QFile &file, QStringList &errors); +}; + +#endif // _GC_RawFile_h + diff --git a/gui/RideItem.cpp b/gui/RideItem.cpp new file mode 100644 index 000000000..530676153 --- /dev/null +++ b/gui/RideItem.cpp @@ -0,0 +1,138 @@ +/* + * $Id: RideItem.cpp,v 1.3 2006/07/09 15:30:34 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "RideItem.h" +#include "RawFile.h" +#include "Settings.h" +#include + +static QString +time_to_string(double secs) { + QString result; + unsigned rounded = (unsigned) round(secs); + bool needs_colon = false; + if (rounded >= 3600) { + result += QString("%1").arg(rounded / 3600); + rounded %= 3600; + needs_colon = true; + } + if (needs_colon || rounded >= 60) { + if (needs_colon) + result += ":"; + result += QString("%1").arg(rounded / 60, 2, 10, QLatin1Char('0')); + rounded %= 60; + needs_colon = true; + } + if (needs_colon) + result += ":"; + result += QString("%1").arg(rounded, 2, 10, QLatin1Char('0')); + return result; +} + +RideItem::RideItem(QTreeWidgetItem *parent, int type, QString path, + QString fileName, const QDateTime &dateTime) : + QTreeWidgetItem(parent, type), path(path), + fileName(fileName), dateTime(dateTime) +{ + setText(0, dateTime.toString("ddd")); + setText(1, dateTime.toString("MMM d, yyyy")); + setText(2, dateTime.toString("h:mm AP")); + setTextAlignment(1, Qt::AlignRight); + setTextAlignment(2, Qt::AlignRight); +} + +QString +RideItem::htmlSummary() +{ + if (summary.isEmpty()) { + QFile file(path + "/" + fileName); + QStringList errors; + raw = RawFile::readFile(file, errors); + summary = ("

" + + dateTime.toString("dddd MMMM d, yyyy, h:mm AP") + + "

"); + if (raw == NULL) { + summary += "

Error: Can't read file."; + } + else if (errors.empty()) { + double secs_moving = 0.0; + double total_watts = 0.0; + double secs_watts = 0.0; + double secs_hr = 0.0; + double total_hr = 0.0; + double secs_cad = 0.0; + double total_cad = 0.0; + double last_secs = 0.0; + QListIterator i(raw->points); + while (i.hasNext()) { + RawFilePoint *point = i.next(); + double secs_delta = point->secs - last_secs; + if (point->mph > 0.0) + secs_moving += secs_delta; + if (point->watts >= 0.0) { + total_watts += point->watts * secs_delta; + secs_watts += secs_delta; + } + if (point->hr > 0) { + total_hr += point->hr * secs_delta; + secs_hr += secs_delta; + } + if (point->cad > 0) { + total_cad += point->cad * secs_delta; + secs_cad += secs_delta; + } + last_secs = point->secs; + } + summary += "
"; + summary += ""; + summary += QString("" + "") + .arg(raw->points.back()->miles, 0, 'f', 1); + summary += QString("" + "") + .arg(raw->points.back()->miles / secs_moving * 3600.0, + 0, 'f', 1); + summary += QString("" + "") + .arg((unsigned) round(total_watts / secs_watts)); + summary +=QString("" + "") + .arg((unsigned) round(total_hr / secs_hr)); + summary += QString("" + "") + .arg((unsigned) round(total_cad / secs_cad)); + summary += "
Total workout time:" + + time_to_string(raw->points.back()->secs); + summary += "
Total time riding:" + + time_to_string(secs_moving) + "
Total distance:%1
Average speed:%1
Average power:%1
Average heart rate:%1
Average cadence:%1
"; + } + else { + summary += "
Errors reading file:

    "; + QStringListIterator i(errors); + while(i.hasNext()) + summary += "
  • " + i.next(); + summary += "
"; + } + } + return summary; +} + + diff --git a/gui/RideItem.h b/gui/RideItem.h new file mode 100644 index 000000000..58fb8d0ee --- /dev/null +++ b/gui/RideItem.h @@ -0,0 +1,42 @@ +/* + * $Id: RideItem.h,v 1.2 2006/07/04 12:55:40 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _GC_RideItem_h +#define _GC_RideItem_h 1 + +#include + +class RawFile; + +struct RideItem : public QTreeWidgetItem { + QString path; + QString fileName; + QDateTime dateTime; + QString summary; + RawFile *raw; + + RideItem(QTreeWidgetItem *parent, int type, QString path, + QString fileName, const QDateTime &dateTime); + + QString htmlSummary(); +}; + +#endif // _GC_RideItem_h + diff --git a/gui/Settings.h b/gui/Settings.h new file mode 100644 index 000000000..5c317df11 --- /dev/null +++ b/gui/Settings.h @@ -0,0 +1,36 @@ +/* + * $Id: Settings.h,v 1.2 2006/07/04 12:55:40 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _GC_Settings_h +#define _GC_Settings_h 1 + +#define GC_SETTINGS_CO "goldencheetah.org" +#define GC_SETTINGS_APP "GoldenCheetah" +#define GC_SETTINGS_LAST "mainwindow/lastOpened" +#define GC_SETTINGS_MAIN_WIDTH "mainwindow/width" +#define GC_SETTINGS_MAIN_HEIGHT "mainwindow/height" +#define GC_SETTINGS_MAIN_X "mainwindow/x" +#define GC_SETTINGS_MAIN_Y "mainwindow/y" +#define GC_SETTINGS_MAIN_GEOM "mainwindow/geometry" +#define GC_SETTINGS_SPLITTER_SIZES "mainwindow/splitterSizes" +#define GC_DATETIME_FORMAT "ddd MMM dd, yyyy, hh:mm AP" + +#endif // _GC_Settings_h + diff --git a/gui/TODO b/gui/TODO new file mode 100644 index 000000000..7163df4fa --- /dev/null +++ b/gui/TODO @@ -0,0 +1,10 @@ + + - Recalc y-max after changing showPower or showHr + - Add cadence, speed, torque to graph + - Remember last settings for showPower, showHr, etc. + - Add interval summary to ride summary + - Switch x-axis from minutes to miles + - Add units to ride summary + - Add weekly summary + - Add cpint + diff --git a/gui/addqt.sh b/gui/addqt.sh new file mode 100755 index 000000000..7b10a0294 --- /dev/null +++ b/gui/addqt.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# $Id: addqt.sh,v 1.1 2006/08/11 20:01:16 srhea Exp $ +QT_PATH=/usr/local/Trolltech/Qt-4.1.1 +APP=GoldenCheetah +rm -rf $APP.app/Contents/Frameworks +mkdir -p $APP.app/Contents/Frameworks +cp -R $QT_PATH/lib/QtCore.framework $APP.app/Contents/Frameworks +cp -R $QT_PATH/lib/QtGui.framework $APP.app/Contents/Frameworks +install_name_tool -id @executable_path/../Frameworks/QtCore.framework/Versions/4.0/QtCore $APP.app/Contents/Frameworks/QtCore.framework/Versions/4.0/QtCore +install_name_tool -id @executable_path/../Frameworks/QtGui.framework/Versions/4.0/QtGui $APP.app/Contents/Frameworks/QtGui.framework/Versions/4.0/QtGui +install_name_tool -change $QT_PATH/lib/QtCore.framework/Versions/4.0/QtCore @executable_path/../Frameworks/QtCore.framework/Versions/4.0/QtCore $APP.app/Contents/MacOs/$APP +install_name_tool -change $QT_PATH/lib/QtGui.framework/Versions/4.0/QtGui @executable_path/../Frameworks/QtGui.framework/Versions/4.0/QtGui $APP.app/Contents/MacOs/$APP +install_name_tool -change $QT_PATH/lib/QtCore.framework/Versions/4.0/QtCore @executable_path/../Frameworks/QtCore.framework/Versions/4.0/QtCore $APP.app/Contents/Frameworks/QtGui.framework/Versions/4.0/QtGui diff --git a/gui/main.cpp b/gui/main.cpp new file mode 100644 index 000000000..1eac332f4 --- /dev/null +++ b/gui/main.cpp @@ -0,0 +1,69 @@ +/* + * $Id: main.cpp,v 1.3 2006/07/04 12:55:40 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include "ChooseCyclistDialog.h" +#include "MainWindow.h" +#include "Settings.h" + +int +main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QDir home = QDir::home(); + if (!home.exists("Library")) + assert(false); + home.cd("Library"); + if (!home.exists("GoldenCheetah")) + if (!home.mkdir("GoldenCheetah")) + assert(false); + home.cd("GoldenCheetah"); + QSettings settings(GC_SETTINGS_CO, GC_SETTINGS_APP); + QVariant lastOpened = settings.value(GC_SETTINGS_LAST); + bool anyOpened = false; + if (lastOpened != QVariant()) { + QStringList list = lastOpened.toStringList(); + QStringListIterator i(list); + while (i.hasNext()) { + QString cyclist = i.next(); + if (home.cd(cyclist)) { + MainWindow *main = new MainWindow(home); + main->show(); + home.cdUp(); + anyOpened = true; + } + } + } + if (!anyOpened) { + ChooseCyclistDialog d(home, true); + d.setModal(true); + if (d.exec() != QDialog::Accepted) + return 0; + home.cd(d.choice()); + if (!home.exists()) + assert(false); + MainWindow *main = new MainWindow(home); + main->show(); + } + return app.exec(); +} + + diff --git a/gui/mkdmg.sh b/gui/mkdmg.sh new file mode 100755 index 000000000..5e2062c51 --- /dev/null +++ b/gui/mkdmg.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# $Id: mkdmg.sh,v 1.2 2006/09/06 23:23:03 srhea Exp $ +CMD_FILES="COPYING ptdl ptunpk intervals.pl smooth.pl cpint ptpk" +GUI_FILE="GoldenCheetah.app" +VERS=`date +'%Y-%m-%d'` +strip GoldenCheetah.app/Contents/MacOS/GoldenCheetah +rm -rf tmp.dmg +SIZE=`cd ..; du -csk $CMD_FILES gui/GoldenCheetah.app | grep total | awk '{printf "%.0fm", $1/1024+5}'` +echo "SIZE=$SIZE, VERS=$VERS" +hdiutil create -size $SIZE -fs HFS+ -volname "Golden Cheetah $VERS" tmp.dmg +hdiutil attach tmp.dmg +cp -R GoldenCheetah.app /Volumes/Golden\ Cheetah\ $VERS/ +cd .. && cp $CMD_FILES /Volumes/Golden\ Cheetah\ $VERS/ && cd - +hdiutil detach /Volumes/Golden\ Cheetah\ $VERS/ +hdiutil convert tmp.dmg -format UDZO -o GoldenCheetah_$VERS.dmg +hdiutil internet-enable -yes GoldenCheetah_$VERS.dmg +rm -rf tmp.dmg diff --git a/intervals.pl b/intervals.pl new file mode 100755 index 000000000..f61b28a7d --- /dev/null +++ b/intervals.pl @@ -0,0 +1,113 @@ +#!/usr/bin/perl -w +# +# $Id: intervals.pl,v 1.2 2006/08/11 19:53:50 srhea Exp $ +# +# Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +use strict; + +my $interval = -1; +my $time_start = 0; +my $time_end = 0; +my $mile_start = 0; +my $mile_end = 0; +my $watts_max = 0; +my $watts_sum = 0; +my $watts_cnt = 0; +my $hrate_max = 0; +my $hrate_sum = 0; +my $hrate_cnt = 0; +my $caden_max = 0; +my $caden_sum = 0; +my $caden_cnt = 0; +my $speed_max = 0; +my $speed_sum = 0; +my $speed_cnt = 0; + +sub sumarize { + my $dur = $time_end - $time_start; + my $len = $mile_end - $mile_start; + my $minutes = int($dur); + my $seconds = int(60 * ($dur - int($dur))); + my $watts_avg = int($watts_sum / $watts_cnt); + my $hrate_avg = ($hrate_cnt == 0) ? 0 : int($hrate_sum / $hrate_cnt); + my $caden_avg = ($caden_cnt == 0) ? 0 : int($caden_sum / $caden_cnt); + my $speed_avg = int($speed_sum / $speed_cnt); + printf "%2d\t%2d:%02d\t%5.1f\t%4d\t%4d\t%3d\t%3d\t%3d\t%3d\t%0.1f\t%0.1f\n", $interval, $minutes, $seconds, $len, $watts_avg, $watts_max, $hrate_avg, $hrate_max, $caden_avg, $caden_max, $speed_avg, $speed_max; + $watts_sum = 0; + $watts_cnt = 0; + $watts_max = 0; + $hrate_max = 0; + $hrate_sum = 0; + $hrate_cnt = 0; + $caden_max = 0; + $caden_sum = 0; + $caden_cnt = 0; + $speed_max = 0; + $speed_sum = 0; + $speed_cnt = 0; +} + +print "\t\t\tPower\t\tHeart Rate\tCadence\t\tSpeed\n"; +print "Int\t Dur\tDist\t Avg\t Max\tAvg\tMax\tAvg\tMax\tAvg\tMax\n"; + +while (<>) { + if (m/^#/) { + } + else { + my @cols = split; + if ($#cols != 7) { + print STDERR "Wrong number of columns: $_"; + exit 1; + } + my ($min, $torq, $speed, $watts, $miles, $caden, $hrate, $id) = @cols; + $mile_end = $miles; + $time_end = $min; + if ($watts != "NaN") { + $watts_sum += $watts; + $watts_cnt += 1; + if ($watts > $watts_max) { $watts_max = $watts; } + } + if ($hrate != "NaN") { + $hrate_sum += $hrate; + $hrate_cnt += 1; + if ($hrate > $hrate_max) { $hrate_max = $hrate; } + } + if ($caden != "NaN") { + $caden_sum += $caden; + $caden_cnt += 1; + if ($caden > $caden_max) { $caden_max = $caden; } + } + if ($speed != "NaN") { + $speed_sum += $speed; + $speed_cnt += 1; + if ($speed > $speed_max) { $speed_max = $speed; } + } + if ($id != $interval) { + if ($interval != -1) { + sumarize(); + } + $interval = $id; + $time_start = $min; + $mile_start = $miles; + } + } +} + +sumarize(); + diff --git a/notes.txt b/notes.txt new file mode 100644 index 000000000..7a30ab8ee --- /dev/null +++ b/notes.txt @@ -0,0 +1,75 @@ + +Version header: + +sometimes bytes added before 0x56 + +00 56 45 52 20 30 .VER 0 +32 2e 32 31 20 50 2.21 P +52 4f 00 00 00 00 RO.... +00 00 00 00 00 00 ...... +00 00 00 00 0d 0a ....\r\n + + +Data: + +Get 12 packets of 1542 (0x606) bytes each. +Where is that encoded? + +Each row is 6 bytes. +Rows that start with 0x8? are the data for one period. + +Column Meaning Format +1 ??? +2, n1 torque 4 most sig bits, N m +2, n2 km/h 4 most sig bits, 0x24000 / value * 0.1 +3 torque 8 least sig bits, N m +4 km/h 8 least sig bits, 0x24000 / value * 0.1 +5 Cadence 1-byte integer +6 Heartrate 1-byte integer + + +First row is always: + +57 56 55 64 02 15 + +Base period in thousands of a minute = 0x15... + + +A row starting with 0x60 encodes the time of day that measurements started. +This is always the second record in the stream. Also, when the computer goes +to sleep and then wakes up again, it writes one of these records first. + +Example: 60 06 04 9c 2a 30 + +byte 2 year +byte 3 month +byte 4, bottom 5 bits day +byte 5, bottom 5 bits hour +byte 6, bottom 6 bits minute + +top 3 bits of byte 4 followed by top 3 bits of byte 5 is pretty close to the +right seconds value, but not perfect... + + +A row starting with 0x40 encodes: + + bytes 2-3 the wheel circumference in mm + byte 4 the interval number + byte 5 the recording interval, where + + byte value interval + 0x0 1 + 0x1 2 + 0x3 5 + 0x7 10 + 0x17 30 + +Example: + +40 08 30 02 07 00 + wheel size in mm = 0x0830 + interval 2 + recording interval 10 + + + diff --git a/package.sh b/package.sh new file mode 100755 index 000000000..7ac321ff7 --- /dev/null +++ b/package.sh @@ -0,0 +1,8 @@ +#!/bin/sh +VERSION=`date +'%Y-%m-%d'` +FILES="COPYING Makefile pt.h pt.c ptdl.c ptunpk.c intervals.pl smooth.pl" +FILES="$FILES cpint.c cpint.h cpint-cmd.c ptpk.c" +mkdir gc_$VERSION +cp $FILES gc_$VERSION +tar cvzf gc_$VERSION.tgz gc_$VERSION +rm -rf gc_$VERSION diff --git a/pt.c b/pt.c new file mode 100644 index 000000000..a30f36bb2 --- /dev/null +++ b/pt.c @@ -0,0 +1,632 @@ +/* + * $Id: pt.c,v 1.9 2006/09/06 23:23:03 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pt.h" + +#define MAGIC_CONSTANT 147375.0 +#define PI 3.14159265 +#define TIME_UNIT_MIN 0.021 + +#define LBFIN_TO_NM 0.11298483 +#define KM_TO_MI 0.62137119 + +#define BAD_LBFIN_TO_NM_1 0.112984 +#define BAD_LBFIN_TO_NM_2 0.1129824 +#define BAD_KM_TO_MI 0.62 + +unsigned pt_debug_level; + +static unsigned char +check(unsigned value) +{ + assert(value < 256); + return (unsigned char) value; +} + +int +pt_find_device(char *result[], int capacity) +{ + regex_t reg; + DIR *dirp; + struct dirent *dp; + int count = 0; + if (regcomp(®, "^cu\\.(usbserial-[0-9A-F]+|KeySerial[0-9])$", + REG_EXTENDED|REG_NOSUB)) { + assert(0); + } + dirp = opendir("/dev"); + while ((count < capacity) && ((dp = readdir(dirp)) != NULL)) { + if (regexec(®, dp->d_name, 0, NULL, 0) == 0) { + result[count] = malloc(6 + strlen(dp->d_name)); + sprintf(result[count], "/dev/%s", dp->d_name); + ++count; + } + } + return count; +} + +#define KSDEVSTR "/dev/cu.KeySerial" +int +pt_hwecho(const char *device) +{ + return strncmp(device, KSDEVSTR, strlen(KSDEVSTR)) == 0; +} + +void +pt_make_async(int fd) +{ + int flags = fcntl(fd, F_GETFL, 0); + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { + perror("fcntl"); + assert(0); + } +} + +int +pt_read_version(struct pt_read_version_state *state, int fd, int hwecho) +{ + char c = 0x56; + int n; + + if (state->state == 0) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Writing 0x%x to device.\n", (unsigned) c); + if ((n = write(fd, &c, 1)) < 1) { + perror("write"); + exit(1); + } + state->state = 1; + state->i = 0; + } + + if (state->state == 1) { + if (hwecho) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Calling read on device.\n"); + n = read(fd, &c, 1); + if (n <= 0) { + if ((n < 0) && (errno == EAGAIN)) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Need read.\n"); + return PT_NEED_READ; + } + perror("read"); + exit(1); + } + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Read %d bytes.\n", n); + assert(n == 1); + } + state->state = 2; + } + + assert(state->state == 2); + while (state->i < 29) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Need %d bytes. Calling read on device.\n", + 29 - state->i); + n = read(fd, state->buf + state->i, sizeof(state->buf) - state->i); + if (n <= 0) { + if ((n < 0) && (errno == EAGAIN)) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Need read.\n"); + return PT_NEED_READ; + } + perror("read"); + exit(1); + } + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Read %d bytes.\n", n); + state->i += n; + } + + return PT_DONE; +} + +int +pt_read_data(struct pt_read_data_state *state, + int fd, int hwecho, + void (*time_cb)(struct tm *, void *), + void (*record_cb)(unsigned char *, void *), + void *user_data) +{ + char c = 0x44; + int j, n; + unsigned csum; + struct tm time; + + if (state->state == 0) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Writing 0x%x to device.\n", (unsigned) c); + if ((n = write(fd, &c, 1)) < 1) { + perror("write"); + exit(1); + } + state->block = 1; + state->i = 0; + state->state = 1; + } + + if (state->state == 1) { + if (hwecho) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Calling read on device.\n"); + n = read(fd, &c, 1); + if (n <= 0) { + if ((n < 0) && (errno == EAGAIN)) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Need read.\n"); + return PT_NEED_READ; + } + perror("read"); + exit(1); + } + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Read %d bytes.\n", n); + assert(n == 1); + } + state->state = 2; + } + + if (state->state == 2) { + while (state->i < (int) sizeof(state->header)) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Calling read on device.\n"); + n = read(fd, state->header + state->i, + sizeof(state->header) - state->i); + if (n <= 0) { + if ((n < 0) && (errno == EAGAIN)) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Need read.\n"); + return PT_NEED_READ; + } + perror("read"); + exit(1); + } + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Read %d bytes.\n", n); + state->i += n; + } + state->state = 3; + state->i = 0; + } + + while (1) { + if (state->state == 3) { + while (state->i < (int) sizeof(state->buf)) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Calling read on device.\n"); + n = read(fd, state->buf + state->i, + sizeof(state->buf) - state->i); + if (n <= 0) { + if ((n < 0) && (errno == EAGAIN)) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Need read.\n"); + return PT_NEED_READ; + } + perror("read"); + exit(1); + } + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Read %d bytes.\n", n); + state->i += n; + /* TODO: why is this next if statement here? */ + if ((state->i == 2) && (state->buf[0] == 0x0d) + && (state->buf[1] == 0x0a)) { + return PT_DONE; + } + } + if (state->block == 1) { + n = 0; + if (pt_is_config(state->buf + n)) + n += 6; + if (!pt_is_time(state->buf + n) + || (pt_unpack_time(state->buf + n, &time) == -1)) + time_cb(NULL, user_data); + else + time_cb(&time, user_data); + record_cb(state->header, user_data); + } + csum = 0; + for (j = 0; j < state->i - 1; ++j) + csum += state->buf[j]; + if ((csum % 256) != state->buf[state->i-1]) { + fprintf(stderr, "\nbad checksum on block %d: %d vs %d", + state->block, state->buf[state->i-1], csum); + } + for (j = 0; j < state->i - 1; j += 6) { + if (state->buf[j]) + record_cb(state->buf + j, user_data); + else + break; + } + c = 0x71; + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Writing 0x%x to device.\n", (unsigned) c); + n = write(fd, &c, 1); + if (n < 1) { + perror("write"); + exit(1); + } + ++(state->block); + state->i = 0; + state->state = 4; + } + + assert(state->state == 4); + if (hwecho) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Calling read on device.\n"); + n = read(fd, &c, 1); + if (n <= 0) { + if ((n < 0) && (errno == EAGAIN)) { + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Need read.\n"); + return PT_NEED_READ; + } + perror("read"); + exit(1); + } + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Read %d bytes.\n", n); + assert(n == 1); + } + state->state = 3; + } + + return PT_DONE; +} + +int +pt_is_time(unsigned char *buf) +{ + return buf[0] == 0x60; +} + +time_t +pt_unpack_time(unsigned char *buf, struct tm *time) +{ + memset(time, 0, sizeof(*time)); + time->tm_year = 2000 + buf[1] - 1900; + time->tm_mon = buf[2] - 1; + time->tm_mday = buf[3] & 0x1f; + time->tm_hour = buf[4] & 0x1f; + time->tm_min = buf[5] & 0x3f; + time->tm_sec = ((buf[3] >> 5) << 3) | (buf[4] >> 5); + time->tm_isdst = -1; + return mktime(time); +} + +int +pt_is_config(unsigned char *buf) +{ + return buf[0] == 0x40; +} + +int +pt_unpack_config(unsigned char *buf, unsigned *interval, + unsigned *last_interval, unsigned *rec_int, + unsigned *wheel_sz_mm) +{ + *wheel_sz_mm = (buf[1] << 8) | buf[2]; + /* Data from device wraps interval after 9... */ + if (buf[3] != *last_interval) { + *last_interval = buf[3]; + ++*interval; + } + switch (buf[4]) { + case 0x0: *rec_int = 1; break; + case 0x1: *rec_int = 2; break; + case 0x3: *rec_int = 5; break; + case 0x7: *rec_int = 10; break; + case 0x17: *rec_int = 30; break; + default: + return -1; + } + return 0; +} + +int +pt_is_data(unsigned char *buf) +{ + return (buf[0] & 0x80) == 0x80; +} + +static double +my_round(double x) +{ + int i = (int) x; + double z = x - i; + /* For some unknown reason, the PowerTap software rounds 196.5 down... */ + if ((z > 0.5) || ((z == 0.5) && (i != 196))) + ++i; + return i; +} + +void +pt_unpack_data(unsigned char *buf, int compat, unsigned rec_int, + unsigned wheel_sz_mm, double *time_secs, double *torque_Nm, + double *mph, double *watts, double *dist_m, unsigned *cad, + unsigned *hr) +{ + double kph10; + unsigned speed; + unsigned torque_inlbs; + double rotations; + double radians; + double joules; + + *time_secs += rec_int * TIME_UNIT_MIN * 60.0; + torque_inlbs = ((buf[1] & 0xf0) << 4) | buf[2]; + if (torque_inlbs == 0xfff) + torque_inlbs = 0; + speed = ((buf[1] & 0x0f) << 8) | buf[3]; + if ((speed == 0) || (speed == 0xfff)) { + *mph = -1.0; + *watts = -1.0; + } + else { + if (compat) + *torque_Nm = torque_inlbs * BAD_LBFIN_TO_NM_2; + else + *torque_Nm = torque_inlbs * LBFIN_TO_NM; + kph10 = MAGIC_CONSTANT / speed; + if (compat) + *mph = my_round(kph10) / 10.0 * BAD_KM_TO_MI; + else + *mph = kph10 / 10.0 * KM_TO_MI; + rotations = rec_int * TIME_UNIT_MIN * 100000.0 * kph10 + / wheel_sz_mm / 60.0; + radians = rotations * 2.0 * PI; + joules = *torque_Nm * radians; + *watts = joules / (rec_int * TIME_UNIT_MIN * 60); + if (compat) + *watts = my_round(*watts); + else + *watts = round(*watts); + } + if (compat) + *torque_Nm = torque_inlbs * BAD_LBFIN_TO_NM_1; + *dist_m += (buf[0] & 0x7f) * wheel_sz_mm / 1000.0; + *cad = buf[4]; + if (*cad == 0xff) + *cad = 0; + *hr = buf[5]; + if (*hr == 0xff) + *hr = 0; +} + +void +pt_write_data(FILE *out, unsigned char *buf) +{ + int i; + for (i = 0; i < 5; ++i) + fprintf(out, "%02x ", buf[i]); + fprintf(out, "%02x\n", buf[i]); +} + +void +pt_pack_header(unsigned char *buf) +{ + unsigned char src[] = { 0x57, 0x56, 0x55, 0x64, 0x02, 0x15 }; + memcpy(buf, src, 6); +} + +void +pt_pack_time(unsigned char *buf, struct tm *time) +{ + buf[0] = 0x60; + buf[1] = check(time->tm_year + 1900 - 2000); + buf[2] = check(time->tm_mon + 1); + buf[3] = check(time->tm_mday) | check((time->tm_sec >> 3) << 5); + buf[4] = check(time->tm_hour) | check((time->tm_sec & 0x7) << 5); + buf[5] = check(time->tm_min); +} + +void +pt_pack_config(unsigned char *buf, unsigned interval, + unsigned rec_int, unsigned wheel_sz_mm) +{ + buf[0] = 0x40; + buf[1] = check(wheel_sz_mm >> 8); + buf[2] = wheel_sz_mm & 0xff; + buf[3] = check(interval % 9); + switch (rec_int) { + case 1: buf[4] = 0x0; break; + case 2: buf[4] = 0x1; break; + case 5: buf[4] = 0x3; break; + case 10: buf[4] = 0x7; break; + case 30: buf[4] = 0x17; break; + default: assert(0); + } + buf[5] = 0x0; +} + +void +pt_pack_data(unsigned char *buf, unsigned wheel_sz_mm, double nm, + double mph, double miles, unsigned cad, unsigned hr) +{ + double rotations = miles / BAD_KM_TO_MI * 1000.00 * 1000.0 / wheel_sz_mm; + unsigned inlbs = round(nm / BAD_LBFIN_TO_NM_2); + double kph10 = mph * 10.0 / BAD_KM_TO_MI; + unsigned speed; + if (mph == -1.0) + speed = 0xfff; + else + speed = round(MAGIC_CONSTANT / kph10); + buf[0] = 0x80 | check(round(rotations)); + buf[1] = ((inlbs & 0xf00) >> 4) | ((speed & 0xf00) >> 8); + buf[2] = inlbs & 0xff; + buf[3] = speed & 0xff; + buf[4] = check(cad); + buf[5] = check(hr); +} + + +void +pt_read_raw(FILE *in, int compat, void *context, + void (*config_cb)(unsigned interval, unsigned rec_int, + unsigned wheel_sz_mm, void *context), + void (*time_cb)(struct tm *time, time_t since_epoch, void *context), + void (*data_cb)(double secs, double nm, double mph, + double watts, double miles, unsigned cad, + unsigned hr, unsigned interval, void *context), + void (*error_cb)(const char *msg, void *context)) +{ + unsigned interval = 0; + unsigned last_interval = 0; + unsigned wheel_sz_mm = 0; + unsigned rec_int = 0; + int i, n, row = 0; + unsigned char buf[6]; + unsigned sbuf[6]; + double meters = 0.0; + double secs = 0.0, start_secs = 0.0; + double miles; + double mph; + double nm; + double watts; + unsigned cad; + unsigned hr; + struct tm time; + time_t since_epoch; + char ebuf[256]; + + while ((n = fscanf(in, "%x %x %x %x %x %x\n", + sbuf, sbuf+1, sbuf+2, sbuf+3, sbuf+4, sbuf+5)) == 6) { + ++row; + for (i = 0; i < 6; ++i) { + if (sbuf[i] > 0xff) { n = 1; break; } + buf[i] = sbuf[i]; + } + if (row == 1) { + /* Serial number? */ + } + else if (pt_is_config(buf)) { + if (pt_unpack_config(buf, &interval, &last_interval, + &rec_int, &wheel_sz_mm) < 0) { + sprintf(ebuf, "Couldn't unpack config record."); + if (error_cb) error_cb(ebuf, context); + return; + } + if (config_cb) config_cb(interval, rec_int, wheel_sz_mm, context); + } + else if (pt_is_time(buf)) { + since_epoch = pt_unpack_time(buf, &time); + if (start_secs == 0.0) + start_secs = since_epoch; + else + secs = since_epoch - start_secs; + if (time_cb) time_cb(&time, since_epoch, context); + } + else if (pt_is_data(buf)) { + if (wheel_sz_mm == 0) { + sprintf(ebuf, "Read data row before wheel size set."); + if (error_cb) error_cb(ebuf, context); + return; + } + pt_unpack_data(buf, compat, rec_int, wheel_sz_mm, &secs, + &nm, &mph, &watts, &meters, &cad, &hr); + if (compat) + miles = round(meters) / 1000.0 * BAD_KM_TO_MI; + else + miles = meters / 1000.0 * KM_TO_MI; + if (data_cb) + data_cb(secs, nm, mph, watts, miles, cad, + hr, interval, context); + } + else { + sprintf(ebuf, "Unknown record type 0x%x on row %d.", buf[0], row); + if (error_cb) error_cb(ebuf, context); + return; + } + } + if (n != -1) { + sprintf(ebuf, "Parse error on row %d.", row); + if (error_cb) error_cb(ebuf, context); + return; + } +} + +#define NMATCH 9 +void +pt_read_dat(FILE *in, void (*record_cb)(double, double, double, int, + double, int, int, int, void*), + void *user_data) +{ + regex_t reg_com, reg_dat; + regmatch_t pmatch[NMATCH]; + char line[256]; + double min, nm, mph, miles; + int watts, cad, hr, intv; + int i, len; + + if (regcomp(®_com, "^#", REG_EXTENDED | REG_NOSUB)) + assert(0); + if (regcomp(®_dat, "^([0-9]+\\.[0-9]+) +([0-9]+\\.[0-9]+) +" + "([0-9]+\\.[0-9]+|NaN) +([0-9]+|NaN) +([0-9]+\\.[0-9]+) +" + "([0-9]+) +([0-9]+|NaN) +([0-9]+)$", REG_EXTENDED)) + assert(0); + + while (fgets(line, sizeof(line), in)) { + len = strlen(line); + if (!line[len-1] == '\n') + assert(0); + line[len-1] = '\0'; + if (regexec(®_com, line, 0, NULL, 0) == 0) { + /* do nothing */ + } + else if (regexec(®_dat, line, NMATCH, pmatch, 0) == 0) { + for (i = 0; i < NMATCH; ++i) + line[pmatch[i].rm_eo] = '\0'; + if (sscanf(line + pmatch[1].rm_so, "%lf", &min) != 1) + assert(0); + if (sscanf(line + pmatch[2].rm_so, "%lf", &nm) != 1) + assert(0); + if (sscanf(line + pmatch[3].rm_so, "%lf", &mph) != 1) + mph = -1.0; + if (sscanf(line + pmatch[4].rm_so, "%d", &watts) != 1) + watts = -1; + if (sscanf(line + pmatch[5].rm_so, "%lf", &miles) != 1) + assert(0); + if (sscanf(line + pmatch[6].rm_so, "%d", &cad) != 1) + assert(0); + if (sscanf(line + pmatch[7].rm_so, "%d", &hr) != 1) + hr = -1; + if (sscanf(line + pmatch[8].rm_so, "%d", &intv) != 1) + assert(0); + record_cb(min, nm, mph, watts, miles, cad, hr, intv, user_data); + } + else { + fprintf(stderr, "Bad line: \"%s\"\n", line); + exit(1); + } + } +} + diff --git a/pt.h b/pt.h new file mode 100644 index 000000000..9b2a1c667 --- /dev/null +++ b/pt.h @@ -0,0 +1,98 @@ +/* + * $Id: pt.h,v 1.9 2006/09/06 23:23:03 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __pt_h +#define __pt_h 1 + +#include +#include +#include + +#define PT_DEBUG_NONE 0 +#define PT_DEBUG_MAX 1 +extern unsigned pt_debug_level; + +extern int pt_find_device(char *result[], int capacity); +extern int pt_hwecho(const char *device); + +#define PT_DONE 0 +#define PT_NEED_READ 1 + +extern void pt_make_async(int fd); + +struct pt_read_version_state { + int state; + int i; + unsigned char buf[30]; +}; +extern int pt_read_version(struct pt_read_version_state *state, + int fd, int hwecho); + +struct pt_read_data_state { + int state; + int i; + unsigned block; + unsigned char header[6]; + unsigned char buf[256 * 6 + 1]; +}; +extern int pt_read_data(struct pt_read_data_state *state, + int fd, int hwecho, + void (*time_cb)(struct tm *, void *), + void (*record_cb)(unsigned char *, void *), + void *user_data); + +extern int pt_is_time(unsigned char *buf); +extern time_t pt_unpack_time(unsigned char *buf, struct tm *time); + +extern int pt_is_config(unsigned char *buf); +extern int pt_unpack_config(unsigned char *buf, unsigned *interval, + unsigned *last_interval, unsigned *rec_int, + unsigned *wheel_sz_mm); + +extern int pt_is_data(unsigned char *buf); +extern void pt_unpack_data(unsigned char *buf, int compat, unsigned rec_int, + unsigned wheel_sz_mm, double *time_secs, + double *torque_Nm, double *mph, double *watts, + double *dist_m, unsigned *cad, unsigned *hr); + +void pt_write_data(FILE *out, unsigned char *buf); +void pt_pack_header(unsigned char *buf); +void pt_pack_time(unsigned char *buf, struct tm *time); +void pt_pack_config(unsigned char *buf, unsigned interval, + unsigned rec_int, unsigned wheel_sz_mm); +void pt_pack_data(unsigned char *buf, unsigned wheel_sz_mm, double nm, + double mph, double miles, unsigned cad, unsigned hr); + +extern void pt_read_raw(FILE *in, int compat, void *context, + void (*config_cb)(unsigned interval, unsigned rec_int, + unsigned wheel_sz_mm, void *context), + void (*time_cb)(struct tm *time, time_t since_epoch, void *context), + void (*data_cb)(double secs, double nm, double mph, + double watts, double miles, unsigned cad, + unsigned hr, unsigned interval, void *context), + void (*error_cb)(const char *msg, void *context)); + +extern void pt_read_dat(FILE *in, + void (*record_cb)(double, double, double, int, + double, int, int, int, void*), + void *user_data); + +#endif /* __pt_h */ + diff --git a/ptdl.c b/ptdl.c new file mode 100644 index 000000000..8a1c53eab --- /dev/null +++ b/ptdl.c @@ -0,0 +1,231 @@ +/* + * $Id: ptdl.c,v 1.10 2006/09/06 23:23:03 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include +#include +#include +#include "pt.h" + +#define MAX_DEVICES 20 + +int force; + +static void +time_cb(struct tm *time, void *user_data) +{ + char outname[24]; + FILE **out = (FILE**) user_data; + if (!*out) { + if (!time) { + fprintf(stderr, "Can't find ride time;" + " specify output file with -o.\n"); + exit(1); + } + sprintf(outname, "%04d_%02d_%02d_%02d_%02d_%02d.raw", + time->tm_year + 1900, time->tm_mon + 1, time->tm_mday, + time->tm_hour, time->tm_min, time->tm_sec); + assert(strlen(outname) == sizeof(outname) - 1); + fprintf(stderr, "done.\nWriting to %s.\n", outname); + if ((*out = fopen(outname, "r")) != NULL) { + if (force) + fclose(*out); + else { + fprintf(stderr, "Error: %s already exists! " + "Specify -f to overwrite it.\n", outname); + exit(1); + } + } + if ((*out = fopen(outname, "w")) == NULL) { + fprintf(stderr, "Couldn't open %s for writing: %s", + outname, strerror(errno)); + exit(1); + } + fprintf(stderr, "Reading ride data..."); + fflush(stderr); + } +} + +static void +record_cb(unsigned char *buf, void *user_data) +{ + static int count = 0; + int i; + FILE **out = (FILE**) user_data; + for (i = 0; i < 6; ++i) + fprintf(*out, "%02x%s", buf[i], (i == 5) ? "\n" : " "); + if ((++count % 256) == 0) { + fprintf(stderr, "."); + fflush(stderr); + } +} + +static void +usage(const char *progname) +{ + fprintf(stderr, "usage: %s [-d ] [-e] [-f] [-o ]\n", + progname); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + int i, ch, fd, r; + char *devices[MAX_DEVICES]; + int dev_cnt = 0; + char *outname; + FILE *out = NULL; + struct pt_read_version_state vstate; + struct pt_read_data_state dstate; + struct timeval timeout; + fd_set readfds; + int hwecho = 0; + + while ((ch = getopt(argc, argv, "d:efho:v")) != -1) { + switch (ch) { + case 'd': + devices[0] = optarg; + dev_cnt = 1; + break; + case 'e': + hwecho = 1; + break; + case 'f': + force = 1; + break; + case 'o': + outname = optarg; + if (strcmp(outname, "-") == 0) + out = stdout; + else if ((out = fopen(outname, "w")) == NULL) { + fprintf(stderr, "Couldn't open %s for writing: %s", + outname, strerror(errno)); + exit(1); + } + break; + case 'v': + pt_debug_level = PT_DEBUG_MAX; + break; + case 'h': + case '?': + default: + usage(argv[0]); + } + } + argc -= optind; + argv += optind; + + if (!dev_cnt) { + dev_cnt = pt_find_device(devices, MAX_DEVICES); + if (dev_cnt == 0) { + fprintf(stderr, "Can't find device; specify one with -d.\n"); + exit(1); + } + if (dev_cnt > 1) { + fprintf(stderr, "Multiple devices present; specify one with -d:\n"); + for (i = 0; i < dev_cnt; ++i) + fprintf(stderr, " %s\n", devices[i]); + exit(1); + } + } + + fprintf(stderr, "Reading from %s.\n", devices[0]); + if (pt_hwecho(devices[0])) + hwecho = 1; + + if (hwecho) + fprintf(stderr, "Expecting hardware echo.\n"); + + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Opening device %s.\n", devices[0]); + fd = open(devices[0], O_RDWR | O_NOCTTY | O_NONBLOCK); + if (fd < 0) { + perror("open"); + exit(1); + } + fprintf(stderr, "Reading version information..."); + fflush(stderr); + + pt_make_async(fd); + memset(&vstate, 0, sizeof(vstate)); + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "\nCalling pt_read_version.\n"); + while ((r = pt_read_version(&vstate, fd, hwecho)) != PT_DONE) { + assert(r == PT_NEED_READ); + FD_ZERO(&readfds); + FD_SET(fd, &readfds); + timeout.tv_sec = 5; + timeout.tv_usec = 0; + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Calling select.\n"); + select(fd + 1, &readfds, NULL, NULL, &timeout); + if (!FD_ISSET(fd, &readfds)) { + fprintf(stderr, "timeout.\n"); + exit(1); + } + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "\nCalling pt_read_version.\n"); + } + fprintf(stderr, "done.\n"); + close(fd); + + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Opening device %s.\n", devices[0]); + fd = open(devices[0], O_RDWR | O_NOCTTY | O_NONBLOCK); + if (fd < 0) { + perror("open"); + exit(1); + } + if (out) + fprintf(stderr, "Writing to %s.\nReading ride data...", outname); + else + fprintf(stderr, "Reading ride time..."); + fflush(stderr); + + pt_make_async(fd); + memset(&dstate, 0, sizeof(dstate)); + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "\nCalling pt_read_data.\n"); + while ((r = pt_read_data(&dstate, fd, hwecho, time_cb, + record_cb, &out)) != PT_DONE) { + assert(r == PT_NEED_READ); + FD_ZERO(&readfds); + FD_SET(fd, &readfds); + timeout.tv_sec = 5; + timeout.tv_usec = 0; + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "Calling select.\n"); + select(fd + 1, &readfds, NULL, NULL, &timeout); + if (!FD_ISSET(fd, &readfds)) { + fprintf(stderr, "timeout.\n"); + exit(1); + } + if (pt_debug_level >= PT_DEBUG_MAX) + fprintf(stderr, "\nCalling pt_read_data.\n"); + } + fprintf(stderr, "done.\n"); + + return 0; +} + diff --git a/ptpk.c b/ptpk.c new file mode 100644 index 000000000..0ee8b55eb --- /dev/null +++ b/ptpk.c @@ -0,0 +1,229 @@ +/* + * $Id: ptpk.c,v 1.1 2006/05/27 16:17:25 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "pt.h" + +void +pt_pack(FILE *in, FILE *out, int wheel_sz_mm, int rec_int) +{ + double mins, nm, mph, watts, miles; + int cad, hr, interval = 0; + double last_mins = 0.0, last_miles = 0.0; + int last_interval = 0; + int i; + struct tm start_tm, inc_tm; + time_t start_time, inc_time; + int lineno = 1; + char line[256]; + unsigned char buf[6]; + regex_t date_reg; + int date_nmatch = 7; + regmatch_t *date_pmatch = + (regmatch_t*) calloc(date_nmatch, sizeof(regmatch_t)); + regex_t rider_reg; + int rider_nmatch = 12; + regmatch_t *rider_pmatch = + (regmatch_t*) calloc(rider_nmatch, sizeof(regmatch_t)); + + if (regcomp(&date_reg, "^([0-9][0-9]?)\\\\([0-9][0-9]?)\\\\" + "([0-9][0-9][0-9][0-9]) +([0-9][0-9]?):([0-9][0-9]?):" + "([0-9][0-9]?) *$", REG_EXTENDED)) + assert(0); + + if (regcomp(&rider_reg, "^([0-9]+\\.[0-9]+)" + "([0-9]+(\\.[0-9]+)?)" + "([0-9]+(\\.[0-9]+)?|NaN)([0-9]+|NaN)" + "([0-9]+(\\.[0-9]+)?)([0-9]+)" + "([0-9]+|NaN)([0-9]+)$", + REG_EXTENDED)) + assert(0); + + pt_pack_header(buf); + pt_write_data(out, buf); + + while (fgets(line, sizeof(line), in)) { + line[strlen(line) - 1] = '\0'; /* drop newline */ + if (strcmp(line, "") == 0) { + /* ignore it */ + } + else if (strcmp(line, "") == 0) { + /* ignore it */ + } + else if (strcmp(line, "") == 0) { + /* ignore it */ + } + else if (!regexec(&date_reg, line, date_nmatch, date_pmatch, 0)) { + for (i = 1; i < date_nmatch; ++i) + line[date_pmatch[i].rm_eo] = '\0'; + memset(&start_tm, 0, sizeof(start_tm)); + start_tm.tm_year = atoi(line + date_pmatch[3].rm_so) - 1900; + start_tm.tm_mon = atoi(line + date_pmatch[1].rm_so) - 1; + start_tm.tm_mday = atoi(line + date_pmatch[2].rm_so); + start_tm.tm_hour = atoi(line + date_pmatch[4].rm_so); + start_tm.tm_min = atoi(line + date_pmatch[5].rm_so); + start_tm.tm_sec = atoi(line + date_pmatch[6].rm_so); + start_tm.tm_isdst = -1; + start_time = mktime(&start_tm); + assert(start_time != -1); + pt_pack_time(buf, &start_tm); + pt_write_data(out, buf); + pt_pack_config(buf, interval, rec_int, wheel_sz_mm); + pt_write_data(out, buf); + } + else if (!regexec(&rider_reg, line, rider_nmatch, rider_pmatch, 0)) { + for (i = 1; i < rider_nmatch; ++i) + line[rider_pmatch[i].rm_eo] = '\0'; + mins = atof(line + rider_pmatch[1].rm_so); + nm = atof(line + rider_pmatch[2].rm_so); + if (strcmp(line + rider_pmatch[4].rm_so, "NaN") == 0) + mph = -1.0; + else + mph = atof(line + rider_pmatch[4].rm_so); + if (strcmp(line + rider_pmatch[6].rm_so, "NaN") == 0) + watts = -1.0; + else + watts = atof(line + rider_pmatch[6].rm_so); + miles = atof(line + rider_pmatch[7].rm_so); + cad = atoi(line + rider_pmatch[9].rm_so); + if (strcmp(line + rider_pmatch[10].rm_so, "NaN") == 0) + hr = 255; + else + hr = atoi(line + rider_pmatch[10].rm_so); + interval = atoi(line + rider_pmatch[11].rm_so); + if (mins - last_mins - 0.021 > 1.0 / 60.0) { + inc_time = start_time + round((mins - 0.021000001) * 60.0); + if (!localtime_r(&inc_time, &inc_tm)) + assert(0); + pt_pack_time(buf, &inc_tm); + pt_write_data(out, buf); + pt_pack_config(buf, interval, rec_int, wheel_sz_mm); + pt_write_data(out, buf); + } + else if (last_interval != interval) { + pt_pack_config(buf, interval, rec_int, wheel_sz_mm); + pt_write_data(out, buf); + } + pt_pack_data(buf, wheel_sz_mm, nm, mph, + miles - last_miles, cad, hr); + pt_write_data(out, buf); + last_mins = mins; + last_interval = interval; + last_miles = miles; + } + else { + fprintf(stderr, "ERROR: line %d unrecognized: \"%s\"\n", + lineno, line); + exit(1); + } + ++lineno; + } +} + +static void +usage(const char *progname) +{ + fprintf(stderr, "usage: %s [-o ] [-r ]" + "[-w ] []\n", + progname); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + int ch, i; + int wheel_sz_mm = 2096; + int rec_int = 1; + char *inname = NULL, *outname = NULL; + FILE *in, *out = NULL; + + while ((ch = getopt(argc, argv, "ho:r:w:")) != -1) { + switch (ch) { + case 'o': + outname = optarg; + if (strcmp(outname, "-") == 0) { + out = stdout; + outname = "STDOUT"; + } + break; + case 'r': + rec_int = atoi(optarg); + break; + case 'w': + wheel_sz_mm = atoi(optarg); + break; + case 'h': + case '?': + default: + usage(argv[0]); + } + } + argc -= optind; + argv += optind; + + if (argc == 0) { + in = stdin; + inname = "STDIN"; + if (outname == NULL) { + out = stdout; + outname = "STDOUT"; + } + } + else { + inname = argv[0]; + if ((in = fopen(inname, "r")) == NULL) { + fprintf(stderr, "Couldn't open %s for reading: %s\n", + inname, strerror(errno)); + exit(1); + } + if (outname == NULL) { + outname = malloc(strlen(inname) + 5); + strcpy(outname, inname); + for (i = strlen(outname); i >= 0; --i) + if (outname[i] == '.') break; + if (i >= 0) + strcpy(outname + i + 1, "raw"); + else + strcpy(outname + strlen(outname), ".raw"); + } + } + + if ((out == NULL) && ((out = fopen(outname, "w")) == NULL)) { + fprintf(stderr, "Couldn't open %s for writing: %s\n", + outname, strerror(errno)); + exit(1); + } + + pt_pack(in, out, wheel_sz_mm, rec_int); + + fclose(in); + fclose(out); + + return 0; +} + diff --git a/ptunpk.c b/ptunpk.c new file mode 100644 index 000000000..eb0bc889b --- /dev/null +++ b/ptunpk.c @@ -0,0 +1,152 @@ +/* + * $Id: ptunpk.c,v 1.4 2006/06/04 14:32:34 srhea Exp $ + * + * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include +#include "pt.h" + +#define KM_TO_MI 0.62137119 +#define BAD_KM_TO_MI 0.62 + +static FILE *out; + +static void +config_cb(unsigned interval, unsigned rec_int, unsigned wheel_sz_mm, + void *context) +{ + context = NULL; + fprintf(out, "# wheel size=%d mm, interval=%d, rec int=%d\n", + wheel_sz_mm, interval, rec_int); +} + +static void +time_cb(struct tm *time, time_t since_epoch, void *context) +{ + context = NULL; + fprintf(out, "# %d/%d/%d %d:%02d:%02d %d\n", + time->tm_year + 1900, time->tm_mon + 1, time->tm_mday, + time->tm_hour, time->tm_min, time->tm_sec, (int) since_epoch); +} + +static void +data_cb(double secs, double nm, double mph, double watts, double miles, + unsigned cad, unsigned hr, unsigned interval, void *context) +{ + context = NULL; + fprintf(out, "%.3f %.1f", secs / 60.0, nm); + if (mph == -1.0) + fprintf(out, " NaN NaN"); + else + fprintf(out, " %0.3f %.0f", mph, watts); + fprintf(out, " %.5f %d", miles, cad); + if (hr == 0) + fprintf(out, " NaN"); + else + fprintf(out, " %d", hr); + fprintf(out, " %d\n", interval); +} + +static void +error_cb(const char *msg, void *context) +{ + context = NULL; + fprintf(stderr, "%s\n", msg); + exit(1); +} + +static void +usage(const char *progname) +{ + fprintf(stderr, "usage: %s [-c] [-o ] []\n", + progname); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + int ch, i, compat = 0; + char *inname = NULL, *outname = NULL; + FILE *in; + + while ((ch = getopt(argc, argv, "cho:")) != -1) { + switch (ch) { + case 'c': + compat = 1; + break; + case 'o': + outname = optarg; + if (strcmp(outname, "-") == 0) { + out = stdout; + outname = "STDOUT"; + } + break; + case 'h': + case '?': + default: + usage(argv[0]); + } + } + argc -= optind; + argv += optind; + + if (argc == 0) { + in = stdin; + inname = "STDIN"; + if (outname == NULL) { + out = stdout; + outname = "STDOUT"; + } + } + else { + inname = argv[0]; + if ((in = fopen(inname, "r")) == NULL) { + fprintf(stderr, "Couldn't open %s for reading: %s\n", + inname, strerror(errno)); + exit(1); + } + if (outname == NULL) { + outname = malloc(strlen(inname) + 5); + strcpy(outname, inname); + for (i = strlen(outname); i >= 0; --i) + if (outname[i] == '.') break; + if (i >= 0) + strcpy(outname + i + 1, "dat"); + else + strcpy(outname + strlen(outname), ".dat"); + } + } + + if ((out == NULL) && ((out = fopen(outname, "w")) == NULL)) { + fprintf(stderr, "Couldn't open %s for writing: %s\n", + outname, strerror(errno)); + exit(1); + } + + fprintf(out, "# Time Torq MPH Watts Miles Cad HR Int\n"); + + pt_read_raw(in, compat, NULL, config_cb, time_cb, data_cb, error_cb); + + return 0; +} + diff --git a/smooth.pl b/smooth.pl new file mode 100755 index 000000000..4f4606641 --- /dev/null +++ b/smooth.pl @@ -0,0 +1,53 @@ +#!/usr/bin/perl -w +# +# $Id: smooth.pl,v 1.1 2006/05/16 14:24:50 srhea Exp $ +# +# Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +use strict; + +if ($#ARGV < 0) { + print "usage: smooth.pl \n"; + exit 1; +} + +my $len = $ARGV[0] / 60.0; +my $count = 0; +my $total = 0; +my $start_time = 0; + +while () { + if (m/^#/) { + } + else { + my @cols = split; + if (!($cols[1] eq "NaN")) { + ++$count; + $total += $cols[1]; + } + if ($cols[0] >= $start_time + $len) { + if ($count > 0) { + printf "$start_time %f\n", $total / $count; + } + $start_time = $cols[0]; + $total = 0; + $count = 0; + } + } +} + diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..88f54c85f --- /dev/null +++ b/test.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# +# $Id: test.sh,v 1.2 2006/05/14 14:17:21 srhea Exp $ +# +# Copyright (c) 2006 Russ Cox (rsc@swtch.com) and Sean Rhea (srhea@srhea.net) +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +grep 'Rider' $1.xml | sed -e 's/<[^>]*>/ /g' > win +if ! ./ptunpk -c < $1.raw | grep -v '^#' > mac +then + echo "Failed to unpack $1.raw"; + exit 1 +fi +./compare_rows.pl win mac $2 +rm win mac +