summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorP. J. McDermott <pj@pehjota.net>2023-10-01 16:32:49 (EDT)
committer P. J. McDermott <pj@pehjota.net>2023-10-01 16:32:49 (EDT)
commitbab4f35fc5c13341fb9cc96a7cb863c5cd5c3f53 (patch)
tree75d9463a02eb5984e1f33aadba830f490f2b103c
downloadsiglo-bab4f35fc5c13341fb9cc96a7cb863c5cd5c3f53.zip
siglo-bab4f35fc5c13341fb9cc96a7cb863c5cd5c3f53.tar.gz
siglo-bab4f35fc5c13341fb9cc96a7cb863c5cd5c3f53.tar.bz2
New upstream version 0.9.9upstream/0.9.9upstream/latest
-rw-r--r--.gitignore135
-rw-r--r--.vscode/settings.json3
-rw-r--r--Dockerfile27
-rw-r--r--LICENSE373
-rw-r--r--README.md110
-rwxr-xr-xbuild-aux/meson/postinstall.py21
-rw-r--r--com.github.theironrobin.siglo.json41
-rw-r--r--data/com.github.theironrobin.siglo.appdata.xml.in50
-rw-r--r--data/com.github.theironrobin.siglo.desktop.in9
-rw-r--r--data/com.github.theironrobin.siglo.gschema.xml5
-rw-r--r--data/icons/com.github.theironrobin.siglo.svg153
-rw-r--r--data/meson.build48
-rw-r--r--data/siglo.service6
-rw-r--r--meson.build15
-rw-r--r--po/LINGUAS1
-rw-r--r--po/POTFILES7
-rw-r--r--po/meson.build1
-rw-r--r--po/nl.po93
-rw-r--r--python3-modules.json69
-rwxr-xr-xreinstall.sh5
-rw-r--r--src/__init__.py0
-rw-r--r--src/ble_dfu.py334
-rw-r--r--src/bluetooth.py221
-rw-r--r--src/config.py55
-rw-r--r--src/daemon.py49
-rw-r--r--src/main.py51
-rw-r--r--src/meson.build41
-rw-r--r--src/ota/Fork.txt1
-rw-r--r--src/ota/LICENSE201
-rw-r--r--src/ota/unpacker.py52
-rw-r--r--src/ota/util.py70
-rw-r--r--src/quick_deploy.py65
-rw-r--r--src/siglo.gresource.xml11
-rwxr-xr-xsrc/siglo.in47
-rw-r--r--src/watch-check.svg135
-rw-r--r--src/watch-error.svg135
-rw-r--r--src/watch-icon.svg157
-rw-r--r--src/watch-progress.svg166
-rw-r--r--src/watch.svg117
-rw-r--r--src/window.py381
-rw-r--r--src/window.ui1355
41 files changed, 4816 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..080b8b0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,135 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# Flatpak
+.flatpak-builder/
+build-dir/
+repo/
+
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..de288e1
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python.formatting.provider": "black"
+} \ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..c84a821
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,27 @@
+FROM ubuntu:21.04
+
+ENV DEBIAN_FRONTEND="noninteractive" TZ=" America/Los_Angeles"
+
+# xvfb is used to mock out the display for testing and is not required for real builds
+RUN apt update && apt install -y \
+ libgtk-3-dev python3-pip meson python3-dbus gtk-update-icon-cache desktop-file-utils gettext appstream-util libglib2.0-dev && \
+ apt install -y xvfb && \
+ rm -rf /var/lib/apt/lists/* && apt clean
+
+RUN pip3 install gatt requests black
+
+COPY . /siglo
+
+WORKDIR /siglo
+
+RUN pwd && ls && mkdir -p ./build && \
+ meson --reconfigure ./build/ && \
+ cd ./build && ninja install
+
+CMD ["/bin/bash"]
+
+# Once the container is running, you should have all the dependencies you need
+# Start system dbus, then kickoff the app. For more details, you can see GTK's setup:
+# https://gitlab.gnome.org/GNOME/gtk/-/blob/fb052c8d2546706b49e5adb87bc88ad600f31752/.gitlab-ci.yml#L122
+#
+# /etc/init.d/dbus start && dbus-run-session xvfb-run -a -s "-screen 0 1024x768x24" siglo
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a612ad9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..94dc20f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,110 @@
+# siglo
+
+GTK app to sync InfiniTime watch with PinePhone
+
+'siglo' means century in Spanish
+
+## Requirements
+Gtk >= 3.30
+
+## Download and Install
+[Download the latest stable version from Flathub](https://flathub.org/apps/details/com.github.theironrobin.siglo) (Warning: SMS Notifications currently broken in flatpak https://github.com/theironrobin/siglo/issues/80).
+
+### Alpine
+Works for Alpine and other Alpine-based distribution, such as [postmarketOS](https://postmarketos.org/).
+
+```sh
+sudo apk add gettext glib-dev meson py3-dbus py3-pip python3
+pip3 install gatt
+```
+
+### Arch Linux
+
+```sh
+sudo pacman -S --needed meson python-pip base-devel bluez bluez-utils dbus-python python-gobject
+pip3 install gatt
+```
+
+### Fedora
+
+```
+sudo dnf install meson glib2-devel
+pip3 install gatt
+```
+
+### Ubuntu
+
+```sh
+sudo apt install libgtk-3-dev python3-pip meson python3-dbus gtk-update-icon-cache desktop-file-utils gettext appstream-util libglib2.0-dev
+pip3 install gatt requests black
+```
+
+## Build/Install
+
+```
+git clone https://github.com/theironrobin/siglo.git
+cd siglo
+mkdir build
+meson build/
+cd build
+sudo ninja install
+```
+
+### Mocked Testing with Docker
+
+While you won't get bluetooth connectivity, you can get some high-level vetting in a container, which
+will open the way forward to better CI testing on GitHub.
+
+The [`Dockerfile`](Dockerfile) contains all required dependencies, in addition to
+[`xvfb`](https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml) which allows us to make sure
+the app can execute.
+
+```sh
+sudo docker build . --tag siglo; and sudo docker run --name siglo --volume (pwd):/siglo --rm -it siglo:latest
+```
+
+Once the container is running, you can launch the app:
+
+```sh
+/etc/init.d/dbus start && dbus-run-session xvfb-run -a -s "-screen 0 1024x768x24" siglo
+```
+
+## Building and installing Flatpak app
+
+### Building and installing on target architecture
+
+```sh
+flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
+flatpak install --user flathub org.gnome.Sdk//42 org.gnome.Platform//42
+
+flatpak-builder --user --install --repo=repo --force-clean build-dir/ com.github.theironrobin.siglo.json
+```
+
+### Cross-compiling for PinePhone
+
+Example cross-compiling for PinePhone on an `x86_64` Fedora machine:
+
+```sh
+sudo dnf install qemu-system-arm qemu-user-static
+flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
+flatpak install --user flathub org.gnome.Sdk/aarch64/42 org.gnome.Platform/aarch64/42
+
+flatpak-builder --arch=aarch64 --repo=repo --force-clean build-dir com.github.theironrobin.siglo.json
+flatpak build-bundle --arch=aarch64 ./repo/ siglo.flatpak com.github.theironrobin.siglo
+```
+
+Transfer the `siglo.flatpak` file on the PinePhone and install it with the following command:
+
+```sh
+sudo flatpak install ./siglo.flatpak
+```
+
+##
+
+If this project helped you, you can buy me a cup of coffee :)
+<br/><br/>
+[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/theironrobin)
+<br/><br/>
+DOGE address: DLDNfkXoJeueb2GRx4scnmRc12SX1H22VW
+
+Icons by svgrepo.com
diff --git a/build-aux/meson/postinstall.py b/build-aux/meson/postinstall.py
new file mode 100755
index 0000000..6a3ea97
--- /dev/null
+++ b/build-aux/meson/postinstall.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+
+from os import environ, path
+from subprocess import call
+
+prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
+datadir = path.join(prefix, 'share')
+destdir = environ.get('DESTDIR', '')
+
+# Package managers set this so we don't need to run
+if not destdir:
+ print('Updating icon cache...')
+ call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
+
+ print('Updating desktop database...')
+ call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
+
+ print('Compiling GSettings schemas...')
+ call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')])
+
+
diff --git a/com.github.theironrobin.siglo.json b/com.github.theironrobin.siglo.json
new file mode 100644
index 0000000..5c04c79
--- /dev/null
+++ b/com.github.theironrobin.siglo.json
@@ -0,0 +1,41 @@
+{
+ "app-id" : "com.github.theironrobin.siglo",
+ "runtime" : "org.gnome.Platform",
+ "runtime-version" : "42",
+ "sdk" : "org.gnome.Sdk",
+ "command" : "siglo",
+ "finish-args" : [
+ "--allow=bluetooth",
+ "--share=network",
+ "--share=ipc",
+ "--socket=fallback-x11",
+ "--system-talk-name=org.bluez",
+ "--socket=wayland"
+ ],
+ "cleanup" : [
+ "/include",
+ "/lib/pkgconfig",
+ "/man",
+ "/share/doc",
+ "/share/gtk-doc",
+ "/share/man",
+ "/share/pkgconfig",
+ "*.la",
+ "*.a"
+ ],
+ "modules" : [
+ "python3-modules.json",
+ {
+ "name" : "siglo",
+ "builddir" : true,
+ "buildsystem" : "meson",
+ "sources" : [
+ {
+ "type" : "git",
+ "url" : "https://github.com/theironrobin/siglo",
+ "tag" : "v0.9.9"
+ }
+ ]
+ }
+ ]
+}
diff --git a/data/com.github.theironrobin.siglo.appdata.xml.in b/data/com.github.theironrobin.siglo.appdata.xml.in
new file mode 100644
index 0000000..e24c5fc
--- /dev/null
+++ b/data/com.github.theironrobin.siglo.appdata.xml.in
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="desktop">
+ <id>com.github.theironrobin.siglo.desktop</id>
+ <name>Siglo</name>
+ <summary>Sync PineTime with your PinePhone</summary>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>MPL-2.0</project_license>
+ <developer_name>Alex R</developer_name>
+ <description>
+ <p>
+ Features:
+ <ul>
+ <li>Scan for one or more InfiniTime devices</li>
+ <li>Sync the time</li>
+ <li>Update your firmware—either manually or directly from the InfiniTime release page</li>
+ <li>Optionally Keep Paired for Chatty notifications (currently broken in flatpak)</li>
+ </ul>
+ </p>
+ <p>
+ Supports all Phosh-based PinePhone distros.
+ </p>
+ </description>
+ <categories>
+ <category>Utility</category>
+ </categories>
+ <url type="homepage">https://github.com/theironrobin/siglo</url>
+ <screenshots>
+ <screenshot>
+ <image type="source">https://ironrobin.net/images/siglo-screenshot-1.png</image>
+ </screenshot>
+ <screenshot>
+ <image type="source">https://ironrobin.net/images/siglo-screenshot-2.png</image>
+ </screenshot>
+ <screenshot>
+ <image type="source">https://ironrobin.net/images/siglo-screenshot-3.png</image>
+ </screenshot>
+ </screenshots>
+ <releases>
+ <release version="0.9.9" date="2022-07-04"/>
+ <release version="0.9.6" date="2021-10-07"/>
+ <release version="0.9.5" date="2021-10-07"/>
+ <release version="0.9.4" date="2021-09-09"/>
+ <release version="0.8.12" date="2021-08-06"/>
+ </releases>
+ <content_rating type="oars-1.1" />
+ <custom>
+ <value key="Purism::form_factor">workstation</value>
+ <value key="Purism::form_factor">mobile</value>
+ </custom>
+</component>
diff --git a/data/com.github.theironrobin.siglo.desktop.in b/data/com.github.theironrobin.siglo.desktop.in
new file mode 100644
index 0000000..4e5cac1
--- /dev/null
+++ b/data/com.github.theironrobin.siglo.desktop.in
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Name=Siglo
+Exec=siglo
+Terminal=false
+Type=Application
+Categories=GTK;
+StartupNotify=true
+Icon=com.github.theironrobin.siglo
+X-Purism-FormFactor=Workstation;Mobile;
diff --git a/data/com.github.theironrobin.siglo.gschema.xml b/data/com.github.theironrobin.siglo.gschema.xml
new file mode 100644
index 0000000..76f3413
--- /dev/null
+++ b/data/com.github.theironrobin.siglo.gschema.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist gettext-domain="siglo">
+ <schema id="com.github.theironrobin.siglo" path="/com/github/theironrobin/siglo/">
+ </schema>
+</schemalist>
diff --git a/data/icons/com.github.theironrobin.siglo.svg b/data/icons/com.github.theironrobin.siglo.svg
new file mode 100644
index 0000000..72415aa
--- /dev/null
+++ b/data/icons/com.github.theironrobin.siglo.svg
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="Layer_1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 485 485"
+ style="enable-background:new 0 0 485 485;"
+ xml:space="preserve"
+ width="512"
+ height="512"
+ sodipodi:docname="com.github.theironrobin.siglo.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><metadata
+ id="metadata26"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs24"><linearGradient
+ inkscape:collect="always"
+ id="linearGradient15354"><stop
+ style="stop-color:#31c0d8;stop-opacity:1;"
+ offset="0"
+ id="stop15350" /><stop
+ style="stop-color:#81e2f2;stop-opacity:1"
+ offset="1"
+ id="stop15352" /></linearGradient><linearGradient
+ y2="812.88245"
+ x2="1662.9901"
+ y1="806.29718"
+ x1="1660.8571"
+ gradientTransform="matrix(1.8293937,-0.81599442,0.81599442,1.8293937,-3645.9624,100.02949)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient1001"
+ xlink:href="#linearGradient950" /><linearGradient
+ id="linearGradient950"><stop
+ style="stop-color:#9a9996;stop-opacity:1"
+ offset="0"
+ id="stop946" /><stop
+ style="stop-color:#77767b;stop-opacity:1"
+ offset="1"
+ id="stop948" /></linearGradient>
+
+
+
+
+
+
+
+
+
+
+
+
+ <mask
+ maskUnits="userSpaceOnUse"
+ id="mask15345"><circle
+ style="display:inline;opacity:1;fill:#d93d3d;fill-opacity:1;stroke:none;stroke-width:0.92832;stroke-miterlimit:4;stroke-dasharray:none"
+ id="circle15347"
+ cx="243.66702"
+ cy="243.28474"
+ r="185.33173"
+ inkscape:label="shadow-mask" /></mask><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient15354"
+ id="linearGradient15356"
+ x1="176.356"
+ y1="77.628166"
+ x2="312.85342"
+ y2="409.47876"
+ gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1016"
+ id="namedview22"
+ showgrid="false"
+ inkscape:zoom="0.44841879"
+ inkscape:cx="479.65355"
+ inkscape:cy="594.30735"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="Layer_1" />
+<circle
+ style="fill:#deddda;fill-opacity:1;stroke:none;stroke-width:12.4566;stroke-miterlimit:4;stroke-dasharray:none"
+ id="path15219-3"
+ cx="243.17287"
+ cy="253.34464"
+ r="224.05338" /><circle
+ style="fill:#f6f5f4;fill-opacity:1;stroke:none;stroke-width:12.4566;stroke-miterlimit:4;stroke-dasharray:none"
+ id="path15219"
+ cx="242.35684"
+ cy="243.4926"
+ r="224.05338" /><path
+ style="fill:url(#linearGradient15356);stroke-width:0.928251;fill-opacity:1"
+ d="M 377.28204,121.64173 C 344.05065,84.697339 295.96725,61.491066 242.5,61.491066 142.52737,61.491066 61.491066,142.52737 61.491066,242.5 c 0,99.97263 81.036304,181.00893 181.008934,181.00893 99.97263,0 181.00893,-81.0363 181.00893,-181.00893 0,-46.41255 -17.45112,-88.83362 -46.22689,-120.85827 z"
+ id="path2" /><path
+ d="M 382.45796,122.66921 C 346.75465,82.977203 295.74262,60.212773 242.5,60.212773 c -103.64757,0 -187.970813,84.323247 -187.970813,187.970817 0,103.64757 84.323243,187.97081 187.970813,187.97081 103.64757,0 187.97081,-84.32324 187.97081,-187.97081 0,-46.48217 -17.05011,-91.05585 -48.01285,-125.51438 z M 249.46188,422.07934 v -39.29936 h -13.92376 v 39.29936 c -46.24639,-1.82866 -87.90908,-21.78327 -118.06517,-52.94651 l 27.84196,-27.84196 v -7.57812 l -9.84503,-2.26691 -27.29522,27.29522 C 84.748396,330.32916 70.156291,294.38449 68.604256,255.14547 H 107.90361 V 241.22171 H 68.604256 c 1.552035,-39.23903 16.14414,-75.18369 39.569474,-103.59466 l 27.29522,27.29521 9.84503,-9.84503 v -7.57812 L 117.47202,127.23528 C 147.62904,96.07111 189.29173,76.117423 235.53812,74.287843 V 113.5872 h 13.92376 V 74.291563 c 44.57647,1.79616 86.9734,20.770539 118.12922,52.878737 l -27.90694,20.32881 v 7.57812 l 9.84596,9.84503 27.36669,-27.36669 c 24.14938,29.27611 37.99424,65.60785 39.50358,103.66614 h -39.304 v 13.92376 h 39.29936 c -1.55204,39.23902 -16.14415,75.18276 -39.56949,103.59466 l -27.29521,-27.29522 -9.84596,2.26691 v 7.57812 l 27.84288,27.84289 c -30.15794,31.16324 -71.8197,51.11692 -118.06609,52.94651 z"
+ id="path10-5"
+ style="opacity:0.512782;fill:#02090a;fill-opacity:1;stroke-width:0.928251"
+ inkscape:label="face-shadow"
+ mask="url(#mask15345)"
+ transform="translate(0,1.8945311)"
+ sodipodi:nodetypes="csssscccccccccccccccccccccccccccccccccccccc" /><path
+ d="M 382.45796,116.98562 C 346.75465,77.29361 295.74262,54.529184 242.5,54.529184 138.85243,54.529184 54.529184,138.85243 54.529184,242.5 c 0,103.64757 84.323246,187.97081 187.970816,187.97081 103.64757,0 187.97081,-84.32324 187.97081,-187.97081 0,-46.48217 -17.05011,-91.05585 -48.01285,-125.51438 z M 249.46188,416.39575 v -39.29936 h -13.92376 v 39.29936 c -46.24639,-1.82866 -87.90908,-21.78327 -118.06517,-52.94651 l 27.84196,-27.84196 -9.84503,-9.84503 -27.29522,27.29522 C 84.748393,324.64557 70.156288,288.7009 68.604253,249.46188 H 107.90361 V 235.53812 H 68.604253 c 1.552035,-39.23903 16.14414,-75.18369 39.569477,-103.59466 l 27.29522,27.29521 9.84503,-9.84503 -27.84196,-27.84195 c 30.15702,-31.164173 71.81971,-51.117855 118.0661,-52.947437 v 39.299357 h 13.92376 V 68.607966 c 44.57647,1.796165 86.9734,20.770543 118.12922,52.878744 l -27.90694,27.90693 9.84596,9.84503 27.36669,-27.36669 c 24.14938,29.27611 37.99424,65.60785 39.50358,103.66614 h -39.304 v 13.92376 h 39.29936 c -1.55204,39.23902 -16.14415,75.18276 -39.56949,103.59466 l -27.29521,-27.29522 -9.84596,9.84503 27.84288,27.84289 c -30.15794,31.16324 -71.8197,51.11692 -118.06609,52.94651 z"
+ id="path10"
+ style="opacity:1;stroke-width:0.928251;fill:#11505b;fill-opacity:1" /><path
+ d="m 311.32795,157.67912 h 19.35589 v -13.92376 h -48.73318 v 48.73317 h 13.92377 v -28.60219 c 28.7405,18.23363 46.41254,50.09677 46.41254,84.29725 0,55.023 -44.7649,99.78697 -99.78697,99.78697 -55.023,0 -99.78698,-44.76397 -99.78698,-99.78697 0,-55.023 42.99107,-99.96107 98.01374,-99.77041 4.65235,0.0161 7.35466,-3.84478 7.3715,-7.50453 0.072,-15.65064 -2.27131,-6.41173 -7.3715,-6.41923 -62.7005,-0.0921 -111.9375,50.9936 -111.9375,113.69417 0,62.70057 51.01017,113.71074 113.71074,113.71074 62.70057,0 113.71074,-51.01017 113.71074,-113.71074 0,-35.69589 -16.88303,-69.16584 -44.88279,-90.50447 z"
+ id="path12-6"
+ style="opacity:0.25;stroke-width:0.92832;stroke-miterlimit:4;stroke-dasharray:none"
+ sodipodi:nodetypes="cccccccsssssssssc" /><path
+ d="m 311.32795,151.99553 h 19.35589 v -13.92376 h -48.73318 v 48.73317 h 13.92377 v -28.60219 c 28.7405,18.23363 46.41254,50.09677 46.41254,84.29725 0,55.023 -44.7649,99.78697 -99.78697,99.78697 -55.023,0 -99.78698,-44.76397 -99.78698,-99.78697 0,-55.023 42.99107,-99.96115 98.01374,-99.77041 8.83092,0.0306 10.77866,-13.90792 0,-13.92376 -62.7005,-0.0922 -111.9375,50.9936 -111.9375,113.69417 0,62.70057 51.01017,113.71074 113.71074,113.71074 62.70057,0 113.71074,-51.01017 113.71074,-113.71074 0,-35.69589 -16.88303,-69.16584 -44.88279,-90.50447 z"
+ id="path12"
+ style="stroke-width:0.92832;stroke-miterlimit:4;stroke-dasharray:none;fill:#11505b;fill-opacity:1"
+ sodipodi:nodetypes="cccccccssssssssc" /><path
+ id="polygon14-7"
+ style="opacity:0.25"
+ transform="matrix(0.92825094,0,0,0.92825094,17.399146,23.082739)"
+ d="m 235,245.606 52.196,52.197 10.608,-10.606 0,-6.12291 L 250,239.394 V 157.5 h -15 z"
+ sodipodi:nodetypes="cccccccc" /><polygon
+ points="287.196,297.803 297.804,287.197 250,239.394 250,157.5 235,157.5 235,245.606 "
+ id="polygon14"
+ transform="matrix(0.92825094,0,0,0.92825094,17.399146,17.399146)"
+ style="fill:#11505b;fill-opacity:1" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+</svg>
diff --git a/data/meson.build b/data/meson.build
new file mode 100644
index 0000000..25d0c11
--- /dev/null
+++ b/data/meson.build
@@ -0,0 +1,48 @@
+desktop_file = i18n.merge_file(
+ input: 'com.github.theironrobin.siglo.desktop.in',
+ output: 'com.github.theironrobin.siglo.desktop',
+ type: 'desktop',
+ po_dir: '../po',
+ install: true,
+ install_dir: join_paths(get_option('datadir'), 'applications')
+)
+
+desktop_utils = find_program('desktop-file-validate', required: false)
+if desktop_utils.found()
+ test('Validate desktop file', desktop_utils,
+ args: [desktop_file]
+ )
+endif
+
+install_data(join_paths('icons', 'com.github.theironrobin.siglo.svg'),
+ install_dir: join_paths(get_option('datadir'), 'icons/hicolor/scalable/apps')
+)
+
+install_data('siglo.service', install_dir: '/etc/systemd/user/')
+
+appstream_file = i18n.merge_file(
+ input: 'com.github.theironrobin.siglo.appdata.xml.in',
+ output: 'com.github.theironrobin.siglo.appdata.xml',
+ po_dir: '../po',
+ install: true,
+ install_dir: join_paths(get_option('datadir'), 'metainfo')
+)
+
+appstream_util = find_program('appstream-util', required: false)
+if appstream_util.found()
+ test('Validate appstream file', appstream_util,
+ args: ['validate', appstream_file]
+ )
+endif
+
+install_data('com.github.theironrobin.siglo.gschema.xml',
+ install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
+)
+
+compile_schemas = find_program('glib-compile-schemas', required: false)
+if compile_schemas.found()
+ test('Validate schema file', compile_schemas,
+ args: ['--strict', '--dry-run', meson.current_source_dir()]
+ )
+endif
+
diff --git a/data/siglo.service b/data/siglo.service
new file mode 100644
index 0000000..fde41b6
--- /dev/null
+++ b/data/siglo.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=siglo service
+[Service]
+ExecStart=siglo --start
+ExecStop=siglo --stop
+Environment=PYTHONUNBUFFERED=1
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..5f7b862
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,15 @@
+project('siglo',
+ version: '0.1.0',
+ meson_version: '>= 0.55.0',
+ default_options: [ 'warning_level=2',
+ ],
+)
+
+i18n = import('i18n')
+
+
+subdir('data')
+subdir('src')
+subdir('po')
+
+meson.add_install_script('build-aux/meson/postinstall.py') \ No newline at end of file
diff --git a/po/LINGUAS b/po/LINGUAS
new file mode 100644
index 0000000..267a150
--- /dev/null
+++ b/po/LINGUAS
@@ -0,0 +1 @@
+nl
diff --git a/po/POTFILES b/po/POTFILES
new file mode 100644
index 0000000..ae36bcd
--- /dev/null
+++ b/po/POTFILES
@@ -0,0 +1,7 @@
+data/com.github.theironrobin.siglo.desktop.in
+data/com.github.theironrobin.siglo.appdata.xml.in
+data/com.github.theironrobin.siglo.gschema.xml
+src/window.ui
+src/main.py
+src/window.py
+
diff --git a/po/meson.build b/po/meson.build
new file mode 100644
index 0000000..c0ade3d
--- /dev/null
+++ b/po/meson.build
@@ -0,0 +1 @@
+i18n.gettext('siglo', preset: 'glib')
diff --git a/po/nl.po b/po/nl.po
new file mode 100644
index 0000000..0d971ea
--- /dev/null
+++ b/po/nl.po
@@ -0,0 +1,93 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the siglo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: siglo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-04-22 18:15+0200\n"
+"PO-Revision-Date: 2021-04-22 18:18+0200\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.4.2\n"
+"Last-Translator: Heimen Stoffels <vistausss@fastmail.com>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Language: nl\n"
+
+#: data/com.github.theironrobin.siglo.desktop.in:3
+msgid "Siglo"
+msgstr "Siglo"
+
+#: src/window.ui:12
+msgid "Multi-Device Mode"
+msgstr "Meerdere apparaten"
+
+#: src/window.ui:21
+msgid "Manual OTA File"
+msgstr "Eigen ota-bestand"
+
+#: src/window.ui:42
+msgid "Scanning..."
+msgstr "Bezig met zoeken…"
+
+#: src/window.ui:102
+msgid "0 / 0"
+msgstr "0 / 0"
+
+#: src/window.ui:127
+msgid "default scan pass text"
+msgstr "standaard scantekst"
+
+#: src/window.ui:158
+msgid "OTA Update:"
+msgstr "OTA-update:"
+
+#: src/window.ui:170
+msgid "OTA Update (.zip)"
+msgstr "OTA-update (.zip)"
+
+#: src/window.ui:208
+msgid "Tag: "
+msgstr "Label: "
+
+#: src/window.ui:220
+msgid "Tag"
+msgstr "Label"
+
+#: src/window.ui:245
+msgid "Asset: "
+msgstr "Waarde: "
+
+#: src/window.ui:289
+msgid "Cancel"
+msgstr "Annuleren"
+
+#: src/window.ui:306
+msgid "Flash It!"
+msgstr "Flashen!"
+
+#: src/window.ui:330
+msgid "Time Sync"
+msgstr "Tijdsynchronisatie"
+
+#: src/window.ui:364
+msgid ""
+"Make sure InfiniTime is ON but NOT connected\n"
+"\n"
+"To check, go to Settings->Bluetooth->Devices\n"
+"\n"
+"Might take a few scans."
+msgstr ""
+"Zorg dat je smartwatch met InfiniTime is opgestart, en dat het NIET is verbonden met bluetooth\n"
+"\n"
+"Controleer dit via Settings → Bluetooth → Devices\n"
+"\n"
+"Wellicht moet je een paar keer opnieuw zoeken."
+
+#: src/window.ui:386
+msgid "Rescan"
+msgstr "Opnieuw zoeken"
diff --git a/python3-modules.json b/python3-modules.json
new file mode 100644
index 0000000..6552b89
--- /dev/null
+++ b/python3-modules.json
@@ -0,0 +1,69 @@
+{
+ "name": "python3-modules",
+ "buildsystem": "simple",
+ "build-commands": [],
+ "modules": [
+ {
+ "name": "python3-gatt",
+ "buildsystem": "simple",
+ "build-commands": [
+ "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"gatt\" --no-build-isolation"
+ ],
+ "sources": [
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/96/d0/d66154053d5b47996731d80ee66f65bdf7b790258addc0b6a5f50bcc3579/gatt-0.2.7.tar.gz",
+ "sha256": "626d9de24a178b6eaff78c31b0bd29f962681da7caf18eb20363f6288d014e3a"
+ }
+ ]
+ },
+ {
+ "name": "python3-dbus-python",
+ "buildsystem": "simple",
+ "build-commands": [
+ "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"dbus-python\" --no-build-isolation"
+ ],
+ "sources": [
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/62/7e/d4fb56a1695fa65da0c8d3071855fa5408447b913c58c01933c2f81a269a/dbus-python-1.2.16.tar.gz",
+ "sha256": "11238f1d86c995d8aed2e22f04a1e3779f0d70e587caffeab4857f3c662ed5a4"
+ }
+ ]
+ },
+ {
+ "name": "python3-requests",
+ "buildsystem": "simple",
+ "build-commands": [
+ "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"requests\" --no-build-isolation"
+ ],
+ "sources": [
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/4f/5a/597ef5911cb8919efe4d86206aa8b2658616d676a7088f0825ca08bd7cb8/urllib3-1.26.6.tar.gz",
+ "sha256": "f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
+ },
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/cb/38/4c4d00ddfa48abe616d7e572e02a04273603db446975ab46bbcd36552005/idna-3.2.tar.gz",
+ "sha256": "467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
+ },
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/e7/4e/2af0238001648ded297fb54ceb425ca26faa15b341b4fac5371d3938666e/charset-normalizer-2.0.4.tar.gz",
+ "sha256": "f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
+ },
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/6d/78/f8db8d57f520a54f0b8a438319c342c61c22759d8f9a1cd2e2180b5e5ea9/certifi-2021.5.30.tar.gz",
+ "sha256": "2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"
+ },
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/e7/01/3569e0b535fb2e4a6c384bdbed00c55b9d78b5084e0fb7f4d0bf523d7670/requests-2.26.0.tar.gz",
+ "sha256": "b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
+ }
+ ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/reinstall.sh b/reinstall.sh
new file mode 100755
index 0000000..1638c8e
--- /dev/null
+++ b/reinstall.sh
@@ -0,0 +1,5 @@
+rm -rf build/
+meson build
+cd build
+sudo ninja install
+
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/__init__.py
diff --git a/src/ble_dfu.py b/src/ble_dfu.py
new file mode 100644
index 0000000..83d20d6
--- /dev/null
+++ b/src/ble_dfu.py
@@ -0,0 +1,334 @@
+from array import array
+import gatt
+import os
+from .util import *
+import math
+from struct import unpack
+
+class InfiniTimeDFU(gatt.Device):
+ # Class constants
+ UUID_DFU_SERVICE = "00001530-1212-efde-1523-785feabcd123"
+ UUID_CTRL_POINT = "00001531-1212-efde-1523-785feabcd123"
+ UUID_PACKET = "00001532-1212-efde-1523-785feabcd123"
+ UUID_VERSION = "00001534-1212-efde-1523-785feabcd123"
+
+ def __init__(self, mac_address, manager, window, firmware_path, datfile_path, verbose):
+ self.firmware_path = firmware_path
+ self.datfile_path = datfile_path
+ self.target_mac = mac_address
+ self.window = window
+ self.verbose = verbose
+ self.current_step = 0
+ self.pkt_receipt_interval = 10
+ self.pkt_payload_size = 20
+ self.size_per_receipt = self.pkt_payload_size * self.pkt_receipt_interval
+ self.done = False
+ self.packet_recipt_count = 0
+ self.total_receipt_size = 0
+ self.update_in_progress = False
+ self.caffeinator = Caffeinator()
+ self.success = False
+
+ super().__init__(mac_address, manager)
+
+ def connect(self):
+ self.successful_connection = True
+ super().connect()
+
+ def input_setup(self):
+ """Bin: read binfile into bin_array"""
+ print(
+ "preparing "
+ + os.path.split(self.firmware_path)[1]
+ + " for "
+ + self.target_mac
+ )
+
+ if self.firmware_path == None:
+ raise Exception("input invalid")
+
+ name, extent = os.path.splitext(self.firmware_path)
+
+ if extent == ".bin":
+ self.bin_array = array("B", open(self.firmware_path, "rb").read())
+
+ self.image_size = len(self.bin_array)
+ print("Binary image size: %d" % self.image_size)
+ print(
+ "Binary CRC32: %d" % crc32_unsigned(array_to_hex_string(self.bin_array))
+ )
+ return
+ raise Exception("input invalid")
+
+ def connect_succeeded(self):
+ super().connect_succeeded()
+ print("[%s] Connected" % (self.mac_address))
+
+ def connect_failed(self, error):
+ super().connect_failed(error)
+ self.successful_connection = False
+ print("[%s] Connection failed: %s" % (self.mac_address, str(error)))
+
+ def disconnect_succeeded(self):
+ super().disconnect_succeeded()
+ if not self.success:
+ self.on_failure()
+ print("[%s] Disconnected" % (self.mac_address))
+
+ def characteristic_enable_notifications_succeeded(self, characteristic):
+ if self.verbose and characteristic.uuid == self.UUID_CTRL_POINT:
+ print("Notification Enable succeeded for Control Point Characteristic")
+ self.step_one()
+
+ def characteristic_write_value_succeeded(self, characteristic):
+ if self.verbose and characteristic.uuid == self.UUID_CTRL_POINT:
+ print(
+ "Characteristic value was written successfully for Control Point Characteristic"
+ )
+ if self.verbose and characteristic.uuid == self.UUID_PACKET:
+ print(
+ "Characteristic value was written successfully for Packet Characteristic"
+ )
+ if self.current_step == 1:
+ self.step_two()
+ elif self.current_step == 3:
+ self.step_four()
+ elif self.current_step == 5:
+ self.step_six()
+ elif self.current_step == 6:
+ print("Begin DFU")
+ self.caffeinator.caffeinate()
+ self.step_seven()
+
+ def characteristic_write_value_failed(self, characteristic, error):
+ print("[WARN ] write value failed", str(error))
+ self.update_in_progress = True
+ self.disconnect()
+
+ def characteristic_value_updated(self, characteristic, value):
+ if self.verbose:
+ if characteristic.uuid == self.UUID_CTRL_POINT:
+ print(
+ "Characteristic value was updated for Control Point Characteristic"
+ )
+ if characteristic.uuid == self.UUID_PACKET:
+ print("Characteristic value was updated for Packet Characteristic")
+ print("New value is:", value)
+
+ hexval = array_to_hex_string(value)
+
+ if hexval[:4] == "1001":
+ # Response::StartDFU
+ if hexval[4:] == "01":
+ self.step_three()
+ else:
+ print("[WARN ] StartDFU failed")
+ self.disconnect()
+ elif hexval[:4] == "1002":
+ # Response::InitDFUParameters
+ if hexval[4:] == "01":
+ self.step_five()
+ else:
+ print("[WARN ] InitDFUParameters failed")
+ self.disconnect()
+ elif hexval[:2] == "11":
+ # PacketReceiptNotification
+ self.packet_recipt_count += 1
+ self.total_receipt_size += self.size_per_receipt
+ # verify that the returned size correspond to what was sent
+ ack_size = unpack('<I', value[1:])[0]
+ if ack_size != self.total_receipt_size:
+ print("[WARN ] PacketReceiptNotification failed")
+ print(" acknowledged {} : expected {}".format(ack_size, self.total_receipt_size))
+ self.disconnect()
+ self.window.update_progress_bar()
+ if self.verbose:
+ print("[INFO ] receipt count", str(self.packet_recipt_count))
+ print("[INFO ] receipt size", self.total_receipt_size, "out of", self.image_size)
+ print("[INFO ] progress:", (self.total_receipt_size / self.image_size)*100, "%")
+ if self.done != True:
+ self.i += self.pkt_payload_size
+ self.step_seven()
+ elif hexval[:4] == "1003":
+ # Response::ReceiveFirmwareImage::NoError
+ if hexval[4:] == "01":
+ self.step_eight()
+ else:
+ print("[WARN ] ReceiveFirmwareImage failed")
+ self.disconnect()
+ elif hexval[:4] == "1004":
+ # Response::ValidateFirmware
+ if hexval[4:] == "01":
+ self.step_nine()
+ else:
+ print("[WARN ] ValidateFirmware failed")
+ self.disconnect()
+
+ def services_resolved(self):
+ super().services_resolved()
+ self.update_in_progress = True
+
+ print("[%s] Resolved services" % (self.mac_address))
+ ble_dfu_serv = next(s for s in self.services if s.uuid == self.UUID_DFU_SERVICE)
+ self.ctrl_point_char = next(
+ c for c in ble_dfu_serv.characteristics if c.uuid == self.UUID_CTRL_POINT
+ )
+ self.packet_char = next(
+ c for c in ble_dfu_serv.characteristics if c.uuid == self.UUID_PACKET
+ )
+
+ if self.verbose:
+ print("[INFO ] Enabling notifications for Control Point Characteristic")
+ self.ctrl_point_char.enable_notifications()
+
+ def step_one(self):
+ self.current_step = 1
+ if self.verbose:
+ print(
+ "[INFO ] Sending ('Start DFU' (0x01), 'Application' (0x04)) to DFU Control Point"
+ )
+ self.ctrl_point_char.write_value(bytearray.fromhex("01 04"))
+
+ def step_two(self):
+ self.current_step = 2
+ if self.verbose:
+ print("[INFO ] Sending Image size to the DFU Packet characteristic")
+ x = len(self.bin_array)
+ hex_size_array_lsb = uint32_to_bytes_le(x)
+ zero_pad_array_le(hex_size_array_lsb, 8)
+ self.packet_char.write_value(bytearray(hex_size_array_lsb))
+ print("[INFO ] Waiting for Image Size notification")
+
+ def step_three(self):
+ self.current_step = 3
+ if self.verbose:
+ print("[INFO ] Sending 'INIT DFU' + Init Packet Command")
+ self.ctrl_point_char.write_value(bytearray.fromhex("02 00"))
+
+ def step_four(self):
+ self.current_step = 4
+ if self.verbose:
+ print("[INFO ] Sending the Init image (DAT)")
+ self.packet_char.write_value(bytearray(self.get_init_bin_array()))
+ if self.verbose:
+ print("[INFO ] Send 'INIT DFU' + Init Packet Complete Command")
+ self.ctrl_point_char.write_value(bytearray.fromhex("02 01"))
+ print("[INFO ] Waiting for INIT DFU notification")
+
+ def step_five(self):
+ self.current_step = 5
+ if self.verbose:
+ print("Setting pkt receipt notification interval")
+ self.ctrl_point_char.write_value(bytearray.fromhex("08 0A"))
+
+ def step_six(self):
+ self.current_step = 6
+ if self.verbose:
+ print(
+ "[INFO ] Send 'RECEIVE FIRMWARE IMAGE' command to set DFU in firmware receive state"
+ )
+ self.ctrl_point_char.write_value(bytearray.fromhex("03"))
+ self.segment_count = 0
+ self.i = 0
+ self.segment_total = int(
+ math.ceil(self.image_size / float(self.pkt_payload_size))
+ )
+
+ def step_seven(self):
+ self.current_step = 7
+ # Send bin_array contents as as series of packets (burst mode).
+ # Each segment is pkt_payload_size bytes long.
+ # For every pkt_receipt_interval sends, wait for notification.
+ segment = self.bin_array[self.i : self.i + self.pkt_payload_size]
+ self.packet_char.write_value(segment)
+ self.segment_count += 1
+ if self.segment_count == self.segment_total:
+ self.done = True
+ elif (self.segment_count % self.pkt_receipt_interval) != 0:
+ self.i += self.pkt_payload_size
+ self.step_seven()
+ else:
+ if self.verbose:
+ print("[INFO ] Waiting for Packet Receipt Notifiation")
+
+ def step_eight(self):
+ self.current_step = 8
+ print("[INFO ] Sending Validate command")
+ self.ctrl_point_char.write_value(bytearray.fromhex("04"))
+
+ def step_nine(self):
+ self.current_step = 9
+ print("[INFO ] Activate and reset")
+ self.ctrl_point_char.write_value(bytearray.fromhex("05"))
+ self.update_in_progress = False
+ self.success = True
+ self.on_success()
+ self.disconnect()
+ self.caffeinator.decaffeinate()
+
+ def get_init_bin_array(self):
+ # Open the DAT file and create array of its contents
+ init_bin_array = array("B", open(self.datfile_path, "rb").read())
+ return init_bin_array
+
+class Caffeinator():
+ def __init__(self):
+ try:
+ from gi.repository import Gio
+ self.gio = Gio
+
+ self.gnome_session = self.safe_lookup(
+ "org.gnome.desktop.session",
+ "GNOME session not found, you're on your own for idle timeouts"
+ )
+ if self.gnome_session:
+ self.idle_delay = self.gnome_session.get_uint("idle-delay")
+
+ self.gnome_power = self.safe_lookup(
+ "org.gnome.settings-daemon.plugins.power",
+ "GNOME power settings not found, you're on your own for system sleep"
+ )
+ if self.gnome_power:
+ self.sleep_inactive_battery_timeout = self.gnome_power.get_int("sleep-inactive-battery-timeout")
+ self.sleep_inactive_ac_timeout = self.gnome_power.get_int("sleep-inactive-ac-timeout")
+ self.idle_dim = self.gnome_power.get_boolean("idle-dim")
+ except ImportError:
+ print("[INFO ] GIO not found, disabling caffeine")
+ except AttributeError:
+ print("[INFO ] Unable to load GIO schemas, disabling caffeine")
+
+ # Look up a Gio Settings schema without crashing if it doesn't exist
+ def safe_lookup(self, path, failmsg=None):
+ try:
+ exists = self.gio.SettingsSchema.lookup(path)
+ except AttributeError:
+ # SettingsSchema is new, if it doesn't exist
+ # then fall back to legacy schema lookup
+ exists = (path in self.gio.Settings.list_schemas())
+
+ if exists:
+ return self.gio.Settings.new(path)
+ else:
+ if failmsg:
+ print("[INFO ] {}".format(failmsg))
+ return None
+
+ def caffeinate(self):
+ if self.gnome_session:
+ print("[INFO ] Disabling GNOME idle timeout")
+ self.gnome_session.set_uint("idle-delay", 0)
+ if self.gnome_power:
+ print("[INFO ] Disabling GNOME inactivity sleeping")
+ self.gnome_power.set_int("sleep-inactive-battery-timeout", 0)
+ self.gnome_power.set_int("sleep-inactive-ac-timeout", 0)
+ self.gnome_power.set_boolean("idle-dim", False)
+
+ def decaffeinate(self):
+ if self.gnome_session:
+ print("[INFO ] Restoring GNOME idle timeout")
+ self.gnome_session.set_uint("idle-delay", self.idle_delay)
+ if self.gnome_power:
+ print("[INFO ] Restoring GNOME inactivity sleeping")
+ self.gnome_power.set_int("sleep-inactive-battery-timeout", self.sleep_inactive_battery_timeout)
+ self.gnome_power.set_int("sleep-inactive-ac-timeout", self.sleep_inactive_ac_timeout)
+ self.gnome_power.set_boolean("idle-dim", self.idle_dim)
diff --git a/src/bluetooth.py b/src/bluetooth.py
new file mode 100644
index 0000000..9c5a40c
--- /dev/null
+++ b/src/bluetooth.py
@@ -0,0 +1,221 @@
+from os import sync
+import gatt
+import datetime
+import struct
+from gi.repository import GObject, Gio
+from .config import config
+
+BTSVC_TIME = "00001805-0000-1000-8000-00805f9b34fb"
+BTSVC_INFO = "0000180a-0000-1000-8000-00805f9b34fb"
+BTSVC_BATT = "0000180f-0000-1000-8000-00805f9b34fb"
+BTSVC_ALERT = "00001811-0000-1000-8000-00805f9b34fb"
+BTCHAR_FIRMWARE = "00002a26-0000-1000-8000-00805f9b34fb"
+BTCHAR_CURRENTTIME = "00002a2b-0000-1000-8000-00805f9b34fb"
+BTCHAR_NEWALERT = "00002a46-0000-1000-8000-00805f9b34fb"
+BTCHAR_BATTLEVEL = "00002a19-0000-1000-8000-00805f9b34fb"
+
+
+def get_current_time():
+ now = datetime.datetime.now()
+
+ # https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.current_time.xml
+ return bytearray(
+ struct.pack(
+ "HBBBBBBBB",
+ now.year,
+ now.month,
+ now.day,
+ now.hour,
+ now.minute,
+ now.second,
+ now.weekday() + 1, # numbered 1-7
+ int(now.microsecond / 1e6 * 256), # 1/256th of a second
+ 0b0001, # adjust reason
+ )
+ )
+
+
+def get_default_adapter():
+ """https://stackoverflow.com/a/49017827"""
+ import dbus
+
+ bus = dbus.SystemBus()
+ try:
+ manager = dbus.Interface(
+ bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager"
+ )
+ except dbus.exceptions.DBusException:
+ raise BluetoothDisabled
+
+ for path, ifaces in manager.GetManagedObjects().items():
+ if ifaces.get("org.bluez.Adapter1") is None:
+ continue
+ return path.split("/")[-1]
+ raise NoAdapterFound
+
+
+class InfiniTimeManager(gatt.DeviceManager):
+ def __init__(self):
+ self.conf = config()
+ self.device_set = set()
+ self.aliases = dict()
+ if not self.conf.get_property("paired"):
+ self.scan_result = False
+ self.adapter_name = get_default_adapter()
+ self.conf.set_property("adapter", self.adapter_name)
+ else:
+ self.scan_result = True
+ self.adapter_name = self.conf.get_property("adapter")
+ self.mac_address = None
+ super().__init__(self.adapter_name)
+
+ def get_scan_result(self):
+ if self.conf.get_property("paired"):
+ self.scan_result = True
+ return self.scan_result
+
+ def get_device_set(self):
+ return self.device_set
+
+ def get_adapter_name(self):
+ if self.conf.get_property("paired"):
+ return self.conf.get_property("adapter")
+ return get_default_adapter()
+
+ def set_mac_address(self, mac_address):
+ self.mac_address = mac_address
+
+ def get_mac_address(self):
+ if self.conf.get_property("paired"):
+ self.mac_address = self.conf.get_property("last_paired_device")
+ return self.mac_address
+
+ def set_timeout(self, timeout):
+ GObject.timeout_add(timeout, self.stop)
+
+ def device_discovered(self, device):
+ for prefix in ["InfiniTime", "Pinetime-JF", "PineTime", "Y7S"]:
+ if device.alias().startswith(prefix):
+ self.scan_result = True
+ self.aliases[device.mac_address] = device.alias()
+ self.device_set.add(device.mac_address)
+
+ def scan_for_infinitime(self):
+ self.start_discovery()
+ self.set_timeout(1.5 * 1000)
+ self.run()
+
+
+class InfiniTimeDevice(gatt.Device):
+ def __init__(self, mac_address, manager, thread):
+ self.conf = config()
+ self.mac = mac_address
+ self.manager = manager
+ self.thread = thread
+ super().__init__(mac_address, manager)
+
+ def connect(self):
+ self.successful_connection = True
+ super().connect()
+
+ def connect_succeeded(self):
+ super().connect_succeeded()
+ print("[%s] Connected" % (self.mac_address))
+ print("self.mac", self.mac)
+ self.conf.set_property("last_paired_device", self.mac)
+
+ def connect_failed(self, error):
+ super().connect_failed(error)
+ self.successful_connection = False
+ print("[%s] Connection failed: %s" % (self.mac_address, str(error)))
+
+ def disconnect_succeeded(self):
+ super().disconnect_succeeded()
+ print("[%s] Disconnected" % (self.mac_address))
+
+ def characteristic_write_value_succeeded(self, characteristic):
+ if not self.conf.get_property("paired"):
+ self.disconnect()
+
+ def services_resolved(self):
+ super().services_resolved()
+ infosvc = None
+ timesvc = None
+ battsvc = None
+ alertsvc = None
+ for svc in self.services:
+ if svc.uuid == BTSVC_INFO:
+ infosvc = svc
+ elif svc.uuid == BTSVC_TIME:
+ timesvc = svc
+ elif svc.uuid == BTSVC_BATT:
+ battsvc = svc
+ elif svc.uuid == BTSVC_ALERT:
+ alertsvc = svc
+
+ if timesvc:
+ currenttime = next(
+ c
+ for c in timesvc.characteristics
+ if c.uuid == BTCHAR_CURRENTTIME
+ )
+
+ # Update watch time on connection
+ currenttime.write_value(get_current_time())
+
+ self.firmware = b"n/a"
+ if infosvc:
+ info_firmware = next(
+ c
+ for c in infosvc.characteristics
+ if c.uuid == BTCHAR_FIRMWARE
+ )
+
+ # Get device firmware
+ self.firmware = info_firmware.read_value()
+
+ if alertsvc:
+ self.new_alert = next(
+ c
+ for c in alertsvc.characteristics
+ if c.uuid == BTCHAR_NEWALERT
+ )
+
+ self.battery = -1
+ if battsvc:
+ battery_level = next(
+ c
+ for c in battsvc.characteristics
+ if c.uuid == BTCHAR_BATTLEVEL
+ )
+
+ # Get device firmware
+ self.battery = int(battery_level.read_value()[0])
+ if self.thread:
+ self.services_done()
+
+ def send_notification(self, alert_dict):
+ message = alert_dict["message"]
+ alert_category = "0" # simple alert
+ alert_number = "0" # 0-255
+ title = alert_dict["sender"]
+ msg = (
+ str.encode(alert_category)
+ + str.encode(alert_number)
+ + str.encode("\0")
+ + str.encode(title)
+ + str.encode("\0")
+ + str.encode(message)
+ )
+
+ # arr = bytearray(message, "utf-8")
+ # self.new_alert_characteristic.write_value(arr)
+ self.new_alert.write_value(msg)
+
+
+class BluetoothDisabled(Exception):
+ pass
+
+
+class NoAdapterFound(Exception):
+ pass
diff --git a/src/config.py b/src/config.py
new file mode 100644
index 0000000..85b405b
--- /dev/null
+++ b/src/config.py
@@ -0,0 +1,55 @@
+import configparser
+import distutils
+import distutils.util
+import os
+from pathlib import Path
+
+
+class config:
+ # Class constants
+ default_config = {
+ "deploy_type": "quick",
+ "last_paired_device": "None",
+ "paired": "False",
+ "adapter": "None",
+ }
+ config_dir = os.environ.get("XDG_CONFIG_HOME") or os.path.join(
+ os.path.expanduser("~"), ".config"
+ )
+ config_file = os.path.join(config_dir, "siglo.ini")
+
+ def load_defaults(self):
+ if not Path(self.config_dir).is_dir():
+ Path.mkdir(Path(self.config_dir))
+ # if config file is not valid, load defaults
+ if not self.file_valid():
+ config = configparser.ConfigParser()
+ config["settings"] = self.default_config
+ with open(self.config_file, "w") as f:
+ config.write(f)
+
+ def file_valid(self):
+ if not Path(self.config_file).is_file():
+ return False
+ else:
+ config = configparser.ConfigParser()
+ config.read(self.config_file)
+ for key in list(self.default_config.keys()):
+ if not key in config["settings"]:
+ return False
+ return True
+
+ def get_property(self, key):
+ config = configparser.ConfigParser()
+ config.read(self.config_file)
+ prop = config["settings"][key]
+ if key == "paired":
+ prop = bool(distutils.util.strtobool(prop))
+ return prop
+
+ def set_property(self, key, val):
+ config = configparser.ConfigParser()
+ config.read(self.config_file)
+ config["settings"][key] = val
+ with open(self.config_file, "w") as f:
+ config.write(f)
diff --git a/src/daemon.py b/src/daemon.py
new file mode 100644
index 0000000..8f2e014
--- /dev/null
+++ b/src/daemon.py
@@ -0,0 +1,49 @@
+import gatt
+
+import gi.repository.GLib as glib
+import dbus
+from dbus.mainloop.glib import DBusGMainLoop
+from .bluetooth import InfiniTimeManager, InfiniTimeDevice, NoAdapterFound
+from .config import config
+
+
+class daemon:
+ def __init__(self):
+ self.conf = config()
+ self.manager = InfiniTimeManager()
+ self.device = InfiniTimeDevice(manager=self.manager, mac_address=self.conf.get_property("last_paired_device"), thread=False)
+ self.mainloop = glib.MainLoop()
+
+ def start(self):
+ self.device.connect()
+ self.scan_for_notifications()
+
+ def stop(self):
+ self.mainloop.quit()
+ self.device.disconnect()
+
+ def scan_for_notifications(self):
+ DBusGMainLoop(set_as_default=True)
+ monitor_bus = dbus.SessionBus(private=True)
+ try:
+ dbus_monitor_iface = dbus.Interface(monitor_bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus'), dbus_interface='org.freedesktop.DBus.Monitoring')
+ dbus_monitor_iface.BecomeMonitor(["interface='org.freedesktop.Notifications', member='Notify'"], 0)
+ except dbus.exceptions.DBusException as e:
+ print(e)
+ return
+ monitor_bus.add_message_filter(self.notifications)
+ self.mainloop.run()
+
+ def notifications(self, bus, message):
+ alert_dict = {}
+ for arg in message.get_args_list():
+ if isinstance(arg, dbus.Dictionary):
+ if arg["desktop-entry"] == "sm.puri.Chatty":
+ alert_dict["category"] = "SMS"
+ alert_dict["sender"] = message.get_args_list()[3]
+ alert_dict["message"] = message.get_args_list()[4]
+ alert_dict_empty = not alert_dict
+ if len(alert_dict) > 0:
+ print(alert_dict)
+ self.device.send_notification(alert_dict)
+
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..a6cdb94
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,51 @@
+import sys
+import gi
+
+gi.require_version("Gtk", "3.0")
+
+from gi.repository import Gtk, Gio, Gdk
+from .window import SigloWindow
+from .config import config
+
+
+class Application(Gtk.Application):
+ def __init__(self):
+ self.manager = None
+ self.conf = config()
+ self.conf.load_defaults()
+ super().__init__(
+ application_id="com.github.theironrobin.siglo", flags=Gio.ApplicationFlags.FLAGS_NONE
+ )
+
+ def do_activate(self):
+ win = self.props.active_window
+ if not win:
+ win = SigloWindow(application=self)
+ win.present()
+ win.do_scanning()
+
+ def do_window_removed(self, window):
+ win = self.props.active_window
+ if win:
+ win.destroy_manager()
+ self.quit()
+
+
+def main(version):
+ def gtk_style():
+ css = b"""
+#multi_mac_label { font-size: 33px; }
+#bluetooth_button { background-color: blue;
+ background-image: none; }
+ """
+ style_provider = Gtk.CssProvider()
+ style_provider.load_from_data(css)
+ Gtk.StyleContext.add_provider_for_screen(
+ Gdk.Screen.get_default(),
+ style_provider,
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
+ )
+
+ gtk_style()
+ app = Application()
+ return app.run(sys.argv)
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..cf633bd
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,41 @@
+pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
+moduledir = join_paths(pkgdatadir, 'siglo')
+gnome = import('gnome')
+
+gnome.compile_resources('siglo',
+ 'siglo.gresource.xml',
+ gresource_bundle: true,
+ install: true,
+ install_dir: pkgdatadir,
+)
+
+python = import('python')
+
+conf = configuration_data()
+conf.set('PYTHON', python.find_installation('python3', modules: ['gatt']).full_path())
+conf.set('VERSION', meson.project_version())
+conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir')))
+conf.set('pkgdatadir', pkgdatadir)
+
+configure_file(
+ input: 'siglo.in',
+ output: 'siglo',
+ configuration: conf,
+ install: true,
+ install_dir: get_option('bindir')
+)
+
+siglo_sources = [
+ '__init__.py',
+ 'daemon.py',
+ 'quick_deploy.py',
+ 'main.py',
+ 'config.py',
+ 'window.py',
+ 'bluetooth.py',
+ 'ble_dfu.py',
+ 'ota/util.py',
+ 'ota/unpacker.py',
+]
+
+install_data(siglo_sources, install_dir: moduledir)
diff --git a/src/ota/Fork.txt b/src/ota/Fork.txt
new file mode 100644
index 0000000..dbc1056
--- /dev/null
+++ b/src/ota/Fork.txt
@@ -0,0 +1 @@
+This directory contains source forked from https://github.com/daniel-thompson/ota-dfu-python.
diff --git a/src/ota/LICENSE b/src/ota/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/src/ota/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/src/ota/unpacker.py b/src/ota/unpacker.py
new file mode 100644
index 0000000..ab2ecbc
--- /dev/null
+++ b/src/ota/unpacker.py
@@ -0,0 +1,52 @@
+import os.path
+import zipfile
+import tempfile
+import random
+import string
+import shutil
+import re
+
+from os.path import basename
+
+class Unpacker(object):
+ #--------------------------------------------------------------------------
+ #
+ #--------------------------------------------------------------------------
+ def entropy(self, length):
+ return ''.join(random.choice('abcdefghijklmnopqrstuvwxyz') for i in range (length))
+
+ #--------------------------------------------------------------------------
+ #
+ #--------------------------------------------------------------------------
+ def unpack_zipfile(self, file):
+
+ if not os.path.isfile(file):
+ raise Exception("Error: file, not found!")
+
+ # Create unique working direction into which the zip file is expanded
+ self.unzip_dir = "{0}/{1}_{2}".format(tempfile.gettempdir(), os.path.splitext(basename(file))[0], self.entropy(6))
+
+ datfilename = ""
+ binfilename = ""
+
+ with zipfile.ZipFile(file, 'r') as zip:
+ files = [item.filename for item in zip.infolist()]
+ datfilename = [m.group(0) for f in files for m in [re.search('.*\.dat', f)] if m].pop()
+ binfilename = [m.group(0) for f in files for m in [re.search('.*\.bin', f)] if m].pop()
+
+ zip.extractall(r'{0}'.format(self.unzip_dir))
+
+ datfile = "{0}/{1}".format(self.unzip_dir, datfilename)
+ binfile = "{0}/{1}".format(self.unzip_dir, binfilename)
+
+ # print "DAT file: " + datfile
+ # print "BIN file: " + binfile
+
+ return binfile, datfile
+
+ #--------------------------------------------------------------------------
+ #
+ #--------------------------------------------------------------------------
+ def delete(self):
+ # delete self.unzip_dir and its contents
+ shutil.rmtree(self.unzip_dir)
diff --git a/src/ota/util.py b/src/ota/util.py
new file mode 100644
index 0000000..401d494
--- /dev/null
+++ b/src/ota/util.py
@@ -0,0 +1,70 @@
+import sys
+import binascii
+import re
+
+def bytes_to_uint32_le(bytes):
+ return (int(bytes[3], 16) << 24) | (int(bytes[2], 16) << 16) | (int(bytes[1], 16) << 8) | (int(bytes[0], 16) << 0)
+
+def uint32_to_bytes_le(uint32):
+ return [(uint32 >> 0) & 0xff,
+ (uint32 >> 8) & 0xff,
+ (uint32 >> 16) & 0xff,
+ (uint32 >> 24) & 0xff]
+
+def uint16_to_bytes_le(value):
+ return [(value >> 0 & 0xFF),
+ (value >> 8 & 0xFF)]
+
+def zero_pad_array_le(data, padsize):
+ for i in range(0, padsize):
+ data.insert(0, 0)
+
+def array_to_hex_string(arr):
+ hex_str = ""
+ for val in arr:
+ if val > 255:
+ raise Exception("Value is greater than it is possible to represent with one byte")
+ hex_str += "%02x" % val
+
+ return hex_str
+
+def crc32_unsigned(bytestring):
+ return binascii.crc32(bytestring.encode('UTF-8')) % (1 << 32)
+
+def mac_string_to_uint(mac):
+ parts = list(re.match('(..):(..):(..):(..):(..):(..)', mac).groups())
+ ints = [int(x, 16) for x in parts]
+
+ res = 0
+ for i in range(0, len(ints)):
+ res += (ints[len(ints)-1 - i] << 8*i)
+
+ return res
+
+def uint_to_mac_string(mac):
+ ints = [0, 0, 0, 0, 0, 0]
+ for i in range(0, len(ints)):
+ ints[len(ints)-1 - i] = (mac >> 8*i) & 0xff
+
+ return ':'.join(['{:02x}'.format(x).upper() for x in ints])
+
+# Print a nice console progress bar
+def print_progress(iteration, total, prefix = '', suffix = '', decimals = 1, barLength = 100):
+ """
+ Call in a loop to create terminal progress bar
+ @params:
+ iteration - Required : current iteration (Int)
+ total - Required : total iterations (Int)
+ prefix - Optional : prefix string (Str)
+ suffix - Optional : suffix string (Str)
+ decimals - Optional : positive number of decimals in percent complete (Int)
+ barLength - Optional : character length of bar (Int)
+ """
+ formatStr = "{0:." + str(decimals) + "f}"
+ percents = formatStr.format(100 * (iteration / float(total)))
+ filledLength = int(round(barLength * iteration / float(total)))
+ bar = 'x' * filledLength + '-' * (barLength - filledLength)
+ sys.stdout.write('\r%s |%s| %s%s %s (%d of %d bytes)' % (prefix, bar, percents, '%', suffix, iteration, total)),
+ if iteration == total:
+ sys.stdout.write('\n')
+ sys.stdout.flush()
diff --git a/src/quick_deploy.py b/src/quick_deploy.py
new file mode 100644
index 0000000..e55079f
--- /dev/null
+++ b/src/quick_deploy.py
@@ -0,0 +1,65 @@
+import requests
+import json
+
+url = "https://api.github.com/repos/JF002/InfiniTime/releases"
+version_blacklist = (
+ "0.6.0",
+ "0.6.1",
+ "0.6.2",
+ "0.7.0",
+ "0.7.1",
+ "0.8.0-develop",
+ "0.8.1-develop",
+ "0.8.2-develop",
+ "0.9.0-develop",
+ "0.9.0",
+ "0.8.3",
+ "0.8.2",
+ "0.10.0",
+ "0.11.0",
+ "0.12.0",
+ "0.12.1",
+)
+
+
+def get_quick_deploy_list():
+ try:
+ r = requests.get(url)
+ except requests.exceptions.ConnectionError:
+ return []
+ d = json.loads(r.content)
+ quick_deploy_list = []
+ for item in d:
+ for asset in item["assets"]:
+ if (
+ asset["content_type"] == "application/zip"
+ and item["tag_name"] not in version_blacklist
+ ):
+ helper_dict = {
+ "tag_name": item["tag_name"],
+ "name": asset["name"],
+ "browser_download_url": asset["browser_download_url"],
+ }
+ quick_deploy_list.append(helper_dict)
+ return quick_deploy_list
+
+
+def get_tags(full_list):
+ tags = set()
+ for element in full_list:
+ tags.add(element["tag_name"])
+ return sorted(tags, reverse=True)
+
+
+def get_assets_by_tag(tag, full_list):
+ asset_list = []
+ for element in full_list:
+ if tag == element["tag_name"]:
+ asset_list.append(element["name"])
+ return asset_list
+
+
+def get_download_url(name, tag, full_list):
+ for element in full_list:
+ if tag == element["tag_name"] and name == element["name"]:
+ return element["browser_download_url"]
diff --git a/src/siglo.gresource.xml b/src/siglo.gresource.xml
new file mode 100644
index 0000000..2c769e8
--- /dev/null
+++ b/src/siglo.gresource.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/com/github/theironrobin/siglo">
+ <file>window.ui</file>
+ <file>watch.svg</file>
+ <file>watch-icon.svg</file>
+ <file>watch-check.svg</file>
+ <file>watch-progress.svg</file>
+ <file>watch-error.svg</file>
+ </gresource>
+</gresources>
diff --git a/src/siglo.in b/src/siglo.in
new file mode 100755
index 0000000..3f27cea
--- /dev/null
+++ b/src/siglo.in
@@ -0,0 +1,47 @@
+#!@PYTHON@
+
+
+import os
+import sys
+import signal
+import gettext
+import argparse
+
+VERSION = '@VERSION@'
+pkgdatadir = '@pkgdatadir@'
+localedir = '@localedir@'
+
+sys.path.insert(1, pkgdatadir)
+signal.signal(signal.SIGINT, signal.SIG_DFL)
+gettext.install('siglo', localedir)
+
+def main():
+ p = argparse.ArgumentParser(description="app to sync InfiniTime watch")
+ p.add_argument('--start', '-d', required=False, action='store_true', help="start daemon")
+ p.add_argument('--stop', '-x', required=False, action='store_true', help="stop daemon")
+ args = p.parse_args()
+
+ from siglo import config
+ config = config.config()
+ config.load_defaults()
+
+ from siglo import daemon
+ d = daemon.daemon()
+
+ if args.start:
+ d.start()
+ elif args.stop:
+ d.stop()
+ else:
+ import gi
+
+ from gi.repository import Gio
+ resource = Gio.Resource.load(os.path.join(pkgdatadir, 'siglo.gresource'))
+ resource._register()
+
+ from siglo import main
+ sys.exit(main.main(VERSION))
+
+if __name__ == '__main__':
+ main()
+
diff --git a/src/watch-check.svg b/src/watch-check.svg
new file mode 100644
index 0000000..1b9ea75
--- /dev/null
+++ b/src/watch-check.svg
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="200"
+ height="200"
+ viewBox="0 0 52.916665 52.916668"
+ version="1.1"
+ id="svg8">
+ <defs
+ id="defs2">
+ <linearGradient
+ id="linearGradient861">
+ <stop
+ style="stop-color:#808080;stop-opacity:1;"
+ offset="0"
+ id="stop857" />
+ <stop
+ style="stop-color:#808080;stop-opacity:0;"
+ offset="1"
+ id="stop859" />
+ </linearGradient>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath845">
+ <rect
+ style="display:none;opacity:1;fill:#ff6600;stroke-width:0.264583"
+ id="rect847"
+ width="70"
+ height="70"
+ x="74.644386"
+ y="114.93724"
+ rx="3"
+ ry="3"
+ d="m 77.644386,114.93724 h 64.000004 c 1.662,0 3,1.338 3,3 v 64 c 0,1.662 -1.338,3 -3,3 H 77.644386 c -1.662,0 -3,-1.338 -3,-3 v -64 c 0,-1.662 1.338,-3 3,-3 z" />
+ <path
+ id="lpe_path-effect849"
+ style="opacity:1;fill:#ff6600;stroke-width:0.264583"
+ class="powerclip"
+ d="M 54.280251,94.821846 H 164.28025 V 204.82185 H 54.280251 Z m 23.364135,20.115394 c -1.662,0 -3,1.338 -3,3 v 64 c 0,1.662 1.338,3 3,3 h 64.000004 c 1.662,0 3,-1.338 3,-3 v -64 c 0,-1.662 -1.338,-3 -3,-3 z" />
+ </clipPath>
+ <linearGradient
+ xlink:href="#linearGradient861"
+ id="linearGradient863"
+ x1="109.39239"
+ y1="100.29983"
+ x2="109.39239"
+ y2="72.938026"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.32315827,0,0,0.32315827,-8.0237488,-21.489193)" />
+ <linearGradient
+ xlink:href="#linearGradient861"
+ id="linearGradient892"
+ gradientUnits="userSpaceOnUse"
+ x1="109.39239"
+ y1="100.29983"
+ x2="109.39239"
+ y2="72.938026"
+ gradientTransform="matrix(0.32315827,0,0,0.32315827,-62.666074,-75.058368)" />
+ </defs>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1">
+ <path
+ style="opacity:1;fill:#808080;stroke-width:0.277829"
+ id="rect833"
+ width="100"
+ height="100"
+ x="59.280251"
+ y="99.821846"
+ ry="20"
+ rx="20"
+ clip-path="url(#clipPath845)"
+ d="m 79.280251,99.821846 h 59.999999 c 11.08,0 20,8.920004 20,20.000004 v 60 c 0,11.08 -8.92,20 -20,20 H 79.280251 c -11.08,0 -20,-8.92 -20,-20 v -60 c 0,-11.08 8.92,-20.000004 20,-20.000004 z"
+ transform="matrix(0.32315827,0,0,0.32315827,-8.0237488,-21.660197)" />
+ <rect
+ style="opacity:1;fill:#808080;stroke-width:0.102984"
+ id="rect853"
+ width="2.3570774"
+ height="7.3487964"
+ x="10.455177"
+ y="22.968632"
+ rx="0.96947479" />
+ <rect
+ style="opacity:1;fill:url(#linearGradient863);fill-opacity:1;stroke-width:0.0855022"
+ id="rect855"
+ width="16.157913"
+ height="9.2445889"
+ x="19.306908"
+ y="1.581627"
+ rx="0.96947628" />
+ <rect
+ style="fill:url(#linearGradient892);fill-opacity:1;stroke-width:0.0855022"
+ id="rect855-7"
+ width="16.157913"
+ height="9.2445889"
+ x="-35.335423"
+ y="-51.987545"
+ rx="0.96947628"
+ transform="scale(-1)" />
+ <rect
+ style="opacity:1;fill:#669900;stroke-width:0.264583"
+ id="rect942"
+ width="3.169292"
+ height="15.559812"
+ x="-39.391964"
+ y="-18.32464"
+ rx="1.8"
+ transform="rotate(-153.0645)" />
+ <rect
+ style="fill:#669900;stroke-width:0.176212"
+ id="rect942-3"
+ width="3.169292"
+ height="6.9016447"
+ x="-0.41750327"
+ y="-42.9496"
+ rx="1.9"
+ transform="rotate(139.47874)" />
+ </g>
+</svg>
diff --git a/src/watch-error.svg b/src/watch-error.svg
new file mode 100644
index 0000000..577e401
--- /dev/null
+++ b/src/watch-error.svg
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="200"
+ height="200"
+ viewBox="0 0 52.916665 52.916668"
+ version="1.1"
+ id="svg8">
+ <defs
+ id="defs2">
+ <linearGradient
+ id="linearGradient861">
+ <stop
+ style="stop-color:#808080;stop-opacity:1;"
+ offset="0"
+ id="stop857" />
+ <stop
+ style="stop-color:#808080;stop-opacity:0;"
+ offset="1"
+ id="stop859" />
+ </linearGradient>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath845">
+ <rect
+ style="display:none;opacity:1;fill:#ff6600;stroke-width:0.264583"
+ id="rect847"
+ width="70"
+ height="70"
+ x="74.644386"
+ y="114.93724"
+ rx="3"
+ ry="3"
+ d="m 77.644386,114.93724 h 64.000004 c 1.662,0 3,1.338 3,3 v 64 c 0,1.662 -1.338,3 -3,3 H 77.644386 c -1.662,0 -3,-1.338 -3,-3 v -64 c 0,-1.662 1.338,-3 3,-3 z" />
+ <path
+ id="lpe_path-effect849"
+ style="opacity:1;fill:#ff6600;stroke-width:0.264583"
+ class="powerclip"
+ d="M 54.280251,94.821846 H 164.28025 V 204.82185 H 54.280251 Z m 23.364135,20.115394 c -1.662,0 -3,1.338 -3,3 v 64 c 0,1.662 1.338,3 3,3 h 64.000004 c 1.662,0 3,-1.338 3,-3 v -64 c 0,-1.662 -1.338,-3 -3,-3 z" />
+ </clipPath>
+ <linearGradient
+ xlink:href="#linearGradient861"
+ id="linearGradient863"
+ x1="109.39239"
+ y1="100.29983"
+ x2="109.39239"
+ y2="72.938026"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.32315827,0,0,0.32315827,-8.0237488,-21.489193)" />
+ <linearGradient
+ xlink:href="#linearGradient861"
+ id="linearGradient892"
+ gradientUnits="userSpaceOnUse"
+ x1="109.39239"
+ y1="100.29983"
+ x2="109.39239"
+ y2="72.938026"
+ gradientTransform="matrix(0.32315827,0,0,0.32315827,-62.666074,-75.058368)" />
+ </defs>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1">
+ <path
+ style="opacity:1;fill:#808080;stroke-width:0.277829"
+ id="rect833"
+ width="100"
+ height="100"
+ x="59.280251"
+ y="99.821846"
+ ry="20"
+ rx="20"
+ clip-path="url(#clipPath845)"
+ d="m 79.280251,99.821846 h 59.999999 c 11.08,0 20,8.920004 20,20.000004 v 60 c 0,11.08 -8.92,20 -20,20 H 79.280251 c -11.08,0 -20,-8.92 -20,-20 v -60 c 0,-11.08 8.92,-20.000004 20,-20.000004 z"
+ transform="matrix(0.32315827,0,0,0.32315827,-8.0237488,-21.660197)" />
+ <rect
+ style="opacity:1;fill:#808080;stroke-width:0.102984"
+ id="rect853"
+ width="2.3570774"
+ height="7.3487964"
+ x="10.455177"
+ y="22.968632"
+ rx="0.96947479" />
+ <rect
+ style="opacity:1;fill:url(#linearGradient863);fill-opacity:1;stroke-width:0.0855022"
+ id="rect855"
+ width="16.157913"
+ height="9.2445889"
+ x="19.306908"
+ y="1.581627"
+ rx="0.96947628" />
+ <rect
+ style="fill:url(#linearGradient892);fill-opacity:1;stroke-width:0.0855022"
+ id="rect855-7"
+ width="16.157913"
+ height="9.2445889"
+ x="-35.335423"
+ y="-51.987545"
+ rx="0.96947628"
+ transform="scale(-1)" />
+ <rect
+ style="opacity:1;fill:#cc0000;stroke-width:0.264583"
+ id="rect942"
+ width="3.169292"
+ height="15.559812"
+ x="37.102192"
+ y="-7.5619044"
+ rx="1.8"
+ transform="rotate(45)" />
+ <rect
+ style="fill:#cc0000;stroke-width:0.264583"
+ id="rect942-5"
+ width="3.169292"
+ height="15.559812"
+ x="-1.7618184"
+ y="30.883358"
+ rx="1.8"
+ transform="rotate(-45)" />
+ </g>
+</svg>
diff --git a/src/watch-icon.svg b/src/watch-icon.svg
new file mode 100644
index 0000000..e155e98
--- /dev/null
+++ b/src/watch-icon.svg
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="48"
+ height="48"
+ viewBox="0 0 12.7 12.7"
+ version="1.1"
+ id="svg8"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
+ sodipodi:docname="watch-icon.svg">
+ <defs
+ id="defs2">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient861">
+ <stop
+ style="stop-color:#808080;stop-opacity:1;"
+ offset="0"
+ id="stop857" />
+ <stop
+ style="stop-color:#808080;stop-opacity:0;"
+ offset="1"
+ id="stop859" />
+ </linearGradient>
+ <inkscape:path-effect
+ effect="powerclip"
+ id="path-effect849"
+ is_visible="true"
+ lpeversion="1"
+ inverse="true"
+ flatten="false"
+ hide_clip="false"
+ message="Use fill-rule evenodd on &lt;b&gt;fill and stroke&lt;/b&gt; dialog if no flatten result after convert clip to paths." />
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath845">
+ <rect
+ style="display:none;opacity:1;fill:#ff6600;stroke-width:0.264583"
+ id="rect847"
+ width="70"
+ height="70"
+ x="74.644386"
+ y="114.93724"
+ rx="3"
+ ry="3"
+ d="m 77.644386,114.93724 h 64.000004 c 1.662,0 3,1.338 3,3 v 64 c 0,1.662 -1.338,3 -3,3 H 77.644386 c -1.662,0 -3,-1.338 -3,-3 v -64 c 0,-1.662 1.338,-3 3,-3 z" />
+ <path
+ id="lpe_path-effect849"
+ style="opacity:1;fill:#ff6600;stroke-width:0.264583"
+ class="powerclip"
+ d="M 54.280251,94.821846 H 164.28025 V 204.82185 H 54.280251 Z m 23.364135,20.115394 c -1.662,0 -3,1.338 -3,3 v 64 c 0,1.662 1.338,3 3,3 h 64.000004 c 1.662,0 3,-1.338 3,-3 v -64 c 0,-1.662 -1.338,-3 -3,-3 z" />
+ </clipPath>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient861"
+ id="linearGradient863"
+ x1="109.39239"
+ y1="100.29983"
+ x2="109.39239"
+ y2="72.938026"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.07262894,0,0,0.07262894,-1.5643869,-4.4150234)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient861"
+ id="linearGradient892"
+ gradientUnits="userSpaceOnUse"
+ x1="109.39239"
+ y1="100.29983"
+ x2="109.39239"
+ y2="72.938026"
+ gradientTransform="matrix(0.07262894,0,0,0.07262894,-14.322959,-17.283779)" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.0078947"
+ inkscape:cx="68.548704"
+ inkscape:cy="32.255316"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ units="px"
+ inkscape:window-width="1920"
+ inkscape:window-height="1043"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ style="opacity:1;fill:#808080;stroke-width:0.277829"
+ id="rect833"
+ width="100"
+ height="100"
+ x="59.280251"
+ y="99.821846"
+ ry="20"
+ rx="20"
+ clip-path="url(#clipPath845)"
+ inkscape:path-effect="#path-effect849"
+ d="m 79.280251,99.821846 h 59.999999 c 11.08,0 20,8.920004 20,20.000004 v 60 c 0,11.08 -8.92,20 -20,20 H 79.280251 c -11.08,0 -20,-8.92 -20,-20 v -60 c 0,-11.08 8.92,-20.000004 20,-20.000004 z"
+ sodipodi:type="rect"
+ transform="matrix(0.07262894,0,0,0.07262894,-1.5643869,-4.4534564)" />
+ <rect
+ style="opacity:1;fill:#808080;stroke-width:0.0231454"
+ id="rect853"
+ width="0.52974671"
+ height="1.6516219"
+ x="2.4688313"
+ y="5.5629687"
+ rx="0.21788682" />
+ <rect
+ style="opacity:1;fill:url(#linearGradient863);fill-opacity:1;stroke-width:0.0192164"
+ id="rect855"
+ width="3.6314468"
+ height="2.0776963"
+ x="4.5781035"
+ y="0.77008057"
+ rx="0.21788716" />
+ <rect
+ style="fill:url(#linearGradient892);fill-opacity:1;stroke-width:0.0192164"
+ id="rect855-7"
+ width="3.6314468"
+ height="2.0776963"
+ x="-8.1804686"
+ y="-12.098674"
+ rx="0.21788716"
+ transform="scale(-1)" />
+ </g>
+</svg>
diff --git a/src/watch-progress.svg b/src/watch-progress.svg
new file mode 100644
index 0000000..155c782
--- /dev/null
+++ b/src/watch-progress.svg
@@ -0,0 +1,166 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="200"
+ height="200"
+ viewBox="0 0 52.916665 52.916668"
+ version="1.1"
+ id="svg8"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
+ sodipodi:docname="watch-progress.svg">
+ <defs
+ id="defs2">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient861">
+ <stop
+ style="stop-color:#808080;stop-opacity:1;"
+ offset="0"
+ id="stop857" />
+ <stop
+ style="stop-color:#808080;stop-opacity:0;"
+ offset="1"
+ id="stop859" />
+ </linearGradient>
+ <inkscape:path-effect
+ effect="powerclip"
+ id="path-effect849"
+ is_visible="true"
+ lpeversion="1"
+ inverse="true"
+ flatten="false"
+ hide_clip="false"
+ message="Use fill-rule evenodd on &lt;b&gt;fill and stroke&lt;/b&gt; dialog if no flatten result after convert clip to paths." />
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath845">
+ <rect
+ style="display:none;opacity:1;fill:#ff6600;stroke-width:0.264583"
+ id="rect847"
+ width="70"
+ height="70"
+ x="74.644386"
+ y="114.93724"
+ rx="3"
+ ry="3"
+ d="m 77.644386,114.93724 h 64.000004 c 1.662,0 3,1.338 3,3 v 64 c 0,1.662 -1.338,3 -3,3 H 77.644386 c -1.662,0 -3,-1.338 -3,-3 v -64 c 0,-1.662 1.338,-3 3,-3 z" />
+ <path
+ id="lpe_path-effect849"
+ style="opacity:1;fill:#ff6600;stroke-width:0.264583"
+ class="powerclip"
+ d="M 54.280251,94.821846 H 164.28025 V 204.82185 H 54.280251 Z m 23.364135,20.115394 c -1.662,0 -3,1.338 -3,3 v 64 c 0,1.662 1.338,3 3,3 h 64.000004 c 1.662,0 3,-1.338 3,-3 v -64 c 0,-1.662 -1.338,-3 -3,-3 z" />
+ </clipPath>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient861"
+ id="linearGradient863"
+ x1="109.39239"
+ y1="100.29983"
+ x2="109.39239"
+ y2="72.938026"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.32315827,0,0,0.32315827,-8.0237488,-21.489193)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient861"
+ id="linearGradient892"
+ gradientUnits="userSpaceOnUse"
+ x1="109.39239"
+ y1="100.29983"
+ x2="109.39239"
+ y2="72.938026"
+ gradientTransform="matrix(0.32315827,0,0,0.32315827,-62.666074,-75.058368)" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.0078947"
+ inkscape:cx="68.548704"
+ inkscape:cy="105.39617"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ units="px"
+ inkscape:window-width="1920"
+ inkscape:window-height="1043"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ style="opacity:1;fill:#808080;stroke-width:0.277829"
+ id="rect833"
+ width="100"
+ height="100"
+ x="59.280251"
+ y="99.821846"
+ ry="20"
+ rx="20"
+ clip-path="url(#clipPath845)"
+ inkscape:path-effect="#path-effect849"
+ d="m 79.280251,99.821846 h 59.999999 c 11.08,0 20,8.920004 20,20.000004 v 60 c 0,11.08 -8.92,20 -20,20 H 79.280251 c -11.08,0 -20,-8.92 -20,-20 v -60 c 0,-11.08 8.92,-20.000004 20,-20.000004 z"
+ sodipodi:type="rect"
+ transform="matrix(0.32315827,0,0,0.32315827,-8.0237488,-21.660197)" />
+ <rect
+ style="opacity:1;fill:#808080;stroke-width:0.102984"
+ id="rect853"
+ width="2.3570774"
+ height="7.3487964"
+ x="10.455177"
+ y="22.968632"
+ rx="0.96947479" />
+ <rect
+ style="opacity:1;fill:url(#linearGradient863);fill-opacity:1;stroke-width:0.0855022"
+ id="rect855"
+ width="16.157913"
+ height="9.2445889"
+ x="19.306908"
+ y="1.581627"
+ rx="0.96947628" />
+ <rect
+ style="fill:url(#linearGradient892);fill-opacity:1;stroke-width:0.0855022"
+ id="rect855-7"
+ width="16.157913"
+ height="9.2445889"
+ x="-35.335423"
+ y="-51.987545"
+ rx="0.96947628"
+ transform="scale(-1)" />
+ <rect
+ style="opacity:1;fill:#3366cc;stroke-width:0.264583"
+ id="rect942"
+ width="3.169292"
+ height="15.559812"
+ x="25.022923"
+ y="-35.227501"
+ rx="1.8"
+ transform="rotate(90)" />
+ </g>
+</svg>
diff --git a/src/watch.svg b/src/watch.svg
new file mode 100644
index 0000000..0c79681
--- /dev/null
+++ b/src/watch.svg
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="200"
+ height="200"
+ viewBox="0 0 52.916665 52.916668"
+ version="1.1"
+ id="svg8">
+ <defs
+ id="defs2">
+ <linearGradient
+ id="linearGradient861">
+ <stop
+ style="stop-color:#808080;stop-opacity:1;"
+ offset="0"
+ id="stop857" />
+ <stop
+ style="stop-color:#808080;stop-opacity:0;"
+ offset="1"
+ id="stop859" />
+ </linearGradient>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath845">
+ <rect
+ style="display:none;opacity:1;fill:#ff6600;stroke-width:0.264583"
+ id="rect847"
+ width="70"
+ height="70"
+ x="74.644386"
+ y="114.93724"
+ rx="3"
+ ry="3"
+ d="m 77.644386,114.93724 h 64.000004 c 1.662,0 3,1.338 3,3 v 64 c 0,1.662 -1.338,3 -3,3 H 77.644386 c -1.662,0 -3,-1.338 -3,-3 v -64 c 0,-1.662 1.338,-3 3,-3 z" />
+ <path
+ id="lpe_path-effect849"
+ style="opacity:1;fill:#ff6600;stroke-width:0.264583"
+ class="powerclip"
+ d="M 54.280251,94.821846 H 164.28025 V 204.82185 H 54.280251 Z m 23.364135,20.115394 c -1.662,0 -3,1.338 -3,3 v 64 c 0,1.662 1.338,3 3,3 h 64.000004 c 1.662,0 3,-1.338 3,-3 v -64 c 0,-1.662 -1.338,-3 -3,-3 z" />
+ </clipPath>
+ <linearGradient
+ xlink:href="#linearGradient861"
+ id="linearGradient863"
+ x1="109.39239"
+ y1="100.29983"
+ x2="109.39239"
+ y2="72.938026"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.32315827,0,0,0.32315827,-8.0237488,-21.489193)" />
+ <linearGradient
+ xlink:href="#linearGradient861"
+ id="linearGradient892"
+ gradientUnits="userSpaceOnUse"
+ x1="109.39239"
+ y1="100.29983"
+ x2="109.39239"
+ y2="72.938026"
+ gradientTransform="matrix(0.32315827,0,0,0.32315827,-62.666074,-75.058368)" />
+ </defs>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1">
+ <path
+ style="opacity:1;fill:#808080;stroke-width:0.277829"
+ id="rect833"
+ width="100"
+ height="100"
+ x="59.280251"
+ y="99.821846"
+ ry="20"
+ rx="20"
+ clip-path="url(#clipPath845)"
+ d="m 79.280251,99.821846 h 59.999999 c 11.08,0 20,8.920004 20,20.000004 v 60 c 0,11.08 -8.92,20 -20,20 H 79.280251 c -11.08,0 -20,-8.92 -20,-20 v -60 c 0,-11.08 8.92,-20.000004 20,-20.000004 z"
+ transform="matrix(0.32315827,0,0,0.32315827,-8.0237488,-21.660197)" />
+ <rect
+ style="opacity:1;fill:#808080;stroke-width:0.102984"
+ id="rect853"
+ width="2.3570774"
+ height="7.3487964"
+ x="10.455177"
+ y="22.968632"
+ rx="0.96947479" />
+ <rect
+ style="opacity:1;fill:url(#linearGradient863);fill-opacity:1;stroke-width:0.0855022"
+ id="rect855"
+ width="16.157913"
+ height="9.2445889"
+ x="19.306908"
+ y="1.581627"
+ rx="0.96947628" />
+ <rect
+ style="fill:url(#linearGradient892);fill-opacity:1;stroke-width:0.0855022"
+ id="rect855-7"
+ width="16.157913"
+ height="9.2445889"
+ x="-35.335423"
+ y="-51.987545"
+ rx="0.96947628"
+ transform="scale(-1)" />
+ </g>
+</svg>
diff --git a/src/window.py b/src/window.py
new file mode 100644
index 0000000..939299c
--- /dev/null
+++ b/src/window.py
@@ -0,0 +1,381 @@
+import subprocess
+import configparser
+import threading
+import urllib.request
+from pathlib import Path
+import gatt
+from gi.repository import Gtk, GObject, GLib
+from .bluetooth import (
+ InfiniTimeDevice,
+ InfiniTimeManager,
+ BluetoothDisabled,
+ NoAdapterFound,
+)
+from .ble_dfu import InfiniTimeDFU
+from .unpacker import Unpacker
+from .quick_deploy import *
+from .config import config
+
+
+class ConnectionThread(threading.Thread):
+ def __init__(self, manager, mac, callback):
+ threading.Thread.__init__(self)
+ self.mac = mac
+ self.manager = manager
+ self.callback = callback
+ self.device = None
+
+ def run(self):
+ self.device = InfiniTimeDevice(
+ manager=self.manager, mac_address=self.mac, thread=True
+ )
+ self.device.services_done = self.data_received
+ self.device.connect()
+
+ def data_received(self):
+ firmware = bytes(self.device.firmware).decode()
+ if self.device.battery == -1:
+ battery = "n/a"
+ else:
+ battery = "{}%".format(self.device.battery)
+ GLib.idle_add(self.callback, [firmware, battery])
+
+
+@Gtk.Template(resource_path="/com/github/theironrobin/siglo/window.ui")
+class SigloWindow(Gtk.ApplicationWindow):
+ __gtype_name__ = "SigloWindow"
+ # Navigation
+ main_stack = Gtk.Template.Child()
+ header_stack = Gtk.Template.Child()
+
+ # Watches view
+ watches_listbox = Gtk.Template.Child()
+
+ # Watch view
+ watch_name = Gtk.Template.Child()
+ watch_address = Gtk.Template.Child()
+ watch_firmware = Gtk.Template.Child()
+ watch_battery = Gtk.Template.Child()
+ ota_pick_tag_combobox = Gtk.Template.Child()
+ ota_pick_asset_combobox = Gtk.Template.Child()
+ firmware_run = Gtk.Template.Child()
+ firmware_file = Gtk.Template.Child()
+ firmware_run_file = Gtk.Template.Child()
+ keep_paired_switch = Gtk.Template.Child()
+
+ # Flasher
+ dfu_stack = Gtk.Template.Child()
+ dfu_progress_bar = Gtk.Template.Child()
+ dfu_progress_text = Gtk.Template.Child()
+
+ def __init__(self, **kwargs):
+ self.ble_dfu = None
+ self.ota_file = None
+ self.manager = None
+ self.current_mac = None
+ self.asset = None
+ self.asset_download_url = None
+ self.tag = None
+ self.conf = config()
+ super().__init__(**kwargs)
+ GObject.threads_init()
+ self.full_list = get_quick_deploy_list()
+ GObject.signal_new(
+ "flash-signal",
+ self,
+ GObject.SIGNAL_RUN_LAST,
+ GObject.TYPE_PYOBJECT,
+ (GObject.TYPE_PYOBJECT,),
+ )
+
+ def disconnect_paired_device(self):
+ try:
+ devices = self.manager.devices()
+ for d in devices:
+ if d.mac_address == self.manager.get_mac_address() and d.is_connected():
+ d.disconnect()
+ finally:
+ self.conf.set_property("paired", "False")
+
+ def destroy_manager(self):
+ if self.manager:
+ self.manager.stop()
+ self.manager = None
+
+ def make_watch_row(self, name, mac):
+ row = Gtk.ListBoxRow()
+ grid = Gtk.Grid()
+ grid.set_hexpand(True)
+ grid.set_row_spacing(8)
+ grid.set_column_spacing(8)
+ grid.set_margin_top(8)
+ grid.set_margin_bottom(8)
+ grid.set_margin_left(8)
+ grid.set_margin_right(8)
+ row.add(grid)
+
+ icon = Gtk.Image.new_from_resource("/com/github/theironrobin/siglo/watch-icon.svg")
+ grid.attach(icon, 0, 0, 1, 2)
+
+ label_alias = Gtk.Label(label="Name", xalign=1.0)
+ label_alias.get_style_context().add_class("dim-label")
+ grid.attach(label_alias, 1, 0, 1, 1)
+ value_alias = Gtk.Label(label=name, xalign=0.0)
+ value_alias.set_hexpand(True)
+ grid.attach(value_alias, 2, 0, 1, 1)
+
+ label_mac = Gtk.Label(label="Address", xalign=1.0)
+ label_mac.get_style_context().add_class("dim-label")
+ grid.attach(label_mac, 1, 1, 1, 1)
+ value_mac = Gtk.Label(label=mac, xalign=0.0)
+ grid.attach(value_mac, 2, 1, 1, 1)
+
+ arrow = Gtk.Image.new_from_icon_name("go-next-symbolic", Gtk.IconSize.BUTTON)
+ grid.attach(arrow, 4, 0, 1, 2)
+
+ row.show_all()
+ return row
+
+ def do_scanning(self):
+ print("Start scanning")
+ self.main_stack.set_visible_child_name("scan")
+ self.header_stack.set_visible_child_name("scan")
+ if not self.manager:
+ # create manager if not present yet
+ try:
+ self.manager = InfiniTimeManager()
+ except (gatt.errors.NotReady, BluetoothDisabled):
+ print("Bluetooth is disabled")
+ self.main_stack.set_visible_child_name("nodevice")
+ except NoAdapterFound:
+ print("No bluetooth adapter found")
+ self.main_stack.set_visible_child_name("nodevice")
+ if not self.manager:
+ return
+
+ if self.conf.get_property("paired"):
+ self.disconnect_paired_device()
+
+ self.depopulate_listbox()
+ self.manager.scan_result = False
+ try:
+ self.manager.scan_for_infinitime()
+ except (gatt.errors.NotReady, gatt.errors.Failed) as e:
+ print(e)
+ self.main_stack.set_visible_child_name("nodevice")
+ self.destroy_manager()
+ try:
+ if len(self.manager.get_device_set()) > 0:
+ self.main_stack.set_visible_child_name("watches")
+ self.header_stack.set_visible_child_name("watches")
+ else:
+ self.main_stack.set_visible_child_name("nodevice")
+ for mac in self.manager.get_device_set():
+ print("Found {}".format(mac))
+ row = self.make_watch_row(self.manager.aliases[mac], mac)
+ row.mac = mac
+ row.alias = self.manager.aliases[mac]
+ self.watches_listbox.add(row)
+ except AttributeError as e:
+ print(e)
+ self.main_stack.set_visible_child_name("nodevice")
+ self.destroy_manager()
+ self.populate_tagbox()
+
+ def depopulate_listbox(self):
+ children = self.watches_listbox.get_children()
+ for child in children:
+ self.watches_listbox.remove(child)
+
+ def populate_tagbox(self):
+ self.ota_pick_tag_combobox.remove_all()
+ for tag in get_tags(self.full_list):
+ self.ota_pick_tag_combobox.append_text(tag)
+
+ def populate_assetbox(self):
+ self.ota_pick_asset_combobox.remove_all()
+ for asset in get_assets_by_tag(self.tag, self.full_list):
+ self.ota_pick_asset_combobox.append_text(asset)
+
+ def callback_device_connect(self, data):
+ firmware, battery = data
+
+ self.watch_firmware.set_text(firmware)
+ self.watch_battery.set_text(battery)
+
+ @Gtk.Template.Callback()
+ def on_watches_listbox_row_activated(self, widget, row):
+ mac = row.mac
+ self.current_mac = mac
+ alias = row.alias
+
+ if self.keep_paired_switch.get_active():
+ # Start daemon
+ subprocess.Popen(["systemctl", "--user", "start", "siglo"])
+ self.conf.set_property("paired", "True")
+
+ if self.manager is not None:
+ thread = ConnectionThread(self.manager, mac, self.callback_device_connect)
+ thread.daemon = True
+ thread.start()
+
+ self.watch_name.set_text(alias)
+ self.watch_address.set_text(mac)
+ self.main_stack.set_visible_child_name("watch")
+ self.header_stack.set_visible_child_name("watch")
+
+ @Gtk.Template.Callback()
+ def on_back_to_devices_clicked(self, *args):
+ self.main_stack.set_visible_child_name("watches")
+ self.header_stack.set_visible_child_name("watches")
+
+ @Gtk.Template.Callback()
+ def ota_pick_tag_combobox_changed_cb(self, widget):
+ self.tag = self.ota_pick_tag_combobox.get_active_text()
+ self.populate_assetbox()
+
+ @Gtk.Template.Callback()
+ def ota_pick_asset_combobox_changed_cb(self, widget):
+ self.asset = self.ota_pick_asset_combobox.get_active_text()
+ if self.asset is not None:
+ self.firmware_run.set_sensitive(True)
+ self.asset_download_url = get_download_url(
+ self.asset, self.tag, self.full_list
+ )
+ else:
+ self.firmware_run.set_sensitive(False)
+ self.asset_download_url = None
+
+ @Gtk.Template.Callback()
+ def firmware_file_file_set_cb(self, widget):
+ print("File set!")
+ filename = widget.get_filename()
+ self.ota_file = filename
+ self.firmware_run_file.set_sensitive(True)
+
+ @Gtk.Template.Callback()
+ def rescan_button_clicked(self, widget):
+ self.do_scanning()
+
+ @Gtk.Template.Callback()
+ def on_bluetooth_settings_clicked(self, widget):
+ subprocess.Popen(["gnome-control-center", "bluetooth"])
+
+ @Gtk.Template.Callback()
+ def ota_file_selected(self, widget):
+ filename = widget.get_filename()
+ self.ota_file = filename
+ self.main_info.set_text("File: " + filename.split("/")[-1])
+ self.ota_picked_box.set_visible(True)
+ self.ota_selection_box.set_visible(False)
+ self.ota_picked_box.set_sensitive(True)
+
+ @Gtk.Template.Callback()
+ def firmware_run_file_clicked_cb(self, widget):
+ self.dfu_stack.set_visible_child_name("ok")
+ self.main_stack.set_visible_child_name("firmware")
+
+ self.firmware_mode = "manual"
+
+ self.start_flash()
+
+ @Gtk.Template.Callback()
+ def on_firmware_run_clicked(self, widget):
+ self.dfu_stack.set_visible_child_name("ok")
+ self.main_stack.set_visible_child_name("firmware")
+
+ self.firmware_mode = "auto"
+
+ file_name = "/tmp/" + self.asset
+
+ print("Downloading {}".format(self.asset_download_url))
+
+ local_filename, headers = urllib.request.urlretrieve(
+ self.asset_download_url, file_name
+ )
+ self.ota_file = local_filename
+
+ self.start_flash()
+
+ def start_flash(self):
+ unpacker = Unpacker()
+ try:
+ binfile, datfile = unpacker.unpack_zipfile(self.ota_file)
+ except Exception as e:
+ print("ERR")
+ print(e)
+ pass
+
+ self.ble_dfu = InfiniTimeDFU(
+ mac_address=self.current_mac,
+ manager=self.manager,
+ window=self,
+ firmware_path=binfile,
+ datfile_path=datfile,
+ verbose=False,
+ )
+ self.ble_dfu.on_failure = self.on_flash_failed
+ self.ble_dfu.on_success = self.on_flash_done
+ self.ble_dfu.input_setup()
+ self.dfu_progress_text.set_text(self.get_prog_text())
+ self.ble_dfu.connect()
+
+ def on_flash_failed(self):
+ self.dfu_stack.set_visible_child_name("fail")
+
+ def on_flash_done(self):
+ self.dfu_stack.set_visible_child_name("done")
+
+ @Gtk.Template.Callback()
+ def on_dfu_retry_clicked(self, widget):
+ if self.firmware_mode == "auto":
+ self.on_firmware_run_clicked(widget)
+
+ @Gtk.Template.Callback()
+ def flash_it_button_clicked(self, widget):
+ if self.deploy_type == "quick":
+ file_name = "/tmp/" + self.asset
+ local_filename, headers = urllib.request.urlretrieve(
+ self.asset_download_url, file_name
+ )
+ self.ota_file = local_filename
+
+ @Gtk.Template.Callback()
+ def deploy_type_toggled(self, widget):
+ if (
+ self.conf.get_property("deploy_type") == "manual"
+ and self.auto_switch_deploy_type
+ ):
+ self.auto_switch_deploy_type = False
+ else:
+ if self.conf.get_property("deploy_type") == "quick":
+ self.conf.set_property("deploy_type", "manual")
+ else:
+ self.conf.set_property("deploy_type", "quick")
+ self.rescan_button.emit("clicked")
+
+ def update_progress_bar(self):
+ self.dfu_progress_bar.set_fraction(
+ self.ble_dfu.total_receipt_size / self.ble_dfu.image_size
+ )
+ self.dfu_progress_text.set_text(self.get_prog_text())
+
+ def get_prog_text(self):
+ return (
+ str(self.ble_dfu.total_receipt_size)
+ + " / "
+ + str(self.ble_dfu.image_size)
+ + " bytes received"
+ )
+
+ def show_complete(self, success):
+ if success:
+ self.rescan_button.set_sensitive("True")
+ self.main_info.set_text("OTA Update Complete")
+ else:
+ self.main_info.set_text("OTA Update Failed")
+ self.bt_spinner.set_visible(False)
+ self.dfu_progress_box.set_visible(False)
+ self.ota_picked_box.set_visible(True)
+ if self.conf.get_property("deploy_type") == "quick":
+ self.auto_bbox_scan_pass.set_visible(True)
diff --git a/src/window.ui b/src/window.ui
new file mode 100644
index 0000000..e183823
--- /dev/null
+++ b/src/window.ui
@@ -0,0 +1,1355 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 -->
+<interface>
+ <requires lib="gtk+" version="3.24"/>
+ <object class="GtkMenu" id="settings_menu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkCheckMenuItem" id="deploy_type_switch">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Manual OTA File</property>
+ <property name="use-underline">True</property>
+ <signal name="toggled" handler="deploy_type_toggled" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ </child>
+ </object>
+ <template class="SigloWindow" parent="GtkApplicationWindow">
+ <property name="can-focus">False</property>
+ <property name="default-width">600</property>
+ <property name="default-height">300</property>
+ <signal name="set-focus" handler="on_window_focus" swapped="no"/>
+ <child>
+ <object class="GtkStack" id="main_stack">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="transition-type">slide-left-right</property>
+ <child>
+ <object class="GtkBox" id="view_scan">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-start">16</property>
+ <property name="margin-end">16</property>
+ <property name="margin-top">16</property>
+ <property name="margin-bottom">16</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">32</property>
+ <property name="margin-bottom">32</property>
+ <property name="label" translatable="yes">Scanning</property>
+ <style>
+ <class name="heading"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="resource">/com/github/theironrobin/siglo/watch.svg</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinner" id="scan_spinner">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">32</property>
+ <property name="margin-bottom">32</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">scan</property>
+ <property name="title" translatable="yes">Scan</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="view_nodevice">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-start">16</property>
+ <property name="margin-end">16</property>
+ <property name="margin-top">16</property>
+ <property name="margin-bottom">16</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">32</property>
+ <property name="margin-bottom">32</property>
+ <property name="label" translatable="yes">Not paired</property>
+ <style>
+ <class name="heading"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="resource">/com/github/theironrobin/siglo/watch-error.svg</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-top">32</property>
+ <property name="margin-bottom">16</property>
+ <property name="label" translatable="yes">There is no InfiniTime watch paired. Make sure the watch is ON but NOT connected.</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="spacing">16</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkButton" id="rescan">
+ <property name="label" translatable="yes">Try again</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <signal name="clicked" handler="rescan_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">Bluetooth settings</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <signal name="clicked" handler="on_bluetooth_settings_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">nodevice</property>
+ <property name="title" translatable="yes">No device</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="view_watches">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-start">16</property>
+ <property name="margin-end">16</property>
+ <property name="margin-top">16</property>
+ <property name="margin-bottom">16</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-bottom">5</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">4</property>
+ <property name="label" translatable="yes">Watches</property>
+ <style>
+ <class name="heading"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Keep paired</property>
+ <style>
+ <class name="heading"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="keep_paired_switch">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label-xalign">0</property>
+ <property name="shadow-type">in</property>
+ <child>
+ <object class="GtkListBox" id="watches_listbox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <signal name="row-activated" handler="on_watches_listbox_row_activated" swapped="no"/>
+ <child type="placeholder">
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">No watches available</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label_item">
+ <placeholder/>
+ </child>
+ <style>
+ <class name="view"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="name">watches</property>
+ <property name="title" translatable="yes">Watches</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="view_watch">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <child>
+ <object class="GtkViewport">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="shadow-type">none</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-start">16</property>
+ <property name="margin-end">16</property>
+ <property name="margin-top">16</property>
+ <property name="margin-bottom">16</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">16</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">4</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Watch</property>
+ <style>
+ <class name="heading"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label-xalign">0</property>
+ <property name="shadow-type">in</property>
+ <child>
+ <!-- n-columns=3 n-rows=4 -->
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-start">8</property>
+ <property name="margin-end">8</property>
+ <property name="margin-top">8</property>
+ <property name="margin-bottom">8</property>
+ <property name="row-spacing">8</property>
+ <property name="column-spacing">8</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="resource">/com/github/theironrobin/siglo/watch-icon.svg</property>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">0</property>
+ <property name="height">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Name</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Address</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Firmware</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="watch_name">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">n/a</property>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="watch_address">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">n/a</property>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="watch_firmware">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">n/a</property>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Battery</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="watch_battery">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">n/a</property>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">3</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="label_item">
+ <placeholder/>
+ </child>
+ <style>
+ <class name="view"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">4</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Firmware update</property>
+ <style>
+ <class name="heading"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label-xalign">0</property>
+ <property name="shadow-type">in</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">8</property>
+ <child>
+ <object class="GtkStackSwitcher">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-start">8</property>
+ <property name="margin-end">8</property>
+ <property name="margin-top">8</property>
+ <property name="margin-bottom">8</property>
+ <property name="stack">firmware_stack</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="firmware_stack">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="transition-type">slide-left-right</property>
+ <child>
+ <!-- n-columns=2 n-rows=4 -->
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-start">8</property>
+ <property name="margin-end">8</property>
+ <property name="margin-top">8</property>
+ <property name="margin-bottom">8</property>
+ <property name="row-spacing">8</property>
+ <property name="column-spacing">8</property>
+ <child>
+ <object class="GtkButton" id="firmware_run">
+ <property name="label" translatable="yes">Update firmware</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <signal name="clicked" handler="on_firmware_run_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="ota_pick_tag_combobox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Tag</property>
+ <property name="active-id">0</property>
+ <signal name="changed" handler="ota_pick_tag_combobox_changed_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Tag</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Asset</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="ota_pick_asset_combobox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="active-id">0</property>
+ <signal name="changed" handler="ota_pick_asset_combobox_changed_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="name">auto</property>
+ <property name="title" translatable="yes">Automatic</property>
+ </packing>
+ </child>
+ <child>
+ <!-- n-columns=2 n-rows=3 -->
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-start">8</property>
+ <property name="margin-end">8</property>
+ <property name="margin-top">8</property>
+ <property name="margin-bottom">8</property>
+ <property name="row-spacing">8</property>
+ <property name="column-spacing">8</property>
+ <child>
+ <object class="GtkFileChooserButton" id="firmware_file">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="create-folders">False</property>
+ <property name="title" translatable="yes">Firmware file</property>
+ <signal name="file-set" handler="firmware_file_file_set_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">File</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="firmware_run_file">
+ <property name="label" translatable="yes">Update firmware</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <signal name="clicked" handler="firmware_run_file_clicked_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="name">file</property>
+ <property name="title" translatable="yes">From file</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="label_item">
+ <placeholder/>
+ </child>
+ <style>
+ <class name="view"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">watch</property>
+ <property name="title" translatable="yes">Watch</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="view_firmware">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-start">16</property>
+ <property name="margin-end">16</property>
+ <property name="margin-top">16</property>
+ <property name="margin-bottom">16</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">32</property>
+ <property name="margin-bottom">32</property>
+ <property name="label" translatable="yes">Firmware update</property>
+ <style>
+ <class name="heading"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="dfu_stack">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkImage" id="firmware_picture1">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="resource">/com/github/theironrobin/siglo/watch-error.svg</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-top">16</property>
+ <property name="margin-bottom">16</property>
+ <property name="label" translatable="yes">The firmware update has failed. Retrying the update will fix it in most cases.</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">Retry update</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <signal name="clicked" handler="on_dfu_retry_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">fail</property>
+ <property name="title" translatable="yes">fail</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkImage" id="firmware_picture">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="resource">/com/github/theironrobin/siglo/watch-progress.svg</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkProgressBar" id="dfu_progress_bar">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="show-text">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">10</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="dfu_progress_text">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">0 / 0</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">ok</property>
+ <property name="title" translatable="yes">ok</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkImage" id="firmware_picture2">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="resource">/com/github/theironrobin/siglo/watch-check.svg</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-top">16</property>
+ <property name="margin-bottom">16</property>
+ <property name="label" translatable="yes">The firmware update has completed sucessfully.
+
+To make the firmware persistent go to the settings on the watch and go to the firmware page to validate the flashing or do a roll-back.</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="name">done</property>
+ <property name="title" translatable="yes">done</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">firmware</property>
+ <property name="title" translatable="yes">Firmware</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="main_box">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="main_info">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">20</property>
+ <property name="label" translatable="yes">Scanning...</property>
+ <attributes>
+ <attribute name="weight" value="heavy"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinner" id="bt_spinner">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="dfu_progress_box">
+ <property name="can-focus">False</property>
+ <property name="margin-start">50</property>
+ <property name="margin-end">50</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="scan_pass_box">
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">15</property>
+ <child>
+ <object class="GtkLabel" id="info_scan_pass">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="vexpand">True</property>
+ <property name="label" translatable="yes">default scan pass text</property>
+ <property name="justify">center</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButtonBox" id="bbox_scan_pass">
+ <property name="width-request">-1</property>
+ <property name="height-request">-1</property>
+ <property name="can-focus">False</property>
+ <property name="halign">center</property>
+ <property name="margin-end">15</property>
+ <property name="orientation">vertical</property>
+ <property name="layout-style">start</property>
+ <child>
+ <object class="GtkBox" id="ota_selection_box">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-end">19</property>
+ <property name="margin-bottom">10</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="label" translatable="yes">OTA Update:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFileChooserButton" id="ota_chooser_button">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="title" translatable="yes">OTA Update (.zip)</property>
+ <property name="width-chars">6</property>
+ <signal name="file-set" handler="ota_file_selected" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButtonBox" id="auto_bbox_scan_pass">
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">5</property>
+ <property name="layout-style">start</property>
+ <child>
+ <object class="GtkBox" id="ota_pick_tag_box">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkLabel" id="ota_pick_tag_label">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Tag: </property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="ota_pick_asset_box">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkLabel" id="ota_pick_asset_label">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Asset: </property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="ota_picked_box">
+ <property name="sensitive">False</property>
+ <property name="can-focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="spacing">10</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkButton" id="flash_it_button">
+ <property name="label" translatable="yes">Flash It!</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ <property name="hexpand">True</property>
+ <signal name="clicked" handler="flash_it_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="scan_fail_box">
+ <property name="can-focus">False</property>
+ <property name="vexpand">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="rescan_button">
+ <property name="label" translatable="yes">Rescan</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="rescan_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">8</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="rescan_button1">
+ <property name="label" translatable="yes">Rescan</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="rescan_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="titlebar">
+ <object class="GtkHeaderBar" id="header_bar">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="title">Siglo</property>
+ <property name="show-close-button">True</property>
+ <child>
+ <object class="GtkStack" id="header_stack">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="hhomogeneous">False</property>
+ <property name="vhomogeneous">False</property>
+ <property name="transition-type">crossfade</property>
+ <child>
+ <object class="GtkMenuButton">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="receives-default">True</property>
+ <property name="popup">settings_menu</property>
+ <property name="use-popover">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="name">main</property>
+ <property name="title" translatable="yes">main</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="back_to_devices">
+ <property name="label" translatable="yes">Back</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <signal name="clicked" handler="on_back_to_devices_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="name">watch</property>
+ <property name="title" translatable="yes">Watch</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="name">scan</property>
+ <property name="title" translatable="yes">scan</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <signal name="clicked" handler="rescan_button_clicked" swapped="no"/>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">view-refresh-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">watches</property>
+ <property name="title" translatable="yes">watches</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>