[med-svn] [galileo] 02/03: Imported Upstream version 0.5.0~git160510+dfsg

Dylan Aïssi bob.dybian-guest at moszumanska.debian.org
Fri May 13 22:15:32 UTC 2016


This is an automated email from the git hooks/post-receive script.

bob.dybian-guest pushed a commit to branch master
in repository galileo.

commit 0b7cf53f6312d2f57e6d19657e4b2c554b71b1f4
Author: Dylan Aïssi <bob.dybian at gmail.com>
Date:   Sat May 14 00:10:37 2016 +0200

    Imported Upstream version 0.5.0~git160510+dfsg
---
 99-fitbit.rules            |   2 +
 CHANGES                    | 190 +++++++++++++++++++++
 COPYING                    | 165 ++++++++++++++++++
 README.txt                 | 162 ++++++++++++++++++
 analysedir.py              |  45 +++++
 analysedump.py             | 239 ++++++++++++++++++++++++++
 contrib/README.txt         |  27 +++
 contrib/galileo.service    |  17 ++
 contrib/galileo.upstart    |   9 +
 diff.py                    | 106 ++++++++++++
 doc/galileo.1              | 205 ++++++++++++++++++++++
 doc/galileorc.5            | 162 ++++++++++++++++++
 galileo/__init__.py        |   5 +
 galileo/config.py          | 359 +++++++++++++++++++++++++++++++++++++++
 galileo/conversation.py    | 203 ++++++++++++++++++++++
 galileo/dongle.py          | 322 +++++++++++++++++++++++++++++++++++
 galileo/dump.py            | 130 ++++++++++++++
 galileo/interactive.py     | 223 ++++++++++++++++++++++++
 galileo/main.py            | 301 ++++++++++++++++++++++++++++++++
 galileo/net.py             | 257 ++++++++++++++++++++++++++++
 galileo/parser.py          | 120 +++++++++++++
 galileo/tracker.py         | 348 +++++++++++++++++++++++++++++++++++++
 galileo/ui.py              | 237 ++++++++++++++++++++++++++
 galileo/utils.py           |  68 ++++++++
 galileorc.sample           |  35 ++++
 run                        |   5 +
 setup.py                   |  84 +++++++++
 tests/__init__.py          |   0
 tests/testCRC.py           |  19 +++
 tests/testConfig.py        |  67 ++++++++
 tests/testDataRing.py      |  59 +++++++
 tests/testDongle.py        |  92 ++++++++++
 tests/testDump.py          |  81 +++++++++
 tests/testFitbitClient.py  | 415 +++++++++++++++++++++++++++++++++++++++++++++
 tests/testFormExtractor.py |  31 ++++
 tests/testGalileoClient.py | 247 +++++++++++++++++++++++++++
 tests/testNetUtils.py      |  79 +++++++++
 tests/testParameters.py    |  75 ++++++++
 tests/testTracker.py       |  24 +++
 tests/testUI.py            |  46 +++++
 tests/testUtils.py         | 111 ++++++++++++
 tests/testYAMLParser.py    | 116 +++++++++++++
 trace.txt                  |  81 +++++++++
 43 files changed, 5569 insertions(+)

