From c85f5680c287fb58f93bb2f2ab45a697356964ed Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 17:13:36 +0000 Subject: [PATCH 01/66] Various re-working based on my experiences and fiddling: * Add a timeout to the wait_for_beacon() function (30 seconds) after which the USB device is reset and the process starts again. * Send a 'sleep' command to the tracker after a successful sync, based on the way the Windows software behaves. * Various changes to the main client function: + Write a brief log file to keep an eye on the status easily (/var/log/fitbit.log) + Make stdout line buffered for running under supervise/multilog for example. + Don't sleep for 15 minutes each time - since we now send a sleep command for the tracker it should make sense for the client program to listen for beacons all the time. --- python/fitbit.py | 16 +++++++-- python/fitbit_client.py | 72 +++++++++++++++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/python/fitbit.py b/python/fitbit.py index 31691f7..0ecb738 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -70,6 +70,10 @@ from antprotocol.bases import FitBitANT, DynastreamANT from antprotocol.protocol import ANTReceiveException +class FitBitBeaconTimeout(Exception): + """ + """ + class FitBit(object): """Class to represent the fitbit tracker device, the portion of the fitbit worn by the user. Stores information about the tracker @@ -181,16 +185,22 @@ def reset_tracker(self): # 0x78 0x01 is apparently the device reset command self.base.send_acknowledged_data([0x78, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + def command_sleep(self): + self.base.send_acknowledged_data([0x7f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c]) + def wait_for_beacon(self): # FitBit device initialization - print "Waiting for receive" - while True: + tries = 0 + while tries < 30: + print "Waiting for receive" + tries += 1 try: d = self.base._receive() if d[2] == 0x4E: - break + return except Exception: pass + raise FitBitBeaconTimeout("Timeout waiting for beacon, will restart") def _get_tracker_burst(self): d = self.base._check_burst_response() diff --git a/python/fitbit_client.py b/python/fitbit_client.py index f91d03a..53970a7 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -44,12 +44,14 @@ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ################################################################# +import usb +import time import sys import urllib import urllib2 import base64 import xml.etree.ElementTree as et -from fitbit import FitBit +from fitbit import FitBit, FitBitBeaconTimeout from antprotocol.bases import FitBitANT, DynastreamANT class FitBitResponse(object): @@ -86,6 +88,7 @@ class FitBitClient(object): def __init__(self): self.info_dict = {} + self.log_info = {} base = FitBitANT(debug=True) for retries in (2,1,0): @@ -113,6 +116,13 @@ def form_base_info(self): self.info_dict["clientId"] = self.CLIENT_UUID if self.remote_info: self.info_dict = dict(self.info_dict, **self.remote_info) + for f in ['deviceInfo.serialNumber','userPublicId']: + if f in self.info_dict: + self.log_info[f] = self.info_dict[f] + + def close(self): + if self.fitbit and self.fitbit.base: + self.fitbit.base.close() def run_upload_request(self): self.fitbit.init_tracker_for_transfer() @@ -140,33 +150,69 @@ def run_upload_request(self): else: print "No URL returned. Quitting." break + self.fitbit.command_sleep() self.fitbit.base.close() -def main(): +def do_sync(): f = FitBitClient() - f.run_upload_request() - return 0 + try: + f.run_upload_request() + except Exception, e: + f.close() + raise + f.close() + return f.log_info -if __name__ == '__main__': - import time +def log_field(log_info, f): + return (log_info[f] if f in log_info else '' % f) + +def log_prefix(log_info): + return '[' + time.ctime() + '] ' + \ + '[' + log_field(log_info, 'deviceInfo.serialNumber') + ' -> ' + log_field(log_info, 'userPublicId') + ']' + +def sleep_minutes(mins): + for m in range(mins, 0, -1): + print time.ctime(), "waiting", m, "minutes and then restarting..." + time.sleep(60) + +def main(): import traceback + import sys, os, signal + sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) - cycle_minutes = 15 + errors = 0 - while True: + while errors < 3: + log = open('/var/log/fitbit.log', 'a') + log_info = {} + signal.alarm(300) # safety limit try: - main() + log_info = do_sync() + except FitBitBeaconTimeout, e: + print e + except usb.USBError, e: + raise except Exception, e: print "Failed with", e print print '-'*60 traceback.print_exc(file=sys.stdout) print '-'*60 + ok = False + log.write('%s ERROR: %s\n' % (log_prefix(log_info), e)) + errors += 1 else: print "normal finish" - - print time.ctime(), "waiting", cycle_minutes, "minutes and then restarting..." - time.sleep(60*cycle_minutes) + log.write('%s SUCCESS\n' % (log_prefix(log_info))) + errors = 0 + log.close() + signal.alarm(0) + time.sleep(10) - #sys.exit(main()) + print 'exiting due to earlier failure' + return 1 + +if __name__ == '__main__': + sys.exit(main()) +# vim: set ts=4 sw=4 expandtab: From db40a104cc1a0e371bb9f310efc7cc4dba245c86 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 17:21:17 +0000 Subject: [PATCH 02/66] Add supervise/svscan run file. --- svscan/run | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 svscan/run diff --git a/svscan/run b/svscan/run new file mode 100755 index 0000000..302d5c9 --- /dev/null +++ b/svscan/run @@ -0,0 +1,7 @@ +#!/bin/sh + +# prevent burning CPU if there is a problem running the python script +sleep 3 + +echo Starting +exec python /opt/libfitbit/python/fitbit_client.py 2>&1 From 715fb39295707a5adf41b5bdffa769ee16917dbc Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 18:03:46 +0000 Subject: [PATCH 03/66] log error to fitbit.log when it is a USB error --- python/fitbit_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 53970a7..89d4eaf 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -191,6 +191,7 @@ def main(): except FitBitBeaconTimeout, e: print e except usb.USBError, e: + log.write('%s ERROR: %s\n' % (log_prefix(log_info), e)) raise except Exception, e: print "Failed with", e From 427c077121a650bda0f34df49b4a69ed041c936c Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 21:58:13 +0000 Subject: [PATCH 04/66] Move main logic into a FitBitDaemon class, and fix some logic to do with object creation/destruction. Also had to split some logic out of the main/run function into try_sync, to avoid storing a reference to the FitBitClient object in the stack frame of main/run when an exception is thrown. --- python/fitbit_client.py | 105 +++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 89d4eaf..7555fbd 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -107,6 +107,10 @@ def __init__(self): self.fitbit = FitBit(base) self.remote_info = None + def __del__(self): + self.close() + self.fitbit = None + def form_base_info(self): self.info_dict.clear() self.info_dict["beaconType"] = "standard" @@ -121,8 +125,12 @@ def form_base_info(self): self.log_info[f] = self.info_dict[f] def close(self): - if self.fitbit and self.fitbit.base: + try: + print 'Closing USB device' self.fitbit.base.close() + self.fitbit.base = None + except AttributeError, e: + pass def run_upload_request(self): self.fitbit.init_tracker_for_transfer() @@ -151,47 +159,35 @@ def run_upload_request(self): print "No URL returned. Quitting." break self.fitbit.command_sleep() - self.fitbit.base.close() - -def do_sync(): - f = FitBitClient() - try: - f.run_upload_request() - except Exception, e: - f.close() - raise - f.close() - return f.log_info - -def log_field(log_info, f): - return (log_info[f] if f in log_info else '' % f) - -def log_prefix(log_info): - return '[' + time.ctime() + '] ' + \ - '[' + log_field(log_info, 'deviceInfo.serialNumber') + ' -> ' + log_field(log_info, 'userPublicId') + ']' - -def sleep_minutes(mins): - for m in range(mins, 0, -1): - print time.ctime(), "waiting", m, "minutes and then restarting..." - time.sleep(60) - -def main(): - import traceback - import sys, os, signal - sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) - - errors = 0 - - while errors < 3: - log = open('/var/log/fitbit.log', 'a') + +class FitBitDaemon(object): + + def do_sync(self): + f = FitBitClient() + f.run_upload_request() + return f.log_info + + def log_field(self, log_info, f): + return (log_info[f] if f in log_info else '' % f) + + def log_prefix(self, log_info): + return '[' + time.ctime() + '] ' + \ + '[' + self.log_field(log_info, 'deviceInfo.serialNumber') + ' -> ' + self.log_field(log_info, 'userPublicId') + ']' + + def sleep_minutes(mins): + for m in range(mins, 0, -1): + print time.ctime(), "waiting", m, "minutes and then restarting..." + time.sleep(60) + + def try_sync(self): + import traceback log_info = {} - signal.alarm(300) # safety limit try: - log_info = do_sync() + log_info = self.do_sync() except FitBitBeaconTimeout, e: print e except usb.USBError, e: - log.write('%s ERROR: %s\n' % (log_prefix(log_info), e)) + self.log.write('%s ERROR: %s\n' % (self.log_prefix(log_info), e)) raise except Exception, e: print "Failed with", e @@ -200,20 +196,31 @@ def main(): traceback.print_exc(file=sys.stdout) print '-'*60 ok = False - log.write('%s ERROR: %s\n' % (log_prefix(log_info), e)) - errors += 1 + self.log.write('%s ERROR: %s\n' % (self.log_prefix(log_info), e)) + self.errors += 1 else: print "normal finish" - log.write('%s SUCCESS\n' % (log_prefix(log_info))) - errors = 0 - log.close() - signal.alarm(0) - time.sleep(10) - - print 'exiting due to earlier failure' - return 1 + self.log.write('%s SUCCESS\n' % (self.log_prefix(log_info))) + self.errors = 0 + + def run(self): + import sys, os, signal + sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) + + self.errors = 0 + + while self.errors < 3: + self.log = open('/var/log/fitbit.log', 'a') + signal.alarm(300) # safety limit + self.try_sync() + self.log.close() + signal.alarm(0) + time.sleep(10) + + print 'exiting due to earlier failure' + sys.exit(1) if __name__ == '__main__': - sys.exit(main()) + FitBitDaemon().run() -# vim: set ts=4 sw=4 expandtab: + # vim: set ts=4 sw=4 expandtab: From b24d27885ffaa5b02837851d106f94eccf5d4727 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 22:18:28 +0000 Subject: [PATCH 05/66] Tidy up the FitBitDaemon class. --- python/fitbit_client.py | 58 ++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 7555fbd..fe95593 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -162,17 +162,14 @@ def run_upload_request(self): class FitBitDaemon(object): + def __init__(self): + self.log_info = {} + self.log = None + def do_sync(self): f = FitBitClient() f.run_upload_request() - return f.log_info - - def log_field(self, log_info, f): - return (log_info[f] if f in log_info else '' % f) - - def log_prefix(self, log_info): - return '[' + time.ctime() + '] ' + \ - '[' + self.log_field(log_info, 'deviceInfo.serialNumber') + ' -> ' + self.log_field(log_info, 'userPublicId') + ']' + self.log_info = f.log_info def sleep_minutes(mins): for m in range(mins, 0, -1): @@ -181,46 +178,71 @@ def sleep_minutes(mins): def try_sync(self): import traceback - log_info = {} + self.log_info = {} try: - log_info = self.do_sync() + self.do_sync() except FitBitBeaconTimeout, e: + # This error is fairly normal, do we don't increase error counter. print e except usb.USBError, e: - self.log.write('%s ERROR: %s\n' % (self.log_prefix(log_info), e)) + # Raise this error up the stack, since USB errors are fairly + # critical. + self.write_log('ERROR: ' + e) raise except Exception, e: + # For other errors, log and increase error counter. print "Failed with", e print print '-'*60 traceback.print_exc(file=sys.stdout) print '-'*60 - ok = False - self.log.write('%s ERROR: %s\n' % (self.log_prefix(log_info), e)) + self.write_log('ERROR: ' + e) self.errors += 1 else: + # Clear error counter after a successful sync. print "normal finish" - self.log.write('%s SUCCESS\n' % (self.log_prefix(log_info))) + self.write_log('SUCCESS') self.errors = 0 def run(self): import sys, os, signal sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) - self.errors = 0 while self.errors < 3: - self.log = open('/var/log/fitbit.log', 'a') signal.alarm(300) # safety limit + self.open_log() self.try_sync() - self.log.close() + self.close_log() signal.alarm(0) time.sleep(10) print 'exiting due to earlier failure' sys.exit(1) + # + # Logging functions + # + + def open_log(self): + self.log = open('/var/log/fitbit.log', 'a') + + def write_log(self, str): + self.log.write('%s %s\n' % (self.log_prefix(self.log_info))) + + def log_prefix(self): + return '[%s] [%s -> %s]' % (time.ctime(), \ + self.log_field(self.log_info, 'deviceInfo.serialNumber'), \ + self.log_field(self.log_info, 'userPublicId')) + + def log_field(self, f): + return (self.log_info[f] if f in self.log_info else '' % f) + + def close_log(self): + if (self.log): + self.log.close() + if __name__ == '__main__': FitBitDaemon().run() - # vim: set ts=4 sw=4 expandtab: +# vim: set ts=4 sw=4 expandtab: From 76b32659cd36027d9a1dd79a494c45000bf83ff8 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 22:52:39 +0000 Subject: [PATCH 06/66] fix missing format argument, and don't use a reserved word as a variable name. --- python/fitbit_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index fe95593..3838be1 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -227,8 +227,8 @@ def run(self): def open_log(self): self.log = open('/var/log/fitbit.log', 'a') - def write_log(self, str): - self.log.write('%s %s\n' % (self.log_prefix(self.log_info))) + def write_log(self, s): + self.log.write('%s %s\n' % (self.log_prefix(self.log_info), s)) def log_prefix(self): return '[%s] [%s -> %s]' % (time.ctime(), \ From 57a393ac5507eebe220862f513b741cdfff057d2 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 23:02:43 +0000 Subject: [PATCH 07/66] tidy up logging code a bit --- python/fitbit_client.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 3838be1..ab43add 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -228,15 +228,12 @@ def open_log(self): self.log = open('/var/log/fitbit.log', 'a') def write_log(self, s): - self.log.write('%s %s\n' % (self.log_prefix(self.log_info), s)) - - def log_prefix(self): - return '[%s] [%s -> %s]' % (time.ctime(), \ + self.log.write('[%s] [%s -> %s] %s\n' % (time.ctime(), \ self.log_field(self.log_info, 'deviceInfo.serialNumber'), \ - self.log_field(self.log_info, 'userPublicId')) + self.log_field(self.log_info, 'userPublicId'), s)) def log_field(self, f): - return (self.log_info[f] if f in self.log_info else '' % f) + return (self.log_info[f] if f in self.log_info else 'UNKNOWN') def close_log(self): if (self.log): From 1aa673cdb28696913ad8aa887e3e2998b909be83 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 23:04:20 +0000 Subject: [PATCH 08/66] use pass instead of dummy/empty string --- python/fitbit.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/fitbit.py b/python/fitbit.py index 0ecb738..5dd72b1 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -71,8 +71,7 @@ from antprotocol.protocol import ANTReceiveException class FitBitBeaconTimeout(Exception): - """ - """ + pass class FitBit(object): """Class to represent the fitbit tracker device, the portion of From 61adda1e6b8d9147f13d1751eded59d1098db9f9 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 23:06:09 +0000 Subject: [PATCH 09/66] move usb import into the only function that needs it --- python/fitbit_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index ab43add..ba3069c 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -44,7 +44,6 @@ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ################################################################# -import usb import time import sys import urllib @@ -178,6 +177,7 @@ def sleep_minutes(mins): def try_sync(self): import traceback + import usb self.log_info = {} try: self.do_sync() From 2c9f8f937dde6f90cbd3a59a80c4efc383529d33 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 23:10:13 +0000 Subject: [PATCH 10/66] slightly more python-esque loop style. --- python/fitbit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/fitbit.py b/python/fitbit.py index 5dd72b1..b5ec38f 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -189,10 +189,8 @@ def command_sleep(self): def wait_for_beacon(self): # FitBit device initialization - tries = 0 - while tries < 30: + for tries in range(30): print "Waiting for receive" - tries += 1 try: d = self.base._receive() if d[2] == 0x4E: @@ -358,3 +356,5 @@ def main(): if __name__ == '__main__': sys.exit(main()) + +# vim: set ts=4 sw=4 expandtab: From 036634be50b25ff1817a917293eac69737e2869f Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 23:16:45 +0000 Subject: [PATCH 11/66] remove unnecessary argument to log_field. --- python/fitbit_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index ba3069c..f699628 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -229,8 +229,8 @@ def open_log(self): def write_log(self, s): self.log.write('[%s] [%s -> %s] %s\n' % (time.ctime(), \ - self.log_field(self.log_info, 'deviceInfo.serialNumber'), \ - self.log_field(self.log_info, 'userPublicId'), s)) + self.log_field('deviceInfo.serialNumber'), \ + self.log_field('userPublicId'), s)) def log_field(self, f): return (self.log_info[f] if f in self.log_info else 'UNKNOWN') From 22632042aea376512fa0bc4ab8b267fee29369e6 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sat, 14 Jan 2012 23:26:40 +0000 Subject: [PATCH 12/66] need explicit string conversion. --- python/fitbit_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index f699628..7973e73 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -187,7 +187,7 @@ def try_sync(self): except usb.USBError, e: # Raise this error up the stack, since USB errors are fairly # critical. - self.write_log('ERROR: ' + e) + self.write_log('ERROR: ' + str(e)) raise except Exception, e: # For other errors, log and increase error counter. @@ -196,7 +196,7 @@ def try_sync(self): print '-'*60 traceback.print_exc(file=sys.stdout) print '-'*60 - self.write_log('ERROR: ' + e) + self.write_log('ERROR: ' + str(e)) self.errors += 1 else: # Clear error counter after a successful sync. From eb81c9d3c318c4c2b1664354817d8f994dca4c23 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sun, 15 Jan 2012 10:45:35 +0000 Subject: [PATCH 13/66] make the init code try harder to open the device and loop forever until it succeeds. --- python/fitbit_client.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 7973e73..093a4fe 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -90,18 +90,19 @@ def __init__(self): self.log_info = {} base = FitBitANT(debug=True) - for retries in (2,1,0): + connected = False + while not connected: try: - if not base.open(): - print "No devices connected!" - return + if base.open(): + connected = True + else: + print "No devices connected, waiting..." + time.sleep(30) except Exception, e: print e - if retries: - print "retrying" - time.sleep(5) - else: - raise + base.close() + print "retrying" + time.sleep(5) self.fitbit = FitBit(base) self.remote_info = None From 9b9ec6927f4cf0153a942871d6061d12c0d88492 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sun, 15 Jan 2012 14:23:59 +0000 Subject: [PATCH 14/66] remove alarm timeout, I don't think it's needed now that wait_for_beacon has its own timeout. --- python/fitbit_client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 093a4fe..90bc68d 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -206,16 +206,14 @@ def try_sync(self): self.errors = 0 def run(self): - import sys, os, signal + import sys, os sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) self.errors = 0 while self.errors < 3: - signal.alarm(300) # safety limit self.open_log() self.try_sync() self.close_log() - signal.alarm(0) time.sleep(10) print 'exiting due to earlier failure' From 2a2bd04906692613eb92fbbe9ae416504444d083 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Sun, 15 Jan 2012 23:48:37 +0000 Subject: [PATCH 15/66] use a larger receive buffer - seems a bit crude, but does seem to help. Also fix what seems like a logic error in a condition. --- python/antprotocol/protocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/antprotocol/protocol.py b/python/antprotocol/protocol.py index 1a1c9f9..b79ab64 100644 --- a/python/antprotocol/protocol.py +++ b/python/antprotocol/protocol.py @@ -245,10 +245,10 @@ def _check_burst_response(self): failure = False while 1: try: - status = self._receive(15) + status = self._receive(1024) except ANTReceiveException: failure = True - if len(status) > 0 and status[2] == 0x50 or status[2] == 0x4f: + if len(status) > 0 and (status[2] == 0x50 or status[2] == 0x4f): response = response + status[4:-1].tolist() if (status[3] >> 4) > 0x8 or status[2] == 0x4f: if failure: From 324129ba61a356eba89ad7308c778094c35a9739 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Mon, 16 Jan 2012 00:42:24 +0000 Subject: [PATCH 16/66] crude installer for svscan bits --- svscan/install.sh | 7 +++++++ svscan/log-run | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 svscan/install.sh create mode 100644 svscan/log-run diff --git a/svscan/install.sh b/svscan/install.sh new file mode 100644 index 0000000..a6a0c11 --- /dev/null +++ b/svscan/install.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +mkdir /etc/service/libfitbit +mkdir /etc/service/libfitbit/log +cp svscan/run /etc/service/libfitbit/run +cp svscan/log-run /etc/service/libfitbit/log/run +chmod +x /etc/service/libfitbit/run /etc/service/libfitbit/log/run diff --git a/svscan/log-run b/svscan/log-run new file mode 100644 index 0000000..52387b9 --- /dev/null +++ b/svscan/log-run @@ -0,0 +1,2 @@ +#!/bin/sh +exec multilog t s1000000 n10 ./main From df2323c2cdc63185cb0984bb4ec154a1d73cea65 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Mon, 16 Jan 2012 13:12:19 +0000 Subject: [PATCH 17/66] terrible hack to prefer newer pyusb instead of system one. --- python/fitbit_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 90bc68d..683f203 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -46,6 +46,7 @@ import time import sys +sys.path.insert(0, '/usr/local/lib/python2.6/dist-packages') import urllib import urllib2 import base64 From 782e485ae12ddad97bdd2ae52309407bf2502ba0 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Mon, 16 Jan 2012 16:29:34 +0000 Subject: [PATCH 18/66] move the evil path hack into svscan run file --- python/fitbit_client.py | 1 - svscan/run | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 683f203..90bc68d 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -46,7 +46,6 @@ import time import sys -sys.path.insert(0, '/usr/local/lib/python2.6/dist-packages') import urllib import urllib2 import base64 diff --git a/svscan/run b/svscan/run index 302d5c9..cbbe990 100755 --- a/svscan/run +++ b/svscan/run @@ -4,4 +4,5 @@ sleep 3 echo Starting +export PYTHONPATH='/usr/local/lib/python2.6/dist-packages' exec python /opt/libfitbit/python/fitbit_client.py 2>&1 From 1d06e6acc2965ee1bbbabdbe9262acf81655b9a6 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Wed, 18 Jan 2012 22:08:40 +0000 Subject: [PATCH 19/66] increase timeout in wait_for_beacon --- python/fitbit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/fitbit.py b/python/fitbit.py index b5ec38f..647d310 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -189,7 +189,7 @@ def command_sleep(self): def wait_for_beacon(self): # FitBit device initialization - for tries in range(30): + for tries in range(60): print "Waiting for receive" try: d = self.base._receive() From 7cb8617e8be544dce5fbfbfcd2443a23381d553d Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Wed, 18 Jan 2012 22:08:55 +0000 Subject: [PATCH 20/66] decrease sleep time during main loop --- python/fitbit_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 90bc68d..ac2c361 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -214,7 +214,7 @@ def run(self): self.open_log() self.try_sync() self.close_log() - time.sleep(10) + time.sleep(3) print 'exiting due to earlier failure' sys.exit(1) From dee10adb99aca6ac016ff2ba2cb308313f2f0379 Mon Sep 17 00:00:00 2001 From: Ben Smithurst Date: Wed, 18 Jan 2012 22:13:03 +0000 Subject: [PATCH 21/66] Send sleep command to tracker after successful sync. --- python/fitbit.py | 3 +++ python/fitbit_client.py | 1 + 2 files changed, 4 insertions(+) diff --git a/python/fitbit.py b/python/fitbit.py index 31691f7..ea30b6c 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -181,6 +181,9 @@ def reset_tracker(self): # 0x78 0x01 is apparently the device reset command self.base.send_acknowledged_data([0x78, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + def command_sleep(self): + self.base.send_acknowledged_data([0x7f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c]) + def wait_for_beacon(self): # FitBit device initialization print "Waiting for receive" diff --git a/python/fitbit_client.py b/python/fitbit_client.py index f91d03a..a0cf6fd 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -140,6 +140,7 @@ def run_upload_request(self): else: print "No URL returned. Quitting." break + self.fitbit.command_sleep() self.fitbit.base.close() def main(): From e6d497c77101e6302a4a84627231985b5339105e Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 26 Jun 2012 19:06:12 +0200 Subject: [PATCH 22/66] protocol: remove a superflous exception handling --- python/antprotocol/protocol.py | 61 ++++++++++++++++------------------ 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/python/antprotocol/protocol.py b/python/antprotocol/protocol.py index 7db0751..63b7846 100644 --- a/python/antprotocol/protocol.py +++ b/python/antprotocol/protocol.py @@ -87,38 +87,35 @@ def __init__(self, chan=0x00, debug=False): self._receiveBuffer = [] def _event_to_string(self, event): - try: - return { 0:"RESPONSE_NO_ERROR", - 1:"EVENT_RX_SEARCH_TIMEOUT", - 2:"EVENT_RX_FAIL", - 3:"EVENT_TX", - 4:"EVENT_TRANSFER_RX_FAILED", - 5:"EVENT_TRANSFER_TX_COMPLETED", - 6:"EVENT_TRANSFER_TX_FAILED", - 7:"EVENT_CHANNEL_CLOSED", - 8:"EVENT_RX_FAIL_GO_TO_SEARCH", - 9:"EVENT_CHANNEL_COLLISION", - 10:"EVENT_TRANSFER_TX_START", - 21:"CHANNEL_IN_WRONG_STATE", - 22:"CHANNEL_NOT_OPENED", - 24:"CHANNEL_ID_NOT_SET", - 25:"CLOSE_ALL_CHANNELS", - 31:"TRANSFER_IN_PROGRESS", - 32:"TRANSFER_SEQUENCE_NUMBER_ERROR", - 33:"TRANSFER_IN_ERROR", - 40:"INVALID_MESSAGE", - 41:"INVALID_NETWORK_NUMBER", - 48:"INVALID_LIST_ID", - 49:"INVALID_SCAN_TX_CHANNEL", - 51:"INVALID_PARAMETER_PROVIDED", - 53:"EVENT_QUE_OVERFLOW", - 64:"NVM_FULL_ERROR", - 65:"NVM_WRITE_ERROR", - 66:"ASSIGN_CHANNEL_ID", - 81:"SET_CHANNEL_ID", - 0x4b:"OPEN_CHANNEL"}[event] - except: - return "%02x" % event + return { 0:"RESPONSE_NO_ERROR", + 1:"EVENT_RX_SEARCH_TIMEOUT", + 2:"EVENT_RX_FAIL", + 3:"EVENT_TX", + 4:"EVENT_TRANSFER_RX_FAILED", + 5:"EVENT_TRANSFER_TX_COMPLETED", + 6:"EVENT_TRANSFER_TX_FAILED", + 7:"EVENT_CHANNEL_CLOSED", + 8:"EVENT_RX_FAIL_GO_TO_SEARCH", + 9:"EVENT_CHANNEL_COLLISION", + 10:"EVENT_TRANSFER_TX_START", + 21:"CHANNEL_IN_WRONG_STATE", + 22:"CHANNEL_NOT_OPENED", + 24:"CHANNEL_ID_NOT_SET", + 25:"CLOSE_ALL_CHANNELS", + 31:"TRANSFER_IN_PROGRESS", + 32:"TRANSFER_SEQUENCE_NUMBER_ERROR", + 33:"TRANSFER_IN_ERROR", + 40:"INVALID_MESSAGE", + 41:"INVALID_NETWORK_NUMBER", + 48:"INVALID_LIST_ID", + 49:"INVALID_SCAN_TX_CHANNEL", + 51:"INVALID_PARAMETER_PROVIDED", + 53:"EVENT_QUE_OVERFLOW", + 64:"NVM_FULL_ERROR", + 65:"NVM_WRITE_ERROR", + 66:"ASSIGN_CHANNEL_ID", + 81:"SET_CHANNEL_ID", + 0x4b:"OPEN_CHANNEL"}.get(event, "%02x" % event) def _check_reset_response(self, status): for tries in range(8): From 59c95d9b46c492584cf3baad6f02644427cae163 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 26 Jun 2012 19:07:05 +0200 Subject: [PATCH 23/66] protocol: Add indentation to the logs --- python/antprotocol/protocol.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/python/antprotocol/protocol.py b/python/antprotocol/protocol.py index 63b7846..2357dc8 100644 --- a/python/antprotocol/protocol.py +++ b/python/antprotocol/protocol.py @@ -54,7 +54,7 @@ def hexList(data): return map(lambda s: chr(s).encode('HEX'), data) def hexRepr(data): - return repr(hexList(data)) + return ' '.join(hexList(data)).upper() def intListToByteList(data): return map(lambda i: struct.pack('!H', i)[1], array.array('B', data)) @@ -65,15 +65,18 @@ class ANTStatusException(Exception): def log(f): def wrapper(self, *args, **kwargs): if self._debug: - print "Start", f.__name__, args, kwargs + print ' '*self._loglevel, "Start", f.__name__, args, kwargs + self._loglevel += 1 try: res = f(self, *args, **kwargs) except: if self._debug: - print "Fail", f.__name__ + print ' '*self._loglevel, "Fail", f.__name__ raise + finally: + self._loglevel -= 1 if self._debug: - print "End", f.__name__, res + print ' '*self._loglevel, "End", f.__name__, res return res return wrapper @@ -85,6 +88,7 @@ def __init__(self, chan=0x00, debug=False): self._state = 0 self._receiveBuffer = [] + self._loglevel = 0 def _event_to_string(self, event): return { 0:"RESPONSE_NO_ERROR", @@ -282,7 +286,7 @@ def _send_message(self, *args): data.append(reduce(operator.xor, data)) if self._debug: - print " sent: " + hexRepr(data) + print ' '*self._loglevel, " ==> " + hexRepr(data) return self._send(map(chr, array.array('B', data))) def _find_sync(self, buf, start=0): @@ -338,7 +342,7 @@ def _receive_message(self, size = 4096): continue self._receiveBuffer = data[l:] if self._debug: - print "received: " + hexRepr(p) + print ' '*self._loglevel," <== " + hexRepr(p) return p def _receive(self, size=4096): From 787b30d349eab2324e20082688af8263ac7ee346 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Wed, 27 Jun 2012 21:34:46 +0200 Subject: [PATCH 24/66] remove unused variable --- python/fitbit_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index d4071d9..1c8e957 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -136,7 +136,7 @@ def close(self): print 'Closing USB device' self.fitbit.base.close() self.fitbit.base = None - except AttributeError, e: + except AttributeError: pass def run_upload_request(self): From ee93082842defff245917d9eb0ab8a8e60a495a4 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Wed, 27 Jun 2012 23:30:45 +0200 Subject: [PATCH 25/66] fitbit_client: Refactor the main loop to make it more readable --- python/fitbit_client.py | 126 ++++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 44 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 1c8e957..00a83cf 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -45,6 +45,7 @@ ################################################################# import time +import yaml import sys import urllib import urllib2 @@ -54,7 +55,18 @@ from antprotocol.bases import FitBitANT, DynastreamANT class FitBitResponse(object): - def __init__(self, response): + + def __init__(self, url): + self.url = url + + def get_response(self, info_dict): + data = urllib.urlencode(info_dict) + req = urllib2.urlopen(self.url, data) + res = req.read() + print res + self.init(res) + + def init(self, response): self.current_opcode = {} self.opcodes = [] self.root = et.fromstring(response.strip()) @@ -68,17 +80,43 @@ def __init__(self, response): # Quick and dirty url encode split self.response = dict([x.split("=") for x in urllib.unquote(self.root.find("response").text).split("&")]) - for opcode in self.root.findall("device/remoteOps/remoteOp"): - op = {} - op["opcode"] = [ord(x) for x in base64.b64decode(opcode.find("opCode").text)] - op["payload"] = None - if opcode.find("payloadData").text is not None: - op["payload"] = [x for x in base64.b64decode(opcode.find("payloadData").text)] - self.opcodes.append(op) - + for remoteop in self.root.findall("device/remoteOps/remoteOp"): + self.opcodes.append(RemoteOp(remoteop)) + + def getNext(self): + if self.host: + return FitBitResponse("http://%s%s" % (self.host, self.path)) + return None + + def dump(self): + ops = [] + for op in self.opcodes: + ops.append(op.dump()) + return ops + def __repr__(self): return "" % (id(self), str(self.opcodes), str(self.response)) +class RemoteOp(object): + def __init__(self, data): + opcode = base64.b64decode(data.find("opCode").text) + self.opcode = [ord(x) for x in opcode] + self.payload = None + if data.find("payloadData").text is not None: + payload = base64.b64decode(data.find("payloadData").text) + self.payload = [x for x in payload] + + def run(self, fitbit): + res = fitbit.run_opcode(self.opcode, self.payload) + res = [chr(x) for x in res] + self.response = ''.join(res) + + def dump(self): + return {'request': + {'opcode': self.opcode, + 'payload': self.payload}, + 'response': self.response} + class FitBitClient(object): CLIENT_UUID = "2ea32002-a079-48f4-8020-0badd22939e3" #FITBIT_HOST = "http://client.fitbit.com:80" @@ -97,7 +135,6 @@ def __init__(self): if base.open(): print "Found %s base" % (base.NAME,) self.fitbit = FitBit(base) - self.remote_info = None break else: break @@ -118,15 +155,15 @@ def __del__(self): self.close() self.fitbit = None - def form_base_info(self): + def form_base_info(self, remote_info=None): self.info_dict.clear() self.info_dict["beaconType"] = "standard" self.info_dict["clientMode"] = "standard" self.info_dict["clientVersion"] = "1.0" self.info_dict["os"] = "libfitbit" self.info_dict["clientId"] = self.CLIENT_UUID - if self.remote_info: - self.info_dict = dict(self.info_dict, **self.remote_info) + if remote_info: + self.info_dict = dict(self.info_dict, **remote_info) for f in ['deviceInfo.serialNumber','userPublicId']: if f in self.info_dict: self.log_info[f] = self.info_dict[f] @@ -139,36 +176,33 @@ def close(self): except AttributeError: pass - def run_upload_request(self): - try: - self.fitbit.init_tracker_for_transfer() - - url = self.FITBIT_HOST + self.START_PATH - - # Start the request Chain - self.form_base_info() - while url is not None: - res = urllib2.urlopen(url, urllib.urlencode(self.info_dict)).read() - print res - r = FitBitResponse(res) - self.remote_info = r.response - self.form_base_info() - op_index = 0 - for o in r.opcodes: - self.info_dict["opResponse[%d]" % op_index] = base64.b64encode(''.join([chr(x) for x in self.fitbit.run_opcode(o["opcode"], o["payload"])])) - self.info_dict["opStatus[%d]" % op_index] = "success" - op_index += 1 - urllib.urlencode(self.info_dict) - print self.info_dict - if r.host: - url = "http://%s%s" % (r.host, r.path) - print url - else: - print "No URL returned. Quitting." - break - except: - self.fitbit.base.close() - raise + def run_request(self, op, index): + response = op.run(self.fitbit) + residx = "opResponse[%d]" % index + statusidx = "opStatus[%d]" % index + self.info_dict[residx] = base64.b64encode(op.response) + self.info_dict[statusidx] = "success" + + def run_upload_requests(self): + self.fitbit.init_tracker_for_transfer() + + conn = FitBitResponse(self.FITBIT_HOST + self.START_PATH) + + # Start the request Chain + self.form_base_info() + while conn is not None: + conn.get_response(self.info_dict) + self.form_base_info(conn.response) + op_index = 0 + for op in conn.opcodes: + self.run_request(op, op_index) + op_index += 1 + data = yaml.dump(conn.dump()) + f = open('response-%d.txt' % int(time.time()), 'w') + f.write(data) + f.close() + print self.info_dict + conn = conn.getNext() self.fitbit.command_sleep() @@ -180,7 +214,11 @@ def __init__(self): def do_sync(self): f = FitBitClient() - f.run_upload_request() + try: + f.run_upload_requests() + except: + f.close() + raise self.log_info = f.log_info def sleep_minutes(mins): From f99533fdfa847bf2d4a645469219f00b7fd1ce22 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Thu, 28 Jun 2012 08:32:12 +0200 Subject: [PATCH 26/66] client: rename FitBitResponse in FitBitRequest --- python/fitbit_client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 00a83cf..ad7fb88 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -54,7 +54,7 @@ from fitbit import FitBit, FitBitBeaconTimeout from antprotocol.bases import FitBitANT, DynastreamANT -class FitBitResponse(object): +class FitBitRequest(object): def __init__(self, url): self.url = url @@ -85,7 +85,7 @@ def init(self, response): def getNext(self): if self.host: - return FitBitResponse("http://%s%s" % (self.host, self.path)) + return FitBitRequest("http://%s%s" % (self.host, self.path)) return None def dump(self): @@ -95,7 +95,7 @@ def dump(self): return ops def __repr__(self): - return "" % (id(self), str(self.opcodes), str(self.response)) + return "" % (id(self), str(self.opcodes), str(self.response)) class RemoteOp(object): def __init__(self, data): @@ -186,7 +186,7 @@ def run_request(self, op, index): def run_upload_requests(self): self.fitbit.init_tracker_for_transfer() - conn = FitBitResponse(self.FITBIT_HOST + self.START_PATH) + conn = FitBitRequest(self.FITBIT_HOST + self.START_PATH) # Start the request Chain self.form_base_info() @@ -274,7 +274,7 @@ def run(self): # def open_log(self): - self.log = open('/var/log/fitbit.log', 'a') + self.log = open('fitbit.log', 'a') def write_log(self, s): self.log.write('[%s] [%s -> %s] %s\n' % (time.ctime(), \ From 6a85bcaf6d70b2eade61a3afc0cc8a7e1cf4d98b Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Sat, 30 Jun 2012 20:11:00 +0200 Subject: [PATCH 27/66] fitbit: unify data type used, everything is an array of number, opcode, payload and response --- python/fitbit.py | 10 +++++----- python/fitbit_client.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/python/fitbit.py b/python/fitbit.py index 3a5a529..97445eb 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -208,7 +208,7 @@ def _get_tracker_burst(self): return [] return d[8:8+size] - def run_opcode(self, opcode, payload = None): + def run_opcode(self, opcode, payload = []): for tries in range(4): try: self.send_tracker_packet(opcode) @@ -222,7 +222,7 @@ def run_opcode(self, opcode, payload = None): return self.get_data_bank() if data[1] == 0x61: # Send payload data to device - if payload is not None: + if len(payload) > 0: self.send_tracker_payload(payload) data = self.base.receive_acknowledged_reply() data.pop(0) @@ -235,8 +235,8 @@ def run_opcode(self, opcode, payload = None): def send_tracker_payload(self, payload): # The first packet will be the packet id, the length of the - # payload, and ends with the payload CRC - p = [0x00, self.gen_packet_id(), 0x80, len(payload), 0x00, 0x00, 0x00, 0x00, reduce(operator.xor, map(ord, payload))] + # payload, and ends with the payload checksum + p = [0x00, self.gen_packet_id(), 0x80, len(payload), 0x00, 0x00, 0x00, 0x00, reduce(operator.xor, payload)] prefix = itertools.cycle([0x20, 0x40, 0x60]) for i in range(0, len(payload), 8): current_prefix = prefix.next() @@ -245,7 +245,7 @@ def send_tracker_payload(self, payload): plist += [(current_prefix + 0x80) | self.base._chan] else: plist += [current_prefix | self.base._chan] - plist += map(ord, payload[i:i+8]) + plist += payload[i:i+8] while len(plist) < 9: plist += [0x0] p += plist diff --git a/python/fitbit_client.py b/python/fitbit_client.py index ad7fb88..04ec202 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -104,12 +104,12 @@ def __init__(self, data): self.payload = None if data.find("payloadData").text is not None: payload = base64.b64decode(data.find("payloadData").text) - self.payload = [x for x in payload] + self.payload = [ord(x) for x in payload] def run(self, fitbit): - res = fitbit.run_opcode(self.opcode, self.payload) - res = [chr(x) for x in res] - self.response = ''.join(res) + self.response = fitbit.run_opcode(self.opcode, self.payload) + res = [chr(x) for x in self.response] + return ''.join(res) def dump(self): return {'request': @@ -180,7 +180,7 @@ def run_request(self, op, index): response = op.run(self.fitbit) residx = "opResponse[%d]" % index statusidx = "opStatus[%d]" % index - self.info_dict[residx] = base64.b64encode(op.response) + self.info_dict[residx] = base64.b64encode(response) self.info_dict[statusidx] = "success" def run_upload_requests(self): From b0b48dd77cd4b9f0ddf7e9c1b436a8063d2ddd51 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Sun, 1 Jul 2012 19:15:32 +0200 Subject: [PATCH 28/66] client: dump only once per connection --- python/fitbit_client.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 04ec202..508f821 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -128,6 +128,8 @@ class FitBitClient(object): def __init__(self): self.info_dict = {} self.log_info = {} + self.time = time.time() + self.data = [] self.fitbit = None for base in [bc(debug=self.DEBUG) for bc in self.BASES]: for retries in (2,1,0): @@ -169,6 +171,10 @@ def form_base_info(self, remote_info=None): self.log_info[f] = self.info_dict[f] def close(self): + data = yaml.dump(self.data) + f = open('connection-%d.txt' % int(self.time), 'w') + f.write(data) + f.close() try: print 'Closing USB device' self.fitbit.base.close() @@ -197,11 +203,7 @@ def run_upload_requests(self): for op in conn.opcodes: self.run_request(op, op_index) op_index += 1 - data = yaml.dump(conn.dump()) - f = open('response-%d.txt' % int(time.time()), 'w') - f.write(data) - f.close() - print self.info_dict + self.data.append(conn.dump()) conn = conn.getNext() self.fitbit.command_sleep() From 7c9a13a138f61e46a3a5062ca2adfdbc8680a131 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Mon, 2 Jul 2012 19:21:28 +0200 Subject: [PATCH 29/66] client: Add command line parameter to run only once --- python/fitbit_client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 508f821..0b14c14 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -50,6 +50,7 @@ import urllib import urllib2 import base64 +import argparse import xml.etree.ElementTree as et from fitbit import FitBit, FitBitBeaconTimeout from antprotocol.bases import FitBitANT, DynastreamANT @@ -257,7 +258,7 @@ def try_sync(self): self.write_log('SUCCESS') self.errors = 0 - def run(self): + def run(self, args): import sys, os sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) self.errors = 0 @@ -266,6 +267,9 @@ def run(self): self.open_log() self.try_sync() self.close_log() + if args.once: + print "I'm done" + return time.sleep(3) print 'exiting due to earlier failure' @@ -291,6 +295,9 @@ def close_log(self): self.log.close() if __name__ == '__main__': - FitBitDaemon().run() + parser = argparse.ArgumentParser() + parser.add_argument("--once", help="Run the request only once", action="store_true") + args = parser.parse_args() + FitBitDaemon().run(args) # vim: set ts=4 sw=4 expandtab: From cbd5c9d3b794180e698c29bb5422b156fc064dd5 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Mon, 2 Jul 2012 23:38:34 +0200 Subject: [PATCH 30/66] client: remove an unused function and correct a typo --- python/fitbit_client.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 0b14c14..11dc40d 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -224,11 +224,6 @@ def do_sync(self): raise self.log_info = f.log_info - def sleep_minutes(mins): - for m in range(mins, 0, -1): - print time.ctime(), "waiting", m, "minutes and then restarting..." - time.sleep(60) - def try_sync(self): import traceback import usb @@ -236,7 +231,7 @@ def try_sync(self): try: self.do_sync() except FitBitBeaconTimeout, e: - # This error is fairly normal, do we don't increase error counter. + # This error is fairly normal, so we don't increase error counter. print e except usb.USBError, e: # Raise this error up the stack, since USB errors are fairly From 8e330dd647854de485147da7027bafb0d6045101 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 3 Jul 2012 20:23:18 +0200 Subject: [PATCH 31/66] client: set debug=False by default --- python/fitbit_client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 5ea0bbc..ca806de 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -65,7 +65,6 @@ def get_response(self, info_dict): data = urllib.urlencode(info_dict) req = urllib2.urlopen(self.url, data) res = req.read() - print res self.init(res) def init(self, response): @@ -125,16 +124,15 @@ class FitBitClient(object): #FITBIT_HOST = "http://client.fitbit.com:80" FITBIT_HOST = "https://client.fitbit.com" # only used for initial request START_PATH = "/device/tracker/uploadData" - DEBUG = True BASES = [FitBitANT, DynastreamANT] - def __init__(self): + def __init__(self, debug=False): self.info_dict = {} self.log_info = {} self.time = time.time() self.data = [] self.fitbit = None - for base in [bc(debug=self.DEBUG) for bc in self.BASES]: + for base in [bc(debug=debug) for bc in self.BASES]: for retries in (2,1,0): try: if base.open(): @@ -178,6 +176,7 @@ def close(self): f = open('connection-%d.txt' % int(self.time), 'w') f.write(data) f.close() + print data try: print 'Closing USB device' self.fitbit.base.close() From f5ca794175216ff06baa0d0ec7eb9e1b8b345c1d Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 3 Jul 2012 20:33:13 +0200 Subject: [PATCH 32/66] First draft at an interactive script: ifitbit --- python/ifitbit.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100755 python/ifitbit.py diff --git a/python/ifitbit.py b/python/ifitbit.py new file mode 100755 index 0000000..2273dcb --- /dev/null +++ b/python/ifitbit.py @@ -0,0 +1,36 @@ +#! /usr/bin/env python + +exit = False + +cmds = {} +helps = {} + +def command(cmd, help): + def decorator(fn): + cmds[cmd] = fn + helps[cmd] = help + def wrapped(*args): + return fn(*args) + return wrapped + return decorator + +@command('exit', 'Quit...') +def quit(): + global exit + exit = True + +@command('help', 'Print possible commands') +def print_help(): + for cmd in sorted(helps.keys()): + print '%s\t%s' % (cmd, helps[cmd]) + +while not exit: + input = raw_input('> ') + input = input.split(' ') + try: + f = cmds[input[0]] + except KeyError: + print 'Command %s not known' % input[0] + print_help() + continue + f(*input[1:]) From fc8e6d81a76592d90f253c60b5629e623d1d2c3c Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 3 Jul 2012 22:51:56 +0200 Subject: [PATCH 33/66] ifitbit: Add some fitbit commands: init, close, info, opcode --- python/ifitbit.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/python/ifitbit.py b/python/ifitbit.py index 2273dcb..e988ae3 100755 --- a/python/ifitbit.py +++ b/python/ifitbit.py @@ -16,6 +16,7 @@ def wrapped(*args): @command('exit', 'Quit...') def quit(): + print 'Bye !' global exit exit = True @@ -24,6 +25,65 @@ def print_help(): for cmd in sorted(helps.keys()): print '%s\t%s' % (cmd, helps[cmd]) +from antprotocol.bases import FitBitANT +from fitbit import FitBit + +base = None +tracker = None + +def checktracker(fn): + def wrapped(*args): + if tracker is None: + print "No tracker, initialize first" + return + return fn(*args) + return wrapped + +@command('init', 'Initialize the tracker') +def init(*args): + global tracker, base + debug = False + if len(args) >= 1: + debug = bool(int(args[0])) + if debug: print "Debug ON" + base = FitBitANT(debug=debug) + if not base.open(): + print "No device connected." + base = None + return + tracker = FitBit(base) + tracker.init_tracker_for_transfer() + +@command('close', 'Close all connections') +def close(): + global base, tracker + if base is not None: + print "Closing connctions" + base.close() + base = None + tracker = None + +@command('>', 'Run opcode') +@checktracker +def opcode(*args): + args = list(args) + while len(args) < 7: + # make it a full opcode + args.append('0') + code = [int(x, 16) for x in args[:7]] + payload = [int(x, 16) for x in args[7:]] + print '==> ', code #' '.join(['%02X' % x for x in code]) + if payload: + print ' -> ', ' '.join(['%02X' % x for x in payload]) + res = tracker.run_opcode(code, payload) + print '<== ',' '.join(['%02X' % x for x in res]) + +@command('info', 'Get tracker info') +@checktracker +def get_info(): + tracker.get_tracker_info() + print tracker + while not exit: input = raw_input('> ') input = input.split(' ') @@ -33,4 +93,11 @@ def print_help(): print 'Command %s not known' % input[0] print_help() continue - f(*input[1:]) + try: + f(*input[1:]) + except Exception, e: + # We need that to be able to close the connection nicely + print "BaD bAd BAd", e + exit = True + +close() From 322fe7b366ecfc808931a6f567297cb527e9ecc6 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 3 Jul 2012 23:23:30 +0200 Subject: [PATCH 34/66] ifitbit: Add a command to read data banks --- python/fitbit.py | 4 +++- python/ifitbit.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/python/fitbit.py b/python/fitbit.py index 34eae00..271c0ab 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -285,6 +285,7 @@ def get_data_bank(self): raise ANTReceiveException("Cannot complete data bank") def parse_bank2_data(self, data): + banklen = {(4, 14):16}.get((self.app_major_version, self.app_minor_version), 14) for i in range(0, len(data), 13): print ["0x%.02x" % x for x in data[i:i+13]] # First 4 bytes are seconds from Jan 1, 1970 @@ -319,7 +320,8 @@ def parse_bank0_data(self, data): time_index = time_index + 1 def parse_bank1_data(self, data): - for i in range(0, len(data), 14): + banklen = {(4, 14):16}.get((self.app_major_version, self.app_minor_version), 14) + for i in range(0, len(data), banklen): print ["0x%.02x" % x for x in data[i:i+13]] # First 4 bytes are seconds from Jan 1, 1970 daily_steps = data[i+7] << 8 | data[i+6] diff --git a/python/ifitbit.py b/python/ifitbit.py index e988ae3..917c536 100755 --- a/python/ifitbit.py +++ b/python/ifitbit.py @@ -84,6 +84,22 @@ def get_info(): tracker.get_tracker_info() print tracker +@command('read', 'Read data bank') +@checktracker +def read_bank(index): + idx = int(index) + data = tracker.run_data_bank_opcode(idx) + def pprint(data): + print ' '.join(["%02X" % x for x in data]) + {0: tracker.parse_bank0_data, + 1: tracker.parse_bank1_data, + 2: tracker.parse_bank2_data, +# 3: tracker.parse_bank3_data, +# 4: tracker.parse_bank4_data, +# 5: tracker.parse_bank5_data, + 6: tracker.parse_bank6_data, + }.get(idx, pprint)(data) + while not exit: input = raw_input('> ') input = input.split(' ') From 0249b25c62191893853c1ca377f0ff86e083e5ac Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Wed, 4 Jul 2012 00:05:16 +0200 Subject: [PATCH 35/66] ifitbit Add support for the erase command --- python/ifitbit.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/python/ifitbit.py b/python/ifitbit.py index 917c536..04e9bd3 100755 --- a/python/ifitbit.py +++ b/python/ifitbit.py @@ -100,6 +100,18 @@ def pprint(data): 6: tracker.parse_bank6_data, }.get(idx, pprint)(data) +@command('erase', 'Erase data bank') +@checktracker +def erase_bank(index, tstamp=None): + idx = int(index) + if tstamp is not None: + tstamp = int(tstamp) + data = tracker.erase_data_bank(idx, tstamp) + if data != [65, 0, 0, 0, 0, 0, 0]: + print "Bad", data + return + print "Done" + while not exit: input = raw_input('> ') input = input.split(' ') From c5c82bde7f2ca1c56ba533cd2ceb36be161fa00f Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Wed, 4 Jul 2012 19:40:20 +0200 Subject: [PATCH 36/66] fitbit: reorder the bank parsing functions --- python/fitbit.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python/fitbit.py b/python/fitbit.py index 928cbd8..3bd0772 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -293,13 +293,6 @@ def get_data_bank(self): data = data + bank raise ANTReceiveException("Cannot complete data bank") - def parse_bank2_data(self, data): - banklen = {(4, 14):16}.get((self.app_major_version, self.app_minor_version), 14) - for i in range(0, len(data), 13): - print ["0x%.02x" % x for x in data[i:i+13]] - # First 4 bytes are seconds from Jan 1, 1970 - print "Time: %s" % (datetime.datetime.fromtimestamp(data[i] | data[i + 1] << 8 | data[i + 2] << 16 | data[i + 3] << 24)) - def parse_bank0_data(self, data): # First 4 bytes are a time i = 0 @@ -337,6 +330,13 @@ def parse_bank1_data(self, data): record_date = datetime.datetime.fromtimestamp(data[i] | data[i + 1] << 8 | data[i + 2] << 16 | data[i + 3] << 24) print "Time: %s Daily Steps: %d" % (record_date, daily_steps) + def parse_bank2_data(self, data): + banklen = {(4, 14):16}.get((self.app_major_version, self.app_minor_version), 14) + for i in range(0, len(data), 13): + print ["0x%.02x" % x for x in data[i:i+13]] + # First 4 bytes are seconds from Jan 1, 1970 + print "Time: %s" % (datetime.datetime.fromtimestamp(data[i] | data[i + 1] << 8 | data[i + 2] << 16 | data[i + 3] << 24)) + def parse_bank6_data(self, data): i = 0 tstamp = 0 @@ -350,7 +350,7 @@ def parse_bank6_data(self, data): d = data[i:i+4] tstamp = d[3] | d[2] << 8 | d[1] << 16 | d[0] << 24 i += 4 - + def main(): #base = DynastreamANT(True) base = FitBitANT(debug=True) From 74e36469212258ff1a9ebde81efdd85bb50ac62b Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Wed, 4 Jul 2012 20:58:41 +0200 Subject: [PATCH 37/66] Document data format, first version --- doc/fitbit_data.rst | 143 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 doc/fitbit_data.rst diff --git a/doc/fitbit_data.rst b/doc/fitbit_data.rst new file mode 100644 index 0000000..5b86a7a --- /dev/null +++ b/doc/fitbit_data.rst @@ -0,0 +1,143 @@ + +==================== + FitBit Data Format +==================== + +:author: Benoît Allard +:date: July 4th, 2012 + +Introduction +============ + +This document is part of the `libfitbit project`_. + +This document aims at describing the data flow between the *fitbit +service* and the *tracker* itself. We will try to abstract as much as +possible the underlying protocol, and focus on the structure of the +data, and the way it is transfered. + +Data from this document has been gathered through reverse in depth +analyse of logs of communication between the *tracker* and the +*service*. See `method of operation`_. + +The motivation behind this analyse is at first the intelectual process +of + +Device Description +================== + +The *fitbit tracker* comes with a USB base and a software to be +installed on the host computer. The software will run in *daemon mode* +and request periodically the data from the *tracker*. This is done +through the air, and of course, only if the tracer is near to the +base. The software on the computer is not *tracker* dependent, and +every *tracker* can use any base to synchronise its data with the +fitbit service. + +Communication +============= + +A dialog between the software and the fitbit service always starts +with a web-request from the software containing the identifier of the +*tracker* found at the moment in the proximity of the base. The fitbit +service then answer with a serie of commands to be sent to the +*tracker*. Once those commands are executed on the *tracker*, the +software sends the raw answers to the fitbit service. This one answers +again with a serie of commands, which are to be executed on the +*tracker*. This go on until the fitbit service has nothing to ask +anymore. This are four round trips most of the time. The software then +put the *tracker* in sleep mode, indicating that he is not interested +in its data for the next 15 minutes. + +Method of operation +=================== + +The ``fitbit_client.py`` script of the original `libfitbit project`_ +has been modified to record on disc every transfered bits from and to +the *tracker*. This allow statistical analysis of days of data +transfer. + +Communication with the tracker +============================== + +The communication with the *tracker* is done through the *base* using +the `ANT protocol`_. + +The tracker receives one *opcode* at a time, optionally followed with +*payload*, and sends its *response*. One request from the *fitbit +service* contains from zero to eight requests for the *tracker*. Those +are read, write, erase and a few others yet to be enciphered. + +Opcodes +======= + +Memory banks +============ + +bank0 +----- + +.. note:: Because the timestamp ``0x80000000`` correspond to the 19th + of January 2038, we might expect the fitbit team to change + their data format before this date. Until this date, every + timestamps will not have their MSB set, and the distinction + between record and timestamps themselves will be easy. + +bank1 +----- + +bank2 +----- + +bank3 +----- + +bank4 +----- + +bank5 +----- + +bank6 +----- + +This bank contains data about **floors climbed**. + +Data format +........... + +This information is transfered on two bytes, the first byte having its +MSB set. There is one record per minute, and the records are prefixed +by a timestamp on four bytes in LSB format (see also `bank0`_). In +case where more than one minute separates two floor climbing record, +instead of an empty record, a new timestamp will be inserted before +the next climbing record. + +The number of floors climbed during the recorded minute is equal to +the value of the second byte divided by ten. + +Example: +........ + +:: + + 4F F0 4A 4B 80 0A 4F F0 4A FF 80 0A 80 14 4F F0 4B EF 80 14 80 14 4F + F0 4C DF 80 14 + +First we have a timestamp 0x4ff04a4b, then a record 0x800a, then a +timestamp 0x4ff04aff, then two records 0x800a and 0x8014, a timestamp +again 0x4ff04bef, two records 0x8014 and 0x8014, a timestamp 4ff04cdf +and one record 0x8014. + +We decode this as follow:: + + Time: 2012-07-01 15:02:03: 1 Floors + Time: 2012-07-01 15:05:03: 1 Floors + Time: 2012-07-01 15:06:03: 2 Floors + Time: 2012-07-01 15:09:03: 2 Floors + Time: 2012-07-01 15:10:03: 2 Floors + Time: 2012-07-01 15:13:03: 2 Floors + + +.. _`libfitbit project`: https://github.com/qdot/libfitbit +.. _`ANT protocol`: something here From e6127a5217d70ec9c817c4d6accf1e4096e6daa3 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Wed, 4 Jul 2012 22:30:24 +0200 Subject: [PATCH 38/66] Improve doc --- doc/fitbit_data.rst | 105 ++++++++++++++++++++++++++++++++++++++-- python/fitbit.py | 4 +- python/fitbit_client.py | 1 - 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/doc/fitbit_data.rst b/doc/fitbit_data.rst index 5b86a7a..068a2fd 100644 --- a/doc/fitbit_data.rst +++ b/doc/fitbit_data.rst @@ -21,7 +21,8 @@ analyse of logs of communication between the *tracker* and the *service*. See `method of operation`_. The motivation behind this analyse is at first the intelectual process -of +of deciphering data, but as well the will of having full control about +a tracker I wear day and night, and by such, data about my lifestyle. Device Description ================== @@ -71,8 +72,60 @@ are read, write, erase and a few others yet to be enciphered. Opcodes ======= -Memory banks -============ +An opcode is **always** seven bytes long. most of the time, only the +first few bytes are not zero. + +The memory read is not the same as the memory written, even if the +*index* can be the same. + +Get Device Information +---------------------- + +:opcode: [0x24, 0 (6 times)] +:response: [serial, hardrev, bslmaj, bslmin, appmaj, appmin, bslon, + onbase] + +The response first contains the serial number of the tracker on five +bytes, then the hardware revision, the BSL major version and minor +version, the App major version and minor version, if the BSL mode is +ON, and if the tracker is plugged on the base. Except the serial +number, every other information is coded on one byte. + +Read memory +----------- + +:opcode: [0x22, index, 0 (5 times)] +:response: data + +Where *index* is the index of the memory bank requested. + +The response is the content of the memory and its meaning differs from +memory to memory. + +Write memory +------------ + +:opcode: [0x23, index, datalen, 0 (4 times) ] +:payload: data +:response: [0x41, 0 (6 times)] + +Where *index* is the index of the memory to be written, and *datalen* the +length of the payload. + +The content of the payload is index dependant. + +Erase memory +------------ + +:opcode: [0x25, index, timestamp, 0] +:response: [0x41, 0 (6 times)] + +Where *index* is the index of the memory bank to be erased, and +*timestamp* (on four bytes, MSB) is the date until which the data +should be erased. + +Read Memory banks +================= bank0 ----- @@ -92,6 +145,31 @@ bank2 bank3 ----- +This bank contains data, but a request to read it is never sent from +the *fitbit service*. + +Data Format +........... + +This bank always contains thirty bytes. The meaning of only the first +ones is known. + +The first five bytes contains the serial number, followed by the +hardware revision. + +Example +....... + +:: + + 01 02 03 04 05 0C 08 10 08 01 08 00 00 FF D8 00 06 A9 1D 9E 43 6A 3A + 63 48 83 BA 6E 1D 64 + +Which can be decoded as follow:: + + Serial: 0102030405 + Hardware revision: 12 + bank4 ----- @@ -129,7 +207,7 @@ timestamp 0x4ff04aff, then two records 0x800a and 0x8014, a timestamp again 0x4ff04bef, two records 0x8014 and 0x8014, a timestamp 4ff04cdf and one record 0x8014. -We decode this as follow:: +This can be decoded as follow:: Time: 2012-07-01 15:02:03: 1 Floors Time: 2012-07-01 15:05:03: 1 Floors @@ -138,6 +216,25 @@ We decode this as follow:: Time: 2012-07-01 15:10:03: 2 Floors Time: 2012-07-01 15:13:03: 2 Floors +bank7 +----- + +This bank is never requested from the *fitbit service*. + +Its content is empty. + +Write memory banks +================== + +bank0 +----- + +This bank always receives 64 bytes. + +bank1 +----- + +This bank always receive 16 bytes. .. _`libfitbit project`: https://github.com/qdot/libfitbit .. _`ANT protocol`: something here diff --git a/python/fitbit.py b/python/fitbit.py index 3bd0772..c24bc5b 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -137,13 +137,13 @@ def __str__(self): """Returns string representation of tracker information""" return "Tracker Serial: %s\n" \ - "Firmware Version: %d\n" \ + "Hardware Version: %d\n" \ "BSL Version: %d.%d\n" \ "APP Version: %d.%d\n" \ "In Mode BSL? %s\n" \ "On Charger? %s\n" % \ ("".join(["%x" % (x) for x in self.serial]), - self.firmware_version, + self.hardware_version, self.bsl_major_version, self.bsl_minor_version, self.app_major_version, diff --git a/python/fitbit_client.py b/python/fitbit_client.py index ca806de..d80878a 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -78,7 +78,6 @@ def init(self, response): self.host = self.root.find("response").attrib["host"] self.path = self.root.find("response").attrib["path"] if self.root.find("response").text: - # Quick and dirty url encode split response = self.root.find("response").text self.response = dict(urlparse.parse_qsl(response)) From 8d326efb8f594e44f2821f6e0f20b6e0cdc4fa16 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Mon, 9 Jul 2012 19:41:34 +0200 Subject: [PATCH 39/66] Correct firmware version into hardware revision --- doc/fitbit_protocol.asciidoc | 4 ++-- python/fitbit.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/fitbit_protocol.asciidoc b/doc/fitbit_protocol.asciidoc index 8d629c2..7edfca3 100644 --- a/doc/fitbit_protocol.asciidoc +++ b/doc/fitbit_protocol.asciidoc @@ -199,7 +199,7 @@ listed below): - Client contacts website at /device/tracker/uploadData, sends basic client and platform information - Website replies with opcode for tracker data request -- Client gets tracker data (serial number, firmware version, etc...), +- Client gets tracker data (serial number, hardware revision, etc...), sends base response. Sends to /device/tracker/dumpData/lookupTracker - Website replies with website tracker and user ids based on tracker serial number, and opcodes for data dumping @@ -389,7 +389,7 @@ the tracker for information about itself. Taking the last two Device Data Portions: * Bytes 0-4 (G) - 5 byte Device Serial Number -* Byte 5 (H) - Firmware Version +* Byte 5 (H) - Hardware revision * Byte 6 (I) - BSL Major Version * Byte 7 (J) - BSL Minor Version (i.e. II.JJ = bsl version) * Byte 8 (K) - App Major Version diff --git a/python/fitbit.py b/python/fitbit.py index c24bc5b..ff9f3d7 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -76,7 +76,7 @@ class FitBitBeaconTimeout(Exception): class FitBit(object): """Class to represent the fitbit tracker device, the portion of the fitbit worn by the user. Stores information about the tracker - (serial number, firmware version, etc...). + (serial number, hardware version, etc...). """ @@ -95,8 +95,8 @@ def __init__(self, base = None): self.current_packet_id = None #: serial number of the tracker self.serial = None - #: firmware version loaded on the tracker - self.firmware_version = None + #: hardware version loaded on the tracker + self.hardware_version = None #: Major version of BSL (?) self.bsl_major_version = None #: Minor version of BSL (?) @@ -125,7 +125,7 @@ def parse_info_packet(self, data): """Parses the information gotten from the 0x24 retrieval command""" self.serial = data[0:5] - self.firmware_version = data[5] + self.hardware_version = data[5] self.bsl_major_version = data[6] self.bsl_minor_version = data[7] self.app_major_version = data[8] From 79c630557237799635a2978c8c9ac6dc2d060006 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Mon, 9 Jul 2012 19:42:22 +0200 Subject: [PATCH 40/66] fitbit: Allow reading of greater data banks --- python/fitbit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/fitbit.py b/python/fitbit.py index ff9f3d7..c1ebfb5 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -284,7 +284,7 @@ def erase_data_bank(self, index, tstamp=None): def get_data_bank(self): data = [] cmd = 0x70 # Send 0x70 on first burst - for parts in range(20): + for parts in range(40): bank = self.check_tracker_data_bank(self.current_bank_id, cmd) self.current_bank_id += 1 cmd = 0x60 # Send 0x60 on subsequent bursts From ecd616f69fc0373ae59fdd0ed52bdc040df3f745 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Mon, 9 Jul 2012 19:44:19 +0200 Subject: [PATCH 41/66] Improve doc --- doc/fitbit_data.rst | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/doc/fitbit_data.rst b/doc/fitbit_data.rst index 068a2fd..eac2b5d 100644 --- a/doc/fitbit_data.rst +++ b/doc/fitbit_data.rst @@ -127,15 +127,28 @@ should be erased. Read Memory banks ================= +.. _bank0r: + bank0 ----- +This bank contains records about **steps** and **score**. + +Data format +........... + +The records are three bytes long. The first always has its MSB +set. The second byte gives the active score, and the third the steps. + .. note:: Because the timestamp ``0x80000000`` correspond to the 19th of January 2038, we might expect the fitbit team to change their data format before this date. Until this date, every timestamps will not have their MSB set, and the distinction between record and timestamps themselves will be easy. +Example +....... + bank1 ----- @@ -173,6 +186,8 @@ Which can be decoded as follow:: bank4 ----- +This bank is the same as `bank0w`_. + bank5 ----- @@ -186,7 +201,7 @@ Data format This information is transfered on two bytes, the first byte having its MSB set. There is one record per minute, and the records are prefixed -by a timestamp on four bytes in LSB format (see also `bank0`_). In +by a timestamp on four bytes in LSB format (see also `bank0r`_). In case where more than one minute separates two floor climbing record, instead of an empty record, a new timestamp will be inserted before the next climbing record. @@ -226,10 +241,13 @@ Its content is empty. Write memory banks ================== +.. _bank0w: + bank0 ----- -This bank always receives 64 bytes. +This bank always receives 64 bytes. Those bytes are about the device +settings as set on the web page *Device Settings*. bank1 ----- From 5d67de76cf84aa984a5eb9627dbf0ed1177aa18a Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Mon, 9 Jul 2012 22:22:58 +0200 Subject: [PATCH 42/66] Add fitbit group on README --- README.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.asciidoc b/README.asciidoc index 75d48fd..473a051 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -10,6 +10,8 @@ Hardware access - http://openyou.org If you find libfitbit useful, please donate to the project at http://pledgie.com/campaigns/14375 +Join the libfitbit group on fitbit.com: http://www.fitbit.com/group/227FRX + == Credits and Thanks == Thanks to Matt Cutts for hooking me up with the hardware - From 6f0248a6119604037cd42cdbc8ae571a0660e380 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 17 Jul 2012 20:09:03 +0200 Subject: [PATCH 43/66] Parse more data types --- doc/fitbit_data.rst | 112 +++++++++++++++++++++++++++++++++++++++++++- python/fitbit.py | 54 +++++++++++++++++---- python/ifitbit.py | 15 +++++- 3 files changed, 169 insertions(+), 12 deletions(-) diff --git a/doc/fitbit_data.rst b/doc/fitbit_data.rst index eac2b5d..047469c 100644 --- a/doc/fitbit_data.rst +++ b/doc/fitbit_data.rst @@ -149,12 +149,90 @@ set. The second byte gives the active score, and the third the steps. Example ....... +.. _bank1r: + bank1 ----- +This bank is about the **daily statistics**. However, a new record +with the current data is appended each time the bank is being read, or +the clock switch to another day (midnight). + +Data format +........... + +Each record is 16 bytes long and starts with a timestamp on four +bytes. Follows then XX, steps, distance and 10 x floors. Both steps +and distance are stored on four bytes, the first one (probably +something with calories) and the floors are on two bytes. The unit +used for the distance is the centimeter. + +.. note:: On the first version of the fitbit tracker (**not** Ultra), + the records are 14 bytes long as they miss the last two + bytes about the amount of floors climbed. + +.. note:: The midnight records, are registered with the date of the + next day. A record for ``2012-07-07 00:00:00`` is actually + about the 6th of July. + +Example: +........ + +:: + + 60 A0 05 50 9C 41 53 19 00 00 31 E5 49 00 1E 00 + +Which can be interpreted as follow: + +- ``0x5005a060``: 2012-07-17 19:26:56 +- ``0x419c``: 16796 : approximately 1845 calories (*.1103 - 7) +- ``0x00001953``: 6483 steps +- ``0x0049e531``: 4.842801km +- ``0x001E``: 3 floors + bank2 ----- +This bank is about **recorded activities**. + +Data format +........... + +Records are 15 bytes long, they are prefixed with a timestamp on four bytes. + +The record can be categorized in multiple kinds. Their kind is decided +by the value of the byte 6. + +Value of 1: + + This is a stop of the recorded activity. the record will contain + information about the length of the activity, the steps, the + floors, and more. + +Value of 2: + + This one seems to always go in pair with the value of 3. + +Value of 3: + + This one seems to always go in pair with the value of 2, A record + with a value of 2 usually follow two seconds after the record with + a value of 3. + +Value of 4: + + Not found yet + +Value of 5: + + This means a start of the activity if all fields are set to 0, + else, the meaning is still to be discovered. + +Example +....... + + + bank3 ----- @@ -191,6 +269,13 @@ This bank is the same as `bank0w`_. bank5 ----- +* This bank is one of the few without timestamps +* This bank is not related to the amount of steps. +* An erase has an effect on the data, however, the values don't go + to 0. +* This bank is always 14 bytes long. + + bank6 ----- @@ -247,7 +332,32 @@ bank0 ----- This bank always receives 64 bytes. Those bytes are about the device -settings as set on the web page *Device Settings*. +settings as set on the web page *Device Settings* and *Profile +Settings*. + +Data format +........... + +The following information is to prefix with a big **it looks +like... ** as those are only estimation based on different seen +values. + +* First four bytes are always zero +* Then are some bytes about yourself: + - length + - preferred unit + - stride length + - gender + - time zone +* what should be displayed on the tracker + - greeting on/off + - left hand / right hand + - clock format +* 7 zeros +* options displayed on tracker +* the greeting name (10 bytes), right padded with 2 zeros +* the three chatter texts (3 x 10 bytes). each right padded with 2 + zeros bank1 ----- diff --git a/python/fitbit.py b/python/fitbit.py index c1ebfb5..6e73010 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -322,20 +322,46 @@ def parse_bank0_data(self, data): time_index = time_index + 1 def parse_bank1_data(self, data): - banklen = {(4, 14):16}.get((self.app_major_version, self.app_minor_version), 14) + ultra = self.hardware_version >= 12 + banklen = {12:16}.get(self.hardware_version, 14) for i in range(0, len(data), banklen): - print ["0x%.02x" % x for x in data[i:i+13]] + d = data[i:i+banklen] # First 4 bytes are seconds from Jan 1, 1970 - daily_steps = data[i+7] << 8 | data[i+6] - record_date = datetime.datetime.fromtimestamp(data[i] | data[i + 1] << 8 | data[i + 2] << 16 | data[i + 3] << 24) - print "Time: %s Daily Steps: %d" % (record_date, daily_steps) + maybe_calories = d[5] << 8| d[4] + daily_steps = d[9] << 24 | d[8] << 16 | d[7] << 8 | d[6] + daily_dist = (d[13] << 24 | d[12] << 16 | d[11] << 8 | d[10]) / 1000000. + daily_floors = 0 + if ultra: + daily_floors = (d[15] << 8 | d[14]) / 10 + record_date = datetime.datetime.fromtimestamp(d[0] | d[1] << 8 | d[2] << 16 | d[3] << 24) + print "Time: %s %d Daily Steps: %d, Daily distance: %fkm Daily floors: %d" % ( + record_date, maybe_calories, daily_steps, daily_dist, daily_floors) def parse_bank2_data(self, data): - banklen = {(4, 14):16}.get((self.app_major_version, self.app_minor_version), 14) - for i in range(0, len(data), 13): - print ["0x%.02x" % x for x in data[i:i+13]] + ultra = self.hardware_version >= 12 + banklen = {12:15}.get(self.hardware_version, 13) + for i in range(0, len(data), banklen): + d = data[i:i+banklen] # First 4 bytes are seconds from Jan 1, 1970 - print "Time: %s" % (datetime.datetime.fromtimestamp(data[i] | data[i + 1] << 8 | data[i + 2] << 16 | data[i + 3] << 24)) + print "Time: %s" % (datetime.datetime.fromtimestamp(d[0] | d[1] << 8 | d[2] << 16 | d[3] << 24)) + if d[6] == 1: + elapsed = (d[5] << 8) | d[4] + steps = (d[9]<< 16) | (d[8] << 8) | d[7] + dist = (d[12] << 16) | (d[11]<< 8) | d[10] + foors = 0 + if ultra: + floors = ((d[14] << 8) | d[13]) / 10 + print "Activity summary: duration: %s, %d steps, %fkm, %d floors" % ( + datetime.timedelta(seconds=elapsed), steps, dist / 100000., floors / 10) + else: + print ' '.join(['%02X' % x for x in d[4:]]) + + + def parse_bank4_data(self, data): + assert len(data) == 64 + print ' '.join(["0x%.02x" % x for x in data[:24]]) + print "Greeting : ", ''.join([chr(x) for x in data[24:24+8]]) + print "Chatter: ", ', '.join([''.join([chr(x) for x in data[i:i+8]]) for i in range(34, 64, 10)]) def parse_bank6_data(self, data): i = 0 @@ -351,6 +377,16 @@ def parse_bank6_data(self, data): tstamp = d[3] | d[2] << 8 | d[1] << 16 | d[0] << 24 i += 4 + def write_settings(self, options ,greetings = "", chatter = []): + greeting = greeting.ljust( 8, '\0') + for i in range(max(len(chatter), 3)): + chatter[i] = chatter[i].ljust(8, '\0') + + self.write_bank(0, payload) + + def write_bank(self, index, data): + self.run_opcode([0x25, index, len(data), 0,0,0,0], data) + def main(): #base = DynastreamANT(True) base = FitBitANT(debug=True) diff --git a/python/ifitbit.py b/python/ifitbit.py index 04e9bd3..9dddcce 100755 --- a/python/ifitbit.py +++ b/python/ifitbit.py @@ -27,6 +27,7 @@ def print_help(): from antprotocol.bases import FitBitANT from fitbit import FitBit +import time base = None tracker = None @@ -58,7 +59,7 @@ def init(*args): def close(): global base, tracker if base is not None: - print "Closing connctions" + print "Closing connection" base.close() base = None tracker = None @@ -95,11 +96,21 @@ def pprint(data): 1: tracker.parse_bank1_data, 2: tracker.parse_bank2_data, # 3: tracker.parse_bank3_data, -# 4: tracker.parse_bank4_data, + 4: tracker.parse_bank4_data, # 5: tracker.parse_bank5_data, 6: tracker.parse_bank6_data, }.get(idx, pprint)(data) +@command('pr5', 'Periodic read 5') +@checktracker +def pr5(sleep = '5', repeat = '100'): + sleep = int(sleep) + repeat = int(repeat) + while repeat > 0: + read_bank(5) + time.sleep(sleep) + repeat -= 1 + @command('erase', 'Erase data bank') @checktracker def erase_bank(index, tstamp=None): From a5b135118302ce9ab4b18e4e6f5c58b12029bf19 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 17 Jul 2012 20:12:55 +0200 Subject: [PATCH 44/66] ifitbit: we need the tracker info to parse correctly the banks --- python/ifitbit.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/python/ifitbit.py b/python/ifitbit.py index 9dddcce..ca96241 100755 --- a/python/ifitbit.py +++ b/python/ifitbit.py @@ -40,6 +40,14 @@ def wrapped(*args): return fn(*args) return wrapped +def checkinfo(fn): + def wrapped(*args): + if tracker.hardware_version is None: + print "You first need to request the tracker info (info)" + return + return fn(*args) + return wrapped + @command('init', 'Initialize the tracker') def init(*args): global tracker, base @@ -87,6 +95,7 @@ def get_info(): @command('read', 'Read data bank') @checktracker +@checkinfo def read_bank(index): idx = int(index) data = tracker.run_data_bank_opcode(idx) From 2f9329014596630eb52218b36667036ecbaf67d5 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 7 Aug 2012 18:08:37 +0200 Subject: [PATCH 45/66] Improve the way the base is found --- python/antprotocol/bases.py | 17 +++++++++++++++++ python/fitbit.py | 5 ++--- python/fitbit_client.py | 28 ++++++---------------------- python/ifitbit.py | 4 ++-- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/python/antprotocol/bases.py b/python/antprotocol/bases.py index adf3be3..8439c3b 100644 --- a/python/antprotocol/bases.py +++ b/python/antprotocol/bases.py @@ -2,6 +2,21 @@ from .libusb import ANTlibusb import usb +def getBase(debug): + for base in [bc(debug=debug) for bc in BASES]: + for retries in (2,1,0): + try: + if base.open(): + print "Found %s base" % (base.NAME,) + return base + except Exception, e: + print e + if retries: + print "retrying" + time.sleep(5) + continue + raise + class DynastreamANT(ANTlibusb): """Class that represents the Dynastream USB stick base, for garmin/suunto equipment. Only needs to set VID/PID. @@ -53,3 +68,5 @@ def init(self): self._receive() except usb.USBError: pass + +BASES = [FitBitANT, DynastreamANT] diff --git a/python/fitbit.py b/python/fitbit.py index 6e73010..cfa8507 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -67,7 +67,7 @@ # - Implementing data clearing import itertools, sys, random, operator, datetime, time -from antprotocol.bases import FitBitANT, DynastreamANT +from antprotocol.bases import getBase from antprotocol.protocol import ANTReceiveException class FitBitBeaconTimeout(Exception): @@ -388,8 +388,7 @@ def write_bank(self, index, data): self.run_opcode([0x25, index, len(data), 0,0,0,0], data) def main(): - #base = DynastreamANT(True) - base = FitBitANT(debug=True) + base = getBase(True) if not base.open(): print "No devices connected!" return 1 diff --git a/python/fitbit_client.py b/python/fitbit_client.py index d80878a..3d119aa 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -54,7 +54,7 @@ import argparse import xml.etree.ElementTree as et from fitbit import FitBit, FitBitBeaconTimeout -from antprotocol.bases import FitBitANT, DynastreamANT +from antprotocol.bases import getBase class FitBitRequest(object): @@ -123,32 +123,17 @@ class FitBitClient(object): #FITBIT_HOST = "http://client.fitbit.com:80" FITBIT_HOST = "https://client.fitbit.com" # only used for initial request START_PATH = "/device/tracker/uploadData" - BASES = [FitBitANT, DynastreamANT] def __init__(self, debug=False): self.info_dict = {} self.log_info = {} self.time = time.time() self.data = [] - self.fitbit = None - for base in [bc(debug=debug) for bc in self.BASES]: - for retries in (2,1,0): - try: - if base.open(): - print "Found %s base" % (base.NAME,) - self.fitbit = FitBit(base) - break - else: - break - except Exception, e: - print e - if retries: - print "retrying" - time.sleep(5) - else: - raise - if self.fitbit: - break + base = getBase(debug) + if not base: + print "No base found!" + exit(1) + self.fitbit = FitBit(base) if not self.fitbit: print "No devices connected!" exit(1) @@ -254,7 +239,6 @@ def try_sync(self): self.errors = 0 def run(self, args): - import sys, os sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) self.errors = 0 diff --git a/python/ifitbit.py b/python/ifitbit.py index ca96241..2109ec7 100755 --- a/python/ifitbit.py +++ b/python/ifitbit.py @@ -25,7 +25,7 @@ def print_help(): for cmd in sorted(helps.keys()): print '%s\t%s' % (cmd, helps[cmd]) -from antprotocol.bases import FitBitANT +from antprotocol.bases import getBase from fitbit import FitBit import time @@ -55,7 +55,7 @@ def init(*args): if len(args) >= 1: debug = bool(int(args[0])) if debug: print "Debug ON" - base = FitBitANT(debug=debug) + base = getBase(debug) if not base.open(): print "No device connected." base = None From d72fb32d341407abe948f0bffcb917825a2990a7 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 7 Aug 2012 18:09:34 +0200 Subject: [PATCH 46/66] fitbit: remove one more extra exception handler --- python/fitbit.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/python/fitbit.py b/python/fitbit.py index cfa8507..cce675f 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -190,13 +190,12 @@ def command_sleep(self): def wait_for_beacon(self): # FitBit device initialization for tries in range(60): - print "Waiting for receive" - try: - d = self.base._receive_message() - if d[2] == 0x4E: - return - except Exception: - pass + print "Waiting for beacon" + d = self.base._receive_message() + if d: print d + if d and d[2] == 0x4E: + print "Got it." + return raise FitBitBeaconTimeout("Timeout waiting for beacon, will restart") def _get_tracker_burst(self): From e9895cb9004dc560f8302b54a9dfca4338a2b698 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 7 Aug 2012 18:11:45 +0200 Subject: [PATCH 47/66] client: log for each tracker in a separate directory --- python/fitbit_client.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 3d119aa..e501640 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -44,6 +44,7 @@ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ################################################################# +import os import time import yaml import sys @@ -157,9 +158,12 @@ def form_base_info(self, remote_info=None): def close(self): data = yaml.dump(self.data) - f = open('connection-%d.txt' % int(self.time), 'w') - f.write(data) - f.close() + if 'userPublicId' in self.log_info: + if not os.path.isdir(self.log_info['userPublicId']): + os.makedirs(self.log_info['userPublicId']) + f = open(os.path.join(self.log_info['userPublicId'],'connection-%d.txt' % int(self.time)), 'w') + f.write(data) + f.close() print data try: print 'Closing USB device' From f34a3261a487fe0d15cc115026de0e32d5accba5 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 7 Aug 2012 23:25:24 +0200 Subject: [PATCH 48/66] Base: beter test existance --- python/fitbit.py | 2 +- python/fitbit_client.py | 2 +- python/ifitbit.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/python/fitbit.py b/python/fitbit.py index cce675f..8d01a4f 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -388,7 +388,7 @@ def write_bank(self, index, data): def main(): base = getBase(True) - if not base.open(): + if base is None: print "No devices connected!" return 1 diff --git a/python/fitbit_client.py b/python/fitbit_client.py index e501640..627f69e 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -131,7 +131,7 @@ def __init__(self, debug=False): self.time = time.time() self.data = [] base = getBase(debug) - if not base: + if base is None: print "No base found!" exit(1) self.fitbit = FitBit(base) diff --git a/python/ifitbit.py b/python/ifitbit.py index 2109ec7..4693f3b 100755 --- a/python/ifitbit.py +++ b/python/ifitbit.py @@ -56,9 +56,8 @@ def init(*args): debug = bool(int(args[0])) if debug: print "Debug ON" base = getBase(debug) - if not base.open(): + if base is None: print "No device connected." - base = None return tracker = FitBit(base) tracker.init_tracker_for_transfer() From e0eb729f73ffa51dab1ed7976d216dd6f4fc50e4 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 14 Aug 2012 09:33:07 +0200 Subject: [PATCH 49/66] protocol: strenghen the message checking --- python/antprotocol/protocol.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/python/antprotocol/protocol.py b/python/antprotocol/protocol.py index 2357dc8..10e9269 100644 --- a/python/antprotocol/protocol.py +++ b/python/antprotocol/protocol.py @@ -131,14 +131,14 @@ def _check_reset_response(self, status): return raise ANTStatusException("Failed to detect reset response") - def _check_ok_response(self): + def _check_ok_response(self, msgid): # response packets will always be 7 bytes status = self._receive_message() if len(status) == 0: raise ANTStatusException("No message response received!") - if status[2] == 0x40 and status[5] == 0x0: + if status[2] == 0x40 and status[4] == msgid and status[5] == 0x0: return raise ANTStatusException("Message status %d does not match 0x0 (NO_ERROR)" % (status[5])) @@ -163,47 +163,47 @@ def reset(self): @log def set_channel_frequency(self, freq): self._send_message(0x45, self._chan, freq) - self._check_ok_response() + self._check_ok_response(0x45) @log def set_transmit_power(self, power): self._send_message(0x47, 0x0, power) - self._check_ok_response() + self._check_ok_response(0x47) @log def set_search_timeout(self, timeout): self._send_message(0x44, self._chan, timeout) - self._check_ok_response() + self._check_ok_response(0x44) @log def send_network_key(self, network, key): self._send_message(0x46, network, key) - self._check_ok_response() + self._check_ok_response(0x46) @log def set_channel_period(self, period): self._send_message(0x43, self._chan, period) - self._check_ok_response() + self._check_ok_response(0x43) @log def set_channel_id(self, id): self._send_message(0x51, self._chan, id) - self._check_ok_response() + self._check_ok_response(0x51) @log def open_channel(self): self._send_message(0x4b, self._chan) - self._check_ok_response() + self._check_ok_response(0x4b) @log def close_channel(self): self._send_message(0x4c, self._chan) - self._check_ok_response() + self._check_ok_response(0x4c) @log def assign_channel(self): self._send_message(0x42, self._chan, 0x00, 0x00) - self._check_ok_response() + self._check_ok_response(0x42) @log def receive_acknowledged_reply(self, size = 13): From e754afe62767f161bf421c825556d08d44e4c183 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 19:02:49 +0200 Subject: [PATCH 50/66] ifitbit: display full traceback when something bad happen --- python/ifitbit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/ifitbit.py b/python/ifitbit.py index 4693f3b..dfb552f 100755 --- a/python/ifitbit.py +++ b/python/ifitbit.py @@ -1,5 +1,7 @@ #! /usr/bin/env python +import traceback, sys + exit = False cmds = {} @@ -145,6 +147,7 @@ def erase_bank(index, tstamp=None): except Exception, e: # We need that to be able to close the connection nicely print "BaD bAd BAd", e + traceback.print_exc(file=sys.stdout) exit = True close() From 10ecaafc7df010549fd80a3fce6d7db754939dca Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 19:28:56 +0200 Subject: [PATCH 51/66] fitbit_client: Add a --debug option --- python/fitbit_client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 627f69e..0d2453d 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -200,12 +200,13 @@ def run_upload_requests(self): class FitBitDaemon(object): - def __init__(self): + def __init__(self, debug): self.log_info = {} self.log = None + self.debug = debug def do_sync(self): - f = FitBitClient() + f = FitBitClient(self.debug) try: f.run_upload_requests() except: @@ -280,7 +281,8 @@ def close_log(self): if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument("--once", help="Run the request only once", action="store_true") + parser.add_argument("--debug", help="Display debug information", action="store_true") args = parser.parse_args() - FitBitDaemon().run(args) + FitBitDaemon(args.debug).run(args) # vim: set ts=4 sw=4 expandtab: From 1040b5a9bebc1e5ea9c68637aa707ab68e6a5aa4 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 19:50:46 +0200 Subject: [PATCH 52/66] reorganize the Exceptions hierarchically --- python/antprotocol/bases.py | 2 +- python/antprotocol/libusb.py | 1 - python/antprotocol/protocol.py | 44 ++++++++++++++++++---------------- python/fitbit.py | 21 ++++++++-------- python/fitbit_client.py | 15 ++++++------ 5 files changed, 43 insertions(+), 40 deletions(-) diff --git a/python/antprotocol/bases.py b/python/antprotocol/bases.py index 8439c3b..0dec5af 100644 --- a/python/antprotocol/bases.py +++ b/python/antprotocol/bases.py @@ -1,6 +1,6 @@ -from .protocol import ANTReceiveException from .libusb import ANTlibusb import usb +import time def getBase(debug): for base in [bc(debug=debug) for bc in BASES]: diff --git a/python/antprotocol/libusb.py b/python/antprotocol/libusb.py index 283f442..489413f 100644 --- a/python/antprotocol/libusb.py +++ b/python/antprotocol/libusb.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python ################################################################# # pyusb access for ant devices # By Kyle Machulis diff --git a/python/antprotocol/protocol.py b/python/antprotocol/protocol.py index 10e9269..152e514 100644 --- a/python/antprotocol/protocol.py +++ b/python/antprotocol/protocol.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python ################################################################# # ant message protocol # By Kyle Machulis @@ -47,8 +46,14 @@ import operator, struct, array, time -class ANTReceiveException(Exception): - pass +class ANTException(Exception): + """ Our Base Exception class """ + +class ReceiveException(ANTException): pass + +class StatusException(ReceiveException): pass + +class SendException(ANTException): pass def hexList(data): return map(lambda s: chr(s).encode('HEX'), data) @@ -59,9 +64,6 @@ def hexRepr(data): def intListToByteList(data): return map(lambda i: struct.pack('!H', i)[1], array.array('B', data)) -class ANTStatusException(Exception): - pass - def log(f): def wrapper(self, *args, **kwargs): if self._debug: @@ -125,23 +127,23 @@ def _check_reset_response(self, status): for tries in range(8): try: data = self._receive_message() - except ANTReceiveException: + except ReceiveException: continue if len(data) > 3 and data[2] == 0x6f and data[3] == status: return - raise ANTStatusException("Failed to detect reset response") + raise StatusException("Failed to detect reset response") def _check_ok_response(self, msgid): # response packets will always be 7 bytes status = self._receive_message() if len(status) == 0: - raise ANTStatusException("No message response received!") + raise StatusException("No message response received!") if status[2] == 0x40 and status[4] == msgid and status[5] == 0x0: return - raise ANTStatusException("Message status %d does not match 0x0 (NO_ERROR)" % (status[5])) + raise StatusException("Message status %d does not match 0x0 (NO_ERROR)" % (status[5])) @log def reset(self): @@ -211,7 +213,7 @@ def receive_acknowledged_reply(self, size = 13): status = self._receive_message(size) if len(status) > 4 and status[2] == 0x4F: return status[4:-1] - raise ANTReceiveException("Failed to receive acknowledged reply") + raise ReceiveException("Failed to receive acknowledged reply") @log def _check_tx_response(self, maxtries = 16): @@ -223,8 +225,8 @@ def _check_tx_response(self, maxtries = 16): if status[5] == 0x05: # TX successful return if status[5] == 0x06: # TX failed - raise ANTReceiveException("Transmission Failed") - raise ANTReceiveException("No Transmission Ack Seen") + raise ReceiveException("Transmission Failed") + raise ReceiveException("No Transmission Ack Seen") @log def _send_burst_data(self, data, sleep = None): @@ -236,10 +238,10 @@ def _send_burst_data(self, data, sleep = None): time.sleep(sleep) try: self._check_tx_response() - except ANTReceiveException: + except ReceiveException: continue return - raise ANTReceiveException("Failed to send burst data") + raise ReceiveException("Failed to send burst data") @log def _check_burst_response(self): @@ -247,7 +249,7 @@ def _check_burst_response(self): for tries in range(128): status = self._receive_message() if len(status) > 5 and status[2] == 0x40 and status[5] == 0x4: - raise ANTReceiveException("Burst receive failed by event!") + raise ReceiveException("Burst receive failed by event!") elif len(status) > 4 and status[2] == 0x4f: response = response + status[4:-1] return response @@ -255,7 +257,7 @@ def _check_burst_response(self): response = response + status[4:-1] if status[3] & 0x80: return response - raise ANTReceiveException("Burst receive failed to detect end") + raise ReceiveException("Burst receive failed to detect end") @log def send_acknowledged_data(self, l): @@ -263,10 +265,10 @@ def send_acknowledged_data(self, l): try: self._send_message(0x4f, self._chan, l) self._check_tx_response() - except ANTReceiveException: + except ReceiveException: continue return - raise ANTReceiveException("Failed to send Acknowledged Data") + raise ReceiveException("Failed to send Acknowledged Data") def send_str(self, instring): if len(instring) > 8: @@ -346,8 +348,8 @@ def _receive_message(self, size = 4096): return p def _receive(self, size=4096): - raise Exception("Need to define _receive function for ANT child class!") + raise NotImplementedError("Need to define _receive function for ANT child class!") def _send(self): - raise Exception("Need to define _send function for ANT child class!") + raise NotImplementedError("Need to define _send function for ANT child class!") diff --git a/python/fitbit.py b/python/fitbit.py index 8d01a4f..773cbd3 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -68,9 +68,9 @@ import itertools, sys, random, operator, datetime, time from antprotocol.bases import getBase -from antprotocol.protocol import ANTReceiveException +from antprotocol.protocol import ANTException, ReceiveException, SendException -class FitBitBeaconTimeout(Exception): +class FitBitBeaconTimeout(ReceiveException): pass class FitBit(object): @@ -201,7 +201,7 @@ def wait_for_beacon(self): def _get_tracker_burst(self): d = self.base._check_burst_response() if d[1] != 0x81: - raise Exception("Response received is not tracker burst! Got %s" % (d[0:2])) + raise ReceiveException("Response received is not tracker burst! Got %s" % (d[0:2])) size = d[3] << 8 | d[2] if size == 0: return [] @@ -226,11 +226,11 @@ def run_opcode(self, opcode, payload = []): data = self.base.receive_acknowledged_reply() data.pop(0) return data - raise Exception("run_opcode: opcode %s, no payload" % (opcode)) + raise SendException("run_opcode: opcode %s, no payload" % (opcode)) if data[1] == 0x41: data.pop(0) return data - raise Exception("Failed to run opcode %s" % (opcode)) + raise ANTException("Failed to run opcode %s" % (opcode)) def send_tracker_payload(self, payload): # The first packet will be the packet id, the length of the @@ -290,7 +290,7 @@ def get_data_bank(self): if len(bank) == 0: return data data = data + bank - raise ANTReceiveException("Cannot complete data bank") + raise ReceiveException("Cannot complete data bank") def parse_bank0_data(self, data): # First 4 bytes are a time @@ -347,7 +347,7 @@ def parse_bank2_data(self, data): elapsed = (d[5] << 8) | d[4] steps = (d[9]<< 16) | (d[8] << 8) | d[7] dist = (d[12] << 16) | (d[11]<< 8) | d[10] - foors = 0 + floors = 0 if ultra: floors = ((d[14] << 8) | d[13]) / 10 print "Activity summary: duration: %s, %d steps, %fkm, %d floors" % ( @@ -377,11 +377,12 @@ def parse_bank6_data(self, data): i += 4 def write_settings(self, options ,greetings = "", chatter = []): - greeting = greeting.ljust( 8, '\0') + greetings = greetings.ljust( 8, '\0') for i in range(max(len(chatter), 3)): chatter[i] = chatter[i].ljust(8, '\0') - - self.write_bank(0, payload) + payload = [] + if False: # not ready yet + self.write_bank(4, payload) def write_bank(self, index, data): self.run_opcode([0x25, index, len(data), 0,0,0,0], data) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 0d2453d..76e7eb3 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -56,6 +56,7 @@ import xml.etree.ElementTree as et from fitbit import FitBit, FitBitBeaconTimeout from antprotocol.bases import getBase +from antprotocol.exception import ANTException class FitBitRequest(object): @@ -223,13 +224,8 @@ def try_sync(self): except FitBitBeaconTimeout, e: # This error is fairly normal, so we don't increase error counter. print e - except usb.USBError, e: - # Raise this error up the stack, since USB errors are fairly - # critical. - self.write_log('ERROR: ' + str(e)) - raise - except Exception, e: - # For other errors, log and increase error counter. + except ANTException, e: + # For ANT errors, log and increase error counter. print "Failed with", e print print '-'*60 @@ -237,6 +233,11 @@ def try_sync(self): print '-'*60 self.write_log('ERROR: ' + str(e)) self.errors += 1 + except usb.USBError, e: + # Raise this error up the stack, since USB errors are fairly + # critical. + self.write_log('ERROR: ' + str(e)) + raise else: # Clear error counter after a successful sync. print "normal finish" From 0179cdc8e9240fa43c2da34651a544aec2e1a1be Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 20:02:00 +0200 Subject: [PATCH 53/66] Add class for representation of ANT Messages --- python/antprotocol/message.py | 115 ++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 python/antprotocol/message.py diff --git a/python/antprotocol/message.py b/python/antprotocol/message.py new file mode 100644 index 0000000..fc30d60 --- /dev/null +++ b/python/antprotocol/message.py @@ -0,0 +1,115 @@ +import operator + +UNASSIGN_CHANNEL = 0x41 +ASSIGN_CHANNEL = 0x42 +CHANNEL_ID = 0x51 +CHANNEL_PERIOD = 0x43 +SEARCH_TIMEOUT = 0x44 +CHANNEL_RF_FREQ = 0x45 +SET_NETWORK = 0x46 +TRANSMIT_POWER = 0x47 +ID_LIST_ADD = 0x59 +ID_LIST_CONFIG = 0x5A +CHANNEL_TX_POWER = 0x60 +LOW_PRIORITY_SEARCH_TIMEOUT = 0x63 +SERIAL_NUMBER_SET_CHANNEL_ID = 0x65 +ENABLE_EXT_RX_MESGS = 0x66 +ENABLE_LED = 0x68 +CRYSTAL_ENABLE = 0x6D +LIB_CONFIG = 0x6E +FREQUENCY_AGILITY = 0x70 +PROXIMITY_SEARCH = 0x71 +CHANNEL_SEARCH_PRIORITY = 0x75 + +STARTUP_MESSAGE = 0x6F +SERIAL_ERROR_MESSAGE = 0xAE + +SYSTEM_RESET = 0x4A +OPEN_CHANNEL = 0x4B +CLOSE_CHANNEL = 0x4C +OPEN_RX_SCAN_MODE = 0x5B +REQUEST_MESSAGE = 0x4D +SLEEP_MESSAGE = 0xC5 + +BROADCAST_DATA = 0x4E +ACKNOWLEDGE_DATA = 0x4F +BURST_TRANSFER_DATA = 0x50 + +CHANNEL_RESPONSE = 0x40 + +RESP_CHANNEL_STATUS = 0x52 +RESP_CHANNEL_ID = 0x51 +RESP_ANT_VERSION = 0x3E +RESP_CAPABILITIES = 0x54 +RESP_SERIAL_NUMBER = 0x61 + +CW_INIT = 0x53 +CW_TEST = 0x48 + +class Message(object): + def __init__(self): + self.sync = 0xa4 + try: + # for MessageOUT + self.len = 0 + self.cs = 0 + except AttributeError: + pass + self.id = None + self.data = [] + + def _raw(self, CS=False): + raw = [self.sync, self.len, self.id] + self.data + if CS: + raw.append(self.cs) + return raw + + def check_CS(self): + raw = [self.sync, self.len, self.id] + self.data + return reduce(operator.xor, self._raw()) == self.cs + + def toBytes(self): + return map(chr, self._raw(True)) + + def __str__(self): + return ' '.join(['%02X' % x for x in self._raw(True)]) + + +class MessageIN(Message): + def __init__(self, raw): + Message.__init__(self) + assert len(raw) >= 4 + assert raw[1] == len(raw) - 4 + self.sync = raw[0] + self.len = raw[1] + self.id = raw[2] + self.data = raw[3:-1] + self.cs = raw[-1] + + def __str__(self): + return '<== ' + Message.__str__(self) + +class MessageOUT(Message): + def __init__(self, msgid, *data): + Message.__init__(self) + self.id = msgid + + for l in list(data): + if isinstance(l, list): + self.data += l + else: + self.data.append(l) + + + @property + def cs(self): + return reduce(operator.xor, self._raw()) + + + @property + def len(self): + return len(self.data) + + + def __str__(self): + return '==> ' + Message.__str__(self) From e0e9cc02a800aed80e99ad142a13699405518317 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 20:32:49 +0200 Subject: [PATCH 54/66] Make use of the Message classes --- python/antprotocol/protocol.py | 75 +++++++++++++++------------------- python/fitbit.py | 16 ++++---- 2 files changed, 41 insertions(+), 50 deletions(-) diff --git a/python/antprotocol/protocol.py b/python/antprotocol/protocol.py index 152e514..23669c5 100644 --- a/python/antprotocol/protocol.py +++ b/python/antprotocol/protocol.py @@ -45,6 +45,7 @@ # import operator, struct, array, time +from message import MessageIN, MessageOUT class ANTException(Exception): """ Our Base Exception class """ @@ -53,6 +54,8 @@ class ReceiveException(ANTException): pass class StatusException(ReceiveException): pass +class NoMessageException(ReceiveException): pass + class SendException(ANTException): pass def hexList(data): @@ -126,24 +129,21 @@ def _event_to_string(self, event): def _check_reset_response(self, status): for tries in range(8): try: - data = self._receive_message() + msg = self._receive_message() except ReceiveException: continue - if len(data) > 3 and data[2] == 0x6f and data[3] == status: + if msg.id == 0x6f and msg.data[0] == status: return raise StatusException("Failed to detect reset response") def _check_ok_response(self, msgid): # response packets will always be 7 bytes - status = self._receive_message() - - if len(status) == 0: - raise StatusException("No message response received!") + msg = self._receive_message() - if status[2] == 0x40 and status[4] == msgid and status[5] == 0x0: + if msg.id == 0x40 and msg.data[1:3] == [msgid, 0x00]: return - raise StatusException("Message status %d does not match 0x0 (NO_ERROR)" % (status[5])) + raise StatusException("Message status %d does not match 0x0 (NO_ERROR)" % (msg.data[2])) @log def reset(self): @@ -210,21 +210,21 @@ def assign_channel(self): @log def receive_acknowledged_reply(self, size = 13): for tries in range(30): - status = self._receive_message(size) - if len(status) > 4 and status[2] == 0x4F: - return status[4:-1] + msg = self._receive_message(size) + if msg.len > 0 and msg.id == 0x4F: + return msg.data[1:] raise ReceiveException("Failed to receive acknowledged reply") @log def _check_tx_response(self, maxtries = 16): for msgs in range(maxtries): - status = self._receive_message() - if len(status) > 5 and status[2] == 0x40: - if status[5] == 0x0a: # TX Start + msg = self._receive_message() + if msg.len > 1 and msg.id == 0x40: + if msg.data[2] == 0x0a: # TX Start continue - if status[5] == 0x05: # TX successful + if msg.data[2] == 0x05: # TX successful return - if status[5] == 0x06: # TX failed + if msg.data[2] == 0x06: # TX failed raise ReceiveException("Transmission Failed") raise ReceiveException("No Transmission Ack Seen") @@ -247,15 +247,15 @@ def _send_burst_data(self, data, sleep = None): def _check_burst_response(self): response = [] for tries in range(128): - status = self._receive_message() - if len(status) > 5 and status[2] == 0x40 and status[5] == 0x4: + msg = self._receive_message() + if msg.len > 1 and msg.id == 0x40 and msg.data[2] == 0x4: raise ReceiveException("Burst receive failed by event!") - elif len(status) > 4 and status[2] == 0x4f: - response = response + status[4:-1] + elif msg.len > 0 and msg.id == 0x4f: + response = response + msg.data[1:] return response - elif len(status) > 4 and status[2] == 0x50: - response = response + status[4:-1] - if status[3] & 0x80: + elif msg.len > 0 and msg.id == 0x50: + response = response + msg.data[1:] + if msg.data[0] & 0x80: return response raise ReceiveException("Burst receive failed to detect end") @@ -274,22 +274,13 @@ def send_str(self, instring): if len(instring) > 8: raise "string is too big" - return self._send_message(*[0x4e] + list(struct.unpack('%sB' % len(instring), instring))) - - def _send_message(self, *args): - data = list() - for l in list(args): - if isinstance(l, list): - data = data + l - else: - data.append(l) - data.insert(0, len(data) - 1) - data.insert(0, 0xa4) - data.append(reduce(operator.xor, data)) + return self._send_message(0x4e, list(struct.unpack('%sB' % len(instring), instring))) + def _send_message(self, msgid, *args): + msg = MessageOUT(msgid, *args) if self._debug: - print ' '*self._loglevel, " ==> " + hexRepr(data) - return self._send(map(chr, array.array('B', data))) + print ' '*self._loglevel, msg + return self._send(msg.toBytes()) def _find_sync(self, buf, start=0): i = 0; @@ -325,7 +316,7 @@ def _receive_message(self, size = 4096): if len(data) == 0: # Failed to find anything.. self._receiveBuffer = [] - return [] + raise NoMessageException() continue data = self._find_sync(data) if len(data) < l: continue @@ -336,16 +327,16 @@ def _receive_message(self, size = 4096): l = data[1] + 4 if len(data) < l: continue - p = data[0:l] - if reduce(operator.xor, p) != 0: + msg = MessageIN(data[:l]) + if not msg.check_CS(): if self._debug: print "Checksum error for proposed packet: " + hexRepr(p) data = self._find_sync(data, 1) continue self._receiveBuffer = data[l:] if self._debug: - print ' '*self._loglevel," <== " + hexRepr(p) - return p + print ' '*self._loglevel, msg + return msg def _receive(self, size=4096): raise NotImplementedError("Need to define _receive function for ANT child class!") diff --git a/python/fitbit.py b/python/fitbit.py index 773cbd3..b12c4fb 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -68,7 +68,7 @@ import itertools, sys, random, operator, datetime, time from antprotocol.bases import getBase -from antprotocol.protocol import ANTException, ReceiveException, SendException +from antprotocol.protocol import ANTException, ReceiveException, SendException, NoMessageException class FitBitBeaconTimeout(ReceiveException): pass @@ -191,9 +191,11 @@ def wait_for_beacon(self): # FitBit device initialization for tries in range(60): print "Waiting for beacon" - d = self.base._receive_message() - if d: print d - if d and d[2] == 0x4E: + try: + msg = self.base._receive_message() + except NoMessageException: + continue + if msg.id == 0x4E: print "Got it." return raise FitBitBeaconTimeout("Timeout waiting for beacon, will restart") @@ -224,12 +226,10 @@ def run_opcode(self, opcode, payload = []): if len(payload) > 0: self.send_tracker_payload(payload) data = self.base.receive_acknowledged_reply() - data.pop(0) - return data + return data[1:] raise SendException("run_opcode: opcode %s, no payload" % (opcode)) if data[1] == 0x41: - data.pop(0) - return data + return data[1:] raise ANTException("Failed to run opcode %s" % (opcode)) def send_tracker_payload(self, payload): From 4870da79c51cac6ccf5a9c79a382743ef57d8f0a Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 21:02:07 +0200 Subject: [PATCH 55/66] some small minor refactoring --- python/antprotocol/message.py | 1 - python/antprotocol/protocol.py | 25 +++++++++++++++++++++---- python/fitbit.py | 20 ++++---------------- python/fitbit_client.py | 4 ++-- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/python/antprotocol/message.py b/python/antprotocol/message.py index fc30d60..bf307c8 100644 --- a/python/antprotocol/message.py +++ b/python/antprotocol/message.py @@ -65,7 +65,6 @@ def _raw(self, CS=False): return raw def check_CS(self): - raw = [self.sync, self.len, self.id] + self.data return reduce(operator.xor, self._raw()) == self.cs def toBytes(self): diff --git a/python/antprotocol/protocol.py b/python/antprotocol/protocol.py index 23669c5..eee23b7 100644 --- a/python/antprotocol/protocol.py +++ b/python/antprotocol/protocol.py @@ -44,7 +44,7 @@ # Added to and untwistedized and fixed up by Kyle Machulis # -import operator, struct, array, time +import struct, array, time from message import MessageIN, MessageOUT class ANTException(Exception): @@ -56,6 +56,8 @@ class StatusException(ReceiveException): pass class NoMessageException(ReceiveException): pass +class FitBitBeaconTimeout(ReceiveException): pass + class SendException(ANTException): pass def hexList(data): @@ -140,10 +142,10 @@ def _check_ok_response(self, msgid): # response packets will always be 7 bytes msg = self._receive_message() - if msg.id == 0x40 and msg.data[1:3] == [msgid, 0x00]: + if msg.id == 0x40 and msg.len == 3 and msg.data[1:3] == [msgid, 0x00]: return - raise StatusException("Message status %d does not match 0x0 (NO_ERROR)" % (msg.data[2])) + raise StatusException("Message status %s does not match 0x0, 0x%x, 0x0 (NO_ERROR)" % (msg.data, msgid)) @log def reset(self): @@ -228,6 +230,20 @@ def _check_tx_response(self, maxtries = 16): raise ReceiveException("Transmission Failed") raise ReceiveException("No Transmission Ack Seen") + @log + def receive_bdcast(self): + # FitBit device initialization + for tries in range(60): + print "Waiting for beacon" + try: + msg = self._receive_message() + except NoMessageException: + continue + if msg.id == 0x4E: + print "Got it." + return + raise FitBitBeaconTimeout("Timeout waiting for beacon, will restart") + @log def _send_burst_data(self, data, sleep = None): for tries in range(2): @@ -330,9 +346,10 @@ def _receive_message(self, size = 4096): msg = MessageIN(data[:l]) if not msg.check_CS(): if self._debug: - print "Checksum error for proposed packet: " + hexRepr(p) + print "Checksum error for proposed packet: ", msg data = self._find_sync(data, 1) continue + # save the rest for later self._receiveBuffer = data[l:] if self._debug: print ' '*self._loglevel, msg diff --git a/python/fitbit.py b/python/fitbit.py index b12c4fb..83c1c71 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -68,10 +68,7 @@ import itertools, sys, random, operator, datetime, time from antprotocol.bases import getBase -from antprotocol.protocol import ANTException, ReceiveException, SendException, NoMessageException - -class FitBitBeaconTimeout(ReceiveException): - pass +from antprotocol.protocol import ANTException, ReceiveException, SendException class FitBit(object): """Class to represent the fitbit tracker device, the portion of @@ -188,17 +185,7 @@ def command_sleep(self): self.base.send_acknowledged_data([0x7f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c]) def wait_for_beacon(self): - # FitBit device initialization - for tries in range(60): - print "Waiting for beacon" - try: - msg = self.base._receive_message() - except NoMessageException: - continue - if msg.id == 0x4E: - print "Got it." - return - raise FitBitBeaconTimeout("Timeout waiting for beacon, will restart") + self.base.receive_bdcast() def _get_tracker_burst(self): d = self.base._check_burst_response() @@ -214,7 +201,8 @@ def run_opcode(self, opcode, payload = []): try: self.send_tracker_packet(opcode) data = self.base.receive_acknowledged_reply() - except: + except ANTException, ae: + print 'Failed to send Opcode %s : ' % opcode, ae continue if data[0] != self.current_packet_id: print "Tracker Packet IDs don't match! %02x %02x" % (data[0], self.current_packet_id) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 76e7eb3..3b256c1 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -54,9 +54,9 @@ import base64 import argparse import xml.etree.ElementTree as et -from fitbit import FitBit, FitBitBeaconTimeout +from fitbit import FitBit from antprotocol.bases import getBase -from antprotocol.exception import ANTException +from antprotocol.protocol import ANTException, FitBitBeaconTimeout class FitBitRequest(object): From b7cdb028cf8c2a2a29f5542c00cd3108279ec20c Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 21:03:18 +0200 Subject: [PATCH 56/66] fitbit: Increase data bank size --- python/fitbit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/fitbit.py b/python/fitbit.py index 83c1c71..4eab174 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -271,7 +271,7 @@ def erase_data_bank(self, index, tstamp=None): def get_data_bank(self): data = [] cmd = 0x70 # Send 0x70 on first burst - for parts in range(40): + for parts in range(400): bank = self.check_tracker_data_bank(self.current_bank_id, cmd) self.current_bank_id += 1 cmd = 0x60 # Send 0x60 on subsequent bursts From 8eac5f704a30e75537f80a03a5a1786c65a5ca57 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 22:17:14 +0200 Subject: [PATCH 57/66] ifitbit: Add the main from fitbit.py under the command 'test' --- python/fitbit.py | 39 --------------------------------------- python/ifitbit.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/python/fitbit.py b/python/fitbit.py index 4eab174..5df165f 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python ################################################################# # python fitbit object # By Kyle Machulis @@ -375,42 +374,4 @@ def write_settings(self, options ,greetings = "", chatter = []): def write_bank(self, index, data): self.run_opcode([0x25, index, len(data), 0,0,0,0], data) -def main(): - base = getBase(True) - if base is None: - print "No devices connected!" - return 1 - - device = FitBit(base) - - device.init_tracker_for_transfer() - - device.get_tracker_info() - # print device.tracker - - device.parse_bank2_data(device.run_data_bank_opcode(0x02)) - print "---" - device.parse_bank0_data(device.run_data_bank_opcode(0x00)) - device.run_data_bank_opcode(0x04) - d = device.run_data_bank_opcode(0x02) # 13 - for i in range(0, len(d), 13): - print ["%02x" % x for x in d[i:i+13]] - d = device.run_data_bank_opcode(0x00) # 7 - print ["%02x" % x for x in d[0:7]] - print ["%02x" % x for x in d[7:14]] - j = 0 - for i in range(14, len(d), 3): - print d[i:i+3] - j += 1 - print "Records: %d" % (j) - device.parse_bank1_data(device.run_data_bank_opcode(0x01)) - - # for i in range(0, len(d), 14): - # print ["%02x" % x for x in d[i:i+14]] - base.close() - return 0 - -if __name__ == '__main__': - sys.exit(main()) - # vim: set ts=4 sw=4 expandtab: diff --git a/python/ifitbit.py b/python/ifitbit.py index dfb552f..3426ed1 100755 --- a/python/ifitbit.py +++ b/python/ifitbit.py @@ -73,6 +73,44 @@ def close(): base = None tracker = None +@command('test', 'Run a test from the old fitbit.py') +def test(): + global base + if base is None: + base = getBase(True) + if base is None: + print "No devices connected!" + return 1 + + device = FitBit(base) + + device.init_tracker_for_transfer() + + device.get_tracker_info() + # print device.tracker + + device.parse_bank2_data(device.run_data_bank_opcode(0x02)) + print "---" + device.parse_bank0_data(device.run_data_bank_opcode(0x00)) + device.run_data_bank_opcode(0x04) + d = device.run_data_bank_opcode(0x02) # 13 + for i in range(0, len(d), 13): + print ["%02x" % x for x in d[i:i+13]] + d = device.run_data_bank_opcode(0x00) # 7 + print ["%02x" % x for x in d[0:7]] + print ["%02x" % x for x in d[7:14]] + j = 0 + for i in range(14, len(d), 3): + print d[i:i+3] + j += 1 + print "Records: %d" % (j) + device.parse_bank1_data(device.run_data_bank_opcode(0x01)) + + # for i in range(0, len(d), 14): + # print ["%02x" % x for x in d[i:i+14]] + base.close() + base = None + @command('>', 'Run opcode') @checktracker def opcode(*args): From 6cd3a8acf2e90b2d9b4301874d514c2dda885de4 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 22:32:15 +0200 Subject: [PATCH 58/66] fitbit_client: put the logs in a subdirectory of the home dir --- python/fitbit_client.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 3b256c1..ad5549b 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -152,26 +152,31 @@ def form_base_info(self, remote_info=None): self.info_dict["os"] = "libfitbit" self.info_dict["clientId"] = self.CLIENT_UUID if remote_info: - self.info_dict = dict(self.info_dict, **remote_info) + self.info_dict.update(remote_info) for f in ['deviceInfo.serialNumber','userPublicId']: if f in self.info_dict: self.log_info[f] = self.info_dict[f] - def close(self): + def dump_connection(self, directory='~/.fitbit'): + directory = os.path.expanduser(directory) data = yaml.dump(self.data) if 'userPublicId' in self.log_info: - if not os.path.isdir(self.log_info['userPublicId']): - os.makedirs(self.log_info['userPublicId']) - f = open(os.path.join(self.log_info['userPublicId'],'connection-%d.txt' % int(self.time)), 'w') + directory = os.path.join(directory, self.log_info['userPublicId']) + if not os.path.isdir(directory): + os.makedirs(directory) + f = open(os.path.join(directory,'connection-%d.txt' % int(self.time)), 'w') f.write(data) f.close() print data + + def close(self): + self.dump_connection() + print 'Closing USB device' try: - print 'Closing USB device' self.fitbit.base.close() - self.fitbit.base = None except AttributeError: pass + self.fitbit.base = None def run_request(self, op, index): response = op.run(self.fitbit) From 910b707bf8f45e7d0dd6e87e0693abf6ca2b0ed7 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 22:33:08 +0200 Subject: [PATCH 59/66] fitbit_client: be more resilient to errors and report them to the online service --- python/fitbit_client.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index ad5549b..15b6f4b 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -105,6 +105,7 @@ def __init__(self, data): opcode = base64.b64decode(data.find("opCode").text) self.opcode = [ord(x) for x in opcode] self.payload = None + self.response = None if data.find("payloadData").text is not None: payload = base64.b64decode(data.find("payloadData").text) self.payload = [ord(x) for x in payload] @@ -179,11 +180,18 @@ def close(self): self.fitbit.base = None def run_request(self, op, index): - response = op.run(self.fitbit) residx = "opResponse[%d]" % index statusidx = "opStatus[%d]" % index + try: + response = op.run(self.fitbit) + status = "success" + except ANTException: + print "failed ..." + response = '' + status = "error" self.info_dict[residx] = base64.b64encode(response) - self.info_dict[statusidx] = "success" + self.info_dict[statusidx] = status + return status = "success" def run_upload_requests(self): self.fitbit.init_tracker_for_transfer() From 7052e4a10272fbca2bbee83d55476ed727235ae1 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 21 Aug 2012 22:59:47 +0200 Subject: [PATCH 60/66] Small remaining errors ... --- python/antprotocol/bases.py | 2 +- python/fitbit_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/antprotocol/bases.py b/python/antprotocol/bases.py index 0dec5af..515adbf 100644 --- a/python/antprotocol/bases.py +++ b/python/antprotocol/bases.py @@ -9,7 +9,7 @@ def getBase(debug): if base.open(): print "Found %s base" % (base.NAME,) return base - except Exception, e: + except Exception, e: # We shouldn't except Exception print e if retries: print "retrying" diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 15b6f4b..753f1ac 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -191,7 +191,7 @@ def run_request(self, op, index): status = "error" self.info_dict[residx] = base64.b64encode(response) self.info_dict[statusidx] = status - return status = "success" + return status == "success" def run_upload_requests(self): self.fitbit.init_tracker_for_transfer() From 47a5e208a2ce977a50fd309526ee791f3dc41487 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Mon, 10 Sep 2012 23:04:47 +0200 Subject: [PATCH 61/66] client: Stop on first opcode failure --- python/fitbit_client.py | 69 +++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 753f1ac..d4a908e 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -60,36 +60,40 @@ class FitBitRequest(object): - def __init__(self, url): - self.url = url + def __init__(self, host, path, https = False, response = None, opcodes = []): + self.current_opcode = {} + self.opcodes = opcodes + self.response = response + self.host = host + self.path = path + if https: + scheme = 'https://' + else: + scheme = 'http://' + self.url = scheme + host + path - def get_response(self, info_dict): - data = urllib.urlencode(info_dict) + def upload(self, params): + data = urllib.urlencode(params) req = urllib2.urlopen(self.url, data) - res = req.read() - self.init(res) + self.rawresponse = req.read() - def init(self, response): - self.current_opcode = {} - self.opcodes = [] - self.root = et.fromstring(response.strip()) - self.host = None - self.path = None - self.response = None - if self.root.find("response") is not None: - self.host = self.root.find("response").attrib["host"] - self.path = self.root.find("response").attrib["path"] - if self.root.find("response").text: - response = self.root.find("response").text - self.response = dict(urlparse.parse_qsl(response)) + def getNext(self): + root = et.fromstring(self.rawresponse.strip()) + xmlresponse = root.find("response") + if xmlresponse is None: + return None + + host = xmlresponse.attrib["host"] + path = xmlresponse.attrib["path"] + response = xmlresponse.text + if response: + response = dict(urlparse.parse_qsl(response)) - for remoteop in self.root.findall("device/remoteOps/remoteOp"): - self.opcodes.append(RemoteOp(remoteop)) + opcodes = [] + for remoteop in root.findall("device/remoteOps/remoteOp"): + opcodes.append(RemoteOp(remoteop)) - def getNext(self): - if self.host: - return FitBitRequest("http://%s%s" % (self.host, self.path)) - return None + return FitBitRequest(host, path, response=response, opcodes=opcodes) def dump(self): ops = [] @@ -123,8 +127,7 @@ def dump(self): class FitBitClient(object): CLIENT_UUID = "2ea32002-a079-48f4-8020-0badd22939e3" - #FITBIT_HOST = "http://client.fitbit.com:80" - FITBIT_HOST = "https://client.fitbit.com" # only used for initial request + FITBIT_HOST = "client.fitbit.com" START_PATH = "/device/tracker/uploadData" def __init__(self, debug=False): @@ -196,17 +199,21 @@ def run_request(self, op, index): def run_upload_requests(self): self.fitbit.init_tracker_for_transfer() - conn = FitBitRequest(self.FITBIT_HOST + self.START_PATH) + conn = FitBitRequest(self.FITBIT_HOST, self.START_PATH, https=True) # Start the request Chain self.form_base_info() while conn is not None: - conn.get_response(self.info_dict) - self.form_base_info(conn.response) op_index = 0 for op in conn.opcodes: - self.run_request(op, op_index) + if not self.run_request(op, op_index): + # Don't continue if error + break op_index += 1 + + self.form_base_info(conn.response) + conn.upload(self.info_dict) + self.data.append(conn.dump()) conn = conn.getNext() From 06fd869cedd1a70fc0896fb53126fee5a7ae908f Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Thu, 13 Sep 2012 22:01:17 +0200 Subject: [PATCH 62/66] client: move the request runing to the connection itself --- python/fitbit_client.py | 59 ++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/python/fitbit_client.py b/python/fitbit_client.py index d4a908e..9b5b52a 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -81,6 +81,7 @@ def getNext(self): root = et.fromstring(self.rawresponse.strip()) xmlresponse = root.find("response") if xmlresponse is None: + print "That was it." return None host = xmlresponse.attrib["host"] @@ -95,6 +96,21 @@ def getNext(self): return FitBitRequest(host, path, response=response, opcodes=opcodes) + def run_opcodes(self, fitbit): + res = {} + op_index = 0 + for op in self.opcodes: + try: + op.run(fitbit) + res["opResponse[%d]" % op_index] = op.response + res["opStatus[%d]" % op_index] = op.status + except ANTException: + print "failed running", op.dump() + break + + op_index += 1 + return res + def dump(self): ops = [] for op in self.opcodes: @@ -108,22 +124,27 @@ class RemoteOp(object): def __init__(self, data): opcode = base64.b64decode(data.find("opCode").text) self.opcode = [ord(x) for x in opcode] + self.status = "failed" self.payload = None - self.response = None - if data.find("payloadData").text is not None: - payload = base64.b64decode(data.find("payloadData").text) + self.rawresponse = [] + self.response = '' + payload = data.find("payloadData").text + if payload is not None: + payload = base64.b64decode(payload) self.payload = [ord(x) for x in payload] def run(self, fitbit): - self.response = fitbit.run_opcode(self.opcode, self.payload) - res = [chr(x) for x in self.response] - return ''.join(res) + self.rawresponse = fitbit.run_opcode(self.opcode, self.payload) + response = [chr(x) for x in self.rawresponse] + self.response = base64.b64encode(''.join(response)) + self.status = "success" def dump(self): return {'request': {'opcode': self.opcode, 'payload': self.payload}, - 'response': self.response} + 'status': self.status, + 'response': self.rawresponse} class FitBitClient(object): CLIENT_UUID = "2ea32002-a079-48f4-8020-0badd22939e3" @@ -182,20 +203,6 @@ def close(self): pass self.fitbit.base = None - def run_request(self, op, index): - residx = "opResponse[%d]" % index - statusidx = "opStatus[%d]" % index - try: - response = op.run(self.fitbit) - status = "success" - except ANTException: - print "failed ..." - response = '' - status = "error" - self.info_dict[residx] = base64.b64encode(response) - self.info_dict[statusidx] = status - return status == "success" - def run_upload_requests(self): self.fitbit.init_tracker_for_transfer() @@ -204,14 +211,10 @@ def run_upload_requests(self): # Start the request Chain self.form_base_info() while conn is not None: - op_index = 0 - for op in conn.opcodes: - if not self.run_request(op, op_index): - # Don't continue if error - break - op_index += 1 - self.form_base_info(conn.response) + + self.info_dict.update(conn.run_opcodes(self.fitbit)) + conn.upload(self.info_dict) self.data.append(conn.dump()) From 372c61904bff3ab345909db70cdc73b265c27568 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 18 Sep 2012 23:26:16 +0200 Subject: [PATCH 63/66] protocol: regroup bases and libusb into 'connection' --HG-- rename : python/antprotocol/bases.py => python/antprotocol/connection.py --- python/antprotocol/bases.py | 72 ----------------- python/antprotocol/connection.py | 128 +++++++++++++++++++++++++++++++ python/antprotocol/libusb.py | 93 ---------------------- python/antprotocol/protocol.py | 13 +--- python/fitbit_client.py | 11 +-- python/ifitbit.py | 18 +++-- 6 files changed, 148 insertions(+), 187 deletions(-) delete mode 100644 python/antprotocol/bases.py create mode 100644 python/antprotocol/connection.py delete mode 100644 python/antprotocol/libusb.py diff --git a/python/antprotocol/bases.py b/python/antprotocol/bases.py deleted file mode 100644 index 515adbf..0000000 --- a/python/antprotocol/bases.py +++ /dev/null @@ -1,72 +0,0 @@ -from .libusb import ANTlibusb -import usb -import time - -def getBase(debug): - for base in [bc(debug=debug) for bc in BASES]: - for retries in (2,1,0): - try: - if base.open(): - print "Found %s base" % (base.NAME,) - return base - except Exception, e: # We shouldn't except Exception - print e - if retries: - print "retrying" - time.sleep(5) - continue - raise - -class DynastreamANT(ANTlibusb): - """Class that represents the Dynastream USB stick base, for - garmin/suunto equipment. Only needs to set VID/PID. - - """ - VID = 0x0fcf - PID = 0x1008 - NAME = "Dynastream" - -class FitBitANT(ANTlibusb): - """Class that represents the fitbit base. Due to the extra - hardware to handle tracker connection and charging, has an extra - initialization sequence. - - """ - - VID = 0x10c4 - PID = 0x84c4 - NAME = "FitBit" - - def open(self, vid = None, pid = None): - if not super(FitBitANT, self).open(vid, pid): - return False - self.init() - return True - - def init(self): - # Device setup - # bmRequestType, bmRequest, wValue, wIndex, data - self._connection.ctrl_transfer(0x40, 0x00, 0xFFFF, 0x0, []) - self._connection.ctrl_transfer(0x40, 0x01, 0x2000, 0x0, []) - # At this point, we get a 4096 buffer, then start all over - # again? Apparently doesn't require an explicit receive - self._connection.ctrl_transfer(0x40, 0x00, 0x0, 0x0, []) - self._connection.ctrl_transfer(0x40, 0x00, 0xFFFF, 0x0, []) - self._connection.ctrl_transfer(0x40, 0x01, 0x2000, 0x0, []) - self._connection.ctrl_transfer(0x40, 0x01, 0x4A, 0x0, []) - # Receive 1 byte, should be 0x2 - self._connection.ctrl_transfer(0xC0, 0xFF, 0x370B, 0x0, 1) - self._connection.ctrl_transfer(0x40, 0x03, 0x800, 0x0, []) - self._connection.ctrl_transfer(0x40, 0x13, 0x0, 0x0, \ - [0x08, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 - ]) - self._connection.ctrl_transfer(0x40, 0x12, 0x0C, 0x0, []) - try: - self._receive() - except usb.USBError: - pass - -BASES = [FitBitANT, DynastreamANT] diff --git a/python/antprotocol/connection.py b/python/antprotocol/connection.py new file mode 100644 index 0000000..035458f --- /dev/null +++ b/python/antprotocol/connection.py @@ -0,0 +1,128 @@ +import usb + +class ANTConnection(object): + """ An abstract class that represents a connection """ + + def open(self): + """ Open the connection """ + raise NotImplementedError() + + def close(self): + """ Close the connection """ + raise NotImplementedError() + + def send(self, bytes): + """ Send some bytes away """ + raise NotImplementedError() + + def receive(self, amount): + """ Get some bytes """ + raise NotImplementedError() + +class ANTUSBConnection(ANTConnection): + ep = { + 'in' : 0x81, + 'out' : 0x01 + } + + def __init__(self): + self._connection = False + self.timeout = 1000 + + def open(self): + self._connection = usb.core.find(idVendor = self.VID, + idProduct = self.PID) + if self._connection is None: + return False + + # For some reason, we have to set config, THEN reset, + # otherwise we segfault back in the ctypes (on linux, at + # least). + self._connection.set_configuration() + self._connection.reset() + # The we have to set our configuration again + self._connection.set_configuration() + + # Then we should get back a reset check, with 0x80 + # (SUSPEND_RESET) as our status + # + # I've commented this out because -- though it should just work + # it does seem to be causing some odd problems for me and does + # work with out it. Reed Wade - 31 Dec 2011 + ##self._check_reset_response(0x80) + return True + + def close(self): + if self._connection is not None: + self._connection = None + + def send(self, command): + # libusb expects ordinals, it'll redo the conversion itself. + c = command + self._connection.write(self.ep['out'], map(ord, c), 0, 100) + + def receive(self, amount): + return self._connection.read(self.ep['in'], amount, 0, self.timeout) + +class DynastreamANT(ANTUSBConnection): + """Class that represents the Dynastream USB stick base, for + garmin/suunto equipment. Only needs to set VID/PID. + + """ + VID = 0x0fcf + PID = 0x1008 + NAME = "Dynastream" + +class FitBitANT(ANTUSBConnection): + """Class that represents the fitbit base. Due to the extra + hardware to handle tracker connection and charging, has an extra + initialization sequence. + + """ + + VID = 0x10c4 + PID = 0x84c4 + NAME = "FitBit" + + def open(self): + if not super(FitBitANT, self).open(): + return False + self.init() + return True + + def init(self): + # Device setup + # bmRequestType, bmRequest, wValue, wIndex, data + self._connection.ctrl_transfer(0x40, 0x00, 0xFFFF, 0x0, []) + self._connection.ctrl_transfer(0x40, 0x01, 0x2000, 0x0, []) + # At this point, we get a 4096 buffer, then start all over + # again? Apparently doesn't require an explicit receive + self._connection.ctrl_transfer(0x40, 0x00, 0x0, 0x0, []) + self._connection.ctrl_transfer(0x40, 0x00, 0xFFFF, 0x0, []) + self._connection.ctrl_transfer(0x40, 0x01, 0x2000, 0x0, []) + self._connection.ctrl_transfer(0x40, 0x01, 0x4A, 0x0, []) + # Receive 1 byte, should be 0x2 + self._connection.ctrl_transfer(0xC0, 0xFF, 0x370B, 0x0, 1) + self._connection.ctrl_transfer(0x40, 0x03, 0x800, 0x0, []) + self._connection.ctrl_transfer(0x40, 0x13, 0x0, 0x0, \ + [0x08, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ]) + self._connection.ctrl_transfer(0x40, 0x12, 0x0C, 0x0, []) + try: + self.receive() + except usb.USBError: + pass + +CONNS = [FitBitANT, DynastreamANT] + + +def getConn(): + for conn in [bc() for bc in CONNS]: + if conn.open(): + print "Found %s base" % (conn.NAME,) + return conn + print "Failed to find a base" + return None diff --git a/python/antprotocol/libusb.py b/python/antprotocol/libusb.py deleted file mode 100644 index 489413f..0000000 --- a/python/antprotocol/libusb.py +++ /dev/null @@ -1,93 +0,0 @@ -################################################################# -# pyusb access for ant devices -# By Kyle Machulis -# http://www.nonpolynomial.com -# -# Licensed under the BSD License, as follows -# -# Copyright (c) 2011, Kyle Machulis/Nonpolynomial Labs -# All rights reserved. -# -# Redistribution and use in source and binary forms, -# with or without modification, are permitted provided -# that the following conditions are met: -# -# * Redistributions of source code must retain the -# above copyright notice, this list of conditions -# and the following disclaimer. -# * Redistributions in binary form must reproduce the -# above copyright notice, this list of conditions and -# the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# * Neither the name of the Nonpolynomial Labs nor the names -# of its contributors may be used to endorse or promote -# products derived from this software without specific -# prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND -# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -################################################################# -# - -from protocol import ANT -import usb - -class ANTlibusb(ANT): - ep = { 'in' : 0x81, \ - 'out' : 0x01 - } - - def __init__(self, chan=0x0, debug=False): - super(ANTlibusb, self).__init__(chan, debug) - self._connection = False - self.timeout = 1000 - - def open(self, vid = None, pid = None): - if vid is None: - vid = self.VID - if pid is None: - pid = self.PID - self._connection = usb.core.find(idVendor = vid, - idProduct = pid) - if self._connection is None: - return False - - # For some reason, we have to set config, THEN reset, - # otherwise we segfault back in the ctypes (on linux, at - # least). - self._connection.set_configuration() - self._connection.reset() - # The we have to set our configuration again - self._connection.set_configuration() - - # Then we should get back a reset check, with 0x80 - # (SUSPEND_RESET) as our status - # - # I've commented this out because -- though it should just work - # it does seem to be causing some odd problems for me and does - # work with out it. Reed Wade - 31 Dec 2011 - ##self._check_reset_response(0x80) - return True - - def close(self): - if self._connection is not None: - self._connection = None - - def _send(self, command): - # libusb expects ordinals, it'll redo the conversion itself. - c = command - self._connection.write(self.ep['out'], map(ord, c), 0, 100) - - def _receive(self, size=4096): - return self._connection.read(self.ep['in'], size, 0, self.timeout) diff --git a/python/antprotocol/protocol.py b/python/antprotocol/protocol.py index eee23b7..c6668d0 100644 --- a/python/antprotocol/protocol.py +++ b/python/antprotocol/protocol.py @@ -89,7 +89,8 @@ def wrapper(self, *args, **kwargs): class ANT(object): - def __init__(self, chan=0x00, debug=False): + def __init__(self, connection, chan=0x00, debug=False): + self.connection = connection self._debug = debug self._chan = chan @@ -296,7 +297,7 @@ def _send_message(self, msgid, *args): msg = MessageOUT(msgid, *args) if self._debug: print ' '*self._loglevel, msg - return self._send(msg.toBytes()) + return self.connection.send(msg.toBytes()) def _find_sync(self, buf, start=0): i = 0; @@ -319,7 +320,7 @@ def _receive_message(self, size = 4096): # data[] too small, try to read some more from usb.core import USBError try: - data += self._receive(size).tolist() + data += self.connection.receive(size).tolist() timeouts = 0 except USBError: timeouts = timeouts+1 @@ -355,9 +356,3 @@ def _receive_message(self, size = 4096): print ' '*self._loglevel, msg return msg - def _receive(self, size=4096): - raise NotImplementedError("Need to define _receive function for ANT child class!") - - def _send(self): - raise NotImplementedError("Need to define _send function for ANT child class!") - diff --git a/python/fitbit_client.py b/python/fitbit_client.py index 9b5b52a..18e6b43 100755 --- a/python/fitbit_client.py +++ b/python/fitbit_client.py @@ -55,8 +55,8 @@ import argparse import xml.etree.ElementTree as et from fitbit import FitBit -from antprotocol.bases import getBase -from antprotocol.protocol import ANTException, FitBitBeaconTimeout +from antprotocol.connection import getConn +from antprotocol.protocol import ANT, ANTException, FitBitBeaconTimeout class FitBitRequest(object): @@ -156,10 +156,11 @@ def __init__(self, debug=False): self.log_info = {} self.time = time.time() self.data = [] - base = getBase(debug) - if base is None: + conn = getConn() + if conn is None: print "No base found!" exit(1) + base = ANT(conn) self.fitbit = FitBit(base) if not self.fitbit: print "No devices connected!" @@ -198,7 +199,7 @@ def close(self): self.dump_connection() print 'Closing USB device' try: - self.fitbit.base.close() + self.fitbit.base.connection.close() except AttributeError: pass self.fitbit.base = None diff --git a/python/ifitbit.py b/python/ifitbit.py index 3426ed1..c2cc2d9 100755 --- a/python/ifitbit.py +++ b/python/ifitbit.py @@ -27,7 +27,8 @@ def print_help(): for cmd in sorted(helps.keys()): print '%s\t%s' % (cmd, helps[cmd]) -from antprotocol.bases import getBase +from antprotocol.connection import getConn +from antprotocol.protocol import ANT from fitbit import FitBit import time @@ -57,10 +58,11 @@ def init(*args): if len(args) >= 1: debug = bool(int(args[0])) if debug: print "Debug ON" - base = getBase(debug) - if base is None: + conn = getConn() + if conn is None: print "No device connected." return + base = ANT(conn) tracker = FitBit(base) tracker.init_tracker_for_transfer() @@ -69,7 +71,7 @@ def close(): global base, tracker if base is not None: print "Closing connection" - base.close() + base.connection.close() base = None tracker = None @@ -77,11 +79,11 @@ def close(): def test(): global base if base is None: - base = getBase(True) - if base is None: + conn = getConn() + if conn is None: print "No devices connected!" return 1 - + base = ANT(conn) device = FitBit(base) device.init_tracker_for_transfer() @@ -108,7 +110,7 @@ def test(): # for i in range(0, len(d), 14): # print ["%02x" % x for x in d[i:i+14]] - base.close() + base.connection.close() base = None @command('>', 'Run opcode') From 71283eb2d22c7333e37505d7a3ae1963854df017 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Wed, 24 Oct 2012 20:04:00 +0200 Subject: [PATCH 64/66] connection: Add forgotten parameter during first receive() --- python/antprotocol/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/antprotocol/connection.py b/python/antprotocol/connection.py index 035458f..d4a8b9d 100644 --- a/python/antprotocol/connection.py +++ b/python/antprotocol/connection.py @@ -112,7 +112,7 @@ def init(self): ]) self._connection.ctrl_transfer(0x40, 0x12, 0x0C, 0x0, []) try: - self.receive() + self.receive(1024) except usb.USBError: pass From d565d4e57a0529e583b0ca87c9452d4c3fab12ea Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Wed, 7 Nov 2012 20:35:51 +0100 Subject: [PATCH 65/66] improve display, make unimportant message take less place --- python/antprotocol/connection.py | 4 ++-- python/antprotocol/protocol.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/python/antprotocol/connection.py b/python/antprotocol/connection.py index d4a8b9d..b4a64d2 100644 --- a/python/antprotocol/connection.py +++ b/python/antprotocol/connection.py @@ -1,4 +1,4 @@ -import usb +import usb, os, sys class ANTConnection(object): """ An abstract class that represents a connection """ @@ -122,7 +122,7 @@ def init(self): def getConn(): for conn in [bc() for bc in CONNS]: if conn.open(): - print "Found %s base" % (conn.NAME,) + os.write(sys.stdout.fileno(), "\n%s: " % conn.NAME) return conn print "Failed to find a base" return None diff --git a/python/antprotocol/protocol.py b/python/antprotocol/protocol.py index c6668d0..147f47b 100644 --- a/python/antprotocol/protocol.py +++ b/python/antprotocol/protocol.py @@ -44,7 +44,7 @@ # Added to and untwistedized and fixed up by Kyle Machulis # -import struct, array, time +import struct, array, time, os, sys from message import MessageIN, MessageOUT class ANTException(Exception): @@ -235,13 +235,13 @@ def _check_tx_response(self, maxtries = 16): def receive_bdcast(self): # FitBit device initialization for tries in range(60): - print "Waiting for beacon" + os.write(sys.stdout.fileno(), '.') try: msg = self._receive_message() except NoMessageException: continue if msg.id == 0x4E: - print "Got it." + os.write(sys.stdout.fileno(), '!') return raise FitBitBeaconTimeout("Timeout waiting for beacon, will restart") From 5f7483ae0ba660567965b258c2ed8971a1c0c134 Mon Sep 17 00:00:00 2001 From: Benoit Allard Date: Tue, 8 Jan 2013 00:41:38 +0100 Subject: [PATCH 66/66] remove an unused import --- python/fitbit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/fitbit.py b/python/fitbit.py index ec236d0..c40ffc2 100755 --- a/python/fitbit.py +++ b/python/fitbit.py @@ -66,7 +66,6 @@ # - Implementing data clearing import itertools, sys, random, operator, datetime, time -from antprotocol.bases import getBase from antprotocol.protocol import ANTException, ReceiveException, SendException class FitBit(object):