diff options
author | P. 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) |
commit | bab4f35fc5c13341fb9cc96a7cb863c5cd5c3f53 (patch) | |
tree | 75d9463a02eb5984e1f33aadba830f490f2b103c | |
download | siglo-bab4f35fc5c13341fb9cc96a7cb863c5cd5c3f53.zip siglo-bab4f35fc5c13341fb9cc96a7cb863c5cd5c3f53.tar.gz siglo-bab4f35fc5c13341fb9cc96a7cb863c5cd5c3f53.tar.bz2 |
New upstream version 0.9.9upstream/0.9.9upstream/latest
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 @@ -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 <b>fill and stroke</b> 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 <b>fill and stroke</b> 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> |