diff --git a/99-fitbit.rules b/99-fitbit.rules
new file mode 100644
index 0000000..626a5d8
--- /dev/null
+++ b/99-fitbit.rules
@@ -0,0 +1,2 @@
+# udev rules.d entry for running as a daemon under the "galileo" account.
+SUBSYSTEM=="usb", ATTR{idVendor}=="2687", ATTR{idProduct}=="fb01", SYMLINK+="fitbit", MODE="0660", OWNER="galileo", GROUP="galileo"
diff --git a/CHANGES b/CHANGES
new file mode 100644
index 0000000..6869c6e
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,190 @@
+galileo 0.5 (????-??-??)
+------------------------
+
+This is the next feature release of galileo, a free utility to securely
+synchronize fitbit bluetooth trackers with the fitbit web-service.
+
+This release ...
+
+Contributor to this release: Dean Giberson, Richard Weait, Chris Wayne,
+David Vasak, Mike Frysinger and Nenad Jankovic.
+
+Main changes since 0.4.4:
+- Add a pair mode (issue#33)
+- Removal of the Timeout class (issue#43)
+- Get a UI abstraction layer (issue#31)
+- Keep a rolling log of the last communication to help debugging (issue#67)
+- Support sending logging output to syslog (issue#134)
+- Catch HTTPError when syncing (issue#147)
+- Improve Charge HR support (issue#148)
+- Improve Discovery process (issue#231)
+- Add Support for python 3.4 (issue#116)
+- Add support for newer dongles (issue#236)
+
+
+galileo 0.4.4 (2015-05-31)
+--------------------------
+
+This is the fourth patch release of galileo 0.4, a free utility to securely
+synchronize fitbit bluetooth trackers with the fitbit web service.
+
+This release adds support for older python version (2.6), fixes an issue with
+the BackOffException, properly cleans up the USB connection when done as well
+as improves support for the Charge HR tracker.
+
+Contributor to this release: Slobodan Miskovic, Noel Jackson and Nenad
+Jankovic.
+
+Main changes since 0.4.3:
+- Add support for python 2.6
+- Fix handling of BackOffException (issue#140)
+- Reset the USB device when we're done with it in order to prevent a "Device
+  Busy" on subsequent tries (issue#142 and a few more ...)
+- Better adjust timeouts for the Charge HR tracker.
+- Discard the BackOff Exception when a payload is transmitted.
+
+
+galileo 0.4.3 (2014-11-27)
+--------------------------
+
+This is the third patch release of galileo 0.4, a free utility to securely
+synchronize fitbit bluetooth trackers with the fitbit web service.
+
+This release adds support for the new Charge tracker.
+
+Main changes since 0.4.2:
+- Increase a timeout to support the Charge tracker (issue#123)
+- Exclude the `tests` packages when installing.
+
+
+galileo 0.4.2 (2014-10-15)
+--------------------------
+
+This is the second patch release of galileo 0.4, a free utility to securely
+synchronize fitbit bluetooth trackers with the fitbit web service.
+
+This release fixes a couple of API changes in the dependent libraries since
+the release of the previous version.
+
+Main changes since 0.4.1:
+- Correctly recognize TimeoutError from libusb0 (issue#82)
+- Fix TypeError with newer version of PyUSB (issue#36, issue#77, and a few
+  more ...)
+- Fix error when displaying the reason for a Connection Error (issue#118).
+
+
+galileo 0.4.1 (2014-06-22)
+--------------------------
+
+This is the first patch release of galileo 0.4, a free utility to securely
+synchronize fitbit bluetooth trackers with the fitbit web-service.
+
+This release fixes a number of issues reported with the release of galileo 0.4.
+All users of galileo 0.4 are encouraged to upgrade.
+
+Main changes since 0.4:
+- Fix a traceback in debug message (part of issue#51)
+- Fix issue when the dongle doesn't reports its version (issue#53)
+- Fix issue when ConnectionError happens during sync (issue#54)
+- Try again a write operation in case of IOError (issue#61)
+- Be more strict during discovery (issue#66)
+- Handle issue when USB backend does not implement non-mandatory methods
+  (issue#75)
+- Recognize one more kind of TimeoutError (issue#82)
+
+
+galileo 0.4 (2014-03-31)
+------------------------
+
+This is the next feature release of galileo, a free utility to securely
+synchronize fitbit bluetooth trackers with the fitbit web-service.
+
+This release introduce a `daemon` mode that synchronize periodically the
+available trackers, making it easier for integration as a service. As well as
+enhance the configurability by introducing more options, and also allowing
+them to be read from configuration files. Man pages have also been added.
+
+Contributors to this release: Stuart Hickinbottom, and Alexander Voronin.
+
+Main changes since 0.3.1:
+- Manual pages added for galileo(1) and galileorc(5) (issue#38, PR #12)
+- Add compatibility with dongles 1.6 (issue#45)
+- Add a lots of tests (issue#32)
+- Add a `daemon` mode (issue#30)
+- Detect when Fitbit server is in 'maintenance mode' (issue#29)
+- Read the configuration from files (issue#18, PR #5)
+- Validation of the CRC value of the dumps (issue#15)
+- Add --include and --exclude to control which trackers to synchronize (PR #4)
+- Add a --no-upload command line parameter to prevent the uploading of the
+  dump to the server
+- Major code reorganisation.
+
+
+galileo 0.3.1 (2014-02-01)
+--------------------------
+
+This is the first patch release of galileo 0.3, a free utility to securely
+synchronize fitbit bluetooth trackers with the fitbit web-service.
+
+This release change the communication protocol used to communicate between
+galileo and the fitbit web-service from a plain-text one (HTTP) to one that
+uses state-of-the-art encryption methods (HTTPS), preventing the data extracted
+from the tracker of being intercepted and read on its way to the fitbit servers.
+
+Main changes since 0.3:
+- Switch the communication protocol from HTTP to HTTPS
+
+
+galileo 0.3 (2014-01-27)
+------------------------
+
+This is the third version of galileo, a free utility to synchronise fitbit
+bluetooth trackers with the fitbit service.
+
+This release greatly enhance the user friendliness by adding support for
+command line switches to control the various aspects of the synchronisation.
+As well as improves the code quality.
+
+New contributors to this release: Stuart Hickinbottom.
+
+Main changes since 0.2:
+- Improve error reporting when insufficient permissions are set on the usb
+  device (PR #3, issue#10)
+- Add --no-dump to prevent writing a backup of the dump to disc (issue#19).
+- Only sync the trackers that have not been sync'd for some time. Use --force
+  to always sync all the discovered trackers (PR #2, issue#13).
+- Warn when the signal from the tracker is too weak (issue#12).
+- Add command-line switches to control verbosity (PR #1, issue#9).
+- Register package to PyPi, and allow installation via pip.
+- Improve detection of the end of the dump (issue#2).
+- Unify the timeout values.
+- Code cleanup.
+
+
+galileo 0.2 (2013-12-30)
+------------------------
+
+This was the second version of galileo, a free utility to synchronise fitbit
+trackers with the fitbit server.
+
+This version fixes an issue when the dump from the tracker was not being
+accepted by the server.
+
+Main changes since 0.1:
+- Unescape some bits before transmitting to the server, this solves an issue
+  with the fitbit data not being accepted by the server (issue#1).
+- Add a udev rules files to allow the utility to run as a non-privileged user.
+- Add a diff.py script to analyse difference in dumps.
+- Also dump the response from the server in the dump file.
+- Code cleanup
+
+
+galileo 0.1 (2013-11-24)
+------------------------
+
+This was the first release of galileo, a free utility to synchronise fitbit
+trackers with the fitbit servers.
+
+Main features:
+- synchronization of any bluetooth based fitbit tracker with the fitbit server.
+- backup of dumps on disc in the ~/.galileo/<TRACKERID>/ directory.
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..65c5ca8
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,165 @@
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..c887e09
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,162 @@
+Galileo
+=======
+
+:author: Benoît Allard <benoit.allard at gmx.de>
+:version: 0.5dev
+:license: LGPLv3+
+:bug tracker: https://bitbucket.org/benallard/galileo/issues
+:mailing list: galileo at freelists.org (subscribe_, archive_)
+:build status: |droneio_badge|_
+
+.. _subscribe: mailto:galileo-request at freelists.org?subject=subscribe
+.. _archive: http://freelists.org/archive/galileo/
+.. |droneio_badge| image:: https://drone.io/bitbucket.org/benallard/galileo/status.png
+.. _droneio_badge: https://drone.io/bitbucket.org/benallard/galileo
+
+Introduction
+------------
+
+Galileo is a Python utility to securely synchronize a Fitbit device with the
+Fitbit web service. It allows you to browse your data on their website, and
+compatible applications.
+
+All Bluetooth-based trackers are supported. Those are:
+
+- Fitbit One
+- Fitbit Zip
+- Fitbit Flex
+- Fitbit Force
+- Fitbit Charge
+- Fitbit Charge HR
+
+.. note:: The Fitbit Ultra tracker is **not supported** as it communicates
+          using the ANT protocol. To synchronize it, please use libfitbit_.
+
+This utility is mainly targeted at Linux because Fitbit does not
+provide any Linux-compatible software, but as Python is
+cross-platform and the libraries used are available on a broad variety
+of platforms, it should not be too difficult to port it to other
+platforms.
+
+.. _libfitbit: https://github.com/openyou/libfitbit
+
+Main features
+-------------
+
+- Synchronize your fitbit tracker with the fitbit server using the provided
+  dongle.
+- Securely communicate (using HTTPS) with the fitbit server.
+- Save all your dumps locally for possible later analyse.
+
+Installation
+------------
+
+The easy way
+~~~~~~~~~~~~
+
+.. warning:: If you want to run the utility as a non-root user, you will have
+             to install the udev rules manually (See `The more complicated
+             way`_, or follow the instructions given when it fails).
+
+::
+
+    $ pip install galileo
+    $ galileo
+
+.. note:: If you don't want to install this utility system-wide, you
+          may want to install it inside a virtualenv_, the behaviour
+          will not be affected.
+
+.. _virtualenv: http://www.virtualenv.org
+
+Distribution packages
+~~~~~~~~~~~~~~~~~~~~~
+
+The following Linux distributions have packages available for installation:
+
+**Arch**:
+  The utility is available from AUR_. You can install it using the yaourt_ package manager: ``yaourt -S galileo``.
+**Fedora**:
+  The utility is packaged in a `COPR repo`_.  Download the relevant repo
+  for your version of Fedora, and then ``yum install galileo``.
+**Gentoo**:
+  The utility is packaged as ``app-misc/galileo`` within the
+  `squeezebox <http://git.overlays.gentoo.org/gitweb/?p=user/squeezebox.git>`_
+  overlay. See https://wiki.gentoo.org/wiki/Layman for details of how
+  to use Gentoo overlays.
+**Ubuntu**:
+  The utility is available over the ppa ``ppa:cwayne18/fitbit``. Use the
+  following commands to install it and start the daemon::
+
+    sudo add-apt-repository ppa:cwayne18/fitbit
+    sudo apt-get update && sudo apt-get install galileo
+    start galileo
+
+.. _AUR: https://aur.archlinux.org/packages/galileo/
+.. _yaourt: https://wiki.archlinux.org/index.php/yaourt
+
+.. _`COPR repo`: https://copr.fedoraproject.org/coprs/stbenjam/galileo/
+
+The more complicated way
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+First, you need to clone this repository locally, and install the required
+dependencies:
+
+**pyusb**:
+  Need at least a 1.0 version, 0.4 and earlier are not compatible.
+  Please use a tagged release as development version might contains bug or
+  interface breakage.
+**requests**:
+  Newer versions (2.x) preferred, although older should also work.
+
+You should copy the file ``99-fitbit.rules`` to the directory
+``/etc/udev/rules.d`` in order to be able to run the utility as a
+non-root user.
+
+Don't forget to:
+
+- restart the udev service: ``sudo service udev restart``
+- unplug and re-insert the dongle to activate the new rule.
+
+Then simply run the ``run`` script located at the root of this repository.
+
+If your system uses systemd then there is an example unit file in the
+``contrib`` directory that you may wish to customize.
+
+Documentation
+-------------
+
+For the moment, this README (and the ``--help`` command line option) is the
+main documentation we have. The wiki_ is meant to gather technical
+information about the project like the communication protocol, or the format
+of the dump. Once this information reached a suffficient level of maturation,
+the goal is to integrate it into the project documentation. So head-on there,
+and start sharing your findings !
+
+Manual pages for the galileo_\(1) utility and the galileorc_\(5) configuration
+file are provided within the ``doc`` directory.
+
+.. _wiki: https://bitbucket.org/benallard/galileo/wiki
+.. _galileo: https://pythonhosted.org/galileo/galileo.1.html
+.. _galileorc: https://pythonhosted.org/galileo/galileorc.5.html
+
+Acknowledgements
+----------------
+
+Special thanks to the folks present @ the `issue 46`_ of libfitbit.
+
+Especially to `sansneural <https://github.com/sansneural>`_ for
+https://docs.google.com/file/d/0BwJmJQV9_KRcSE0ySGxkbG1PbVE/edit and
+`Ingo Lütkebohle`_ for http://pastebin.com/KZS2inpq.
+
+.. _`issue 46`: https://github.com/openyou/libfitbit/issues/46
+.. _`Ingo Lütkebohle`: https://github.com/iluetkeb
+
+Disclaimer
+----------
+
+Fitbit is a registered trademark and service mark of Fitbit, Inc.  galileo is
+designed for use with the Fitbit platform.  This product is not put out by
+Fitbit, and Fitbit does not service or warrant the functionality of this
+product.
diff --git a/analysedir.py b/analysedir.py
new file mode 100755
index 0000000..3634d6a
--- /dev/null
+++ b/analysedir.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+import os
+import re
+
+from analysedump import readdump
+from analysedump import analyse as longanalyse
+
+TYPES = {0xF4: 'Zip', 0x26: 'One', 0x28: 'Flex'}
+
+
+def analyse(filename):
+    s = []
+    with open(filename, 'rt') as dump:
+        data, response = readdump(dump)
+    s.append(TYPES[data[0]])
+    s.append(str(len(data)))
+    s.append(str(len(response)))
+    print ' '.join(s)
+    try:
+        longanalyse(data)
+    except:
+        print filename
+        raise
+
+
+def main(dirname):
+    for root, dirs, files in os.walk(dirname):
+        for dump in sorted(files):
+            if not re.match('dump-\d{10}.txt', dump):
+                continue
+            filename = os.path.join(root, dump)
+            print dump
+            analyse(filename)
+
+if __name__ == "__main__":
+    import sys
+    try:
+        os.path.exists(sys.argv[1])
+        filename = sys.argv[1]
+        print "Single file mode: ", filename
+        analyse(filename) 
+
+    except:
+        main(sys.argv[1])
diff --git a/analysedump.py b/analysedump.py
new file mode 100755
index 0000000..236ca9c
--- /dev/null
+++ b/analysedump.py
@@ -0,0 +1,239 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import time
+import base64
+
+
+def readlog(f):
+    """ input is from f in the format of lines of long string starting
+    with a tab ('\t') representing the hexadcimal representation of the data
+    (megadump)"""
+    d = []
+    for line in f:
+        if line[0] != '\t':
+            if d:
+                return d
+            continue
+        line = line.strip()
+        for i in range(0, len(line), 2):
+            d.append(int(line[i:i + 2], 16))
+    return d
+
+
+def readdump(f):
+    """ imput is from ./galileo.py """
+    d = []
+    r = []
+    current = d
+    for line in f:
+        if line.strip() == '':
+            current = r
+            continue
+        current.extend(int(x, 16) for x in line.strip().split())
+    return d, r
+
+
+def a2s(array):
+    """ array of int to string """
+    return ''.join(chr(c) for c in array)
+
+
+def a2x(array):
+    """ array of int to hex representation """
+    return ' '.join("%02X" % i for i in array)
+
+
+def a2lsbi(array):
+    """ array to int (LSB first) """
+    integer = 0
+    for i in range(len(array) - 1, -1, -1):
+        integer *= 256
+        integer += array[i]
+#        print a2x(array), hex(integer)
+    return integer
+
+
+def a2msbi(array):
+    """ array to int (MSB first) """
+    integer = 0
+    for i in range(len(array)):
+        integer *= 256
+        integer += array[i]
+    return integer
+
+
+def header(data):
+    index = 40
+    walkStrideLen = a2lsbi(data[index:index + 2])
+    index += 2
+    runStrideLen = a2lsbi(data[index:index + 2])
+    index += 2
+    print "Stride lengths: %dmm, %dmm" % (walkStrideLen, runStrideLen)
+    print a2x(data[index:index + 4])
+    index += 4
+    # empirical value
+    index += 12
+
+    if index >= len(data): return
+    # Greetings
+    print "Greetings: '%s'" % a2s(data[index:index + 10])
+    index += 10
+
+    # Cheering
+    print "Cheering"
+    for i in range(3):
+        print "'%s'" % a2s(data[index:index + 10])
+        index += 10
+
+
+def first_field(rec_len):
+    def unknown(data):
+        assert data[:3] == [END, END, 0xdd], a2x(data[:3])
+        index = 3
+        while index < len(data) - 1:
+            tstamp = a2lsbi(data[index:index + 4])
+            print time.strftime("%x %X", time.localtime(tstamp)), hex(tstamp)
+            index += 4
+            print "\t%s" % a2x(data[index:index + rec_len])
+            index += rec_len
+    return unknown
+
+
+def minutely(rec_len):
+    """ this analyses the minute-by-minute information
+    """
+    def minutes(data):
+        assert data[:3] == [END, END, 0xdd], a2x(data[:3])
+        index = 3
+        tstamp = 0
+        while index < len(data) - 1:
+            if not (data[index] & 0x80):
+                tstamp = a2msbi(data[index:index + 4])
+                index += 4
+            else:
+                print time.strftime("%x %X", time.localtime(tstamp)), a2x(data[index:index + rec_len])
+                tstamp += 60
+                index += rec_len
+    return minutes
+
+
+def stairs(data):
+    """ Looks like stairs informations are put here """
+    assert data[:3] == [END, END, 0xdd], a2x(data)
+    index = 3
+    index = 3
+    tstamp = 0
+    while index < len(data) - 1:
+        if not (data[index] & 0x80):
+            tstamp = a2msbi(data[index:index + 4])
+            index += 4
+        else:
+            if data[index] != 0x80:
+                #print a2x([array[index]])
+                index += 1
+            print time.strftime("%x %X", time.localtime(tstamp)), a2x(data[index:index + 2])
+            tstamp += 60
+            index += 2
+
+
+def daily(data):
+    if len(data) == 2:
+        assert data == [END, END], a2x(data)
+        return
+    assert data[:3] == [END, END, 0xdd], a2x(data[:3])
+    index = 3
+    while index < len(data) - 1:
+        tstamp = a2lsbi(data[index:index + 4])
+        index += 4
+        print time.strftime("%x %X", time.localtime(tstamp)), a2x(data[index:index + 12])
+        index += 12
+
+
+def footer(data):
+    assert len(data) == 9, data
+    print 'Dump size: %d bytes' % a2lsbi(data[5:7])
+    print a2x(data)
+
+ESC = 0xdb
+END = 0xc0
+ESC_ = {0xdc: END, 0xdd: ESC}
+
+
+def unSLIP(data):
+    """ This remove SLIP escaping and yield the parts
+    The magic are: The first part doesn't ends with 0xC0
+    there are empty parts
+    >>> list(unSLIP([1, 2, 0xc0, 5, 4, 0xc0, 0xc0, 8, 4]))
+    [[1, 2], [192, 5, 4, 192], [192, 8, 4]]
+    >>> list(unSLIP([12, 0xc0, 0, 0, 0xc0, 1, 2, 0xc0, 8, 9]))
+    """
+    first = True
+    part = []
+    escape = False
+    for c in data:
+#        print "%x" % c
+        if not escape:
+            if c == ESC and part and part[0] == END:
+                escape = True
+            else:
+                part.append(c)
+                if c == END:
+                    if len(part) != 1:
+                        if first or (part[0] != END):
+                            yield part[:-1]
+                            part = [part[-1]]
+                            first = False
+                        else:
+                            yield part
+                            part = []
+        else:
+            part.append(ESC_[c])
+            escape = False
+    yield part
+
+
+def analyse(data):
+
+    def onscreen(data):
+        print a2x(data)
+
+    def skip(data):
+        pass
+
+    display = [onscreen] * 20
+
+    analyses_ZIP = [header, first_field(9), minutely(3), daily, footer]
+
+    analyses_ONE = [header, first_field(11), minutely(4), stairs, daily, skip, onscreen, skip, footer]
+
+    analyses = {
+        0x26: analyses_ONE,
+        0xF4: analyses_ZIP,
+    }.get(data[0], display)
+
+    for i, part in enumerate(unSLIP(data)):
+        f = analyses[i]
+        print "%s (%d): %d bytes" % (f.__name__, i, len(part))
+        f(part)
+
+
+def analysedump(dump_dir, index):
+    for root, dirs, files in os.walk(dir):
+        file = sorted(files)[idx]
+    print "Analysing %s" % file
+    with open(os.path.join(root, file)) as f:
+        dump, response = readdump(f)
+        analyse(dump)
+
+if __name__ == "__main__":
+    if len(sys.argv) == 1:
+        dump, response = readdump(sys.stdin)
+        analyse(dump)
+    else:
+        dir = sys.argv[1]
+        idx = -1
+        if len(sys.argv) > 2:
+            idx = int(sys.argv[2])
+        analysedump(dir, idx)
diff --git a/contrib/README.txt b/contrib/README.txt
new file mode 100644
index 0000000..e1f6739
--- /dev/null
+++ b/contrib/README.txt
@@ -0,0 +1,27 @@
+Miscellaneous Contributions
+===========================
+
+This directory contains a number of additional files that may be of
+interest. These are not necessary to use Galileo, but can help those
+packaging or customising the utility.
+
+galileo.service
+---------------
+
+This is a **systemd unit** file that can be used to start and stop the
+Galileo utility when running in 'daemon mode'. This was originally
+created for the Gentoo package and will likely need customization for
+other distributions. In particular, the user and group that the daemon
+runs as, as well as the location of the configuration file, may need
+to be changed.
+
+This service unit file should be installed into the lib/systemd/system
+directory.
+
+
+galileo.upstart
+---------------
+
+This is an **upstart** file that can be used to start and stop the galileo
+utility when running in 'daemon mode'. This was originally created for the
+Ubuntu distribution, and might need some customization to run elsewhere.
diff --git a/contrib/galileo.service b/contrib/galileo.service
new file mode 100644
index 0000000..f81a17c
--- /dev/null
+++ b/contrib/galileo.service
@@ -0,0 +1,17 @@
+# Unit file for Galileo
+#
+# See systemd.service(5) for further information.
+
+[Unit]
+Description=Synchronisation utility for Bluetooth LE-based Fitbit trackers
+Documentation=man:galileo(1) man:galileorc(5)
+Documentation=https://bitbucket.org/benallard/galileo
+After=network.target
+
+[Service]
+User=galileo
+Group=galileo
+ExecStart=/usr/bin/galileo --config /etc/galileorc daemon
+
+[Install]
+WantedBy=network.target
diff --git a/contrib/galileo.upstart b/contrib/galileo.upstart
new file mode 100644
index 0000000..1c23bc8
--- /dev/null
+++ b/contrib/galileo.upstart
@@ -0,0 +1,9 @@
+description "Galileo to sync fitbit devices"
+
+start on started dbus
+stop on desktop-end
+
+respawn
+respawn limit unlimited
+
+exec /usr/bin/galileo daemon
diff --git a/diff.py b/diff.py
new file mode 100755
index 0000000..1929fb2
--- /dev/null
+++ b/diff.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+
+import os
+
+from analysedump import readdump
+
+
+def s2a(s):
+    return [ord(c) for c in s]
+
+
+def LCS(X, Y):
+    m = len(X)
+    n = len(Y)
+    C = [[0] * (n + 1) for i in range(m + 1)]
+    for i in range(1, m + 1):
+        for j in range(1, n + 1):
+            if X[i - 1] == Y[j - 1]:
+                C[i][j] = C[i - 1][j - 1] + 1
+            else:
+                C[i][j] = max(C[i][j - 1], C[i - 1][j])
+    return C
+
+SYMBOLS = {0: ' ',
+           -1: '-',
+           1: '+'}
+
+
+def _diff(C, X, Y, i, j):
+    while (i, j) != (0, 0):
+        if i > 0 and j > 0 and X[i - 1] == Y[j - 1]:
+            yield (0, X[i - 1])
+            i -= 1
+            j -= 1
+        elif j > 0 and ((i == 0) or (C[i][j - 1] >= C[i - 1][j])):
+            yield (1, Y[j - 1])
+            j -= 1
+        elif i > 0 and ((j == 0) or C[i][j - 1] < C[i - 1][j]):
+            yield (-1, X[i - 1])
+            i -= 1
+        else:
+            assert False, ' '.join(str(c) for c in [i, j, C[i][j - 1], C[i - 1][j]])
+
+
+def diff(X, Y, maxL=20):
+
+    start = 0
+    oldmode = 0
+    s = []
+    while start < len(X) and start < len(Y) and X[start] == Y[start]:
+        if len(s) == maxL:
+            print SYMBOLS[oldmode], ' '.join('%02X' % i for i in s)
+            s = []
+        s.append(X[start])
+        start += 1
+    print SYMBOLS[oldmode], ' '.join('%02X' % i for i in s)
+    s = []
+    X = X[start:]
+    Y = Y[start:]
+
+    C = LCS(X, Y)
+    for chunk in reversed(list(_diff(C, X, Y, len(X), len(Y)))):
+        if s and ((len(s) == maxL) or chunk[0] != oldmode):
+            print SYMBOLS[oldmode], ' '.join('%02X' % i for i in s)
+            s = []
+        s.append(chunk[1])
+        oldmode = chunk[0]
+    # Print the last one
+    print SYMBOLS[oldmode], ' '.join('%02X' % i for i in s)
+    return oldmode * len(s)
+
+
+def dumpdiff(dump1, dump2):
+    with open(dump1) as f:
+        data1, resp1 = readdump(f)
+
+    with open(dump2) as f:
+        data2, resp2 = readdump(f)
+
+    diff(data1, data2)
+    print '-' * 20
+    diff(resp1, resp2)
+
+
+def diffdir(basedir):
+
+    for root, dirs, files in os.walk(basedir):
+        files = sorted(files)
+        for i in range(0, len(files) - 1):
+            print files[i]
+            print files[i + 1]
+            try:
+                dumpdiff(os.path.join(root, files[i]), os.path.join(root, files[i + 1]))
+            except RuntimeError:
+                print 'Trouble with %s and %s' % (files[i], files[i + 1])
+            print '------------------------------------'
+
+
+if __name__ == "__main__":
+    import sys
+    if len(sys.argv) == 2:
+        # one param
+        diffdir(sys.argv[1])
+    elif len(sys.argv) == 3:
+        # Two params
+        dumpdiff(sys.argv[1], sys.argv[2])
diff --git a/doc/galileo.1 b/doc/galileo.1
new file mode 100644
index 0000000..d1afc47
--- /dev/null
+++ b/doc/galileo.1
@@ -0,0 +1,205 @@
+.\" galileo python command-line utility manual page.
+.\"
+.\" View this file before installing it with:
+.\"   groff -man -Tascii galileo.1
+.\" or
+.\"   man ./galileo.1
+.TH galileo  1  "June 2014" 0.5dev "User Commands"
+.SH NAME
+galileo \- synchronize Fitbit devices
+
+.SH SYNOPSIS
+.B galileo
+.RB [ "\-h" ]
+.RB [ "\-c \fIFILENAME\fR" ]
+.RB [ "\-\-dump\-dir \fIDIR\fR" ]
+.RB [ "\-\-daemon\-period \fIPERIOD\fR" ]
+.RB [ "\-I \fIID\fR" "[ \fIID \.\.\.\fR ] ]"
+.RB [ "\-X \fIID\fR" "[ \fIID \.\.\.\fR ] ]"
+.RB [ "\-v" | "\-d" | "\-q" ]
+.RB [ "\-\-force" | "\-\-no\-force" ]
+.RB [ "\-\-dump" | "\-\-no\-dump" ]
+.RB [ "\-\-upload" | "\-\-no\-upload" ]
+.RB [ "\-\-https\-only" | "\-\-no\-https\-only" ]
+.RB [ "\-\-log\-size \fISIZE\fR" ]
+.RB [ "\-\-syslog" | "\-\-no\-syslog" ]
+.RB [ "sync" | "daemon" | "version" ]
+
+.SH DESCRIPTION
+Synchronize Fitbit wearable fitness tracker devices with the Fitbit web service.
+Visit <
+.B https://www.fitbit.com
+>, or use a Fitbit-compatible app in order
+to browse your data.
+
+.SH MODES
+.TP
+.B sync
+Perform the synchronization of all found trackers, then exit. This is
+the default mode if none is specified.
+.TP
+.B daemon
+Periodically perform synchronization of all found trackers.
+.B galileo
+will periodically perform synchronization until the daemon is killed. The
+period can be controlled via the
+.B \-\-daemon\-period
+option.
+.TP
+.B version
+Display the
+.B galileo
+version and exit.
+.TP
+.B interactive
+This spawn an interactive shell to allow sending arbitrary commands to
+the dongle and the tracker. This is meant to allow experimenting with
+new commands, or different command orders. To be used by experts
+only.
+.TP
+.B pair
+This mode, in an experimental state, allow you to link your tracker with your
+Fitbit online account. This is needed before being able to use any new tracker.
+In order to use this mode, you need an account in the Fitbit online platform,
+and a tracker. The parameters for this mode are taken from the
+.B hardcoded-ui
+section of the
+.BR galileorc (5)
+file.
+
+.SH OPTIONS
+.TP
+.BR \-h ", " \-\-help
+show command-line usage and exit.
+.TP
+.BR "\-c \fIRCCONFIGNAME\fR" ", " "\-\-config \fIRCCONFIGNAME\fR"
+use \fIRCCONFIGNAME\fR as extra configuration file in order to allow overriding
+of settings.
+
+.P
+The remaining options are first read from configuration file, and can be
+overridden by using command line switches. For this reason, positive and
+negative versions are available (\fB\-\-foo\fR and \fB\-\-no\-foo\fR). Please
+see
+.BR galileorc (5)
+for more information about the configuration files.
+
+.SS Logging options:
+.TP
+.BR \-v ", " \-\-verbose
+display general information on progress during synchronization.
+.TP
+.BR \-d ", " \-\-debug
+as \fB\-\-verbose\fR, but also shows internal activity useful for
+diagnosing problems.
+.TP
+.BR \-q ", " \-\-quiet
+show no output except for errors and a summary. This is the default
+if no other logging options are specified.
+.TP
+.BR \-\-syslog
+send logging output to the syslog facility.
+Due to the rate-limiting of some syslog servers, this option might not work in
+combination with the debug log level.
+.TP
+.BR \-\-no\-syslog
+send logging output to stderr.
+.SS Synchronization control options:
+.TP
+\fB\-I\fR \fIID\fR [\fIID\fR ...], \
+\fB\-\-include\fR \fIID\fR [\fIID\fR ...]
+list of tracker IDs to synchronize (if not set, all found trackers are
+synchronized).
+.TP
+\fB\-X\fR \fIID\fR [\fIID\fR ...], \
+\fB\-\-exclude\fR \fIID\fR [\fIID\fR ...]
+list of tracker IDs to avoid synchronizing (no trackers are excluded
+by default).
+.TP
+.B \-\-force
+a tracker will not be synchronized with the Fitbit web service if it reports
+that it was recently synchronized. This option overrides that behavior.
+.TP
+.B \-\-no\-force
+if the configuration file includes the \fBforce\-sync\fR option to
+always force synchronization, this option will restore the default
+behaviour.
+.TP
+.BI \-\-daemon\-period " PERIOD"
+set the time to wait between synchronizations when running in
+\fBdaemon\fR mode. The period is specified in milliseconds and
+defaults to 15000 (15 seconds).
+.SS Tracker data saving options:
+.TP
+.B \-\-dump
+save a copy of the tracker data. Tracker data is stored under a
+tracker-specific subdirectory of a directory that is set using the
+\fB\-\-dump\-dir\fR option. This is the default behavior.
+.TP
+.B \-\-no\-dump
+disables the saving of tracker data.
+.TP
+.BI \-\-dump\-dir " DIR"
+the directory used to store the tracker dumps (defaults to
+\fB~/.galileo\fR).
+.SS Data transfer options:
+.TP
+.B \-\-upload
+synchronize tracker data with the Fitbit web service. This is the
+default.
+.TP
+.B \-\-no\-upload
+prevent the uploading of tracker data to the Fitbit web service. Data
+is not deleted from trackers until it is acknowledged by the fitbit server
+so this will not result in data loss.
+.TP
+.B \-\-https\-only
+data sent to the Fitbit web service will be transferred via a secure connection
+using HTTPS. This is the default.
+.TP
+.B \-\-no\-https\-only
+if HTTPS connection is not possible, this will allow the fallback to HTTP.
+This should only be required if problems with encryption libraries prevent
+data transfer without this option.
+.TP
+.BI \-\-log\-size " SIZE"
+indicate the amount of communication that should be displayed in case of
+errors. Galileo will keep in memory the last \fISIZE\fR communications to help
+debugging if an error happen. This is particularly useful in case of
+hard-to-reproduce issues, where it is too late to collect debug information.
+Default to 10. Set to 0 to disable this functionality.
+
+.SH REQUIREMENTS
+An original Fitbit Bluetooth-LE USB synchronization dongle is
+required.
+.PP
+The Fitbit tracker must already be registered to your Fitbit account
+(see the BUGS section).
+.SH FILES
+.TP
+.IR /etc/galileo/config ", " $XDG_CONFIG_HOME/galileo/config ", " ~/.galileorc
+The configuration files used for default settings. See
+.BR galileorc (5)
+for further details about those files
+
+.SH SEE ALSO
+.TP
+<\fBhttp://www.fitbit.com\fR>
+The Fitbit web service where synchronized tracker data may be viewed.
+.TP
+<\fBhttps://bitbucket.org/benallard/galileo\fR>
+The \fBgalileo\fR homepage where additional information is available.
+.TP
+.BR galileorc (5)
+The format of the configuration file providing default settings.
+
+.SH AUTHOR
+Written and maintained by Benoît Allard, with contributions from other
+authors.
+
+.SH BUGS
+There are no current facilities to make use of the data stored with
+the \fB\-\-dump\fR command.
+.PP
+Please report additional bugs to
+<\fBhttps://bitbucket.org/benallard/galileo/issues\fR>
diff --git a/doc/galileorc.5 b/doc/galileorc.5
new file mode 100644
index 0000000..876772a
--- /dev/null
+++ b/doc/galileorc.5
@@ -0,0 +1,162 @@
+.\" galileorc galileo configuration file manual page.
+.\"
+.\" View this file before installing it with:
+.\"   groff -man -Tascii galileorc.5
+.\" or
+.\"   man ./galileorc.5
+.TH galileorc 5 "June 2014" 0.5dev "File Formats Manual"
+
+.SH NAME
+galileorc \- configuration files for the galileo Fitbit synchronization
+utility
+
+.SH DESCRIPTION
+The
+.B galileorc
+file is used to provide default settings to the
+.BR galileo (1)
+utility. Any settings that would normally be passed as
+command\-line arguments to galileo can, instead, be present in this
+configuration file to prevent having to repeat them again and again.
+.PP
+Settings provided in the configuration files can be overridden by
+run\-time command\-line switches. See
+.BR galileo (1)
+.
+
+.SH FILES
+The following files will be read if present. Later one override
+previous settings and settings provided on the command-line override
+settings defined in configuration files.
+.IP \(bu
+.I /etc/galileo/config
+.IP \(bu
+.I $XDG_CONFIG_HOME/galileo/config
+(The \fBXDG_CONFIG_HOME\fR environment variable default to
+\fI~/.config\fR if not defined)
+.IP \(bu
+.I ~/.galileorc
+.IP \(bu
+any file specified with the \fB-c\fR command\-line switch
+
+.SH SYNTAX
+The settings file is defined in \fIYAML\fR format. Blank lines and
+comments (from the first hash character \(aq#\(aq to the end of the
+line) are ignored.
+.PP
+The configuration file is parsed as a dictionary of settings, which
+means that each setting is defined using a keyword followed by a colon
+character. For single\-value settings (the majority), the value follows
+the colon, for example:
+.PP
+.nf
+do-upload: true
+.fi
+.PP
+For settings of type \fIlist\fR (such as the tracker ID inclusion and
+exclusion lists), the values appear with an indentation on subsequent
+lines and prefixed with a dash, for example:
+.PP
+.nf
+include:
+  - '123456789ABC'
+  - '9876543210AB'
+.fi
+
+.SH SETTINGS
+The following settings can be added to the configuration
+files \- not all options have to be specified; any that are not
+mentioned will leave the defaults in effect. See
+.BR galileo (1)
+for details about the default values.
+.TP
+.B logging
+controls the amount of progress output. Can be \fBverbose\fR to
+display progress during synchronization, \fBdebug\fR for more
+detailed information useful for diagnosing problems, or \fBquiet\fR to
+display only a warning and error messages.
+.TP
+.B syslog
+setting this to \fBtrue\fR will send all logging output to the syslog
+facility. Due to the rate-limiting of some syslog servers, this option might
+not work in combination with the debug log level.
+.TP
+.B include
+the list of tracker IDs to synchronize. If this is specified then only
+trackers from this list will be synchronised.
+.TP
+.B exclude
+the list of tracker IDs not to synchronize.
+.TP
+.B force-sync
+setting this to \fBtrue\fR causes trackers to be synchronized even if
+they report that they already have been synchronized recently.
+.TP
+.B daemon-period
+this defines, in milliseconds, the period at which a synchronisation
+attempt will be performed when galileo is run in \fBdaemon\fR mode.
+.TP
+.B keep-dumps
+setting this to \fBtrue\fR causes galileo to save the data retrieved
+from trackers to the directory specified in \fBdump-dir\fR.
+.TP
+.B dump-dir
+the directory used for saving tracker data if the \fBkeep-dumps\fR
+option is set.
+.TP
+.B do-upload
+setting this to \fBfalse\fR will prevent galileo from sending tracker
+data to the Fitbit web service.
+.TP
+.B https-only
+setting this to \fBfalse\fR will allow galileo to fallback to
+unencrypted HTTP if HTTPS fails for sending tracker data to the Fitbit
+web service.
+.TP
+.B hardcoded-ui
+This is a structured section that includes the answers needed during the
+pairing/firmware update process.
+
+.SH EXAMPLE
+The following is an example configuration file:
+.PP
+.nf
+daemon-period: 60000
+keep-dumps: false
+do-upload: true
+dump-dir: ~/.galileo-tracker-data
+logging: verbose
+force-sync: false
+https-only: false
+include:
+  - '123456789ABC'
+  - '9876543210AB'
+exclude:
+  - 'AABBCCDDEEFF'
+  - '881144BB1234'
+.fi
+
+.SH SEE ALSO
+.TP
+<\fBhttp://www.yaml.org\fR>
+The official YAML homepage, with more background information on the
+YAML file format.
+.TP
+.BR galileo (1)
+The \fBgalileo\fR utility which uses these configuration files for
+default settings.
+.TP
+<\fBhttps://bitbucket.org/benallard/galileo\fR>
+The \fBgalileo\fR homepage where additional information is available.
+
+.SH AUTHOR
+Written and maintained by Benoît Allard, with contributions from other
+authors.
+
+.SH BUGS
+Tracker IDs which consist of only numbers must be surrounded with
+single quotes (as in the \fIEXAMPLE\fR section above). It's probably a
+good idea to always quote tracker IDs to avoid possible confusion.
+.PP
+Please report additional bugs to
+<\fBhttps://bitbucket.org/benallard/galileo/issues\fR>.
diff --git a/galileo/__init__.py b/galileo/__init__.py
new file mode 100644
index 0000000..883bb2c
--- /dev/null
+++ b/galileo/__init__.py
@@ -0,0 +1,5 @@
+"""\
+galileo.py Utility to synchronize a fitbit tracker with the fitbit server.
+"""
+
+__version__ = '0.5dev'
diff --git a/galileo/config.py b/galileo/config.py
new file mode 100644
index 0000000..e568363
--- /dev/null
+++ b/galileo/config.py
@@ -0,0 +1,359 @@
+import os
+
+import argparse
+import logging
+logger = logging.getLogger(__name__)
+
+try:
+    import yaml
+except ImportError:
+    from . import parser as yaml
+
+from .utils import a2x
+
+class ConfigError(Exception): pass
+
+class ConfigFileError(ConfigError):
+    def __init__(self, filename, paramName, msg=""):
+        self.filename = filename
+        self.paramName = paramName
+        self.msg = msg
+
+    def __str__(self):
+        s = "Error parsing parameter '%s' in file '%s'" % (
+            self.paramName, self.filename)
+        if self.msg:
+            s += ": %s" % self.msg
+        return s
+
+class Parameter(object):
+    def __init__(self, varName, name, paramName, default, paramOnly, helpText):
+        # The name of the variable that will be used
+        self.varName = varName
+        # the internal name
+        self.name = name
+        # Tuple about the parameter names (short, long)
+        self.paramName = paramName
+        # the default Value
+        self.default = default
+        self.helpText = helpText
+        self.paramOnly = paramOnly
+
+    def toArgParse(self, parser):
+        """ Add the parameter to the 'argparse' parser given in parameter """
+        raise NotImplementedError
+
+    def fromArgs(self, args, optdict):
+        """ Take the value from the args parameter (from 'argparse'), and fill
+        it in the dict """
+        val = getattr(args, self.name)
+        if val:
+            optdict[self.varName] = val
+
+    def fromFile(self, filedict, optdict):
+        """ Take the value from the filedict parameter and fill it in the dict
+        :returns: False if something went wrong
+        """
+        if self.paramOnly: return True
+        if self.name in filedict:
+            optdict[self.varName] = filedict[self.name]
+        return True
+
+
+class StrParameter(Parameter):
+    def toArgParse(self, parser):
+        parser.add_argument(*self.paramName,
+                            dest=self.name,
+                            help=self.helpText +
+                            " (default to %s)" % self.default)
+
+
+class IntParameter(Parameter):
+    def toArgParse(self, parser):
+        parser.add_argument(*self.paramName,
+                            dest=self.name, type=int,
+                            help=self.helpText +
+                            " (default to %s)" % self.default)
+
+
+class BoolParameter(Parameter):
+    def toArgParse(self, parser):
+        if self.paramOnly:
+            parser.add_argument(*self.paramName,
+                                action={True:  "store_false",
+                                        False: "store_true"}[self.defaultVal],
+                                dest=self.name,
+                                help=self.helpText)
+        else:
+            # We need the True and False version
+            assert len(self.paramName) == 1, len(self.paramName)
+            self.paramName = self.paramName[0]
+            if self.paramName.startswith('--'):
+                self.paramName = self.paramName[2:]
+            group = parser.add_argument_group(
+                description="whether or not to "+self.helpText)
+            mut_ex_group = group.add_mutually_exclusive_group()
+            _help = {}
+            if self.default:
+                _help['help'] = "DEFAULT"
+            mut_ex_group.add_argument("--%s" % self.paramName,
+                                      action="store_true", dest=self.name,
+                                      **_help)
+            _help = {}
+            if not self.default:
+                _help['help'] = "DEFAULT"
+            mut_ex_group.add_argument("--no-%s" % self.paramName,
+                                      action="store_true",
+                                      dest="no_%s" % self.name, **_help)
+
+    def fromArgs(self, args, optdict):
+        if self.paramOnly:
+            optdict[self.varName] = getattr(args, self.name)
+        else:
+            if getattr(args, "no_"+self.name):
+                optdict[self.varName] = False
+            elif getattr(args, self.name):
+                optdict[self.varName] = True
+
+
+class SetParameter(Parameter):
+    def toArgParse(self, parser):
+        parser.add_argument(*self.paramName,
+                            nargs="+", metavar="ID", dest=self.name,
+                            help=self.helpText)
+
+    def fromArgs(self, args, optdict):
+        # Now make sure the list of trackers is all in upper-case to
+        # make comparisons easier later.
+        values = [x.upper() for x in (getattr(args, self.name) or [])]
+        if optdict[self.varName] is None and values:
+            optdict[self.varName] = set()
+        if values:
+            optdict[self.varName].update(values)
+
+    def fromFile(self, filedict, optdict):
+        if self.paramOnly: return True
+        if self.name in filedict:
+            values = [x.upper() for x in filedict[self.name]]
+            if optdict[self.varName] is None and values:
+                optdict[self.varName] = set()
+            optdict[self.varName].update(values)
+        return True
+
+
+class LogLevelParameter(Parameter):
+    """ A class extra for setting the LogLevel """
+    def __init__(self):
+        Parameter.__init__(self, 'logLevel', 'logging', (),  logging.WARNING,
+                           False, "logging Verbosity")
+        self.__logLevelMap = {'quiet': logging.WARNING,
+                              'verbose': logging.INFO,
+                              'debug': logging.DEBUG}
+        self.__logLevelMapReverse = {}
+        for key, value in self.__logLevelMap.items():
+            self.__logLevelMapReverse[value] = key
+        self.default = logging.WARNING
+
+    def toArgParse(self, parser):
+        verbosity_arggroup = parser.add_argument_group(title=self.helpText)
+        verbosity_arggroup2 = verbosity_arggroup.add_mutually_exclusive_group()
+        verbosity_arggroup2.add_argument("-v", "--verbose",
+                                         action="store_true",
+                                         help="display synchronization progress")
+        verbosity_arggroup2.add_argument("-d", "--debug",
+                                         action="store_true",
+                                         help="show internal activity (implies verbose)")
+        verbosity_arggroup2.add_argument("-q", "--quiet",
+                                         action="store_true",
+                                         help="only show errors and summary (default)")
+
+    def fromArgs(self, args, optdict):
+        value = None
+        if args.verbose:
+            value = self.__logLevelMap['verbose']
+        elif args.debug:
+            value = self.__logLevelMap['debug']
+        elif args.quiet:
+            value = self.__logLevelMap['quiet']
+        if value is not None:
+            optdict[self.varName] = value
+
+    def fromFile(self, filedict, optdict):
+        if self.paramOnly: return
+        if self.name in filedict:
+            loglevel = filedict[self.name].lower()
+            try:
+                optdict[self.varName] = self.__logLevelMap[loglevel]
+            except KeyError:
+                return False
+        return True
+
+
+class Argument(StrParameter):
+    """ Extra class for the positional argument """
+    def __init__(self):
+        StrParameter.__init__(self, 'mode', 'mode', ('mode',), 'sync', True,
+                              'The mode to run')
+
+    def toArgParse(self, parser):
+        parser.add_argument(*self.paramName,
+                            nargs='?', choices=['version', 'sync', 'daemon',
+                                                'pair', 'firmware',
+                                                'interactive'],
+                            help=self.helpText +
+                            " (default to %s)" % self.default)
+
+
+class HardCodedUIConfig(Parameter):
+    """\
+    A Config parameter for the config of the HardCodedUI class
+    """
+    def __init__(self):
+        self.name = 'hardcoded-ui'
+        self.varName = self.name.replace('-', '_')
+        self.default = {}
+    def toArgParse(self, parser):
+        """ no-op """
+    def fromArgs(self, args, optdict):
+        """ no-op """
+    def fromFile(self, filedict, optdict):
+        optdict[self.varName] = filedict.get(self.name, {})
+        return True
+
+
+class Config(object):
+    """Class holding the configuration to be applied during synchronization.
+    The configuration can be loaded from a file in which case the defaults
+    can be overridden; loading from multiple files allows the settings from
+    later files to override those defined in earlier files. Finally, each
+    configuration option can also be set directly, which is used to allow
+    overriding of file-based configuration settings with those explicitly
+    specified on the command line.
+    """
+
+    DEFAULT_RCFILE_NAME = "~/.galileorc"
+    DEFAULT_DUMP_DIR = "~/.galileo"
+
+    # NOTE TO SELF: When modifying something here, don't forget to propagate the
+    # modifications to the man-pages (under /doc)
+
+    def __init__(self, opts=None):
+        """ The opts parameter is used by the testsuite """
+        if opts is None:
+            opts = [
+                StrParameter('rcConfigName', 'rcconfigname', ('-c', '--config'), None, True, "use alternative configuration file"),
+                StrParameter('dumpDir', 'dump-dir', ('--dump-dir',), "~/.galileo", False, "directory for storing dumps"),
+                IntParameter('daemonPeriod', 'daemon-period', ('--daemon-period',), 15000, False, "sleep time in msec between sync runs when in daemon mode"),
+                SetParameter('includeTrackers', 'include', ('-I', '--include'), None, False, "list of tracker IDs to sync (all if not specified)"),
+                SetParameter('excludeTrackers', 'exclude', ('-X', '--exclude'), set(), False, "list of tracker IDs to not sync"),
+                LogLevelParameter(),
+                BoolParameter('forceSync', 'force-sync', ('force',), False, False, "synchronize even if tracker reports a recent sync"),
+                BoolParameter('keepDumps', 'keep-dumps', ('dump',), True, False, "enable saving of the megadump to file"),
+                BoolParameter('doUpload', 'do-upload',  ('upload',), True, False, "upload the dump to the server"),
+                BoolParameter('httpsOnly', 'https-only', ('https-only',), True, False, "use http if https is not available"),
+                IntParameter('logSize', 'log-size', ('--log-size',), 10, False, "Amount of communication to display in case of error"),
+                BoolParameter('syslog', 'syslog', ('syslog',), False, False, "send output to syslog instead of stderr"),
+                Argument(),
+                HardCodedUIConfig(),
+                ]
+        self.__opts = opts
+        self.__optdict = {}
+        for opt in self.__opts:
+            self.__optdict[opt.varName] = opt.default
+
+        logger.debug("Config default values: %s", self)  # not logged
+
+    def __getattr__(self, name):
+        """ Allow accessing the attributes as config.XXX """
+        if name not in self.__optdict:
+            raise AttributeError(name)
+        return self.__optdict[name]
+
+    def parseSystemConfig(self):
+        """ Load the system-wide configuration file """
+        self.load('/etc/galileo/config')
+
+    def parseUserConfig(self):
+        """ Load the user based configuration file """
+        self.load(os.path.join(
+            os.environ.get('XDG_CONFIG_HOME', '~/.config'),
+            'galileo', 'config'))
+        self.load('~/.galileorc')
+
+    def load(self, filename):
+        """Load configuration settings from the named YAML-format
+        configuration file. This configuration file can include a
+        subset of possible parameters in which case only those
+        parameters are changed by the load operation.
+
+        Arguments:
+        - `filename`: The name of the file to load parameters from.
+
+        """
+        filename = os.path.expanduser(filename)
+        if not os.path.exists(filename):
+            # Not logged
+            logger.warning('Config file %s does not exists' % filename)
+            return
+
+        logger.debug('Reading config file %s' % filename)  # not logged
+
+        with open(filename, 'rt') as f:
+            config = yaml.load(f)
+
+        for param in self.__opts:
+            if not param.fromFile(config, self.__optdict):
+                raise ConfigFileError(filename, param.name)
+
+    def parseArgs(self):
+        argparser = argparse.ArgumentParser(description="synchronize Fitbit trackers with Fitbit web service",
+                                            epilog="""Access your synchronized data at http://www.fitbit.com.""")
+        for param in self.__opts:
+            param.toArgParse(argparser)
+
+        self.cmdlineargs = argparser.parse_args()
+
+        # And we apply them immediately
+        self.applyArgs()
+
+    def applyArgs(self):
+        for param in self.__opts:
+            param.fromArgs(self.cmdlineargs, self.__optdict)
+
+    def shouldSkip(self, tracker):
+        """Method to check, based on the configuration, whether a particular
+        tracker should be skipped and not synchronized. The
+        includeTrackers and excludeTrackers properties are checked to
+        determine this.
+
+        Arguments:
+        - `tracker`: Tracker (object), to check.
+
+        """
+        trackerid = a2x(tracker.id, delim='')
+
+        # If a list of trackers to sync is configured then was
+        # provided then ignore this tracker if it's not in that list.
+        if (self.includeTrackers is not None) and (trackerid not in self.includeTrackers):
+            logger.info("Include list not empty, and tracker %s not there, skipping.", trackerid)
+            tracker.status = "Skipped because not in include list"
+            return True
+
+        # If a list of trackers to avoid syncing is configured then
+        # ignore this tracker if it is in that list.
+        if trackerid in self.excludeTrackers:
+            logger.info("Tracker %s in exclude list, skipping.", trackerid)
+            tracker.status = "Skipped because in exclude list"
+            return True
+
+        if tracker.syncedRecently:
+            if not self.forceSync:
+                logger.info('Tracker %s was recently synchronized; skipping for now', trackerid)
+                tracker.status = "Skipped because recently synchronised"
+                return True
+            logger.info('Tracker %s was recently synchronized, but forcing synchronization anyway', trackerid)
+
+        return False
+
+    def __str__(self):
+        return str(self.__optdict)
diff --git a/galileo/conversation.py b/galileo/conversation.py
new file mode 100644
index 0000000..bbf9349
--- /dev/null
+++ b/galileo/conversation.py
@@ -0,0 +1,203 @@
+"""\
+The conversationnal part between the server and the client ...
+"""
+
+import base64
+import time
+import uuid
+
+import logging
+logger = logging.getLogger(__name__)
+
+from .dongle import FitBitDongle
+from .net import GalileoClient
+from .tracker import FitbitClient, MICRODUMP, MEGADUMP
+from .ui import MissingConfigError
+from .utils import a2x, s2a
+
+
+FitBitUUID = uuid.UUID('{ADAB0000-6E7D-4601-BDA2-BFFAA68956BA}')
+
+
+class Conversation(object):
+    def __init__(self, mode, ui):
+        self.mode = mode
+        self.ui = ui
+
+    def __call__(self, config):
+        self.dongle = FitBitDongle(config.logSize)
+        if not self.dongle.setup():
+            logger.error("No dongle connected, aborting")
+            return
+
+        self.fitbit = FitbitClient(self.dongle)
+
+        self.galileo = GalileoClient('https', 'client.fitbit.com',
+                                'tracker/client/message')
+
+        if self.mode == 'firmware':
+            # Fake the version to let him believe we can handle that ...
+            self.galileo._version = '1.0.0.2575'
+
+        self.fitbit.disconnect()
+
+        self.trackers = {}  # Dict indexed by trackerId
+        self.connected = None
+
+        if not self.fitbit.getDongleInfo():
+            logger.warning('Failed to get connected Fitbit dongle information')
+
+        action = ''
+        uiresp = []
+        resp = [('ui-response', {'action': action}, uiresp)]
+
+        while True:
+            answ = self.galileo.post(self.mode, self.dongle, resp)
+            html = ''
+            commands = None
+            trackers = []
+            action = None
+            containsForm = False
+            for tple in answ:
+                tag, attribs, childs, _ = tple
+                if tag == "ui-request":
+                    action = attribs['action']
+                    for child in childs:
+                        tag, attribs, _, body = child
+                        if tag == "client-display":
+                            containsForm = attribs.get('containsForm', 'false') == 'true'
+                            html = body
+                elif tag == 'tracker':
+                    trackers.append(tple)
+                elif tag == 'commands':
+                    commands = childs
+            if ((not containsForm) and (len(trackers) == 0) and
+                (commands is None)):
+                break
+            resp = []
+            if trackers:
+                # First: Do what is asked
+                for tracker in trackers:
+                    self.do_tracker(tracker)
+            if commands:
+                # Prepare an answer for the server
+                res = []
+                for command in commands:
+                    r = self.do_command(command)
+                    print(r)
+                    if r is not None:
+                        res.append(r)
+                if res:
+                    resp.extend(res)
+            if containsForm:
+                # Get an answer from the ui
+                try:
+                    ui_resp = self.ui.request(action, html)
+                except MissingConfigError as mce:
+                    print(mce)
+                    break
+                resp.append(('ui-response', {'action': action}, ui_resp))
+
+        print('Done')
+
+    def __connect(self, id):
+        tracker = self.trackers[id]
+        self.fitbit.establishLink(tracker)
+        self.fitbit.toggleTxPipe(True)
+        self.fitbit.initializeAirlink(tracker)
+        self.connected = tracker
+
+
+    #-------- The commands
+
+    def do_command(self, cmd):
+        tag, elems, childs, body = cmd
+        f = {'pair-to-tracker': self._pair,
+            'connect-to-tracker': self._connect,
+            'list-trackers': self._list,
+            'ack-tracker-data': self._ack}[tag]
+        return f(*childs, **elems)
+
+    def _pair(self, **params):
+        """ Establish a connection with the tracker.
+            :returns: the minidump
+        """
+        displayCode = bool(params['displayCode'])
+        waitForUserInput = bool(params['waitForUserInput'])
+        trackerId = params['tracker-id']
+        self.__connect(trackerId)
+        if displayCode:
+            self.fitbit.displayCode()
+            if waitForUserInput:
+                # XXX: That's waiting, but not for user input ...
+                time.sleep(10)
+        dump = self.fitbit.getDump(MICRODUMP)
+        return ('tracker', {'tracker-id':trackerId},
+                 [('data', {}, [], dump.toBase64())])
+
+    def _connect(self, **params):
+        """ :returns: nothing
+        """
+        trackerId = params['tracker-id']
+        if self.connected is None:
+            self.__connect(trackerId)
+
+        if a2x(self.connected.id, delim="") != trackerId:
+            raise ValueError(trackerId)
+        if 'connection' in params:
+            disconnect = params['connection'] == 'disconnect'
+            if disconnect:
+                self.fitbit.terminateAirlink()
+                self.fitbit.toggleTxPipe(False)
+                self.fitbit.ceaseLink()
+                self.connected = None
+            return
+        elif 'response-data' in params:
+            responseData = params['response-data']
+            dumptype = {'megadump': MEGADUMP,
+                        'microdump': MICRODUMP}[responseData]
+            dump = self.fitbit.getDump(dumptype)
+            return ('tracker', {'tracker-id': trackerId},
+                     [('data', {}, [], dump.toBase64())])
+        else:
+            raise ValueError(params)
+
+
+    def _list(self, *childs, **params):
+        immediateRsi = int(params['immediateRsi'])
+        minDuration = int(params['minDuration'])
+        maxDuration = int(params['maxDuration'])
+
+        self.trackers = {}
+        res = []
+        for tracker in self.fitbit.discover(FitBitUUID, minRSSI=immediateRsi,
+                                             minDuration=minDuration):
+            trackerId = a2x(tracker.id, delim="")
+            self.trackers[trackerId] = tracker
+            res.append(('available-tracker', {},
+                        [('tracker-id', {}, [], trackerId),
+                         ('tracker-attributes', {}, [], a2x(tracker.serviceData, delim="")),
+                         ('rsi', {}, [], str(tracker.RSSI))]))
+        return ('command-response', {}, [('list-trackers', {}, res)])
+
+    def _ack(self, **params):
+        trackerId = params['tracker-id']
+        # Not much to do here as our ack is part of our upload ...
+        return ('command-response', {}, [('ack-tracker-data', {'tracker-id':trackerId})])
+
+    # ------
+
+    def do_tracker(self, tracker):
+        tag, elems, childs, body = tracker
+        trackerId = elems['tracker-id']
+        if a2x(self.connected.id, delim="") != trackerId:
+            raise ValueError(trackerId)
+        _type = elems['type']
+        if _type != 'megadumpresponse':
+            raise NotImplementedError(_type)
+        data = None
+        for child in childs:
+            tag, _, _, body = child
+            if tag == 'data':
+                data = s2a(base64.b64decode(body))
+        self.fitbit.uploadResponse(data)
diff --git a/galileo/dongle.py b/galileo/dongle.py
new file mode 100644
index 0000000..660fde8
--- /dev/null
+++ b/galileo/dongle.py
@@ -0,0 +1,322 @@
+from __future__ import print_function
+
+import errno
+
+import logging
+logger = logging.getLogger(__name__)
+
+try:
+    import usb.core
+except ImportError as ie:
+    # if ``usb`` is there, but not ``usb.core``, a pre-1.0 version of pyusb
+    # is installed.
+    try:
+        import usb
+    except ImportError:
+        pass
+    else:
+        print("You have an older pyusb version installed. This utility needs")
+        print("at least version 1.0.0a2 to work properly.")
+        print("Please upgrade your system to a newer version.")
+    raise ie
+
+from .utils import a2x, a2s
+
+IN, OUT = 1, -1
+
+class DataRing(object):
+    """ A 'stupid' data structure that store not more that capacity elements,
+    and keeps them in order
+
+    head points to the next spot
+    queue points to the last spot
+    fill tell us how much is filled
+    """
+    def __init__(self, capacity):
+        self.capacity = capacity
+        self.ring = [None] * self.capacity
+        self.head = 0
+        self.queue = 0
+        # We can't distinguish empty from full without the fillage
+        self.fill = 0
+
+    @property
+    def empty(self):
+        return self.fill == 0
+
+    @property
+    def full(self):
+        return self.fill == self.capacity
+
+    def add(self, data):
+        if self.capacity == 0:
+            # Special case, do nothing
+            return
+        if self.full:
+            # full, don't forget to increase the queue
+            self.queue = (self.queue + 1) % self.capacity
+        self.ring[self.head] = data
+        self.head = (self.head + 1) % self.capacity
+        self.fill = min(self.fill + 1, self.capacity)
+
+    def remove(self):
+        """ For the fun, doesnt fit into our use case """
+        if self.empty:
+            # NOOP
+            return
+        self.queue = (self.queue - 1)  % self.capacity
+
+    def getData(self):
+        if self.empty:
+            return []
+        elif self.queue < self.head:
+            return self.ring[self.queue:self.head]
+        else:
+            return self.ring[self.queue:] + self.ring[:self.head]
+
+
+class USBDevice(object):
+    def __init__(self, vid, pid):
+        self.vid = vid
+        self.pid = pid
+        self._dev = None
+
+    @property
+    def dev(self):
+        if self._dev is None:
+            self._dev = usb.core.find(idVendor=self.vid, idProduct=self.pid)
+        return self._dev
+
+    def __del__(self):
+        if hasattr(self, '_dev') and self._dev is not None:
+            self._dev.reset()
+
+
+class CtrlMessage(object):
+    """ A message that get communicated over the ctrl link """
+    def __init__(self, INS, data=[]):
+        if INS is None:  # incoming
+            self.len = data[0]
+            self.INS = data[1]
+            self.payload = data[2:self.len]
+        else:  # outgoing
+            self.len = len(data) + 2
+            self.INS = INS
+            self.payload = data
+
+    def asList(self):
+        return [self.len, self.INS] + self.payload
+
+    def __eq__(self, other):
+        if other is None: return False
+        return self.asList() == other.asList()
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __str__(self):
+        d = []
+        if self.payload:
+            d = ['(', a2x(self.payload), ')']
+        return ' '.join(['%02X' % self.INS] + d + ['-', str(self.len)])
+
+CM = CtrlMessage
+
+
+class DataMessage(object):
+    """ A message that get communicated over the data link """
+    LENGTH = 32
+
+    def __init__(self, data, out=True):
+        if out:  # outgoing
+            if len(data) > (self.LENGTH - 1):
+                raise ValueError('data %s (%d) too big' % (data, len(data)))
+            self.data = data
+            self.len = len(data)
+        else:  # incoming
+            if len(data) != self.LENGTH:
+                raise ValueError('data %s with wrong length' % data)
+            # last byte is length
+            self.len = data[-1]
+            self.data = list(data[:self.len])
+
+    def asList(self):
+        return self.data + [0] * (self.LENGTH - 1 - self.len) + [self.len]
+
+    def __eq__(self, other):
+        if other is None: return False
+        return self.data == other.data
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __str__(self):
+        return ' '.join(['[', a2x(self.data), ']', '-', str(self.len)])
+
+DM = DataMessage
+
+
+def isATimeout(excpt):
+    if excpt.errno == errno.ETIMEDOUT:
+        return True
+    elif excpt.errno is None and excpt.args == ('Operation timed out',):
+        return True
+    elif excpt.errno is None and excpt.strerror == 'Connection timed out':
+        return True
+    else:
+        return False
+
+
+class DongleWriteException(Exception): pass
+
+
+class PermissionDeniedException(Exception): pass
+
+
+def isStatus(data, msg=None, logError=True):
+    if data is None:
+        return False
+    if data.INS != 1:
+        if logError:
+            logging.warning("Message is not a status message: %x", data.INS)
+        return False
+    if msg is None:
+        return True
+    message = a2s(data.payload)
+    if not message.startswith(msg):
+        if logError:
+            logging.warning("Message '%s' (received) is not '%s' (expected)",
+                            message, msg)
+        return False
+    return True
+
+
+class FitBitDongle(USBDevice):
+    VID = 0x2687
+    PID = 0xfb01
+
+    def __init__(self, logsize):
+        USBDevice.__init__(self, self.VID, self.PID)
+        self.hasVersion = False
+        self.establishLinkEx = False
+        self.newerPyUSB = None
+        global log
+        log = DataRing(logsize)
+
+    def setup(self):
+        if self.dev is None:
+            return False
+
+        try:
+            if self.dev.is_kernel_driver_active(0):
+                self.dev.detach_kernel_driver(0)
+            if self.dev.is_kernel_driver_active(1):
+                self.dev.detach_kernel_driver(1)
+        except usb.core.USBError as ue:
+            if ue.errno == errno.EACCES:
+                logger.error('Insufficient permissions to access the Fitbit'
+                             ' dongle')
+                # Don't try to cleanup the connection in the destructor
+                del self._dev
+                raise PermissionDeniedException
+            raise
+        except NotImplementedError as nie:
+            logger.error("Hit some 'Not Implemented Error': '%s', moving on ...", nie)
+
+        cfg = self.dev.get_active_configuration()
+        self.DataIF = cfg[(0, 0)]
+        self.CtrlIF = cfg[(1, 0)]
+        self.dev.set_configuration()
+        return True
+
+    def setVersion(self, major, minor):
+        self.major = major
+        self.minor = minor
+        self.hasVersion = True
+        self.establishLinkEx = (major, minor) >= (7, 5)
+        logger.debug('Fitbit dongle version major:%d minor:%d', self.major,
+                     self.minor)
+
+    def write(self, endpoint, data, timeout):
+        if self.newerPyUSB:
+            params = (endpoint, data, timeout)
+        else:
+            interface = {0x02: self.CtrlIF.bInterfaceNumber,
+                         0x01: self.DataIF.bInterfaceNumber}[endpoint]
+            params = (endpoint, data, interface, timeout)
+        log.add((OUT, data))
+        try:
+            return self.dev.write(*params)
+        except TypeError:
+            if self.newerPyUSB is not None:
+                # Already been there, something else is happening ...
+                raise
+            logger.debug('Switching to a newer pyusb compatibility mode')
+            self.newerPyUSB = True
+            return self.write(endpoint, data, timeout)
+        except usb.core.USBError as ue:
+            if ue.errno != errno.EIO:
+                raise
+            logger.info('Caught an I/O Error while writing, trying again ...')
+            # IO Error, try again ...
+            return self.dev.write(*params)
+
+    def read(self, endpoint, length, timeout):
+        if self.newerPyUSB:
+            params = (endpoint, length, timeout)
+        else:
+            interface = {0x82: self.CtrlIF.bInterfaceNumber,
+                         0x81: self.DataIF.bInterfaceNumber}[endpoint]
+            params = (endpoint, length, interface, timeout)
+        data = None
+        try:
+            data = self.dev.read(*params)
+        except TypeError:
+            if self.newerPyUSB is not None:
+                # Already been there, something else is happening ...
+                raise
+            logger.debug('Switching to a newer pyusb compatibility mode')
+            self.newerPyUSB = True
+            return self.read(endpoint, length, timeout)
+        except usb.core.USBError as ue:
+            if not isATimeout(ue):
+                raise
+            logger.info('Got an I/O Timeout (> %dms) while reading!', timeout)
+        log.add((IN, data))
+        return data
+
+    def ctrl_write(self, msg, timeout=2000):
+        logger.debug('--> %s', msg)
+        l = self.write(0x02, msg.asList(), timeout)
+        if l != msg.len:
+            logger.error('Bug, sent %d, had %d', l, msg.len)
+            raise DongleWriteException
+
+    def ctrl_read(self, timeout=2000, length=32):
+        msg = None
+        data = self.read(0x82, length, timeout)
+        if data is not None:
+            # 'None' parameter in next line means incoming
+            msg = CM(None, list(data))
+        if msg is None:
+            logger.debug('<-- ...')
+        elif isStatus(msg, logError=False):
+            logger.debug('<-- %s', a2s(msg.payload))
+        else:
+            logger.debug('<-- %s', msg)
+        return msg
+
+    def data_write(self, msg, timeout=2000):
+        logger.debug('==> %s', msg)
+        l = self.write(0x01, msg.asList(), timeout)
+        if l != msg.LENGTH:
+            logger.error('Bug, sent %d, had %d', l, msg.LENGTH)
+            raise DongleWriteException
+
+    def data_read(self, timeout=2000):
+        msg = None
+        data = self.read(0x81, DM.LENGTH, timeout)
+        if data is not None:
+            msg = DM(data, out=False)
+        logger.debug('<== %s', msg or '...')
+        return msg
diff --git a/galileo/dump.py b/galileo/dump.py
new file mode 100644
index 0000000..51e87e8
--- /dev/null
+++ b/galileo/dump.py
@@ -0,0 +1,130 @@
+import base64
+import logging
+
+logger = logging.getLogger(__name__)
+
+from .utils import a2x, a2lsbi, a2b
+
+
+class CRC16(object):
+    """ A rather generic CRC16 class """
+    def __init__(self, poly=0x1021, Invert=True, IV=0x0000, FV=0x0000):
+        self.poly = poly
+        self.value = IV
+        self.FV = FV
+        if Invert:
+            self.update_byte = self.update_byte_MSB
+        else:
+            self.update_byte = self.update_byte_LSB
+
+    def update_byte_MSB(self, byte):
+        self.value ^= byte << 8
+        for i in range(8):
+            if self.value & 0x8000:
+                self.value = (self.value << 1) ^ self.poly
+            else:
+                self.value <<= 1
+        self.value &= 0xffff
+
+    def update_byte_LSB(self, byte):
+        self.value ^= byte
+        for i in range(8):
+            if self.value & 0x0001:
+                self.value = (self.value >> 1) ^ self.poly
+            else:
+                self.value >>= 1
+
+    def update(self, array):
+        for c in array:
+            self.update_byte(c)
+
+    def final(self):
+        return self.value ^ self.FV
+
+
+class Dump(object):
+    def __init__(self, _type):
+        self._type = _type
+        self.data = []
+        self.footer = []
+        self.crc = CRC16()
+        self.esc = [0, 0]
+
+    def unSLIP1(self, data):
+        """ The protocol uses a particular version of SLIP (RFC 1055) applied
+        only on the first byte of the data"""
+        END = 0xC0
+        ESC = 0xDB
+        ESC_ = {0xDC: END,
+                0xDD: ESC}
+        if data[0] == ESC:
+            # increment the escape counter
+            self.esc[data[1] - 0xDC] += 1
+            # return the escaped value
+            return [ESC_[data[1]]] + data[2:]
+        return data
+
+    def add(self, data):
+        if data[0] == 0xc0:
+            assert self.footer == []
+            self.footer = data
+            return
+        data = self.unSLIP1(data)
+        self.crc.update(data)
+        self.data.extend(data)
+
+    @property
+    def len(self):
+        return len(self.data)
+
+    def isValid(self):
+        if not self.footer:
+            return False
+        dataType = self.footer[2]
+        if dataType != self._type:
+            logger.error('Dump is not of requested type: %x != %x',
+                         dataType, self._type)
+            return False
+        crcVal = self.crc.final()
+        transportCRC = a2lsbi(self.footer[3:5])
+        if transportCRC != crcVal:
+            logger.error("Error in communication, Expected CRC: 0x%04X,"
+                         " received 0x%04X", crcVal, transportCRC)
+            return False
+        nbBytes = a2lsbi(self.footer[5:9])
+        if self.len != nbBytes:
+            logger.error("Error in communication, Expected length: %d bytes,"
+                         " received %d bytes", nbBytes, self.len)
+            return False
+        return True
+
+    def toFile(self, filename):
+        logger.debug("Dumping megadump to %s", filename)
+        with open(filename, 'wt') as dumpfile:
+            for i in range(0, self.len, 20):
+                dumpfile.write(a2x(self.data[i:i + 20]) + '\n')
+            dumpfile.write(a2x(self.footer) + '\n')
+
+    def toBase64(self):
+        return base64.b64encode(a2b(self.data + self.footer)).decode('utf-8')
+
+class DumpResponse(object):
+    def __init__(self, data, chunk_len):
+        self.data = data
+        self._chunk_len = chunk_len
+        self.__index = 0
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        if self.__index >= len(self.data):
+            raise StopIteration
+        if self.data[self.__index] not in (0xC0, 0xDB):
+            self.__index += self._chunk_len
+            return self.data[self.__index-self._chunk_len:self.__index]
+        b = self.data[self.__index]
+        self.__index += self._chunk_len - 1
+        return [0xDB] + [{0xC0: 0xDC, 0xDB: 0xDD}[b]] + self.data[self.__index-self._chunk_len+2:self.__index]
+    # For python2
+    next = __next__
diff --git a/galileo/interactive.py b/galileo/interactive.py
new file mode 100644
index 0000000..531dc58
--- /dev/null
+++ b/galileo/interactive.py
@@ -0,0 +1,223 @@
+"""\
+This is the implementation of the interactive mode
+
+This is the same idea as ifitbit I wrote for libfitbit some years ago
+
+https://bitbucket.org/benallard/libfitbit/src/tip/python/ifitbit.py?at=default
+
+"""
+
+from __future__ import print_function
+
+#---------------------------
+# The engine
+
+import readline
+import traceback
+import sys
+
+exit = None
+
+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
+
+
+ at command('x', "Quit")
+def quit():
+    print('Bye !')
+    global exit
+    exit = True
+
+
+ at command('?', 'Print possible commands')
+def print_help():
+    for cmd in sorted(helps.keys()):
+        print('%s\t%s' % (cmd, helps[cmd]))
+    print("""Note:
+ - You can enter multiple commands separated by ';'
+ - To establish a link with the tracker, enter the following command:
+      c ; d ; l ; tx 1 ; al
+""")
+
+def main(config):
+    global exit
+    exit = False
+    print_help()
+    while not exit:
+        orders = raw_input('> ').strip()
+        if ';' in orders:
+            orders = orders.split(';')
+        else:
+            orders = [orders]
+        for order in orders:
+            order = order.strip().split(' ')
+            try:
+                f = cmds[order[0]]
+            except KeyError:
+                if order[0] == '':
+                    continue
+                print('Command %s not known' % order[0])
+                print_help()
+                continue
+            try:
+                f(*order[1:])
+            except TypeError as te:
+                print("Wrong number of argument given: %s" % te)
+            except Exception as e:
+                # We need that to be able to close the connection nicely
+                print("BaD bAd BAd", e)
+                traceback.print_exc(file=sys.stdout)
+                return
+
+
+#---------------------------
+# The commands
+
+from .dongle import FitBitDongle, CM, DM
+from .tracker import FitbitClient
+from .utils import x2a
+
+import uuid
+
+dongle = None
+fitbit = None
+trackers = []
+tracker = None
+
+ at command('c', "Connect")
+def connect():
+    global dongle
+    dongle = FitBitDongle(0)  # No DataRing needed
+    if not dongle.setup():
+        print("No dongle connected, aborting")
+        quit()
+    global fitbit
+    fitbit = FitbitClient(dongle)
+    print('Ok')
+
+
+def needfitbit(fn):
+    def wrapped(*args):
+        if dongle is None:
+            print("No connection, connect (c) first")
+            return
+        return fn(*args)
+    return wrapped
+
+ at command('->', "Send on the control channel")
+ at needfitbit
+def send_ctrl(INS, *payload):
+    if payload:
+        payload = x2a(' '.join(payload))
+    else:
+        payload = []
+    m = CM(int(INS, 16), payload)
+    dongle.ctrl_write(m)
+
+ at command('<-', "Receive once on the control channel")
+ at needfitbit
+def receive_ctrl(param='1'):
+    if param == '-':
+        goOn = True
+        while goOn:
+            goOn = dongle.ctrl_read() is not None
+    else:
+        for i in range(int(param)):
+            dongle.ctrl_read()
+
+ at command('=>', "Send on the control channel")
+ at needfitbit
+def send_data(*payload):
+    m = DM(x2a(' '.join(payload)))
+    dongle.data_write(m)
+
+ at command('<=', "Receive once on the control channel")
+ at needfitbit
+def receive_data(param='1'):
+    if param == '-':
+        goOn = True
+        while goOn:
+            goOn = dongle.data_read() is not None
+    else:
+        for i in range(int(param)):
+            dongle.data_read()
+
+ at command('d', "Discovery")
+ at needfitbit
+def discovery(UUID="{ADAB0000-6E7D-4601-BDA2-BFFAA68956BA}"):
+    UUID = uuid.UUID(UUID)
+    global trackers
+    trackers = [t for t in fitbit.discover(UUID)]
+
+
+def needtrackers(fn):
+    def wrapped(*args):
+        if not trackers:
+            print("No trackers, run a discovery (d) first")
+            return
+        return fn(*args)
+    return wrapped
+
+ at command('l', "establishLink")
+ at needtrackers
+def establishLink(idx='0'):
+    global tracker
+    tracker = trackers[int(idx)]
+    if fitbit.establishLink(tracker):
+        print('Ok')
+    else:
+        tracker = None
+
+ at command('L', "ceaseLink")
+ at needfitbit
+def ceaseLink():
+    if not fitbit.ceaseLink():
+        print('Bad')
+    else:
+        print('Ok')
+
+def needtracker(fn):
+    def wrapped(*args):
+        if tracker is None:
+            print("No tracker, establish a Link (l) first")
+            return
+        return fn(*args)
+    return wrapped
+
+ at command('tx', "toggle Tx Pipe")
+ at needfitbit
+def toggleTxPipe(on):
+    if fitbit.toggleTxPipe(bool(int(on))):
+        print('Ok')
+
+ at command('al', "initialise airLink")
+ at needtracker
+def initialiseAirLink():
+    if fitbit.initializeAirlink(tracker):
+        print('Ok')
+
+ at command('AL', "terminate airLink")
+ at needfitbit
+def terminateairLink():
+    if fitbit.terminateAirlink():
+        print('Ok')
+
+ at command('D', 'getDump')
+ at needfitbit
+def getDump(type="13"):
+    fitbit.getDump(int(type))
+
+ at command('R', 'uploadResponse')
+ at needfitbit
+def uploadResponse(*response):
+    response = x2a(' '.join(response))
+    fitbit.uploadResponse(response)
diff --git a/galileo/main.py b/galileo/main.py
new file mode 100644
index 0000000..eb1e7e3
--- /dev/null
+++ b/galileo/main.py
@@ -0,0 +1,301 @@
+from __future__ import print_function
+
+import datetime
+import os
+import sys
+import time
+import uuid
+
+import logging
+import logging.handlers
+logger = logging.getLogger(__name__)
+
+import requests
+
+from . import __version__
+from .config import Config, ConfigError
+from .conversation import Conversation
+from .net import GalileoClient, SyncError, BackOffException
+from .tracker import FitbitClient
+from .ui import InteractiveUI
+from .utils import a2x
+from . import dongle as dgl
+from . import interactive
+
+FitBitUUID = uuid.UUID('{ADAB0000-6E7D-4601-BDA2-BFFAA68956BA}')
+
+
+def syncAllTrackers(config):
+    logger.debug('%s initialising', os.path.basename(sys.argv[0]))
+    dongle = dgl.FitBitDongle(config.logSize)
+    if not dongle.setup():
+        logger.error("No dongle connected, aborting")
+        return
+
+    fitbit = FitbitClient(dongle)
+
+    galileo = GalileoClient('https', 'client.fitbit.com',
+                            'tracker/client/message')
+
+    if not fitbit.disconnect():
+        logger.error("Dirty state, not able to start synchronisation.")
+        fitbit.exhaust()
+        return
+
+    if not fitbit.getDongleInfo():
+        logger.warning('Failed to get connected Fitbit dongle information')
+
+    logger.info('Discovering trackers to synchronize')
+
+    trackers = [t for t in fitbit.discover(FitBitUUID)]
+
+    logger.info('%d trackers discovered', len(trackers))
+    for tracker in trackers:
+        logger.debug('Discovered tracker with ID %s',
+                     a2x(tracker.id, delim=""))
+
+    for tracker in trackers:
+
+        trackerid = a2x(tracker.id, delim="")
+
+        # Skip this tracker based on include/exclude lists.
+        if config.shouldSkip(tracker):
+            logger.info('Tracker %s skipped due to configuration', trackerid)
+            yield tracker
+            continue
+
+        logger.info('Attempting to synchronize tracker %s', trackerid)
+
+        if config.doUpload:
+            logger.debug('Connecting to Fitbit server and requesting status')
+            if not galileo.requestStatus(not config.httpsOnly):
+                yield tracker
+                break
+
+        logger.debug('Establishing link with tracker')
+        if not (fitbit.establishLink(tracker) and fitbit.toggleTxPipe(True)
+                and fitbit.initializeAirlink(tracker)):
+            logger.warning('Unable to connect with tracker %s. Skipping',
+                           trackerid)
+            tracker.status = 'Unable to establish a connection.'
+            yield tracker
+            continue
+
+        #fitbit.displayCode()
+        #time.sleep(5)
+
+        logger.info('Getting data from tracker')
+        dump = fitbit.getDump()
+        if dump is None:
+            logger.error("Error downloading the dump from tracker")
+            tracker.status = "Failed to download the dump"
+            yield tracker
+            continue
+
+        if config.keepDumps:
+            # Write the dump somewhere for archiving ...
+            dirname = os.path.expanduser(os.path.join(config.dumpDir,
+                                                      trackerid))
+            if not os.path.exists(dirname):
+                logger.debug("Creating non-existent directory for dumps %s",
+                             dirname)
+                os.makedirs(dirname)
+
+            filename = os.path.join(dirname, 'dump-%d.txt' % int(time.time()))
+            dump.toFile(filename)
+        else:
+            logger.debug("Not dumping anything to disk")
+
+        if not config.doUpload:
+            logger.info("Not uploading, as asked ...")
+        else:
+            logger.info('Sending tracker data to Fitbit')
+            try:
+                response = galileo.sync(fitbit.dongle, trackerid, dump)
+
+                if config.keepDumps:
+                    logger.debug("Appending answer from server to %s",
+                                 filename)
+                    with open(filename, 'at') as dumpfile:
+                        dumpfile.write('\n')
+                        for i in range(0, len(response), 20):
+                            dumpfile.write(a2x(response[i:i + 20]) + '\n')
+
+                # Even though the next steps might fail, fitbit has accepted
+                # the data at this point.
+                tracker.status = "Dump successfully uploaded"
+                logger.info('Successfully sent tracker data to Fitbit')
+
+                logger.info('Passing Fitbit response to tracker')
+                if not fitbit.uploadResponse(response):
+                    logger.warning("Error while trying to give Fitbit response"
+                                   " to tracker %s", trackerid)
+                    tracker.status = "Failed to upload fitbit response to tracker"
+                else:
+                    tracker.status = "Synchronisation successful"
+
+            except SyncError as e:
+                logger.error("Fitbit server refused data from tracker %s,"
+                             " reason: %s", trackerid, e.errorstring)
+                tracker.status = "Synchronisation failed: %s" % e.errorstring
+
+        logger.debug('Disconnecting from tracker')
+        if not (fitbit.terminateAirlink() and fitbit.toggleTxPipe(False) and fitbit.ceaseLink()):
+            logger.warning('Error while disconnecting from tracker %s',
+                           trackerid)
+            tracker.status += " (Error disconnecting)"
+        yield tracker
+
+PERMISSION_DENIED_HELP = """
+To be able to run the fitbit utility as a non-privileged user, you first
+should install a 'udev rule' that lower the permissions needed to access the
+fitbit dongle. In order to do so, as root, create the file
+/etc/udev/rules.d/99-fitbit.rules with the following content (in one line):
+
+SUBSYSTEM=="usb", ATTR{idVendor}=="%(VID)x", ATTR{idProduct}=="%(PID)x", SYMLINK+="fitbit", MODE="0666"
+
+The dongle must then be removed and reinserted to receive the new permissions.""" % {
+    'VID': dgl.FitBitDongle.VID, 'PID': dgl.FitBitDongle.PID}
+
+
+def version(verbose, delim='\n'):
+    s = ['%s: %s' % (sys.argv[0], __version__)]
+    if verbose:
+        import usb
+        import platform
+        from .config import yaml
+        # To get it on one line
+        s.append('Python: %s' % ' '.join(sys.version.split()))
+        s.append('Platform: %s' % ' '.join(platform.uname()))
+        if not hasattr(usb, '__version__'):
+            s.append('pyusb: < 1.0.0b1')
+        else:
+            s.append('pyusb: %s' % usb.__version__)
+        s.append('requests: %s' % requests.__version__)
+        if hasattr(yaml, '__with_libyaml__'):
+            # Genuine PyYAML
+            s.append('yaml: %s (%s libyaml)' % (
+                yaml.__version__,
+                yaml.__with_libyaml__ and 'with' or 'without'))
+        else:
+            # Custom version
+            s.append('yaml: own version')
+    return delim.join(s)
+
+
+def version_mode(config):
+    print(version(config.logLevel in (logging.INFO, logging.DEBUG)))
+
+
+def sync(config):
+    statuses = []
+    try:
+        for tracker in syncAllTrackers(config):
+            statuses.append("Tracker: %s: %s" % (a2x(tracker.id, ''),
+                                                 tracker.status))
+    except BackOffException as boe:
+        print("The server requested that we come back between %d and %d"\
+            " minutes." % (boe.min / (60*1000), boe.max / (60*1000)))
+        later = datetime.datetime.now() + datetime.timedelta(
+            microseconds=boe.getAValue()*1000)
+        print("I suggest waiting until %s" % later)
+        return
+    except dgl.PermissionDeniedException:
+        print(PERMISSION_DENIED_HELP)
+        return
+    print('\n'.join(statuses))
+
+
+def daemon(config):
+    goOn = True
+    while goOn:
+        try:
+            # TODO: Extract the initialization part, and do it once for all
+            try:
+                for tracker in syncAllTrackers(config):
+                    logger.info("Tracker %s: %s" % (a2x(tracker.id, ''),
+                                                    tracker.status))
+            except BackOffException as boe:
+                logger.warning("Received a back-off notice from the server,"
+                               " waiting for a bit longer.")
+                time.sleep(boe.getAValue() / 1000.)
+            else:
+                logger.info("Sleeping for %d seconds before next sync",
+                            config.daemonPeriod / 1000)
+                time.sleep(config.daemonPeriod / 1000.)
+        except KeyboardInterrupt:
+            logger.info("Ctrl-C, caught, stopping ...")
+            goOn = False
+
+
+def main():
+    """ This is the entry point """
+
+    # Set the null handler to avoid complaining about no handler presents
+    import galileo
+    logging.getLogger(galileo.__name__).addHandler(logging.NullHandler())
+
+    try:
+        config = Config()
+
+        config.parseSystemConfig()
+        config.parseUserConfig()
+
+        # This gives us the config file name
+        config.parseArgs()
+
+        if config.rcConfigName:
+            config.load(config.rcConfigName)
+            # We need to re-apply our arguments as last
+            config.applyArgs()
+    except ConfigError as e:
+        print(e, file=sys.stderr)
+        sys.exit(os.EX_CONFIG)
+
+    # --- All logging actions before this line are not active ---
+    # This means that the whole Config parsing is not logged because we don't
+    # know which logLevel we should use.
+    if config.syslog:
+        # Syslog messages must have the time/name first.
+        format = ('%(asctime)s ' + galileo.__name__ + ': '
+                  '%(levelname)s: %(module)s: %(message)s')
+        # TODO: Make address into a config option.
+        handler = logging.handlers.SysLogHandler(
+            address='/dev/log',
+            facility=logging.handlers.SysLogHandler.LOG_DAEMON)
+        handler.setFormatter(logging.Formatter(fmt=format))
+        core_logger = logging.getLogger(galileo.__name__)
+        core_logger.handlers = []
+        core_logger.addHandler(handler)
+        core_logger.setLevel(config.logLevel)
+    else:
+        format = '%(asctime)s:%(levelname)s: %(message)s'
+        logging.basicConfig(format=format, level=config.logLevel)
+    # --- All logger actions from now on will be effective ---
+
+    logger.debug("Configuration: %s", config)
+
+    ui = InteractiveUI(config.hardcoded_ui)
+
+    try:
+        {
+            'version': version_mode,
+            'sync': sync,
+            'daemon': daemon,
+            'pair': Conversation('pair', ui),
+            'firmware': Conversation('firmware', ui),
+            'interactive': interactive.main,
+        }[config.mode](config)
+    except:
+        logger.critical("# A serious error happened, which is probably due to a")
+        logger.critical("# programming error. Please open a new issue with the following")
+        logger.critical("# information on the galileo bug tracker:")
+        logger.critical("#    https://bitbucket.org/benallard/galileo/issues/new")
+        logger.critical('# %s', version(True, '\n# '))
+        if hasattr(dgl, 'log'):
+            logger.critical('# Last communications:')
+            for comm in dgl.log.getData():
+                dir, dat = comm
+                logger.critical('# %s %s' % ({dgl.IN: '<', dgl.OUT: '>'}.get(dir, '-'), a2x(dat or [])))
+        logger.critical("#", exc_info=True)
+        sys.exit(os.EX_SOFTWARE)
diff --git a/galileo/net.py b/galileo/net.py
new file mode 100644
index 0000000..378ca3f
--- /dev/null
+++ b/galileo/net.py
@@ -0,0 +1,257 @@
+
+import base64
+import random
+import socket
+
+from io import BytesIO
+
+
+import xml.etree.ElementTree as ET
+
+import logging
+logger = logging.getLogger(__name__)
+
+import requests
+
+from . import __version__
+from .utils import s2a
+
+
+class SyncError(Exception):
+    def __init__(self, errorstring='Undefined'):
+        self.errorstring = errorstring
+
+
+class BackOffException(Exception):
+    def __init__(self, min, max):
+        self.min = min
+        self.max = max
+
+    def getAValue(self):
+        return random.randint(self.min, self.max)
+
+
+def toXML(name, attrs={}, childs=[], body=None):
+    elem = ET.Element(name, attrib=attrs)
+    if childs:
+        for XMLElem in tuplesToXML(childs):
+            elem.append(XMLElem)
+    if body is not None:
+        elem.text = body
+    return elem
+
+
+def tuplesToXML(tuples):
+    """ tuples is an array (or not) of (name, attrs, childs, body) """
+    if isinstance(tuples, tuple):
+        tuples = [tuples]
+    for tpl in tuples:
+        yield toXML(*tpl)
+
+
+def XMLToTuple(elem):
+    """ Transform an XML element into the following tuple:
+    (tagname, attributes, subelements, text) where:
+     - tagname is the element tag as string
+     - attributes is a dictionnary of the element attributes
+     - subelements are the sub elements as an array of tuple
+     - text is the content of the element, as string or None if no content is
+       there
+    """
+    childs = []
+    for child in elem:
+        childs.append(XMLToTuple(child))
+    return elem.tag, elem.attrib, childs, elem.text
+
+
+def ConnectionErrorToMessage(ce):
+    excpt = ce.args[0]
+    if isinstance(excpt, socket.error):
+        return excpt.reason.strerror
+    return 'ConnectionError'
+
+
+class GalileoClient(object):
+    ID = '6de4df71-17f9-43ea-9854-67f842021e05'
+
+    def __init__(self, scheme, host, path, port=None):
+        self.scheme = scheme
+        self.host = host
+        self.path = path
+        self._port = port
+        self.server_state = None
+        self._version = None
+
+    @property
+    def port(self):
+        if self._port is None:
+            return {'http': 80, 'https': 443}[self.scheme]
+        return self._port
+
+    @property
+    def url(self):
+        return "%(scheme)s://%(host)s:%(port)d/%(path)s" % {
+            'scheme': self.scheme,
+            'host': self.host,
+            'port': self.port,
+            'path': self.path}
+
+    @property
+    def version(self):
+        if self._version is not None:
+            # We're not completely lying ;)
+            return self._version + ' (really: %s)' % __version__
+        return __version__
+
+    def post(self, mode, dongle=None, data=None):
+        client = toXML('galileo-client', {'version': "2.0"})
+        info = toXML('client-info', childs=[
+            ('client-id', {}, [], self.ID),
+            ('client-version', {}, [], self.version),
+            ('client-mode', {}, [], mode)])
+        if (dongle is not None) and dongle.hasVersion:
+            info.append(toXML(
+                'dongle-version',
+                {'major': str(dongle.major),
+                 'minor': str(dongle.minor)}))
+        client.append(info)
+        if self.server_state is not None:
+            client.append(toXML('server-state', body=self.server_state))
+        if data is not None:
+            for XMLElem in tuplesToXML(data):
+                client.append(XMLElem)
+
+        f = BytesIO()
+
+        tree = ET.ElementTree(client)
+        tree.write(f, "utf-8", xml_declaration=True)
+
+        logger.debug('HTTP POST=%s', f.getvalue())
+        r = requests.post(self.url,
+                          data=f.getvalue(),
+                          headers={"Content-Type": "text/xml"})
+        f.close()
+        r.raise_for_status()
+
+        try:
+            answer = r.text
+        except AttributeError:
+            answer = r.content
+
+        logger.debug('HTTP response=%s', answer)
+
+        tag, attrib, childs, body = XMLToTuple(ET.fromstring(
+            answer.encode('utf-8')))
+
+        if tag != 'galileo-server':
+            logger.error("Unexpected root element: %s", tag)
+
+        if attrib['version'] != "2.0":
+            logger.warning("Unexpected server version: %s", attrib['version'])
+
+        excpt = None
+        for child in childs:
+            stag, _, schilds, sbody = child
+            if stag == 'error':
+                excpt = SyncError(sbody)
+            elif stag == 'back-off':
+                minD = 0
+                maxD = 0
+                for schild in schilds:
+                    sstag, _, _, ssbody = schild
+                    if sstag == 'min': minD = int(ssbody)
+                    if sstag == 'max': maxD = int(ssbody)
+                excpt = BackOffException(minD, maxD)
+            elif stag == 'server-state':
+                self.server_state = sbody
+            elif stag == 'redirect':
+                for schild in schilds:
+                    sstag, _, _, ssbody = schild
+                    if sstag == 'protocol': self.scheme = ssbody
+                    if sstag == 'host': self.host = ssbody
+                    if sstag == 'port': self._port = int(ssbody)
+                logger.info('Found redirect to %s' % self.url)
+            elif stag == 'tracker':
+                # We rely on the fact that this tag comes at last
+                if excpt is not None:
+                    logger.warning("Discarding exception: %s", excpt)
+                excpt = None
+
+        if excpt is not None:
+            raise excpt
+
+        return childs
+
+    def requestStatus(self, allowHTTP=False):
+        try:
+            self.post('status')
+        except requests.exceptions.ConnectionError as ce:
+            error_msg = ConnectionErrorToMessage(ce)
+            # No internet connection or fitbit server down
+            logger.error("Not able to connect to the Fitbit server using %s:"
+                         " %s.", self.scheme.upper(), error_msg)
+        else:
+            return True
+
+        if self.scheme == 'https' and not allowHTTP:
+            logger.warning('Config disallow the fallback to HTTP, you might'
+                           ' want to give it a try (--no-https-only)')
+
+        if self.scheme == 'http' or not allowHTTP:
+            return False
+
+        logger.info('Trying http as a backup.')
+        self.scheme = 'http'
+        try:
+            self.post('status')
+        except requests.exceptions.ConnectionError as ce:
+            error_msg = ConnectionErrorToMessage(ce)
+            # No internet connection or fitbit server down
+            logger.error("Not able to connect to the Fitbit server using"
+                         " either HTTP or HTTPS (%s). Check your internet"
+                         " connection", error_msg)
+        else:
+            return True
+
+        return False
+
+    def sync(self, dongle, trackerId, megadump):
+        try:
+            server = self.post('sync', dongle, (
+                'tracker', {'tracker-id': trackerId}, (
+                    'data', {}, [], megadump.toBase64())))
+        except requests.exceptions.ConnectionError as ce:
+            error_msg = ConnectionErrorToMessage(ce)
+            raise SyncError('ConnectionError: %s' % error_msg)
+        except requests.exceptions.HTTPError as he:
+            status_code = 500
+            if getattr(he, 'response', None) is not None:
+                status_code = he.response.status_code
+            msg = he.args[0]
+            raise SyncError("HTTPError: %s (%d)" % (msg, status_code))
+
+        tracker = None
+        for elem in server:
+            if elem[0] == 'tracker':
+                tracker = elem
+                break
+
+        if tracker is None:
+            raise SyncError('no tracker')
+
+        _, a, c, _ = tracker
+        if a['tracker-id'] != trackerId:
+            logger.error("Got the response for tracker %s, expected tracker"
+                         " %s", a['tracker-id'], trackerId)
+        if a['type'] != 'megadumpresponse':
+            logger.error('Not a megadumpresponse: %s', a['type'])
+
+        if not c:
+            raise SyncError('no data')
+        if len(c) != 1:
+            logger.error("Unexpected childs length: %d", len(c))
+        t, _, _, d = c[0]
+        if t != 'data':
+            raise SyncError('not data: %s' % t)
+
+        return s2a(base64.b64decode(d))
diff --git a/galileo/parser.py b/galileo/parser.py
new file mode 100644
index 0000000..61c1b15
--- /dev/null
+++ b/galileo/parser.py
@@ -0,0 +1,120 @@
+"""\
+This is a custom implementation of the yaml parser in order to prevent an
+extra dependency in the PyYAML module. This implementation will be used when
+the PyYAML module will not be found.
+
+The configurability of galileo should not be based on the possibility of this
+parser. This parser should be adapted to allow the correct configuration.
+
+Known limitations:
+- Only spaces, no tabs
+- Blank lines in the middle of an indented block is pretty bad ...
+"""
+
+from __future__ import print_function  # for the __main__ block
+
+import json
+import textwrap
+
+def _stripcomment(line):
+    s = []
+    for c in line:
+        if c == '#':
+            break
+        s.append(c)
+    # And we strip the trailing spaces
+    return ''.join(s).rstrip()
+
+
+def _getident(line):
+    i = 0
+    for c in line:
+        if c != ' ':
+            break
+        i += 1
+    return i
+
+
+def _addKey(d, key):
+    if d is None and key:
+        d = {}
+    d[key] = None
+    return d
+
+
+def unJSONize(s):
+    """ json is not good enough ...
+    "'a'" doesn't get decoded,
+    even worst, "a" neither """
+    try:
+        return json.loads(s)
+    except ValueError:
+        s = s.strip()
+        if s[0] == "'" and s[-1] == "'":
+            return s[1:-1]
+        return s
+
+
+def _dedent(lines, start):
+    res = [lines[start]]
+    idx = start + 1
+    minident = _getident(lines[start])
+    while idx < len(lines):
+        curident = _getident(lines[idx])
+        if curident < minident:
+            break
+        res.append(lines[idx])
+        idx += 1
+    return res
+
+
+def loads(s):
+    res = None
+    current_key = None
+    lines = s.split('\n')
+    i = 0
+    while i < len(lines):
+        line = _stripcomment(lines[i])
+        i += 1
+        if not line: continue
+        if _getident(line) == 0:
+            if line.startswith('-'):
+                if res is None:
+                    res = []
+                line = line[1:].strip()
+                if line:
+                    res.append(loads(line))
+                elif i == len(lines):
+                    res.append(None)
+            elif ':' in line:
+                current_key = None
+                k, v = line.split(':')
+                res = _addKey(res, k)
+                if not v:
+                    current_key = k
+                else:
+                    res[k] = unJSONize(v)
+            else:
+                return unJSONize(line)
+        else:
+            subblock = _dedent(lines, i-1)
+            subres = loads(textwrap.dedent('\n'.join(subblock)))
+            if isinstance(res, dict):
+                res[current_key] = subres
+            elif isinstance(res, list):
+                res.append(subres)
+            else:
+                raise ValueError(res, subres)
+            i += len(subblock) - 1
+
+    return res
+
+
+def load(f):
+    return loads(f.read())
+
+if __name__ == "__main__":
+    import sys
+    # For fun and quick test
+    with open(sys.argv[1], 'rt') as f:
+        print(load(f))
diff --git a/galileo/tracker.py b/galileo/tracker.py
new file mode 100644
index 0000000..c54aa11
--- /dev/null
+++ b/galileo/tracker.py
@@ -0,0 +1,348 @@
+from ctypes import c_byte
+
+import logging
+logger = logging.getLogger(__name__)
+
+from .dongle import CM, DM, isStatus
+from .dump import Dump, DumpResponse
+from .utils import a2s, a2x, i2lsba, a2lsbi
+
+MICRODUMP = 3
+MEGADUMP = 13
+
+
+class Tracker(object):
+    def __init__(self, Id, addrType, serviceData, RSSI, serviceUUID=None):
+        self.id = Id
+        self.addrType = addrType
+        if serviceUUID is None:
+            self.serviceUUID = a2lsbi([Id[1] ^ Id[3] ^ Id[5],
+                                       Id[0] ^ Id[2] ^ Id[4]])
+        else:
+            self.serviceUUID = serviceUUID
+        self.serviceData = serviceData
+        # following three are coded somewhere here ...
+        # specialMode
+        # canDisplayNumber
+        # colorCode
+        self.RSSI = RSSI
+        self.status = 'unknown'  # If we happen to read it before anyone set it
+
+    @property
+    def productId(self):
+        return self.serviceData[0]
+
+    @property
+    def syncedRecently(self):
+        return self.serviceData[1] != 4
+
+    @classmethod
+    def fromDiscovery(klass, data, minRSSI=-255):
+        trackerId = data[:6]
+        addrType = data[6]
+        RSSI = c_byte(data[7]).value
+        serviceDataLen = data[8]
+        serviceData = data[9:9+serviceDataLen+1]  # '+1': go figure !
+        sUUID = a2lsbi(data[15:17])
+        serviceUUID = a2lsbi([trackerId[1] ^ trackerId[3] ^ trackerId[5],
+                              trackerId[0] ^ trackerId[2] ^ trackerId[4]])
+        tracker = klass(trackerId, addrType, serviceData, RSSI, sUUID)
+        if not tracker.syncedRecently and (serviceUUID != sUUID):
+            logger.debug("Cannot acknowledge the serviceUUID: %s vs %s",
+                         a2x(i2lsba(serviceUUID, 2), ':'), a2x(i2lsba(sUUID, 2), ':'))
+        logger.debug('Tracker: %s, %s, %s, %s', a2x(trackerId, ':'),
+                     addrType, RSSI, a2x(serviceData, ':'))
+        if RSSI < -80:
+            logger.info("Tracker %s has low signal power (%ddBm), higher"
+                        " chance of miscommunication",
+                        a2x(trackerId, delim=""), RSSI)
+
+        if not tracker.syncedRecently:
+            logger.debug('Tracker %s was not recently synchronized',
+                         a2x(trackerId, delim=""))
+        if RSSI < minRSSI:
+            logger.warning("Tracker %s below power threshold (%ddBm),"
+                           "dropping", a2x(trackerId, delim=""), minRSSI)
+            #continue
+        return tracker
+
+
+class FitbitClient(object):
+    def __init__(self, dongle):
+        self.dongle = dongle
+
+    def disconnect(self):
+        logger.info('Disconnecting from any connected trackers')
+
+        self.dongle.ctrl_write(CM(2))
+        if not isStatus(self.dongle.ctrl_read(), 'CancelDiscovery'):
+            return False
+        # Next one is not critical. It can happen that it does not comes
+        isStatus(self.dongle.ctrl_read(), 'TerminateLink')
+
+        self.exhaust()
+
+        return True
+
+    def exhaust(self):
+        """ We exhaust the pipe, then we know that we have a clean state """
+        logger.debug("Exhausting the communication pipe")
+        goOn = True
+        while goOn:
+            goOn = self.dongle.ctrl_read() is not None
+
+    def getDongleInfo(self):
+        self.dongle.ctrl_write(CM(1))
+        d = self.dongle.ctrl_read()
+        if (d is None) or (d.INS != 8):
+            return False
+        self.dongle.setVersion(d.payload[0], d.payload[1])
+        self.dongle.address = d.payload[2:8]
+        self.dongle.flashEraseTime = a2lsbi(d.payload[8:10])
+        self.dongle.firmwareStartAddress = a2lsbi(d.payload[10:14])
+        self.dongle.firmwareEndAddress = a2lsbi(d.payload[14:18])
+        self.dongle.ccIC = d.payload[18]
+        # Not sure how the last ones fit in the last byte
+#        self.dongle.hardwareRevision = d.payload[19]
+#        self.dongle.revision = d.payload[19]
+        return True
+
+    def discover(self, uuid, service1=0xfb00, write=0xfb01, read=0xfb02,
+                 minRSSI=-255, minDuration=4000):
+        """\
+        The uuid is a mask on the service (characteristics ?) we understand
+        service1 parameter is unused (at lease for the 'One')
+        read and write are the uuid of the characteristics we use for
+        transmission and reception.
+        """
+        logger.debug('Discovering for UUID %s: %s', uuid,
+                     ', '.join(hex(s) for s in (service1, write, read)))
+        data = i2lsba(uuid.int, 16)
+        for i in (service1, write, read, minDuration):
+            data += i2lsba(i, 2)
+        self.dongle.ctrl_write(CM(4, data))
+        amount = 0
+        while True:
+            # Give the dongle 100ms margin
+            d = self.dongle.ctrl_read(minDuration + 100)
+            if d is None: break
+            elif isStatus(d, None, False):
+                # We know this can happen almost any time during 'discovery'
+                logger.info('Ignoring message: %s' % a2s(d.payload))
+                continue
+            elif d.INS == 2:
+                # Last instruction of a discovery sequence has INS==1
+                break
+            elif (d.INS != 3) or (len(d.payload) < 17):
+                logger.error('payload unexpected: %s', d)
+                break
+            yield Tracker.fromDiscovery(d.payload, minRSSI)
+            amount += 1
+
+        if d != CM(2, [amount]):
+            logger.error('%d trackers discovered, dongle says %s', amount, d)
+        # tracker found, cancel discovery
+        self.dongle.ctrl_write(CM(5))
+        d = self.dongle.ctrl_read()
+        if isStatus(d, 'StartDiscovery', False):
+            # We had not received the 'StartDiscovery' yet
+            d = self.dongle.ctrl_read()
+        isStatus(d, 'CancelDiscovery')
+
+    def setPowerLevel(self, level):
+        # This is quite weird as in the log I took this from, they send:
+        # 020D05 (level5), but as the length is 02, I believe the 05 is not
+        # even acknowledged by the dongle ...
+        self.dongle.ctrl_write(CM(0xd, [level]))
+        r = self.dongle.ctrl_read()
+        if r != CM(0xFE):
+            return False
+        return True
+
+    def establishLink(self, tracker):
+        if self.dongle.establishLinkEx:
+            return self.establishLinkEx(tracker)
+        self.dongle.ctrl_write(CM(6, tracker.id + [tracker.addrType] +
+                                  i2lsba(tracker.serviceUUID, 2)))
+        d = self.dongle.ctrl_read()
+        if d == CM(0xff, [2, 3]):
+            # Our detection based on the dongle version is not perfect :(
+            logger.warning("Older tracker %d.%d also needs EstablishLinkEx",
+                           self.dongle.major, self.dongle.minor)
+            self.dongle.establishLinkEx = True
+            return self.establishLinkEx(tracker)
+        elif not isStatus(d, 'EstablishLink'):
+            return False
+        d = self.dongle.ctrl_read(5000)
+        if d != CM(4, [0]):
+            logger.error('Unexpected message: %s', d)
+            return False
+        # established, waiting for service discovery
+        # - This one takes long
+        if not isStatus(self.dongle.ctrl_read(8000),
+                        'GAP_LINK_ESTABLISHED_EVENT'):
+            return False
+        # This one can also take some time (Charge tracker)
+        d = self.dongle.ctrl_read(5000)
+        if d != CM(7):
+            logger.error('Unexpected 2nd message: %s', d)
+            return False
+        return True
+
+    def establishLinkEx(self, tracker):
+        """ First heard from in #236 """
+        nums = [6, 6, 0, 200]  # Looks familiar ?
+        data = tracker.id + [tracker.addrType]
+        for n in nums:
+            data.extend(i2lsba(n, 2))
+        self.dongle.ctrl_write(CM(0x12, data))
+        if not isStatus(self.dongle.ctrl_read(), 'CancelDiscovery'):
+            return False
+        if not isStatus(self.dongle.ctrl_read(), 'EstablishLinkEx'):
+            return False
+        d = self.dongle.ctrl_read(5000)
+        if d != CM(4, [0]):
+            logger.error('Unexpected message: %s', d)
+            return False
+        if not isStatus(self.dongle.ctrl_read(),
+                        'GAP_LINK_ESTABLISHED_EVENT'):
+            return False
+        d = self.dongle.ctrl_read()
+        if d != CM(7):
+            logger.error('Unexpected 2nd message: %s', d)
+            return False
+        return True
+
+    def toggleTxPipe(self, on):
+        """ `on` is a boolean that dictate the status of the pipe
+        :returns: a boolean about the successful execution
+        """
+        self.dongle.ctrl_write(CM(8, [int(on)]))
+        d = self.dongle.data_read(5000)
+        return d == DM([0xc0, 0xb])
+
+    def initializeAirlink(self, tracker=None):
+        """ :returns: a boolean about the successful execution """
+        nums = [10, 6, 6, 0, 200]
+        #nums = [1, 8, 16, 0, 200]
+        #nums = [1034, 6, 6, 0, 200]
+        data = []
+        for n in nums:
+            data.extend(i2lsba(n, 2))
+        #data = data + [1]
+        self.dongle.data_write(DM([0xc0, 0xa] + data))
+        if not self.dongle.establishLinkEx:
+            # Not necessary when using establishLinkEx
+            d = self.dongle.ctrl_read(10000)
+            if d != CM(6, data[-6:]):
+                logger.error("Unexpected message: %s != %s", d, CM(6, data[-6:]))
+                return False
+        d = self.dongle.data_read()
+        if d is None:
+            return False
+        if d.data[:2] != [0xc0, 0x14]:
+            logger.error("Wrong header: %s", a2x(d.data[:2]))
+            return False
+        if (tracker is not None) and (d.data[6:12] != tracker.id):
+            logger.error("Connected to wrong tracker: %s", a2x(d.data[6:12]))
+            return False
+        logger.debug("Connection established: %d, %d",
+                     a2lsbi(d.data[2:4]), a2lsbi(d.data[4:6]))
+        return True
+
+    def displayCode(self):
+        """ :returns: a boolean about the successful execution """
+        logger.debug('Displaying code on tracker')
+        self.dongle.data_write(DM([0xc0, 6]))
+        r = self.dongle.data_read()
+        return (r is not None) and (r.data == [0xc0, 2])
+
+    def getDump(self, dumptype=MEGADUMP):
+        """ :returns: a `Dump` object or None """
+        logger.debug('Getting dump type %d', dumptype)
+
+        # begin dump of appropriate type
+        self.dongle.data_write(DM([0xc0, 0x10, dumptype]))
+        r = self.dongle.data_read()
+        if r and (r.data[:3] != [0xc0, 0x41, dumptype]):
+            logger.error("Tracker did not acknowledged the dump type: %s", r)
+            return None
+
+        dump = Dump(dumptype)
+        # Retrieve the dump
+        d = self.dongle.data_read()
+        if d is None:
+            return None
+        dump.add(d.data)
+        while d.data[0] != 0xc0:
+            d = self.dongle.data_read()
+            if d is None:
+                return None
+            dump.add(d.data)
+        # Analyse the dump
+        if not dump.isValid():
+            logger.error('Dump not valid')
+            return None
+        logger.debug("Dump done, length %d, transportCRC=0x%04x, esc1=0x%02x,"
+                     " esc2=0x%02x", dump.len, dump.crc.final(), dump.esc[0],
+                     dump.esc[1])
+        return dump
+
+    def uploadResponse(self, response):
+        """ 4 and 6 are magic values here ...
+        :returns: a boolean about the success of the operation.
+        """
+        dumptype = 4  # ???
+        self.dongle.data_write(DM([0xc0, 0x24, dumptype] + i2lsba(len(response), 6)))
+        d = self.dongle.data_read()
+        if d != DM([0xc0, 0x12, dumptype, 0, 0]):
+            logger.error("Tracker did not acknowledged upload type: %s", d)
+            return False
+
+        CHUNK_LEN = 20
+        response = DumpResponse(response, CHUNK_LEN)
+
+        for i, chunk in enumerate(response):#range(0, len(response), CHUNK_LEN):
+            self.dongle.data_write(DM(chunk))
+            # This one can also take some time (Charge HR tracker)
+            d = self.dongle.data_read(20000)
+            expected = DM([0xc0, 0x13, (((i+1) % 16) << 4) + dumptype, 0, 0])
+            if d != expected:
+                logger.error("Wrong sequence number: %s, expected: %s", d, expected)
+                return False
+
+        self.dongle.data_write(DM([0xc0, 2]))
+        # Next one can be very long. He is probably erasing the memory there
+        d = self.dongle.data_read(60000)
+        if d != DM([0xc0, 2]):
+            logger.error("Unexpected answer from tracker: %s", d)
+            return False
+
+        return True
+
+    def terminateAirlink(self):
+        """ contrary to ``initializeAirlink`` """
+
+        self.dongle.data_write(DM([0xc0, 1]))
+        d = self.dongle.data_read()
+        if d != DM([0xc0, 1]):
+            return False
+        return True
+
+    def ceaseLink(self):
+        """ contrary to ``establishLink`` """
+
+        self.dongle.ctrl_write(CM(7))
+        if not isStatus(self.dongle.ctrl_read(5000), 'TerminateLink'):
+            return False
+
+        d = self.dongle.ctrl_read(3000)
+        if (d is None) or (d.INS != 5):
+            # Payload can be either 0x16 or 0x08
+            return False
+        if not isStatus(self.dongle.ctrl_read(), 'GAP_LINK_TERMINATED_EVENT'):
+            return False
+        if not isStatus(self.dongle.ctrl_read()):
+            # This one doesn't always return '22'
+            return False
+        return True
diff --git a/galileo/ui.py b/galileo/ui.py
new file mode 100644
index 0000000..be26673
--- /dev/null
+++ b/galileo/ui.py
@@ -0,0 +1,237 @@
+"""\
+This is where to look for for all user interaction stuff ...
+"""
+import logging
+import sys
+
+logger = logging.getLogger(__name__)
+
+try:
+    from html.parser import HTMLParser
+except ImportError:
+    # Python2
+    from HTMLParser import HTMLParser
+
+class Form(object):
+    def __init__(self):
+        self.fields = set()
+        self.submit = None
+
+    def addField(self, field):
+        self.fields.add(field)
+
+    def commonFields(self, answer, withValues=True):
+        res = 0
+        for field in self.fields:
+            if field.name in answer:
+                if withValues:
+                    if field.value is not None and field.value == answer[field.name]:
+                        res += 1
+                else:
+                    res += 1
+        return res
+
+    def takeValuesFromAnswer(self, answer):
+        """\
+        Transfer the answers from the config to the form
+        """
+        for field in self.fields:
+            field.value = answer.get(field.name, field.value)
+            if (field.name in answer) and (field.type == 'submit'):
+                self.submit = field.name
+
+    def asXML(self):
+        """\
+        Return the XML tuples. The trick is: THere can be only one 'submit'
+        """
+        res = []
+        for field in self.fields:
+            if field.type == 'submit':
+                if self.submit != field.name:
+                    continue
+            res.append(field.asXMLParam())
+        return res
+
+    def __str__(self):
+        return ', '.join(str(f) for f in self.fields)
+    __repr__ = __str__  # To get it printed
+
+    def asDict(self):
+        """ for comparison in the test suites """
+        return dict((f.name, f.value) for f in self.fields)
+
+class FormField(object):
+    def __init__(self, name, type='text', value=None, **kw):
+        self.name = name
+        self.type = type
+        self.value = value
+
+    def asXMLParam(self):
+        return ('param', {'name': self.name}, [], self.value)
+
+    def __str__(self):
+        return '%r: %r' % (self.name, self.value)
+
+
+class FormExtractor(HTMLParser):
+    """ This read a whole html page and extract the forms """
+    def __init__(self):
+        self.forms = []
+        self.curForm = None
+        self.curSelect = None
+        HTMLParser.__init__(self)
+
+    def handle_starttag(self, tag, attrs):
+        attrs = dict(attrs)
+        if tag == 'form':
+            self.curForm = Form()
+        if tag == 'input':
+            if 'name' in attrs:
+                if self.curForm is None:
+                    # In case the input happen outside of a form, just create
+                    # one, and adds it immediatly
+                    f = Form()
+                    f.addField(FormField(**attrs))
+                    self.forms.append(f)
+                else:
+                    self.curForm.addField(FormField(**attrs))
+
+        if tag == 'select':
+            self.curSelect = FormField(type='select', **attrs)
+        if tag == 'option' and 'selected' in attrs:
+            self.curSelect.value = attrs['value']
+
+
+    def handle_endtag(self, tag):
+        if tag == 'form':
+            self.forms.append(self.curForm)
+            self.curForm = None
+        if tag == 'select':
+            self.curForm.addField(self.curSelect)
+            self.curSelect = None
+
+    def handle_data(self, data): pass
+
+
+class BaseUI(object):
+    """\
+    This is the base of all ui classes, it provides an interface and handy
+    methods
+    """
+    def request(self, action, client_display):
+        raise NotImplementedError
+
+class MissingConfigError(Exception):
+    def __init__(self, action, forms):
+        self.action = action
+        self.forms = forms
+    def __str__(self):
+        s = ["The server is asking a question to which I don't know any"
+             " answer.",]
+        s.append("Please add the section '%s' in the galileorc configuration"
+                 " file under 'hardcoded-ui'" % self.action)
+        s.append("Under this section, you should add the answer for one of the"
+                 " following forms:")
+        for f in self.forms:
+            s.append(" - %s" % f.asDict())
+        s.append("To help you decide, you can run the pairing process with the"
+                 " `--debug` command line switch,")
+        s.append("this will print the HTML code from which the questions have"
+                 " been extracted.")
+        return '\n'.join(s)
+
+class HardCodedUI(BaseUI):
+    """\
+    This ui class doesn't show anything to the user and takes its answers
+    from a list of hard-coded ones
+    """
+    def __init__(self, answers):
+        self.answers = answers
+
+    def request(self, action, html):
+        if html.startswith('<![CDATA[') and html.endswith(']]>'):
+            html = html[len('<![CDATA['):-len(']]>')]
+        fe = FormExtractor()
+        fe.feed(html)
+        if action not in self.answers:
+            logger.error("No answers provided for '%s'" % action)
+            logger.info("I only know about %s" % self.answers.keys())
+            raise MissingConfigError(action, fe.forms)
+        answer = self.answers[action]
+        # Figure out which of the form we should fill
+        goodForm = None
+        if len(fe.forms) == 1:
+            # Only one there, no need to search for the correct one ...
+            goodForm = fe.forms[0]
+        else:
+            # We need to find the one that match the most our answers
+            max = 0
+            for form in fe.forms:
+                v = form.commonFields(answer)
+                if v > max:
+                    goodForm = form
+                    max = v
+            if max == 0:
+                # Not found, search again, less picky
+                for form in fe.forms:
+                    v = form.commonFields(answer, False)
+                    if v > max:
+                        goodForm = form
+                        max = v
+        if goodForm is None:
+            raise ValueError('no answer found')
+        goodForm.takeValuesFromAnswer(answer)
+        return goodForm.asXML()
+
+
+def query_yes_no(question, default="y"):
+    """Ask a yes/no question via raw_input() and return their answer.
+
+    "question" is a string that is presented to the user.
+    "default" is the presumed answer if the user just hits <Enter>.
+        It must be "yes" (the default), "no" or None (meaning
+        an answer is required of the user).
+
+    The "answer" return value is one of True or False.
+
+    This is from http://stackoverflow.com/a/3041990/1182619
+    Itself from http://code.activestate.com/recipes/577058/
+    """
+    valid = {"yes":True,   "y":True,  "ye":True,
+             "no":False,   "n":False}
+    if default is None:
+        prompt = " [y/n] "
+    elif valid.get(default, False):
+        prompt = " [Y/n] "
+    elif not valid.get(default, True):
+        prompt = " [y/N] "
+    else:
+        raise ValueError("invalid default answer: '%s'" % default)
+
+    while True:
+        sys.stdout.write(question + prompt)
+        choice = raw_input().lower()
+        if default is not None and choice == '':
+            return valid[default]
+        elif choice in valid:
+            return valid[choice]
+        else:
+            sys.stdout.write("Please respond with 'yes' or 'no' "\
+                             "(or 'y' or 'n').\n")
+
+
+class InteractiveUI(HardCodedUI):
+    """ We can't avoid asking the user to type what's written on the dongle """
+
+    def request(self, action, html):
+        if action == 'requestSecret':
+            return self.handle_requestSecret()
+        return HardCodedUI.request(self, action, html)
+
+    def handle_requestSecret(self):
+        if not query_yes_no("Do you see a number ?"):
+            return [('param', {'name': 'secret'}, [], ''),
+                     ('param', {'name': 'tryOther'}, [], 'TRY_OTHER')]
+        sys.stdout.write("Type here the number you see:")
+        secret = raw_input()
+        return [('param', {'name': 'secret'}, [], secret)]
diff --git a/galileo/utils.py b/galileo/utils.py
new file mode 100644
index 0000000..b6ba618
--- /dev/null
+++ b/galileo/utils.py
@@ -0,0 +1,68 @@
+"""\
+We internally use array of int as data representation, those routines
+translate them to one or the other format
+"""
+
+import sys
+
+def a2x(a, delim=' '):
+    """ array to string of hexa
+    delim is the delimiter between the hexa
+    """
+    return delim.join('%02X' % x for x in a)
+
+
+def x2a(hexstr):
+    """ String of hex a to array """
+    return [int(x, 16) for x in hexstr.split(' ')]
+
+
+def a2s(a, toPrint=True):
+    """ array to string
+    toPrint indicates that the resulting string is to be printed (stop at the
+    first \0)
+    """
+    s = []
+    for c in a:
+        if toPrint and (c == 0):
+            break
+        s.append(chr(c))
+    return ''.join(s)
+
+def a2b(a):
+    """ array to `bytes` """
+    if sys.version_info > (3, 0):
+        return bytes(a)
+    return a2s(a, False)
+
+def a2lsbi(array):
+    """ array to int (LSB first) """
+    integer = 0
+    for i in range(len(array) - 1, -1, -1):
+        integer *= 256
+        integer += array[i]
+    return integer
+
+
+def a2msbi(array):
+    """ array to int (MSB first) """
+    integer = 0
+    for i in range(len(array)):
+        integer *= 256
+        integer += array[i]
+    return integer
+
+
+def i2lsba(value, width):
+    """ int to array (LSB first) """
+    a = [0] * width
+    for i in range(width):
+        a[i] = (value >> (i*8)) & 0xff
+    return a
+
+
+def s2a(s):
+    """ string to array """
+    if isinstance(s, str):
+        return [ord(c) for c in s]
+    return [c for c in s]
diff --git a/galileorc.sample b/galileorc.sample
new file mode 100644
index 0000000..6c55b77
--- /dev/null
+++ b/galileorc.sample
@@ -0,0 +1,35 @@
+
+# -*- mode: yaml; -*-
+
+# Default settings for galileo.py. Settings may be changed here or
+# overridden using command-line switches to galileo.py.
+
+# if in daemon mode, delay between sync runs
+# specified in milliseconds
+daemon-period: 15000
+
+# keep dump files
+keep-dumps: true
+
+# upload data to Fitbit
+do-upload: true
+
+# directory to store the dumps
+dump-dir: ~/.galileo
+
+# logging (default/verbose/debug)
+logging: verbose
+
+# synchronize even if trackers were recently synchronized
+force-sync: false
+
+# trackers to include
+include:
+  - 123456789ABC
+  - 9876543210AB
+  - '112233445566' # tracker id composed of only numbers need to be quoted
+
+# trackers to exclude
+exclude:
+  - AABBCCDDEEFF
+  - 881144BB1234
diff --git a/run b/run
new file mode 100755
index 0000000..90b30b8
--- /dev/null
+++ b/run
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+from galileo.main import main
+
+main()
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..65aecdb
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import re
+import sys
+
+try:
+    from setuptools import setup, find_packages, Command
+except ImportError:
+    import distribute_setup
+    distribute_setup.use_setuptools()
+    from setuptools import setup
+
+from galileo import __version__
+
+
+class CheckVersion(Command):
+    """ Check that the version in the docs is the correct one """
+    description = "Check the version consistency"
+    user_options = []
+    def initialize_options(self):
+        """init options"""
+        pass
+
+    def finalize_options(self):
+        """finalize options"""
+        pass
+
+    def run(self):
+        readme_re = re.compile(r'^:version:\s+' + __version__ + r'\s*$',
+                               re.MULTILINE | re.IGNORECASE)
+        man_re = re.compile(r'^\.TH.+[\s"]+' + __version__ + r'[\s"]+',
+                            re.MULTILINE | re.IGNORECASE)
+        for filename, regex in (
+                ('README.txt', readme_re),
+                ('doc/galileo.1', man_re),
+                ('doc/galileorc.5', man_re)):
+            with open(filename) as f:
+                content = f.read()
+            if regex.search(content) is None:
+                raise ValueError('file %s mention the wrong version' % filename)
+
+with open('README.txt') as file:
+    long_description = file.read()
+
+setup(
+    name="galileo",
+    version=__version__,
+    description="Utility to securely synchronize a Fitbit tracker with the"
+                " Fitbit server",
+    long_description=long_description,
+    author="Benoît Allard",
+    author_email="benoit.allard at gmx.de",
+    url="https://bitbucket.org/benallard/galileo",
+    platforms=['any'],
+    keywords=['fitbit', 'synchronize', 'health', 'tracker'],
+    license="LGPL",
+    install_requires=[
+        "requests",
+        "pyusb>=1a"],  # version 1a doesn't exists, but is smaller than 1.0.0a2
+    test_suite="tests",
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'License :: OSI Approved :: GNU Lesser General Public License v3 or'
+        ' later (LGPLv3+)',
+        'Environment :: Console',
+        'Topic :: Utilities',
+        'Topic :: Internet',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.4',
+    ],
+    packages=find_packages(exclude=["tests"]),
+    entry_points={
+        'console_scripts': [
+            'galileo = galileo.main:main'
+        ],
+    },
+    cmdclass={
+        'checkversion': CheckVersion,
+    },
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/testCRC.py b/tests/testCRC.py
new file mode 100644
index 0000000..e2bf26e
--- /dev/null
+++ b/tests/testCRC.py
@@ -0,0 +1,19 @@
+import unittest
+
+from galileo.dump import CRC16
+
+class testCRC(unittest.TestCase):
+    """ CRC unit tests """
+
+    def test_XMODEM_123456789(self):
+        # Default values, used by Fitbit
+        crc = CRC16(0x1021, True, 0x0000, 0x0000)
+        a = [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39]
+        crc.update(a)
+        self.assertEqual(crc.final(), 0x31c3)
+
+    def test_XMODEM_two_parts(self):
+        crc = CRC16()
+        crc.update([0x31, 0x32, 0x33, 0x34, 0x35])
+        crc.update([0x36, 0x37, 0x38, 0x39])
+        self.assertEqual(crc.final(), 0x31c3)
diff --git a/tests/testConfig.py b/tests/testConfig.py
new file mode 100644
index 0000000..d9a22f6
--- /dev/null
+++ b/tests/testConfig.py
@@ -0,0 +1,67 @@
+import unittest
+
+from galileo.config import Config
+
+class MyTracker(object):
+    def __init__(self, id, syncedRecently):
+        self.id = id
+        self.syncedRecently = syncedRecently
+
+class MyParam(object):
+    def __init__(self, name, value):
+        self.varName = name
+        self.default = value
+P=MyParam
+
+class testShouldSkip(unittest.TestCase):
+
+    def testRecentForce(self):
+        t = MyTracker([42], True)
+        c = Config([P('forceSync', True),
+                    P('includeTrackers', None),
+                    P('excludeTrackers', set())])
+        self.assertFalse(c.shouldSkip(t))
+
+    def testRecentNotForce(self):
+        t = MyTracker([42], True)
+        c = Config([P('forceSync', False),
+                    P('includeTrackers', None),
+                    P('excludeTrackers', set())])
+        self.assertTrue(c.shouldSkip(t))
+
+    def testIncludeNotExclude(self):
+        t = MyTracker([0x42], False)
+        c = Config([P('forceSync', False),
+                    P('includeTrackers', set(['42'])),
+                    P('excludeTrackers', set())])
+        self.assertFalse(c.shouldSkip(t))
+    def testIncludeNoneExclude(self):
+        t = MyTracker([0x42], False)
+        c = Config([P('forceSync', False),
+                    P('includeTrackers', None),
+                    P('excludeTrackers', set(['42']))])
+        self.assertTrue(c.shouldSkip(t))
+    def testNotIncludeExclude(self):
+        t = MyTracker([0x42], False)
+        c = Config([P('forceSync', False),
+                    P('includeTrackers', set(['21'])),
+                    P('excludeTrackers', set(['42']))])
+        self.assertTrue(c.shouldSkip(t))
+    def testIncludeExclude(self):
+        t = MyTracker([0x42], False)
+        c = Config([P('forceSync', False),
+                    P('includeTrackers', set(['42'])),
+                    P('excludeTrackers', set(['42']))])
+        self.assertTrue(c.shouldSkip(t))
+    def testIncludeNoneNotExclude(self):
+        t = MyTracker([0x42], False)
+        c = Config([P('forceSync', False),
+                    P('includeTrackers', None),
+                    P('excludeTrackers', set())])
+        self.assertFalse(c.shouldSkip(t))
+    def testNotIncludeNotExclude(self):
+        t = MyTracker([0x42], False)
+        c = Config([P('forceSync', False),
+                    P('includeTrackers', set(['21'])),
+                    P('excludeTrackers', set())])
+        self.assertTrue(c.shouldSkip(t))
diff --git a/tests/testDataRing.py b/tests/testDataRing.py
new file mode 100644
index 0000000..b270e2b
--- /dev/null
+++ b/tests/testDataRing.py
@@ -0,0 +1,59 @@
+import unittest
+
+from galileo.dongle import DataRing
+
+class testRing(unittest.TestCase):
+    def testEmpty(self):
+        r = DataRing(5)
+        self.assertEqual([], r.getData())
+        self.assertTrue(r.empty)
+        self.assertFalse(r.full)
+
+    def testCapaNull(self):
+        r = DataRing(0)
+        r.add(5)
+        self.assertEqual([], r.getData())
+        self.assertTrue(r.empty)
+        self.assertTrue(r.full)
+
+    def testOneElement(self):
+        r = DataRing(10)
+        r.add('data')
+        self.assertEqual(['data'], r.getData())
+        self.assertFalse(r.empty)
+        self.assertFalse(r.full)
+        self.assertEqual(r.queue + 1, r.head)
+        self.assertEqual(1, r.fill)
+
+    def testTwoElement(self):
+        r = DataRing(10)
+        r.add('data1')
+        r.add('data2')
+        self.assertFalse(r.empty)
+        self.assertEqual(['data1', 'data2'], r.getData())
+        self.assertEqual(2, r.fill)
+
+    def testThreeElement(self):
+        r = DataRing(10)
+        r.add('data1')
+        r.add('data2')
+        r.add('data3')
+        self.assertFalse(r.empty)
+        self.assertEqual(['data1', 'data2', 'data3'], r.getData())
+        self.assertEqual(3, r.fill)
+
+    def testOverflow(self):
+        r = DataRing(2)
+        self.assertFalse(r.full)
+        self.assertEqual(0, r.fill)
+        r.add('data1')
+        self.assertFalse(r.full)
+        self.assertEqual(1, r.fill)
+        r.add('data2')
+        self.assertTrue(r.full)
+        self.assertEqual(2, r.fill)
+        r.add('data3')
+        self.assertFalse(r.empty)
+        self.assertTrue(r.full)
+        self.assertEqual(2, r.fill)
+        self.assertEqual(['data2', 'data3'], r.getData())
diff --git a/tests/testDongle.py b/tests/testDongle.py
new file mode 100644
index 0000000..3e9375f
--- /dev/null
+++ b/tests/testDongle.py
@@ -0,0 +1,92 @@
+import errno
+import unittest
+
+import galileo.dongle
+from galileo.dongle import isStatus, FitBitDongle, CM, DM, isATimeout
+
+USBError = galileo.dongle.usb.core.USBError
+
+class MyCM(object):
+    def __init__(self, ins, payload):
+        self.INS = ins
+        self.payload = payload
+
+class testisStatus(unittest.TestCase):
+
+    def testNotAStatus(self):
+        self.assertFalse(isStatus(MyCM(3, [])))
+
+    def testIsaStatus(self):
+        self.assertTrue(isStatus(MyCM(1, [])))
+
+    def testEquality(self):
+        self.assertTrue(isStatus(MyCM(1, [0x61, 0x62, 0x63, 0x64 , 0]), 'abcd'))
+
+    def testStartsWith(self):
+        self.assertTrue(isStatus(MyCM(1, [0x61, 0x62, 0x63, 0x64 , 0]), 'ab'))
+
+
+class testCM(unittest.TestCase):
+    r2 = list(range(2))
+    r5 = list(range(5))
+
+    def testEquals(self):
+        self.assertTrue(CM(8) == CM(8))
+        self.assertTrue(CM(5) == CM(5, []))
+        self.assertTrue(CM(2, self.r5), CM(2, self.r5))
+        self.assertEqual(CM(8), CM(8))
+        self.assertEqual(CM(5), CM(5, []))
+        self.assertEqual(CM(2, self.r5), CM(2, self.r5))
+
+    def testNotEquals(self):
+        self.assertFalse(CM(7) == CM(8))
+        self.assertFalse(CM(9) == CM(9, [5]))
+        self.assertFalse(CM(3, self.r2) == CM(3, self.r5))
+        self.assertFalse(None == CM(3, self.r5))
+
+
+class testDM(unittest.TestCase):
+
+    def testEquals(self):
+        self.assertTrue(DM(range(3)) == DM(range(3)))
+        self.assertEqual(DM(range(8)), DM(range(8)))
+
+    def testNotEquals(self):
+        self.assertFalse(DM([87]) == DM([42]))
+        self.assertFalse(DM(range(2)) == DM(range(5)))
+        self.assertFalse(None == DM(range(5)))
+
+
+class MyDev(object):
+    """ Minimal object to reproduce issue#75 """
+    def is_kernel_driver_active(self, a): raise NotImplementedError()
+    def get_active_configuration(self): return {(0,0): None, (1,0): None}
+    def set_configuration(self): pass
+    def reset(self): pass
+
+class testDongle(unittest.TestCase):
+
+    def testNIE(self):
+        def myFind(*args, **kwargs):
+            return MyDev()
+        galileo.dongle.usb.core.find = myFind
+        d = FitBitDongle(0)
+        d.setup()
+
+class testisATimeout(unittest.TestCase):
+
+    def testErrnoTIMEOUT(self):
+        """ usb.core.USBError: [Errno 110] Operation timed out """
+        self.assertTrue(isATimeout(USBError('Operation timed out', errno=errno.ETIMEDOUT)))
+
+    def testpyusb1a2(self):
+        """\
+        issue#17
+        usb.core.USBError: Operation timed out """
+        self.assertTrue(isATimeout(IOError('Operation timed out')))
+
+    def testlibusb0(self):
+        """\
+        issue#82
+        usb.core.USBError: [Errno None] Connection timed out """
+        self.assertTrue(isATimeout(USBError('Connection timed out')))
diff --git a/tests/testDump.py b/tests/testDump.py
new file mode 100644
index 0000000..3ff9e21
--- /dev/null
+++ b/tests/testDump.py
@@ -0,0 +1,81 @@
+import unittest
+
+from galileo.dump import Dump
+
+class testDump(unittest.TestCase):
+
+    def testEmptyNonValid(self):
+        d = Dump(6)
+        self.assertFalse(d.isValid())
+
+    def testAddIncreasesLen(self):
+        d = Dump(5)
+        self.assertEqual(d.len, 0)
+        d.add(range(10))
+        self.assertEqual(d.len, 10)
+
+    def testFooterIsSet(self):
+        d = Dump(0)
+        self.assertEqual(d.footer, [])
+        d.add([0xc0] + list(range(5)))
+        self.assertEqual(d.len, 0)
+        self.assertEqual(d.footer, [0xc0] + list(range(5)))
+
+    def testOnlyFooterInvalid(self):
+        """ A dump with only a footer is an invalid dump """
+        d = Dump(0)
+        d.add([0xc0] + list(range(5)))
+        self.assertFalse(d.isValid())
+
+    def testEsc1(self):
+        d = Dump(0)
+        self.assertEqual(d.esc[0], 0)
+        d.add([0xdb, 0xdc])
+        self.assertEqual(d.len, 1)
+        self.assertEqual(d.esc[0], 1)
+        self.assertEqual(d.data, [0xc0])
+
+    def testEsc2(self):
+        d = Dump(0)
+        self.assertEqual(d.esc[1], 0)
+        d.add([0xdb, 0xdd])
+        self.assertEqual(d.len, 1)
+        self.assertEqual(d.esc[1], 1)
+        self.assertEqual(d.data, [0xdb])
+
+    def testToBase64(self):
+        d = Dump(0)
+        d.add(range(10))
+        d.add([0xc0] + list(range(8)))
+        self.assertEqual(d.toBase64(), 'AAECAwQFBgcICcAAAQIDBAUGBw==')
+
+    def testNonValidDataType(self):
+        d = Dump(0)
+        d.add(range(10))
+        d.add([0xc0]+[0, 3])
+        self.assertFalse(d.isValid())
+
+    def testNonValidCRC(self):
+        d = Dump(0)
+        d.add(range(10))
+        d.add([0xc0]+[0, 0, 0, 0])
+        self.assertFalse(d.isValid())
+
+    def testNonValidLen(self):
+        d = Dump(0)
+        d.add(range(10))
+        d.add([0xc0]+[0, 0, 0x78, 0x23, 0, 0])
+        self.assertFalse(d.isValid())
+
+    def testValid(self):
+        d = Dump(0)
+        d.add(range(10))
+        d.add([0xc0]+[0, 0, 0x78, 0x23, 10, 0])
+        self.assertTrue(d.isValid())
+
+    def testHugeDump(self):
+        # issue 177
+        d = Dump(0)
+        d.add([5] * 71318)
+        d.add([0xc0]+[0, 0, 0x44, 0x95, 0x96, 0x16, 0x01, 0x00])
+        self.assertTrue(d.isValid())
diff --git a/tests/testFitbitClient.py b/tests/testFitbitClient.py
new file mode 100644
index 0000000..ca0b10a
--- /dev/null
+++ b/tests/testFitbitClient.py
@@ -0,0 +1,415 @@
+import unittest
+
+from galileo.tracker import FitbitClient
+
+
+class MyDM(object):
+    def __init__(self, data):
+        self.data = data
+    def __str__(self): return str(self.data)
+
+
+class MyCM(object):
+    def __init__(self, data):
+        self.len = data[0]
+        self.INS = data[1]
+        self.payload = data[2:]
+    def asList(self): return [self.len, self.INS] + self.payload
+    def __str__(self): return str(self.asList())
+
+
+class MyDongle(object):
+    def __init__(self, responses):
+        self.responses = responses
+        self.idx = 0
+        self.establishLinkEx = False
+    def read(self, ctrl):
+        response = self.responses[self.idx]
+        self.idx += 1
+        if not response:
+            return None
+        if ctrl:
+            return MyCM(list(response))
+        else:
+            return MyDM(list(response))
+    def ctrl_write(self, *args): pass
+    def ctrl_read(self, *args):
+        return self.read(True)
+    def data_read(self, *args):
+        return self.read(False)
+    def data_write(self, *args): pass
+    def setVersion(self, M, m): self.v = (M, m)
+
+
+class MyDongleWithTimeout(MyDongle):
+    """ A Dongle that starts timeouting at threshold """
+    def __init__(self, data, threshold):
+        MyDongle.__init__(self, data[:threshold] + [()] * (len(data) - threshold))
+
+
+class MyUUID(object):
+    @property
+    def int(self): return 0
+
+
+class MyTracker(object):
+    pass
+
+
+GOOD_SCENARIO = [
+    # CancelDiscovery
+    (0x20, 1, 0x43, 0x61, 0x6E, 0x63, 0x65, 0x6C, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0),
+    # TerminateLink
+    (0x20, 1, 0x54, 0x65, 0x72, 0x6D, 0x69, 0x6E, 0x61, 0x74, 0x65, 0x4C, 0x69, 0x6E, 0x6B, 0),
+    (),
+    (0x15, 8, 1, 1, 0x6F, 0x7B, 0xAD, 0x29, 0x6A, 0xBC, 0x74, 0x09, 0, 0x20, 0, 0, 0xFF, 0xE7, 3, 0, 1),
+    (0x20, 1, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0),
+    (0x13, 3, 0,0,42,0,0,0, 1, 0x80, 2, 6,4, 0,0,0,0,0,0),
+    (3, 2, 1),
+    # CancelDiscovery
+    (0x20, 1, 0x43, 0x61, 0x6E, 0x63, 0x65, 0x6C, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0),
+    (0x20, 1, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6C, 0x69, 0x73, 0x68, 0x4C, 0x69, 0x6E, 0x6B, 0),
+    (3, 4, 0),
+    (0x20, 1, 0x47, 0x41, 0x50, 0x5F, 0x4C, 0x49, 0x4E, 0x4B, 0x5F, 0x45, 0x53, 0x54, 0x41, 0x42, 0x4C, 0x49, 0x53, 0x48, 0x45, 0x44, 0x5F, 0x45, 0x56, 0x45, 0x4E, 0x54, 0),
+    (2, 7),
+    (0xc0, 0xb),
+    (8, 6, 6, 0, 0, 0, 0xc8, 0),
+    (0xc0, 0x14, 0xc,1, 0,0, 0,0,42,0,0,0),
+    # getDump
+    (0xc0, 0x41, 0xd),
+    (0x26, 2, 0, 0, 0, 0, 0),
+    (0xc0, 0,0xd,0x93,0x44,7, 0),
+    #response
+    (0xc0, 0x12, 4, 0, 0),
+    (0xc0, 0x13, 0x14, 0, 0),
+    (0xc0, 0x13, 0x24, 0, 0),
+    (0xc0, 2),
+    (0xc0, 1),
+    (0xc0, 0xb),
+    (0x20, 1, 0x54, 0x65, 0x72, 0x6D, 0x69, 0x6E, 0x61, 0x74, 0x65, 0x4C, 0x69, 0x6E, 0x6B, 0),
+    (3, 5, 0x16, 0),
+    (0x20, 1, 0x47, 0x41, 0x50, 0x5F, 0x4C, 0x49, 0x4E, 0x4B, 0x5F, 0x54, 0x45, 0x52, 0x4D, 0x49, 0x4E, 0x41, 0x54, 0x45, 0x44, 0x5F, 0x45, 0x56, 0x45, 0x4E, 0x54, 0),
+    (0x20, 1, 0x32, 0x32, 0),
+]
+
+SURGE_SCENARIO = [
+    (0x16, 0x08, 0x02, 0x05, 0x05, 0xDF, 0x5E, 0x5E, 0xB8, 0xF4, 0x74, 0x04, 0x00, 0x20, 0x00, 0x00, 0xFF, 0xE7, 0x01, 0x00, 0x02, 0x00),
+    # CancelDiscovery
+    (0x20, 1, 0x43, 0x61, 0x6E, 0x63, 0x65, 0x6C, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0),
+    # TerminateLink
+    (0x20, 1, 0x54, 0x65, 0x72, 0x6D, 0x69, 0x6E, 0x61, 0x74, 0x65, 0x4C, 0x69, 0x6E, 0x6B, 0),
+    (0x16, 0x08, 0x02, 0x05, 0x05, 0xDF, 0x5E, 0x5E, 0xB8, 0xF4, 0x74, 0x04, 0x00, 0x20, 0x00, 0x00, 0xFF, 0xE7, 0x01, 0x00, 0x02, 0x00),
+
+]
+
+
+class testScenarii(unittest.TestCase):
+
+    def testOk(self):
+        d = MyDongle(GOOD_SCENARIO)
+        c = FitbitClient(d)
+        self.assertTrue(c.disconnect())
+        self.assertTrue(c.getDongleInfo())
+        ts = [t for t in c.discover(MyUUID())]
+        self.assertEqual(1, len(ts))
+        self.assertEqual(ts[0].id, [0,0,42,0,0,0])
+        self.assertTrue(c.establishLink(ts[0]))
+        self.assertTrue(c.toggleTxPipe(True))
+        self.assertTrue(c.initializeAirlink(ts[0]))
+        dump = c.getDump()
+        self.assertFalse(dump is None)
+        self.assertEqual(dump.data, [0x26, 2, 0,0,0,0,0])
+        self.assertTrue(c.uploadResponse((0x26, 2, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)))
+        self.assertTrue(c.terminateAirlink())
+        self.assertTrue(c.toggleTxPipe(False))
+        self.assertTrue(c.ceaseLink())
+
+    def testTimeout(self):
+        # the test will have to be re-writen if the scenario changes
+        self.assertEqual(28, len(GOOD_SCENARIO))
+        for i in range(len(GOOD_SCENARIO) + 1):
+            d = MyDongleWithTimeout(GOOD_SCENARIO, i)
+            c = FitbitClient(d)
+            if i < 1:
+                self.assertFalse(c.disconnect(), i)
+                continue
+            self.assertTrue(c.disconnect())
+            if i < 4:
+                self.assertFalse(c.getDongleInfo(), i)
+                continue
+            self.assertTrue(c.getDongleInfo())
+            ts = [t for t in c.discover(MyUUID())]
+            if i < 6:
+                self.assertEqual([], ts, i)
+                continue
+            self.assertEqual(1, len(ts), i)
+            self.assertEqual(ts[0].id, [0,0,42,0,0,0])
+            if i < 12:
+                self.assertFalse(c.establishLink(ts[0]), i)
+                continue
+            self.assertTrue(c.establishLink(ts[0]), i)
+            if i < 13:
+                self.assertFalse(c.toggleTxPipe(True), i)
+                continue
+            self.assertTrue(c.toggleTxPipe(True))
+            if i < 15:
+                self.assertFalse(c.initializeAirlink(ts[0]))
+                continue
+            self.assertTrue(c.initializeAirlink(ts[0]))
+            if i < 18:
+                self.assertEqual(None, c.getDump())
+                continue
+            dump = c.getDump()
+            self.assertFalse(dump is None)
+            self.assertEqual(dump.data, [0x26, 2, 0,0,0,0,0])
+            if i < 22:
+                self.assertFalse(c.uploadResponse((0x26, 2, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)))
+                continue
+            self.assertTrue(c.uploadResponse((0x26, 2, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)))
+            if i < 23:
+                self.assertFalse(c.terminateAirlink())
+                continue
+            self.assertTrue(c.terminateAirlink())
+            if i < 24:
+                self.assertFalse(c.toggleTxPipe(False))
+                continue
+            self.assertTrue(c.toggleTxPipe(False))
+            if i < 28:
+                self.assertFalse(c.ceaseLink())
+                continue
+            self.assertTrue(c.ceaseLink())
+            self.assertEqual(len(GOOD_SCENARIO), i)
+
+
+class testDiscover(unittest.TestCase):
+
+    def testNoTracker(self):
+        d = MyDongle([(0x20, 1, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0 ),
+                      (3, 2, 0),
+                      (0x20, 1, 0x43, 0x61, 0x6E, 0x63, 0x65, 0x6C, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0),
+                     ])
+        c = FitbitClient(d)
+        ts = [t for t in c.discover(MyUUID())]
+        self.assertEqual(len(ts), 0)
+
+    def testOnetracker(self):
+        d = MyDongle([(0x20, 1, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0 ),
+                      (0x13, 3, 0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,1,-30, 2,6,4, 3,
+                       0x2c, 0x31, 0xf6, 0xd8, 0x58),
+                      (3, 2, 1),
+                      (0x20, 1, 0x43, 0x61, 0x6E, 0x63, 0x65, 0x6C, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0),
+                     ])
+        c = FitbitClient(d)
+        ts = [t for t in c.discover(MyUUID())]
+        self.assertEqual(len(ts), 1)
+        t = ts[0]
+        self.assertEqual(t.id, [0xaa] * 6)
+
+    def testTwotracker(self):
+        d = MyDongle([(0x20, 1, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0 ),
+                      (0x13, 3, 0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,1,-30, 2,6,4, 3,
+                       0x2c, 0x31, 0xf6, 0xd8, 0x58),
+                      (0x13, 3, 0xbb,0xbb,0xbb,0xbb,0xbb,0xbb,1,-30, 2,6,4, 3,
+                       0x2c, 0x31, 0xf6, 0xd8, 0x58),
+                      (3, 2, 2),
+                      (0x20, 1, 0x43, 0x61, 0x6E, 0x63, 0x65, 0x6C, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0),
+                     ])
+        c = FitbitClient(d)
+        ts = [t for t in c.discover(MyUUID())]
+        self.assertEqual(len(ts), 2)
+        t = ts[0]
+        self.assertEqual(t.id, [0xaa] * 6)
+        t = ts[1]
+        self.assertEqual(t.id, [0xbb] * 6)
+
+    def testTimeout(self):
+        d = MyDongle([(0x20, 1, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0 ),
+                      (),
+                      ()])
+        c = FitbitClient(d)
+        ts = [t for t in c.discover(MyUUID())]
+        self.assertEqual(len(ts), 0)
+
+    def testWrongParams(self):
+        """ Sometime, we get the amount before the Status """
+        d = MyDongle([(3, 2, 0),
+                      (0x20, 1, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0 ),
+                      (0x20, 1, 0x43, 0x61, 0x6E, 0x63, 0x65, 0x6C, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0),
+                      ])
+        c = FitbitClient(d)
+        ts = [t for t in c.discover(MyUUID())]
+        self.assertEqual(len(ts), 0)
+
+    def testIssue96(self):
+        """ Sometime, we don't get payload """
+        d = MyDongle([(2, 0xa), ()])
+        c = FitbitClient(d)
+        ts = [t for t in c.discover(MyUUID())]
+        self.assertEqual(len(ts), 0)
+
+    def testIssue231(self):
+        """ Some weird Status Messages in the middle """
+        d = MyDongle([(0x20, 1, 0x45, 0x52, 0x52, 0x4F, 0x52, 0x3A, 0x20, 0x50, 0x31, 0x5B, 0x37, 0x3A, 0x31, 0x5D, 0x20, 0x73, 0x68, 0x6F, 0x75, 0x6C, 0x64, 0x20, 0x62, 0x65, 0x20, 0x30),
+                      (0x20, 1, 0x33),
+                      (0x20, 1, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79),
+                      (0x13, 0x03, 0xD2, 0xCD, 0x91, 0xC1, 0x01, 0xF8, 0x01, 0xB6, 0x02, 0x07, 0x06, 0x3E, 0x00, 0x09, 0x4A, 0x00, 0xFB),
+                      (3, 2, 1), ()])
+        c = FitbitClient(d)
+        ts = [t for t in c.discover(MyUUID())]
+        self.assertEqual(len(ts), 1)
+
+class testGetDongleInfo(unittest.TestCase):
+    def testIssue136(self):
+        d = MyDongle([(0x20, 1, 0x54, 0x65, 0x72, 0x6D, 0x69, 0x6E, 0x61, 0x74, 0x65, 0x4C, 0x69, 0x6E, 0x6B, 0),])
+        c = FitbitClient(d)
+        self.assertFalse(c.getDongleInfo())
+
+    def testOkOld(self):
+        d = MyDongle([(0x15, 8, 1, 1, 0x6F, 0x7B, 0xAD, 0x29, 0x6A, 0xBC, 0x74, 0x09, 0, 0x20, 0, 0, 0xFF, 0xE7, 3, 0, 1),])
+        c = FitbitClient(d)
+        self.assertTrue(c.getDongleInfo())
+        self.assertEqual(d.v, (1,1))
+        self.assertEqual(d.flashEraseTime, 2420)
+        self.assertEqual(d.firmwareStartAddress, 8192)
+        self.assertEqual(d.firmwareEndAddress, 255999)
+        self.assertEqual(d.ccIC, 1)
+
+    def testOk(self):
+        d = MyDongle([(0x16, 8, 2, 5, 0x71, 0x59, 0x46, 0x16, 0x4A, 0x54, 0x74, 4, 0, 0x20, 0, 0, 0xFF, 0xE7, 1, 0, 2, 0),])
+        c = FitbitClient(d)
+        self.assertTrue(c.getDongleInfo())
+        self.assertEqual(d.v, (2,5))
+
+    def testSurgeDongle(self):
+        d = MyDongle([(0x16, 0x08, 0x02, 0x05, 0x05, 0xDF, 0x5E, 0x5E, 0xB8, 0xF4, 0x74, 0x04, 0x00, 0x20, 0x00, 0x00, 0xFF, 0xE7, 0x01, 0x00, 0x02, 0),])
+        c = FitbitClient(d)
+        self.assertTrue(c.getDongleInfo())
+        self.assertEqual(d.v, (2,5))
+        self.assertEqual(d.flashEraseTime, 1140)
+        self.assertEqual(d.firmwareStartAddress, 8192)
+        self.assertEqual(d.firmwareEndAddress, 124927)
+        self.assertEqual(d.ccIC, 2)
+
+    def testNewerDongle75(self):
+        d = MyDongle([(0x16, 0x08, 0x07, 0x05, 0xA4, 0xA6, 0x69, 0xF3, 0x7B, 0x98, 0x74, 0x04, 0x00, 0x20, 0x00, 0x00, 0xFF, 0xE7, 0x01, 0x00, 0x02, 0x00)])
+        c = FitbitClient(d)
+        self.assertTrue(c.getDongleInfo())
+        self.assertEqual(d.v, (7,5))
+        self.assertEqual(d.flashEraseTime, 1140)
+        self.assertEqual(d.firmwareStartAddress, 8192)
+        self.assertEqual(d.firmwareEndAddress, 124927)
+        self.assertEqual(d.ccIC, 2)
+
+class testestablishLink(unittest.TestCase):
+
+    def testestablishLinkExOk(self):
+        d = MyDongle([(0x20, 1, 0x43, 0x61, 0x6E, 0x63, 0x65, 0x6C, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0),
+                      (0x20, 1, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6C, 0x69, 0x73, 0x68, 0x4C, 0x69, 0x6E, 0x6B, 0x45, 0x78, 0x20, 0x63, 0x61, 0x6C, 0x6C, 0x65, 0x64, 0x2E, 0x2E, 0x2E, 0x00),
+                      (3, 4, 0),
+                      (0x20, 1, 0x47, 0x41, 0x50, 0x5F, 0x4C, 0x49, 0x4E, 0x4B, 0x5F, 0x45, 0x53, 0x54, 0x41, 0x42, 0x4C, 0x49, 0x53, 0x48, 0x45, 0x44, 0x5F, 0x45, 0x56, 0x45, 0x4E, 0x54, 0),
+                      (2, 7),])
+        d.establishLinkEx = True
+        c = FitbitClient(d)
+        t = MyTracker()
+        t.id = [0,0,42,0,0,43]
+        t.addrType = 1
+        self.assertTrue(c.establishLink(t))
+
+    def testestablishLinkExNotOk(self):
+        """ When our version test is wrong """
+        d = MyDongle([(4, 0xff, 2, 3),
+                      (0x20, 1, 0x43, 0x61, 0x6E, 0x63, 0x65, 0x6C, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x79, 0),
+                      (0x20, 1, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6C, 0x69, 0x73, 0x68, 0x4C, 0x69, 0x6E, 0x6B, 0x45, 0x78, 0x20, 0x63, 0x61, 0x6C, 0x6C, 0x65, 0x64, 0x2E, 0x2E, 0x2E, 0),
+                      (3, 4, 0),
+                      (0x20, 1, 0x47, 0x41, 0x50, 0x5F, 0x4C, 0x49, 0x4E, 0x4B, 0x5F, 0x45, 0x53, 0x54, 0x41, 0x42, 0x4C, 0x49, 0x53, 0x48, 0x45, 0x44, 0x5F, 0x45, 0x56, 0x45, 0x4E, 0x54, 0),
+                      (2, 7),])
+        d.major = 169; d.minor=78
+        c = FitbitClient(d)
+        t = MyTracker()
+        t.id = [0,0,42,0,0,43]
+        t.addrType = 1
+        t.serviceUUID = 0xa005
+        self.assertTrue(c.establishLink(t))
+        # verify the value is set for later tests
+        self.assertTrue(d.establishLinkEx)
+
+class testinitAirLink(unittest.TestCase):
+
+    def testCharge(self):
+        d = MyDongle([(8, 6, 6, 0, 0, 0, 0xc8, 0),
+                      (0xc0, 0x14, 0xc,0xa, 0,0, 0,0,42,0,0,0, 0x17,0),])
+        c = FitbitClient(d)
+        t = MyTracker()
+        t.id = [0,0,42,0,0,0]
+        self.assertTrue(c.initializeAirlink(t))
+
+    def testOthers(self):
+        d = MyDongle([(8, 6, 6, 0, 0, 0, 0xc8, 0),
+                      (0xc0, 0x14, 0xc,1, 0,0, 0,0,42,0,0,0),])
+        c = FitbitClient(d)
+        t = MyTracker()
+        t.id = [0,0,42,0,0,0]
+        self.assertTrue(c.initializeAirlink(t))
+
+    def testEstablishEx(self):
+        """ When the dongle uses establishEx, he doesn't read back on the
+            ctrl channel """
+        d = MyDongle([(0xc0, 0x14, 0xc,1, 0,0, 0,0,42,0,0,0),])
+        d.establishLinkEx = True
+        c = FitbitClient(d)
+        t = MyTracker()
+        t.id = [0,0,42,0,0,0]
+        self.assertTrue(c.initializeAirlink(t))
+
+class testUpload(unittest.TestCase):
+
+    def testLongMessage(self):
+        """ Validate that the seq number rounds up """
+
+        class MyDongle(object):
+            def __init__(self, len):
+                 self.i = -1
+                 self.len = len
+            def data_read(self ,*args):
+                self.i += 1
+                if self.i == 0:
+                    return MyDM([0xc0, 0x12, 4, 0, 0])
+                if self.i < self.len:
+                    return MyDM([0xc0, 0x13, (((self.i) % 16) << 4) + 4, 0, 0])
+                return MyDM([0xc0, 2])
+            def data_write(self, *args): pass
+
+        d = MyDongle(20)
+        c = FitbitClient(d)
+        self.assertTrue(c.uploadResponse([0] * 380))
+
+
+class testDownload(unittest.TestCase):
+
+    def testPreSurge(self):
+        d = MyDongle([
+            (0xc0, 0x41, 0xd),
+            (0x26, 2, 0, 0, 0, 0, 0),
+            (0xc0, 0,0xd,0x93,0x44,7, 0)])
+        c = FitbitClient(d)
+        dump = c.getDump(0xd)
+        self.assertTrue(dump.isValid())
+        self.assertEqual(dump.data, [38, 2, 0, 0, 0, 0, 0])
+        self.assertEqual(dump.footer, [192, 0, 13, 147, 68, 7, 0])
+
+    def testSurge(self):
+        # This is not completely correct
+        d = MyDongle([
+            (0xc0, 0x41, 0xd, 0x42, 0xa, 0, 0),
+            (0x26, 2, 0, 0, 0, 0, 0),
+            (0xc0, 0,0xd,0x93,0x44,7, 0)])
+        c = FitbitClient(d)
+        dump = c.getDump(0xd)
+        self.assertTrue(dump.isValid())
+
+class testSetPowerLevel(unittest.TestCase):
+
+    def testOk(self):
+        d = MyDongle([(2, 0xfe),])
+        c = FitbitClient(d)
+        self.assertTrue(c.setPowerLevel(5))
diff --git a/tests/testFormExtractor.py b/tests/testFormExtractor.py
new file mode 100644
index 0000000..1546353
--- /dev/null
+++ b/tests/testFormExtractor.py
@@ -0,0 +1,31 @@
+import unittest
+
+from galileo.ui import FormExtractor, FormField
+
+class testFormExtractor(unittest.TestCase):
+
+    def testEasy(self):
+        fe = FormExtractor()
+        fe.feed('<html><body><form><input type="text" name="username"><input type="text" name="password"></form></body></html>')
+        self.assertEqual(len(fe.forms), 1)
+        self.assertEqual(len(fe.forms[0].fields), 2)
+        self.assertEqual(fe.forms[0].asDict(), {'username':None, 'password':None})
+
+    def testOneHidden(self):
+        fe = FormExtractor()
+        fe.feed('<html><body><form><input name="username" type="hidden" value="User"><input type="text" name="password"></form></body></html>')
+        self.assertEqual(len(fe.forms), 1)
+        self.assertEqual(fe.forms[0].asDict(), {'username': 'User', 'password': None})
+
+    def testSelect(self):
+        fe = FormExtractor()
+        fe.feed('<html><body><form><select name="choice" ><option value="A" /><option value="B" selected></select></form></body></html>')
+        self.assertEqual(len(fe.forms), 1)
+        self.assertEqual(fe.forms[0].asDict(), {'choice': 'B'})
+
+    def testInputOutOfForm(self):
+        """ From the 'done' action """
+        fe = FormExtractor()
+        fe.feed(u'''<!DOCTYPE html><input class="button" type="submit" name="again" value="Next" />''')
+        self.assertEqual(len(fe.forms), 1)
+        self.assertEqual(fe.forms[0].asDict(), {'again': 'Next'})
diff --git a/tests/testGalileoClient.py b/tests/testGalileoClient.py
new file mode 100644
index 0000000..2c3012a
--- /dev/null
+++ b/tests/testGalileoClient.py
@@ -0,0 +1,247 @@
+import unittest
+import sys
+
+from galileo import __version__
+
+import galileo.net
+from galileo.net import GalileoClient, SyncError, BackOffException
+
+class requestResponse(object):
+    def __init__(self, text, server_version='<server-version>\n\n</server-version>'):
+        self.text = """<?xml version="1.0" encoding="utf-8" standalone="yes"?><galileo-server version="2.0">%s%s</galileo-server>""" % (server_version, text)
+    def raise_for_status(self): pass
+
+class testStatus(unittest.TestCase):
+
+    def testOk(self):
+        def mypost(url, data, headers):
+            self.assertEqual(url, 'scheme://host:8888/path/to/stuff')
+            self.assertEqual(data.decode('utf-8'), """\
+<?xml version='1.0' encoding='utf-8'?>
+<galileo-client version="2.0"><client-info><client-id>%(id)s</client-id><client-version>%(version)s</client-version><client-mode>status</client-mode></client-info></galileo-client>""" % {
+    'id': GalileoClient.ID, 'version': __version__})
+            self.assertEqual(headers['Content-Type'], 'text/xml')
+            return requestResponse('')
+
+        galileo.net.requests.post = mypost
+        gc = GalileoClient('scheme', 'host', 'path/to/stuff', 8888)
+        gc.requestStatus()
+
+    def testError(self):
+        def mypost(url, data, headers):
+            self.assertEqual(url, 'h://c:8/p')
+            self.assertEqual(data.decode('utf-8'), """\
+<?xml version='1.0' encoding='utf-8'?>
+<galileo-client version="2.0"><client-info><client-id>%(id)s</client-id><client-version>%(version)s</client-version><client-mode>status</client-mode></client-info></galileo-client>""" % {
+    'id': GalileoClient.ID,
+    'version': __version__})
+            self.assertEqual(headers['Content-Type'], 'text/xml')
+            return requestResponse('<error>Something is Wrong</error>')
+
+        galileo.net.requests.post = mypost
+        gc = GalileoClient('h', 'c', 'p', 8)
+        self.assertRaises(SyncError, gc.requestStatus)
+
+    def testBackOff(self):
+        # no support for ``with assertRaises`` in python 2.6
+        if sys.version_info < (2,7): return
+        def mypost(url, data, headers):
+            self.assertEqual(url, 'h://c:4/p')
+            self.assertEqual(data.decode('utf-8'), """\
+<?xml version='1.0' encoding='utf-8'?>
+<galileo-client version="2.0"><client-info><client-id>%(id)s</client-id><client-version>%(version)s</client-version><client-mode>status</client-mode></client-info></galileo-client>""" % {
+    'id': GalileoClient.ID,
+    'version': __version__})
+            self.assertEqual(headers['Content-Type'], 'text/xml')
+            return requestResponse("""
+    <back-off>
+        <min>1800000</min>
+        <max>3600000</max>
+    </back-off>
+    <ui-request action="login">
+        <client-display height="450" width="650" minDisplayTimeMs="20000" containsForm="false">
+            Server is in maintenance mode. We'll be back soon!
+        </client-display>
+    </ui-request>""", '')
+
+        galileo.net.requests.post = mypost
+        gc = GalileoClient('h', 'c', 'p', 4)
+        with self.assertRaises(BackOffException) as cm:
+            gc.requestStatus()
+        e = cm.exception
+        self.assertEqual(e.min, 1800000)
+        self.assertEqual(e.max, 3600000)
+        val = e.getAValue()
+        self.assertTrue(e.min <= val <= e.max)
+
+
+    def testStatusRequests082(self):
+        """ Older versions of requests only have ``content`` and no ``text`` """
+        def mypost(url, data, headers):
+            self.assertEqual(url, 'scheme://host:8888/path/to/stuff')
+            self.assertEqual(data.decode('utf-8'), """\
+<?xml version='1.0' encoding='utf-8'?>
+<galileo-client version="2.0"><client-info><client-id>%(id)s</client-id><client-version>%(version)s</client-version><client-mode>status</client-mode></client-info></galileo-client>""" % {
+    'id': GalileoClient.ID, 'version': __version__})
+            self.assertEqual(headers['Content-Type'], 'text/xml')
+            res = requestResponse('')
+            res.content = res.text
+            delattr(res, 'text')
+            return res
+
+        galileo.net.requests.post = mypost
+        gc = GalileoClient('scheme', 'host', 'path/to/stuff', 8888)
+        gc.requestStatus()
+
+class MyDongle(object):
+    def __init__(self, M, m): self.major=M; self.minor=m; self.hasVersion=True
+class MyMegaDump(object):
+    def __init__(self, b64): self.b64 = b64
+    def toBase64(self): return self.b64
+
+class testSync(unittest.TestCase):
+
+    def testOk(self):
+        T_ID = 'abcd'
+        D = MyDongle(0, 0)
+        d = MyMegaDump('YWJjZA==')
+        def mypost(url, data, headers):
+            self.assertEqual(url, 'a://b:0/c')
+            self.assertEqual(data.decode('utf-8'), """\
+<?xml version='1.0' encoding='utf-8'?>
+<galileo-client version="2.0"><client-info><client-id>%(id)s</client-id><client-version>%(version)s</client-version><client-mode>sync</client-mode><dongle-version major="%(M)d" minor="%(m)d" /></client-info><tracker tracker-id="%(t_id)s"><data>%(b64dump)s</data></tracker></galileo-client>""" % {
+    'id': GalileoClient.ID,
+    'version': __version__,
+    'M': D.major,
+    'm': D.minor,
+    't_id': T_ID,
+    'b64dump': d.toBase64()})
+            self.assertEqual(headers['Content-Type'], 'text/xml')
+            return requestResponse('<tracker tracker-id="abcd" type="megadumpresponse"><data>ZWZnaA==</data></tracker>')
+
+        galileo.net.requests.post = mypost
+        gc = GalileoClient('a', 'b', 'c', 0)
+        self.assertEqual(gc.sync(D, T_ID, d),
+                         [101, 102, 103, 104])
+
+    def testNoTracker(self):
+        T_ID = 'aaaabbbb'
+        D = MyDongle(34, 88)
+        d = MyMegaDump('base64Dump')
+        def mypost(url, data, headers):
+            self.assertEqual(url, 'z://y:42/u')
+            self.assertEqual(data.decode('utf-8'), """\
+<?xml version='1.0' encoding='utf-8'?>
+<galileo-client version="2.0"><client-info><client-id>%(id)s</client-id><client-version>%(version)s</client-version><client-mode>sync</client-mode><dongle-version major="%(M)d" minor="%(m)d" /></client-info><tracker tracker-id="%(t_id)s"><data>%(b64dump)s</data></tracker></galileo-client>""" % {
+    'id': GalileoClient.ID,
+    'version': __version__,
+    'M': D.major,
+    'm': D.minor,
+    't_id': T_ID,
+    'b64dump': d.toBase64()})
+            self.assertEqual(headers['Content-Type'], 'text/xml')
+            return requestResponse('')
+
+        galileo.net.requests.post = mypost
+        gc = GalileoClient('z', 'y', 'u', 42)
+        self.assertRaises(SyncError, gc.sync, D, T_ID, d)
+
+    def testNoData(self):
+        T_ID = 'aaaa'
+        D = MyDongle(-2, 42)
+        d = MyMegaDump('base64Dump')
+        def mypost(url, data, headers):
+            self.assertEqual(url, 'y://t:8000/v')
+            self.assertEqual(data.decode('utf-8'), """\
+<?xml version='1.0' encoding='utf-8'?>
+<galileo-client version="2.0"><client-info><client-id>%(id)s</client-id><client-version>%(version)s</client-version><client-mode>sync</client-mode><dongle-version major="%(M)d" minor="%(m)d" /></client-info><tracker tracker-id="%(t_id)s"><data>%(b64dump)s</data></tracker></galileo-client>""" % {
+    'id': GalileoClient.ID,
+    'version': __version__,
+    'M': D.major,
+    'm': D.minor,
+    't_id': T_ID,
+    'b64dump': d.toBase64()})
+            self.assertEqual(headers['Content-Type'], 'text/xml')
+            return requestResponse('<tracker tracker-id="abcd" type="megadumpresponse"></tracker>')
+
+        galileo.net.requests.post = mypost
+        gc = GalileoClient('y', 't', 'v', 8000)
+        self.assertRaises(SyncError, gc.sync, D, T_ID, d)
+
+    def testNotData(self):
+        T_ID = 'aaaabbbbccccdddd'
+        D = MyDongle(-2, 42)
+        d = MyMegaDump('base64Dump')
+        def mypost(url, data, headers):
+            self.assertEqual(url, 'rsync://ssh:22/a/b/c')
+            self.assertEqual(data.decode('utf-8'), """\
+<?xml version='1.0' encoding='utf-8'?>
+<galileo-client version="2.0"><client-info><client-id>%(id)s</client-id><client-version>%(version)s</client-version><client-mode>sync</client-mode><dongle-version major="%(M)d" minor="%(m)d" /></client-info><tracker tracker-id="%(t_id)s"><data>%(b64dump)s</data></tracker></galileo-client>""" % {
+    'id': GalileoClient.ID,
+    'version': __version__,
+    'M': D.major,
+    'm': D.minor,
+    't_id': T_ID,
+    'b64dump': d.toBase64()})
+            self.assertEqual(headers['Content-Type'], 'text/xml')
+            return requestResponse('<tracker tracker-id="abcd" type="megadumpresponse"><not_data /></tracker>')
+
+        galileo.net.requests.post = mypost
+        gc = GalileoClient('rsync', 'ssh', 'a/b/c', 22)
+        self.assertRaises(SyncError, gc.sync, D, T_ID, d)
+
+    def testConnectionError(self):
+        T_ID = 'abcd'
+        D = MyDongle(0, 0)
+        d = MyMegaDump('YWJjZA==')
+        def mypost(url, data, headers):
+            class Reason(object):
+                class Error(object): strerror = ''
+                reason = Error()
+            raise galileo.net.requests.exceptions.ConnectionError(Reason())
+
+        galileo.net.requests.post = mypost
+        gc = GalileoClient('a', 'b', 'c', 0)
+        self.assertRaises(SyncError, gc.sync,D, T_ID, d)
+
+    def testHTTPError(self):  # issue147
+        def mypost(url, data, headers):
+            class Response(object): status_code=500
+            if galileo.net.requests.__build__ > 0x020000:
+                # Only newer requests exceptions inherit from IOError
+                e = galileo.net.requests.exceptions.HTTPError('bad', response=Response())
+            else:
+                # older inherit from RuntimeError (no kwargs)
+                e = galileo.net.requests.exceptions.HTTPError('bad')
+            raise e
+
+        galileo.net.requests.post = mypost
+
+        T_ID = 'abcd'
+        D = MyDongle(0, 0)
+        d = MyMegaDump('YWJjZA==')
+        gc = GalileoClient('a', 'b', 'c', 0)
+        with self.assertRaises(SyncError) as cm:
+            gc.sync(D, T_ID, d)
+        self.assertEqual(cm.exception.errorstring, 'HTTPError: bad (500)')
+
+class testURL(unittest.TestCase):
+
+    def testWithPort(self):
+        gc = GalileoClient('scheme', 'host', 'path/to/stuff', 8000)
+        self.assertEqual(gc.url, 'scheme://host:8000/path/to/stuff')
+
+    def testHTTPPort(self):
+        gc = GalileoClient('http', 'h', 'a/b/c')
+        self.assertEqual(gc.url, 'http://h:80/a/b/c')
+
+    def testHTTPSPort(self):
+        gc = GalileoClient('https', 'h', 'a/b/c')
+        self.assertEqual(gc.url, 'https://h:443/a/b/c')
+
+    def testUnknownPort(self):
+        # no support for ``with assertRaises`` in python 2.6
+        if sys.version_info < (2,7): return
+        gc = GalileoClient('blah', 'h', 'a')
+        with self.assertRaises(KeyError):
+            gc.url
diff --git a/tests/testNetUtils.py b/tests/testNetUtils.py
new file mode 100644
index 0000000..777378f
--- /dev/null
+++ b/tests/testNetUtils.py
@@ -0,0 +1,79 @@
+import unittest
+
+import xml.etree.ElementTree as ET
+from io import BytesIO
+
+from galileo.net import toXML, tuplesToXML, XMLToTuple
+
+class testtoXML(unittest.TestCase):
+
+    def _testEqual(self, xml, xmlStr):
+        tree = ET.ElementTree(xml)
+        f = BytesIO()
+        tree.write(f)
+        self.assertEqual(f.getvalue().decode('utf-8'), xmlStr)
+        f.close()
+
+    def testSimple(self):
+        self._testEqual(toXML('elem'), '<elem />')
+    def testSimpleWithAttrs(self):
+        self._testEqual(toXML('elem', {'attr1': 'val',
+                                       'attr2': 'val'}),
+                        '<elem attr1="val" attr2="val" />')
+    def testSimpleWithBody(self):
+        self._testEqual(toXML('elem', body="body"), '<elem>body</elem>')
+    def testSimpleWithChilds(self):
+        self._testEqual(toXML('parent', childs=[('child1',), ('child2',)]),
+                        '<parent><child1 /><child2 /></parent>')
+    def testFull(self):
+        self._testEqual(toXML('parent', {'a':'c'}, [('c',),
+                                                    ('c', {}, [], 'b')], 'b'),
+                        '<parent a="c">b<c /><c>b</c></parent>')
+
+    def testOnetuplesToXML(self):
+        xmls = list(tuplesToXML(('p',{'a':'a'}, [], 'b')))
+        self.assertEqual(len(xmls), 1)
+        self._testEqual(xmls[0], '<p a="a">b</p>')
+    def testOnetuplesToXML2(self):
+        xmls = list(tuplesToXML([('p',{'a':'a'}, [], 'b')]))
+        self.assertEqual(len(xmls), 1)
+        self._testEqual(xmls[0], '<p a="a">b</p>')
+
+    def testMultipletuplesToXML(self):
+        xmls = list(tuplesToXML([('p',{'a':'a'}, [], 'b'),
+                                 ('p'),
+                                 ('p', {}, [], 'b')]))
+        self.assertEqual(len(xmls), 3)
+        self._testEqual(xmls[0], '<p a="a">b</p>')
+        self._testEqual(xmls[1], '<p />')
+        self._testEqual(xmls[2], '<p>b</p>')
+
+class testtoTuple(unittest.TestCase):
+    def _testEqual(self, xmlStr, tpls):
+        tpl =  XMLToTuple(ET.fromstring(xmlStr))
+        self.assertEqual(tpl, tpls)
+
+    def testSimple(self):
+        self._testEqual('<e />', ('e', {}, [], None))
+
+    def testSimpleWithAttrs(self):
+        self._testEqual('<e a1="v1" a2="v2"/>', ('e', {'a1': 'v1', 'a2': 'v2'},
+                                                 [], None))
+
+    def testSimpleWithBody(self):
+        self._testEqual('<e>b</e>', ('e', {}, [], 'b'))
+
+    def testSimpleWithChilds(self):
+        self._testEqual('<p><c1 /><c2>b</c2></p>', ('p',
+                                                    {},
+                                                    [('c1', {}, [], None),
+                                                     ('c2', {}, [], 'b')],
+                                                    None))
+
+    def testFull(self):
+        self._testEqual('<p a1="v1">b1<c1 /><c2><sc1 /></c2></p>',
+                        ('p',
+                         {'a1':'v1'},
+                         [('c1', {}, [], None),
+                          ('c2', {}, [('sc1', {}, [], None)], None)],
+                         'b1'))
diff --git a/tests/testParameters.py b/tests/testParameters.py
new file mode 100644
index 0000000..f9c3f63
--- /dev/null
+++ b/tests/testParameters.py
@@ -0,0 +1,75 @@
+import unittest
+import logging
+
+from galileo.config import (
+    StrParameter, IntParameter, BoolParameter, SetParameter, LogLevelParameter
+)
+
+
+class MyArgParse(object):
+    def __init__(self, tester):
+        self.tester = tester
+    def add_argument(self, *args, **kwargs):
+        self.name = kwargs['dest']
+    def parse_args(self, args):
+        class Args(object): pass
+        a = Args()
+        setattr(a, self.name, args[1])
+        return a
+
+
+class testStrParameter(unittest.TestCase):
+    def testArgParse(self):
+        p = StrParameter('varName', 'name', ('--paramNames'), 'default', False,
+                         "Some help text")
+        ap = MyArgParse(self)
+        p.toArgParse(ap)
+        args = ap.parse_args(['--paramNames', 'value'])
+        d = {}
+        p.fromArgs(args, d)
+        self.assertTrue('varName' in d)
+        self.assertEqual(d['varName'], 'value')
+
+    def testFile(self):
+        p = StrParameter('varName', 'name', ('--paramNames'), 'default', False,
+                         "Some help text")
+        ap = MyArgParse(self)
+        d = {}
+        c = {'name': 'abcd'}
+        self.assertTrue(p.fromFile(c, d))
+        self.assertTrue('varName' in d)
+        self.assertEqual(d['varName'], 'abcd')
+
+    def testFileparamOnly(self):
+        p = StrParameter('varName', 'name', ('--paramNames'), 'default', True,
+                         "Some help text")
+        ap = MyArgParse(self)
+        d = {}
+        c = {'name': 'abcd'}
+        self.assertTrue(p.fromFile(c, d))
+        self.assertFalse('varName' in d)
+
+
+class testBoolParameter(unittest.TestCase): pass
+
+
+class testLogLevelParameter(unittest.TestCase):
+    def testWrongValuefromFile(self):
+        p = LogLevelParameter()
+        d = {}
+        c = {'logging': 'foo'}
+        self.assertFalse(p.fromFile(c, d))
+
+    def testCorrectValueUpper(self):
+        p = LogLevelParameter()
+        d = {}
+        c = {'logging': 'DEBUG'}
+        self.assertTrue(p.fromFile(c, d))
+        self.assertEqual(d['logLevel'], logging.DEBUG)
+
+    def testCorrectValueLower(self):
+        p = LogLevelParameter()
+        d = {}
+        c = {'logging': 'quiet'}
+        self.assertTrue(p.fromFile(c, d))
+        self.assertEqual(d['logLevel'], logging.WARNING)
diff --git a/tests/testTracker.py b/tests/testTracker.py
new file mode 100644
index 0000000..2e49bc6
--- /dev/null
+++ b/tests/testTracker.py
@@ -0,0 +1,24 @@
+import unittest
+
+
+from galileo.tracker import Tracker
+
+class testfromDiscovery(unittest.TestCase):
+
+    def testOk(self):
+        t = Tracker.fromDiscovery([0xE5, 0x14, 0x53, 0x33, 0xEE, 0xFF, 0x01, 0xBC, 0x02, 0x05, 0x04, 0x03, 0x2C, 0x31, 0xF6, 0xD8, 0x58])
+        self.assertEqual(t.id, [0xE5, 0x14, 0x53, 0x33, 0xEE, 0xFF])
+        self.assertEqual(t.addrType, 1)
+        self.assertEqual(t.RSSI, -68)
+        self.assertEqual(len(t.serviceData), 2 + 1)
+        self.assertEqual(t.serviceData, [5,4,3])
+        self.assertEqual(t.serviceUUID, 22744)
+
+    def testSurge(self):
+        t = Tracker.fromDiscovery([0xB2, 0x94, 0x82, 0x6E, 0x0C, 0xC8, 0x01, 0xD1, 0x05, 0x10, 0x06, 0xA7, 0x66, 0x03, 0x4A, 0x00, 0xFB])
+        self.assertEqual(t.id, [178,148,130,110,12,200])
+        self.assertEqual(t.addrType, 1)
+        self.assertEqual(t.RSSI, -47)
+        self.assertEqual(len(t.serviceData), 5 + 1)
+        self.assertEqual(t.serviceData, [16,6,167,102,3,74])
+        self.assertEqual(t.serviceUUID, 64256)
diff --git a/tests/testUI.py b/tests/testUI.py
new file mode 100644
index 0000000..03ca939
--- /dev/null
+++ b/tests/testUI.py
@@ -0,0 +1,46 @@
+import unittest
+
+from galileo.ui import Form, FormField, MissingConfigError
+
+class testFormField(unittest.TestCase):
+    def teststr(self):
+        self.assertEqual("'name': 'value'", str(FormField('name', 'text', 'value')))
+        self.assertEqual("'name': None", str(FormField('name', 'text')))
+
+    def testasXML(self):
+        self.assertEqual(FormField('name').asXMLParam(), ('param', {'name': 'name'}, [], None))
+        self.assertEqual(FormField('name', value='value').asXMLParam(), ('param', {'name': 'name'}, [], 'value'))
+
+class testHTMLForm(unittest.TestCase):
+    def testasXML(self):
+        f = Form()
+        f.addField(FormField('name'))
+        f.addField(FormField('name2'))
+        tpl = f.asXML()
+        self.assertEqual(len(tpl), 2)
+        self.assertIn(('param', {'name': 'name'}, [], None), tpl)
+        self.assertIn(('param', {'name': 'name2'}, [], None), tpl)
+
+    def testasXML2Submit(self):
+        f = Form()
+        f.addField(FormField('name', 'submit'))
+        f.addField(FormField('name2', 'submit'))
+        f.takeValuesFromAnswer({'name2': None})
+        self.assertEqual(f.asXML(), [('param', {'name': 'name2'}, [], None)])
+
+class testMissingConfigClass(unittest.TestCase):
+    def testStr(self):
+        f = Form()
+        f.addField(FormField('name'))
+        f.addField(FormField('name2'))
+        f2 = Form()
+        f2.addField(FormField('a'))
+        f2.addField(FormField('b'))
+        f2.addField(FormField('c'))
+        mce = MissingConfigError('test', [f, f2])
+        s = str(mce)
+        self.assertTrue(str(f.asDict()) in s)
+        self.assertTrue(str(f2.asDict()) in s)
+        self.assertTrue('`--debug`' in s)
+        self.assertTrue("'test'" in s)
+        self.assertTrue("'hardcoded-ui'" in s)
diff --git a/tests/testUtils.py b/tests/testUtils.py
new file mode 100644
index 0000000..1e06e99
--- /dev/null
+++ b/tests/testUtils.py
@@ -0,0 +1,111 @@
+import unittest
+
+from galileo.utils import a2x, a2s, a2lsbi, a2msbi, i2lsba, s2a, x2a
+
+class testa2x(unittest.TestCase):
+
+    def testSimple(self):
+        self.assertEqual(a2x(range(10)), '00 01 02 03 04 05 06 07 08 09')
+
+    def testNotShorten(self):
+        self.assertEqual(a2x([0] * 5), '00 00 00 00 00')
+
+    def testDelim(self):
+        self.assertEqual(a2x(range(190, 196), '|'), 'BE|BF|C0|C1|C2|C3')
+
+
+class testx2a(unittest.TestCase):
+
+    def testSimple(self):
+        self.assertEqual(x2a('2'), [2])
+        self.assertEqual(x2a('02'), [2])
+        self.assertEqual(x2a('2 3'), [2, 3])
+
+class testa2s(unittest.TestCase):
+
+    def testSimple(self):
+        self.assertEqual(a2s(range(ord('a'), ord('d') + 1)), 'abcd')
+
+    def testWithNUL(self):
+        self.assertEqual(
+            a2s(list(range(ord('a'), ord('d')+1)) + [0]*3 + list(range(ord('e'), ord('i')+1))),
+            'abcd')
+
+    def testWithNULNotPrint(self):
+        self.assertEqual(
+            a2s(list(range(ord('a'), ord('d')+1)) + [0]*3 + list(range(ord('e'), ord('i')+1)), False),
+            'abcd\0\0\0efghi')
+
+
+class testa2lsbi(unittest.TestCase):
+
+    def test0(self):
+        self.assertEqual(a2lsbi([0]), 0)
+        self.assertEqual(a2lsbi([0]*3), 0)
+        self.assertEqual(a2lsbi([0]*10), 0)
+
+    def test1byte(self):
+        self.assertEqual(a2lsbi([8]), 8)
+        self.assertEqual(a2lsbi([0xff]), 0xff)
+        self.assertEqual(a2lsbi([0x80]), 0x80)
+
+    def test2bytes(self):
+        self.assertEqual(a2lsbi([1, 0]), 1)
+        self.assertEqual(a2lsbi([0xff, 0]), 0xff)
+        self.assertEqual(a2lsbi([0x80, 0]), 0x80)
+        self.assertEqual(a2lsbi([0, 1]), 0x100)
+        self.assertEqual(a2lsbi([0, 0xff]), 0xff00)
+        self.assertEqual(a2lsbi([0, 0x80]), 0x8000)
+
+
+class testa2msbi(unittest.TestCase):
+
+    def test0(self):
+        self.assertEqual(a2msbi([0]), 0)
+        self.assertEqual(a2msbi([0]*3), 0)
+        self.assertEqual(a2msbi([0]*10), 0)
+
+    def test1byte(self):
+        self.assertEqual(a2msbi([8]), 8)
+        self.assertEqual(a2msbi([0xff]), 0xff)
+        self.assertEqual(a2msbi([0x80]), 0x80)
+
+    def test2bytes(self):
+        self.assertEqual(a2msbi([1, 0]), 0x100)
+        self.assertEqual(a2msbi([0xff, 0]), 0xff00)
+        self.assertEqual(a2msbi([0x80, 0]), 0x8000)
+        self.assertEqual(a2msbi([0, 1]), 0x1)
+        self.assertEqual(a2msbi([0, 0xff]), 0xff)
+        self.assertEqual(a2msbi([0, 0x80]), 0x80)
+
+
+class testi2lsba(unittest.TestCase):
+
+    def test0(self):
+        self.assertEqual(i2lsba(0, 1), [0])
+        self.assertEqual(i2lsba(0, 3), [0]*3)
+        self.assertEqual(i2lsba(0, 5), [0]*5)
+
+    def test1byte(self):
+        self.assertEqual(i2lsba(1, 1), [1])
+        self.assertEqual(i2lsba(0xff, 1), [0xff])
+        self.assertEqual(i2lsba(0x80, 1), [0x80])
+
+    def test2bytes(self):
+        self.assertEqual(i2lsba(1, 2), [1, 0])
+        self.assertEqual(i2lsba(0xff, 2), [0xff, 0])
+        self.assertEqual(i2lsba(0x80, 2), [0x80, 0])
+        self.assertEqual(i2lsba(0x100, 2), [0, 1])
+        self.assertEqual(i2lsba(0xff00, 2), [0, 0xff])
+        self.assertEqual(i2lsba(0x8000, 2), [0, 0x80])
+
+
+class tests2a(unittest.TestCase):
+
+    def testSimple(self):
+        self.assertEqual(s2a('abcd'), list(range(ord('a'), ord('d')+1)))
+
+    def testWithNUL(self):
+        self.assertEqual(s2a('abcd\0\0\0efghi'),
+                         list(range(ord('a'), ord('d')+1)) +
+                        [0] * 3 + list(range(ord('e'), ord('i') + 1)))
diff --git a/tests/testYAMLParser.py b/tests/testYAMLParser.py
new file mode 100644
index 0000000..28d3a1a
--- /dev/null
+++ b/tests/testYAMLParser.py
@@ -0,0 +1,116 @@
+import unittest
+
+from galileo import parser
+
+class testUtilities(unittest.TestCase):
+
+    def testStripCommentEmpty(self):
+        self.assertEqual(parser._stripcomment(""), "")
+    def testStripCommentOnlyComment(self):
+        self.assertEqual(parser._stripcomment("# abcd"), "")
+    def testStripCommentSmallLine(self):
+        self.assertEqual(parser._stripcomment("ab # cd"), "ab")
+    def testStripCommentDoubleComment(self):
+        self.assertEqual(parser._stripcomment("ab # cd # ef"), "ab")
+
+    def testdedent1(self):
+        self.assertEqual(parser._dedent("""\
+a:
+  - a
+  - b
+""".split('\n'), 1), ['  - a', '  - b'])
+    def testdedent2(self):
+        self.assertEqual(parser._dedent("""\
+-
+  a:
+    b
+  c:
+    5
+""".split('\n'), 1), ['  a:', '    b', '  c:', '    5'])
+
+class testload(unittest.TestCase):
+
+    def testEmpty(self):
+        self.assertEqual(parser.loads(""), None)
+
+    def testSimpleComment(self):
+        self.assertEqual(parser.loads("""\
+# This is a comment
+"""), None)
+
+    def testOneKey(self):
+        self.assertEqual(parser.loads("""\
+test:
+"""), {"test":None})
+
+    def testOneKeyWithComment(self):
+        self.assertEqual(parser.loads("""\
+test: # This is the test Key
+"""), {"test": None})
+
+    def testMultiLines(self):
+        self.assertEqual(parser.loads("\n"*5 + "test: # This is the test Key" + "\n" * 8), {"test": None})
+
+    def testMultipleKeys(self):
+        self.assertEqual(parser.loads("""
+test:
+test_2:
+test-3:
+"""), {"test": None, 'test_2': None, 'test-3': None})
+
+    def testOnlyOneValue(self):
+        self.assertEqual(parser.loads('5'), 5)
+        self.assertEqual(parser.loads('a'), 'a')
+        self.assertEqual(parser.loads('true'), True)
+
+    def testOneArray(self):
+        self.assertEqual(parser.loads('- a\n- b'), ['a', 'b'])
+
+    def testIntegerValue(self):
+        self.assertEqual(parser.loads("t: 5"), {'t': 5})
+    def testSimpleStringValue(self):
+        self.assertEqual(parser.loads('t: abcd'), {'t': 'abcd'})
+    def testStringValue(self):
+        self.assertEqual(parser.loads("t: '5'"), {'t': '5'})
+    def testOtherStringValue(self):
+        self.assertEqual(parser.loads('t: "5"'), {'t': '5'})
+    def testBoolValue(self):
+        self.assertEqual(parser.loads("t: false"), {'t': False})
+    def testInlineArrayValue(self):
+        self.assertEqual(parser.loads("t: [4, 6]"), {'t': [4, 6]})
+    def testArrayValue(self):
+        self.assertEqual(parser.loads("""
+test:
+  - a
+  - 5
+"""), {'test': ['a', 5]})
+
+    def testDoubleDict(self):
+        self.assertEqual(parser.loads("""\
+a:
+  b: c
+"""), {'a': {'b': 'c'}})
+    def testDoubleDict2(self):
+        self.assertEqual(parser.loads("""\
+a:
+  b:
+    c
+"""), {'a': {'b': 'c'}})
+
+    def testMultiArray(self):
+        self.assertEqual(parser.loads("""\
+-
+  -
+    a:
+      b
+    c:
+      5
+  -
+    a:
+      8
+"""), [[{'a':'b', 'c': 5}, {'a': 8}]])
+
+    def testArrayOfDict(self):
+        self.assertEqual(parser.loads("""\
+- a: b
+"""), [{'a':'b'}])
diff --git a/trace.txt b/trace.txt
new file mode 100644
index 0000000..b083ccd
--- /dev/null
+++ b/trace.txt
@@ -0,0 +1,81 @@
+--> 02 - 1
+<-- CancelDiscovery
+<-- TerminateLink
+<-- ...
+--> 01 - 2
+<-- 08 ( 01 01 6F 7B AD 29 6A BC 74 09 00 20 00 00 FF E7 03 00 01) - 21
+--> 04 ( BA 56 89 A6 FA BF A2 BD 01 46 7D 6E 00 00 AB AD 00 FB 01 FB 02 FB A0 0F) - 26
+<-- StartDiscovery
+<-- 03 ( E5 14 53 33 EE FF 01 BC 02 05 04 03 2C 31 F6 D8 58 ) - 19
+--> 05 - 2
+<-- 02 ( 01 ) - 3
+<-- CancelDiscovery
+--> 06 ( E5 14 53 33 EE FF 01 D8 58 ) - 11
+<-- EstablishLink
+<-- 04 ( 00 ) - 3
+<-- GAP_LINK_ESTABLISHED_EVENT
+<-- 07 - 2
+--> 08 ( 01 ) - 3
+<== [ C0 0B ] - 2
+==> [ C0 0A 0A 00 06 00 06 00 00 00 C8 00 ] - 12
+<-- 06 ( 06 00 00 00 C8 00 ) - 8
+<== [ C0 14 0C 01 00 00 E5 14 53 33 EE FF ] - 12
+Megadump
+==> [ C0 10 0D ] - 3
+<== [ C0 41 0D ] - 3
+<== [ 26 02 00 00 00 00 00 00 00 00 DF 29 F5 4B 29 05 06 2E 06 2E ] - 20
+<== [ 00 00 91 7E 75 03 00 00 00 00 14 14 FD 0F 14 64 00 00 00 00 ] - 20
+<== [ EB 02 A8 03 81 2A 52 09 1A 1F 00 00 00 00 00 00 00 FB CB 00 ] - 20
+<== [ 42 45 4E 20 20 20 20 20 20 20 43 48 45 45 52 53 20 20 20 20 ] - 20
+<== [ 48 45 4C 4C 4F 20 20 20 20 20 47 4F 20 20 20 20 20 20 20 20 ] - 20
+<== [ 1F 00 00 00 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ] - 20
+<== [ 00 00 01 00 C0 DB DC DD D2 25 92 52 08 01 05 92 52 00 D2 25 ] - 20
+<== [ 00 00 00 D2 25 92 52 0B 00 05 00 00 00 00 00 00 00 00 D2 25 ] - 20
+<== [ 92 52 0B 01 05 00 00 00 03 00 00 00 00 D2 25 92 52 0B 02 05 ] - 20
+<== [ 00 00 00 00 00 00 00 00 D2 25 92 52 0B 03 05 00 00 00 00 00 ] - 20
+<== [ 00 00 00 C0 C0 DB DC DD 52 92 26 04 81 1C 1B 01 81 0A 00 02 ] - 20
+<== [ 81 18 00 02 81 0A 00 03 81 0A 00 03 81 1E 15 07 81 0A 00 03 ] - 20
+<== [ 81 0A 00 03 81 0A 00 02 81 0A 00 02 81 0A 00 02 52 92 28 D4 ] - 20
+<== [ 81 1A 00 05 81 0A 00 05 81 0A 00 02 81 0A 00 02 81 0A 00 02 ] - 20
+<== [ 81 0A 00 06 81 1C 18 05 52 92 2A B4 81 20 1F 06 81 1A 0D 06 ] - 20
+<== [ 81 0A 00 03 81 0A 00 07 81 0A 00 03 C0 C0 DB DC DD C0 C0 DB ] - 20
+<== [ DC DD C1 2B 92 52 D8 34 84 14 00 00 2C DD 3B 00 1E 00 C0 00 ] - 20
+<== [ 00 00 00 C0 DB DC DD C0 40 1F 00 00 00 00 00 00 43 01 00 ] - 19
+<== [ C0 42 0D 0C 37 67 01 00 00 ] - 9
+Done
+<?xml version='1.0' encoding='UTF-8'?>
+<galileo-client version="2.0"><client-info><client-id>6de4df71-17f9-43ea-9854-67f842021e05</client-id><client-version>0.1</client-version><client-mode>sync</client-mode><dongle-version major="1" minor="1" /></client-info><tracker tracker-id="E5145333EEFF"><data>JgIAAAAAAAAAAN8p9UspBQYuBi4AAJF+dQMAAAAAFBT9DxRkAAAAAOsCqAOBKlIJGh8AAAAAAAAA+8sAQkVOICAgICAgIENIRUVSUyAgICBIRUxMTyAgICAgR08gICAgICAgIB8AAAAAIAAAAAAAAAAAAAAAAAAAAAABAMDb3N3SJZJSCAEFklIA0iUAAADSJZJSCwAFAAAAAAAAAADSJZJSCwEFAAAAAwAAAA [...]
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?><galileo-server version="2.0"><server-version>
+
+</server-version><tracker tracker-id="E5145333EEFF" type="megadumpresponse"><data>JgIAAAAAAQAAAAAAAADrAqgDgSpSCRofAAAAAAAAAPvLAEJFTiAgICAgICBIT1dEWSAgICAgU1RFUEdFRUsgIFJPQ0sgT04gICAfAAAAACAAAAAAAAAAAAAAAAAAAAAAAQDDK5JSAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAUAAAAFwyuSUgLSJZJSAaQrklIEwSuSUgfDK5JSqgMAAAAAAACPAAA=</data></tracker></galileo-server>
+==> [ C0 24 04 A4 00 00 00 00 00 ] - 9
+<== [ C0 12 04 00 00 ] - 5
+==> [ 26 02 00 00 00 00 01 00 00 00 00 00 00 00 EB 02 A8 03 81 2A ] - 20
+<== [ C0 13 14 00 00 ] - 5
+==> [ 52 09 1A 1F 00 00 00 00 00 00 00 FB CB 00 42 45 4E 20 20 20 ] - 20
+<== [ C0 13 24 00 00 ] - 5
+==> [ 20 20 20 20 48 4F 57 44 59 20 20 20 20 20 53 54 45 50 47 45 ] - 20
+<== [ C0 13 34 00 00 ] - 5
+==> [ 45 4B 20 20 52 4F 43 4B 20 4F 4E 20 20 20 1F 00 00 00 00 20 ] - 20
+<== [ C0 13 44 00 00 ] - 5
+==> [ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 C3 2B ] - 20
+<== [ C0 13 54 00 00 ] - 5
+==> [ 92 52 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 ] - 20
+<== [ C0 13 64 00 00 ] - 5
+==> [ 00 00 00 00 05 00 00 00 05 C3 2B 92 52 02 D2 25 92 52 01 A4 ] - 20
+<== [ C0 13 74 00 00 ] - 5
+==> [ 2B 92 52 04 C1 2B 92 52 07 C3 2B 92 52 AA 03 00 00 00 00 00 ] - 20
+<== [ C0 13 84 00 00 ] - 5
+==> [ 00 8F 00 00 ] - 4
+<== [ C0 13 94 00 00 ] - 5
+==> [ C0 02 ] - 2
+<== [ C0 02 ] - 2
+==> [ C0 01 ] - 2
+<== [ C0 01 ] - 2
+--> 08 ( 00 ) - 3
+<== [ C0 0B ] - 2
+--> 07 - 2
+<-- TerminateLink
+<-- 05 ( 16 ) - 3
+<-- GAP_LINK_TERMINATED_EVENT
+<-- 22

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/galileo.git



More information about the debian-med-commit mailing list