From 10e2c89b6395bf2392254a4b56197b0a77cc11e9 Mon Sep 17 00:00:00 2001 From: Djuri Baars Date: Sun, 19 May 2024 02:42:36 +0200 Subject: [PATCH] Initial commit --- .gitignore | 270 ++++++++++++++++++++++++++++++++ app.py | 7 + app/__init__.py | 0 app/api.py | 24 +++ app/espota.py | 325 +++++++++++++++++++++++++++++++++++++++ app/fw_update.py | 0 app/gui.py | 313 +++++++++++++++++++++++++++++++++++++ app/zeroconf_listener.py | 29 ++++ firmware/.gitkeep | 270 ++++++++++++++++++++++++++++++++ requirements.txt | 3 + update-icon.icns | Bin 0 -> 75765 bytes update-icon.png | Bin 0 -> 8511 bytes 12 files changed, 1241 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 app/__init__.py create mode 100644 app/api.py create mode 100644 app/espota.py create mode 100644 app/fw_update.py create mode 100644 app/gui.py create mode 100644 app/zeroconf_listener.py create mode 100644 firmware/.gitkeep create mode 100644 requirements.txt create mode 100644 update-icon.icns create mode 100644 update-icon.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05c5808 --- /dev/null +++ b/.gitignore @@ -0,0 +1,270 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,macos,windows,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,macos,windows,linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Python ### +# 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/ +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/ +cover/ + +# 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 +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .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 + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__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/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,macos,windows,linux +firmware/*.bin \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..7fa4641 --- /dev/null +++ b/app.py @@ -0,0 +1,7 @@ +import wx + +from app.gui import BTClockOTAUpdater + +app = wx.App(False) +frame = BTClockOTAUpdater(None, 'BTClock OTA updater') +app.MainLoop() \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..7ebba98 --- /dev/null +++ b/app/api.py @@ -0,0 +1,24 @@ +import time +import requests +from threading import Thread + +class ApiHandler: + def identify_btclock(self, address): + self.make_api_call(address, "api/identify") + return + + def check_fs_hash(self, address): + ret = self.run_api_call(address, "fs_hash.txt") + return ret + + def make_api_call(self, address, path): + thread = Thread(target=self.run_api_call, args=(address,path)) + thread.start() + + def run_api_call(self, address, path): + try: + url = f"http://{address}/{path}" + response = requests.get(url) + return response.text + except requests.RequestException as e: + print("error") \ No newline at end of file diff --git a/app/espota.py b/app/espota.py new file mode 100644 index 0000000..487f8ed --- /dev/null +++ b/app/espota.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python +# +# Original espota.py by Ivan Grokhotkov: +# https://gist.github.com/igrr/d35ab8446922179dc58c +# +# Modified since 2015-09-18 from Pascal Gollor (https://github.com/pgollor) +# Modified since 2015-11-09 from Hristo Gochkov (https://github.com/me-no-dev) +# Modified since 2016-01-03 from Matthew O'Gorman (https://githumb.com/mogorman) +# +# This script will push an OTA update to the ESP +# use it like: +# python espota.py -i -I -p -P [-a password] -f +# Or to upload SPIFFS image: +# python espota.py -i -I -p -P [-a password] -s -f +# +# Changes +# 2015-09-18: +# - Add option parser. +# - Add logging. +# - Send command to controller to differ between flashing and transmitting SPIFFS image. +# +# Changes +# 2015-11-09: +# - Added digest authentication +# - Enhanced error tracking and reporting +# +# Changes +# 2016-01-03: +# - Added more options to parser. +# +# Changes +# 2023-05-22: +# - Replaced the deprecated optparse module with argparse. +# - Adjusted the code style to conform to PEP 8 guidelines. +# - Used with statement for file handling to ensure proper resource cleanup. +# - Incorporated exception handling to catch and handle potential errors. +# - Made variable names more descriptive for better readability. +# - Introduced constants for better code maintainability. + +from __future__ import print_function +import socket +import sys +import os +import argparse +import logging +import hashlib +import random + +# Commands +FLASH = 0 +SPIFFS = 100 +AUTH = 200 + +# Constants +PROGRESS_BAR_LENGTH = 60 + + +# update_progress(): Displays or updates a console progress bar +def update_progress(progress): + if PROGRESS: + status = "" + if isinstance(progress, int): + progress = float(progress) + if not isinstance(progress, float): + progress = 0 + status = "Error: progress var must be float\r\n" + if progress < 0: + progress = 0 + status = "Halt...\r\n" + if progress >= 1: + progress = 1 + status = "Done...\r\n" + block = int(round(PROGRESS_BAR_LENGTH * progress)) + text = "\rUploading: [{0}] {1}% {2}".format( + "=" * block + " " * (PROGRESS_BAR_LENGTH - block), int(progress * 100), status + ) + sys.stderr.write(text) + sys.stderr.flush() + else: + sys.stderr.write(".") + sys.stderr.flush() + + +def serve(remote_addr, local_addr, remote_port, local_port, password, filename, command=FLASH, progress_handler=False): # noqa: C901 + # Create a TCP/IP socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_address = (local_addr, local_port) + logging.info("Starting on %s:%s", str(server_address[0]), str(server_address[1])) + try: + sock.bind(server_address) + sock.listen(1) + except Exception as e: + logging.error("Listen Failed: %s", str(e)) + return 1 + + content_size = os.path.getsize(filename) + file_md5 = hashlib.md5(open(filename, "rb").read()).hexdigest() + logging.info("Upload size: %d", content_size) + message = "%d %d %d %s\n" % (command, local_port, content_size, file_md5) + + # Wait for a connection + inv_tries = 0 + data = "" + msg = "Sending invitation to %s " % remote_addr + sys.stderr.write(msg) + sys.stderr.flush() + while inv_tries < 10: + inv_tries += 1 + sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + remote_address = (remote_addr, int(remote_port)) + try: + sent = sock2.sendto(message.encode(), remote_address) # noqa: F841 + except: # noqa: E722 + sys.stderr.write("failed\n") + sys.stderr.flush() + sock2.close() + logging.error("Host %s Not Found", remote_addr) + return 1 + sock2.settimeout(TIMEOUT) + try: + data = sock2.recv(37).decode() + break + except: # noqa: E722 + sys.stderr.write(".") + sys.stderr.flush() + sock2.close() + sys.stderr.write("\n") + sys.stderr.flush() + if inv_tries == 10: + logging.error("No response from the ESP") + return 1 + if data != "OK": + if data.startswith("AUTH"): + nonce = data.split()[1] + cnonce_text = "%s%u%s%s" % (filename, content_size, file_md5, remote_addr) + cnonce = hashlib.md5(cnonce_text.encode()).hexdigest() + passmd5 = hashlib.md5(password.encode()).hexdigest() + result_text = "%s:%s:%s" % (passmd5, nonce, cnonce) + result = hashlib.md5(result_text.encode()).hexdigest() + sys.stderr.write("Authenticating...") + sys.stderr.flush() + message = "%d %s %s\n" % (AUTH, cnonce, result) + sock2.sendto(message.encode(), remote_address) + sock2.settimeout(10) + try: + data = sock2.recv(32).decode() + except: # noqa: E722 + sys.stderr.write("FAIL\n") + logging.error("No Answer to our Authentication") + sock2.close() + return 1 + if data != "OK": + sys.stderr.write("FAIL\n") + logging.error("%s", data) + sock2.close() + sys.exit(1) + return 1 + sys.stderr.write("OK\n") + else: + logging.error("Bad Answer: %s", data) + sock2.close() + return 1 + sock2.close() + + logging.info("Waiting for device...") + try: + sock.settimeout(10) + connection, client_address = sock.accept() + sock.settimeout(None) + connection.settimeout(None) + except: # noqa: E722 + logging.error("No response from device") + sock.close() + return 1 + try: + with open(filename, "rb") as f: + if PROGRESS: + progress_handler(0) + else: + sys.stderr.write("Uploading") + sys.stderr.flush() + offset = 0 + while True: + chunk = f.read(1024) + if not chunk: + break + offset += len(chunk) + progress_handler(offset / float(content_size)) + connection.settimeout(10) + try: + connection.sendall(chunk) + res = connection.recv(10) + last_response_contained_ok = "OK" in res.decode() + except Exception as e: + sys.stderr.write("\n") + logging.error("Error Uploading: %s", str(e)) + connection.close() + return 1 + + if last_response_contained_ok: + logging.info("Success") + connection.close() + return 0 + + sys.stderr.write("\n") + logging.info("Waiting for result...") + count = 0 + while count < 5: + count += 1 + connection.settimeout(60) + try: + data = connection.recv(32).decode() + logging.info("Result: %s", data) + + if "OK" in data: + logging.info("Success") + connection.close() + return 0 + + except Exception as e: + logging.error("Error receiving result: %s", str(e)) + connection.close() + return 1 + + logging.error("Error response from device") + connection.close() + return 1 + + finally: + connection.close() + + sock.close() + return 1 + + +def parse_args(unparsed_args): + parser = argparse.ArgumentParser(description="Transmit image over the air to the ESP32 module with OTA support.") + + # destination ip and port + parser.add_argument("-i", "--ip", dest="esp_ip", action="store", help="ESP32 IP Address.", default=False) + parser.add_argument("-I", "--host_ip", dest="host_ip", action="store", help="Host IP Address.", default="0.0.0.0") + parser.add_argument("-p", "--port", dest="esp_port", type=int, help="ESP32 OTA Port. Default: 3232", default=3232) + parser.add_argument( + "-P", + "--host_port", + dest="host_port", + type=int, + help="Host server OTA Port. Default: random 10000-60000", + default=random.randint(10000, 60000), + ) + + # authentication + parser.add_argument("-a", "--auth", dest="auth", help="Set authentication password.", action="store", default="") + + # image + parser.add_argument("-f", "--file", dest="image", help="Image file.", metavar="FILE", default=None) + parser.add_argument( + "-s", + "--spiffs", + dest="spiffs", + action="store_true", + help="Transmit a SPIFFS image and do not flash the module.", + default=False, + ) + + # output + parser.add_argument( + "-d", + "--debug", + dest="debug", + action="store_true", + help="Show debug output. Overrides loglevel with debug.", + default=False, + ) + parser.add_argument( + "-r", + "--progress", + dest="progress", + action="store_true", + help="Show progress output. Does not work for Arduino IDE.", + default=False, + ) + parser.add_argument( + "-t", + "--timeout", + dest="timeout", + type=int, + help="Timeout to wait for the ESP32 to accept invitation.", + default=10, + ) + + return parser.parse_args(unparsed_args) + + +def main(args): + options = parse_args(args) + log_level = logging.WARNING + if options.debug: + log_level = logging.DEBUG + + logging.basicConfig(level=log_level, format="%(asctime)-8s [%(levelname)s]: %(message)s", datefmt="%H:%M:%S") + logging.debug("Options: %s", str(options)) + + # check options + global PROGRESS + PROGRESS = options.progress + + global TIMEOUT + TIMEOUT = options.timeout + + if not options.esp_ip or not options.image: + logging.critical("Not enough arguments.") + return 1 + + command = FLASH + if options.spiffs: + command = SPIFFS + + return serve( + options.esp_ip, options.host_ip, options.esp_port, options.host_port, options.auth, options.image, command + ) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/app/fw_update.py b/app/fw_update.py new file mode 100644 index 0000000..e69de29 diff --git a/app/gui.py b/app/gui.py new file mode 100644 index 0000000..8d3ef62 --- /dev/null +++ b/app/gui.py @@ -0,0 +1,313 @@ +import random +from threading import Thread +import threading +import wx +from zeroconf import ServiceBrowser, Zeroconf +import requests +import os +import webbrowser + +from app import espota +from app.api import ApiHandler +from app.zeroconf_listener import ZeroconfListener + +from espota import FLASH,SPIFFS + +class DevicesPanel(wx.ListCtrl): + def __init__(self, parent): + wx.ListCtrl.__init__(self, parent, style=wx.LC_REPORT) + self.InsertColumn(0, 'Name', width=150) + self.InsertColumn(1, 'Version', width=50) + self.InsertColumn(2, 'SW Revision', width=310) + self.InsertColumn(3, 'HW Revision', width=130) + self.InsertColumn(4, 'IP', width=110) + self.InsertColumn(5, 'FS version', width=110) + +class BTClockOTAUpdater(wx.Frame): + release_name = "" + commit_hash = "" + + def __init__(self, parent, title): + wx.Frame.__init__(self, parent, title=title, size=(800,500)) + self.SetMinSize((800,500)) + ubuntu_it = wx.Font(32, wx.FONTFAMILY_MODERN, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, faceName="Ubuntu") + # "Ubuntu-RI.ttf") + + + + panel = wx.Panel(self) + + self.title = wx.StaticText(panel, label="BTClock OTA firmware updater") + self.title.SetFont(ubuntu_it) + vbox = wx.BoxSizer(wx.VERTICAL) + vbox.Add(self.title, 0, wx.EXPAND | wx.ALL, 20, 0) + + self.device_list = DevicesPanel(panel) + self.device_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_item_selected) + self.device_list.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.on_item_deselected) + + vbox.Add(self.device_list, proportion = 2, flag=wx.EXPAND | wx.ALL, border = 20) + hbox = wx.BoxSizer(wx.HORIZONTAL) + bbox = wx.BoxSizer(wx.HORIZONTAL) + + gs = wx.GridSizer(1, 4, 1, 1) + + self.fw_label = wx.StaticText(panel, label=f"Checking latest version...") + self.update_button = wx.Button(panel, label="Update Firmware") + self.update_button.Bind(wx.EVT_BUTTON, self.on_click_update_firmware) + self.update_fs_button = wx.Button(panel, label="Update Filesystem") + self.update_fs_button.Bind(wx.EVT_BUTTON, self.on_click_update_fs) + + self.identify_button = wx.Button(panel, label="Identify") + self.identify_button.Bind(wx.EVT_BUTTON, self.on_click_identify) + self.open_webif_button = wx.Button(panel, label="Open WebUI") + self.open_webif_button.Bind(wx.EVT_BUTTON, self.on_click_webui) + self.update_button.Disable() + self.update_fs_button.Disable() + self.identify_button.Disable() + self.open_webif_button.Disable() + + hbox.Add(self.fw_label, 1, wx.EXPAND | wx.ALL, 5) + bbox.Add(self.update_button) + bbox.Add(self.update_fs_button) + bbox.Add(self.identify_button) + bbox.Add(self.open_webif_button) + + hbox.AddStretchSpacer() + hbox.Add(bbox, 2, wx.EXPAND | wx.ALL, 5) + vbox.Add(hbox, 0, wx.EXPAND | wx.ALL, 20) + + self.progress_bar = wx.Gauge(panel, range=100) + vbox.Add(self.progress_bar, 0, wx.EXPAND | wx.ALL, 20) + + panel.SetSizer(vbox) + + filemenu= wx.Menu() + menuAbout = filemenu.Append(wx.ID_ABOUT, "&About"," Information about this program") + menuExit = filemenu.Append(wx.ID_EXIT,"E&xit"," Terminate the program") + + menuBar = wx.MenuBar() + menuBar.Append(filemenu,"&File") # Adding the "filemenu" to the MenuBar + self.SetMenuBar(menuBar) # Adding the MenuBar to the Frame content. + + + + self.Bind(wx.EVT_MENU, self.OnAbout, menuAbout) + self.Bind(wx.EVT_MENU, self.OnExit, menuExit) + self.status_bar = self.CreateStatusBar(2) + # self.StatusBar.SetFieldsCount(2) +# self.StatusBar.SetStatusWidths(-3, -1) + self.Show(True) + + self.zeroconf = Zeroconf() + self.listener = ZeroconfListener(self.on_zeroconf_state_change) + self.browser = ServiceBrowser(self.zeroconf, "_http._tcp.local.", self.listener) + self.api_handler = ApiHandler() + + wx.CallAfter(self.fetch_latest_release) + + def on_item_selected(self, event): + self.update_button.Enable() + self.update_fs_button.Enable() + self.identify_button.Enable() + self.open_webif_button.Enable() + + def on_item_deselected(self, event): + if self.device_list.GetFirstSelected() == -1: + self.update_button.Disable() + self.update_fs_button.Disable() + self.identify_button.Disable() + self.open_webif_button.Disable() + + def on_zeroconf_state_change(self, type, name, state, info): + index = self.device_list.FindItem(0, name) + + if state == "Added": + if index == wx.NOT_FOUND: + index = self.device_list.InsertItem(self.device_list.GetItemCount(), type) + self.device_list.SetItem(index, 0, name) + self.device_list.SetItem(index, 1, info.properties.get(b"version").decode()) + self.device_list.SetItem(index, 2, info.properties.get(b"rev").decode()) + if (info.properties.get(b"hw_rev") is not None): + self.device_list.SetItem(index, 3, info.properties.get(b"hw_rev").decode()) + self.device_list.SetItem(index, 4, info.parsed_addresses()[0]) + + else: + self.device_list.SetItem(index, 0, name) + self.device_list.SetItem(index, 1, info.properties.get(b"version").decode()) + self.device_list.SetItem(index, 2, info.properties.get(b"rev").decode()) + if (info.properties.get(b"hw_rev").decode()): + self.device_list.SetItem(index, 3, info.properties.get(b"hw_rev").decode()) + self.device_list.SetItem(index, 4, info.parsed_addresses()[0]) + self.device_list.SetItem(index, 5, self.api_handler.check_fs_hash(info.parsed_addresses()[0])) + elif state == "Removed": + if index != wx.NOT_FOUND: + self.device_list.DeleteItem(index) + + def on_click_update_firmware(self, event): + selected_index = self.device_list.GetFirstSelected() + if selected_index != -1: + service_name = self.device_list.GetItemText(selected_index, 0) + hw_rev = self.device_list.GetItemText(selected_index, 3) + + info = self.listener.services.get(service_name) + if info: + address = info.parsed_addresses()[0] if info.parsed_addresses() else "N/A" + self.start_firmware_update(address, hw_rev) + else: + wx.MessageBox("No service information available for selected device", "Error", wx.ICON_ERROR) + else: + wx.MessageBox("Please select a device to update", "Error", wx.ICON_ERROR) + + def on_click_webui(self, event): + selected_index = self.device_list.GetFirstSelected() + if selected_index != -1: + service_name = self.device_list.GetItemText(selected_index, 0) + info = self.listener.services.get(service_name) + if info: + address = info.parsed_addresses()[0] if info.parsed_addresses() else "N/A" + thread = threading.Thread(target=lambda: webbrowser.open(f"http://{address}")) + thread.start() + + def run_fs_update(self, address, firmware_file, type): + global PROGRESS + PROGRESS = True + espota.PROGRESS = True + global TIMEOUT + TIMEOUT = 10 + espota.TIMEOUT = 10 + + espota.serve(address, "0.0.0.0", 3232, random.randint(10000,60000), "", firmware_file, type, self.call_progress) + + wx.CallAfter(self.update_progress, 100) + self.SetStatusText(f"Finished!") + + def call_progress(self, progress): + progressPerc = int(progress*100) + self.SetStatusText(f"Progress: {progressPerc}%") + wx.CallAfter(self.update_progress, progress) + + def update_progress(self, progress): + self.progress_bar.SetValue(int(progress*100)) + wx.YieldIfNeeded() + + def on_click_update_fs(self, event): + selected_index = self.device_list.GetFirstSelected() + if selected_index != -1: + service_name = self.device_list.GetItemText(selected_index, 0) + info = self.listener.services.get(service_name) + if info: + address = info.parsed_addresses()[0] if info.parsed_addresses() else "N/A" + self.start_fs_update(address) + else: + wx.MessageBox("No service information available for selected device", "Error", wx.ICON_ERROR) + else: + wx.MessageBox("Please select a device to update", "Error", wx.ICON_ERROR) + + def start_firmware_update(self, address, hw_rev): + self.SetStatusText(f"Starting firmware update") + + model_name = "lolin_s3_mini_213epd" + if (hw_rev == "REV_B_EPD_2_13"): + model_name = "btclock_rev_b_213epd" + + local_filename = f"firmware/{self.release_name}_{model_name}_firmware.bin" + + if os.path.exists(os.path.abspath(local_filename)): + thread = Thread(target=self.run_fs_update, args=(address, os.path.abspath(local_filename), FLASH)) + thread.start() + + def start_fs_update(self, address): + # Path to the firmware file + self.SetStatusText(f"Starting filesystem update") + local_filename = f"firmware/{self.release_name}_littlefs.bin" + + if os.path.exists(os.path.abspath(local_filename)): + thread = Thread(target=self.run_fs_update, args=(address, os.path.abspath(local_filename), SPIFFS)) + thread.start() + + wx.CallAfter(self.update_progress, 100) + + def on_click_identify(self, event): + selected_index = self.device_list.GetFirstSelected() + if selected_index != -1: + service_name = self.device_list.GetItemText(selected_index, 0) + info = self.listener.services.get(service_name) + if info: + address = info.parsed_addresses()[0] if info.parsed_addresses() else "N/A" + port = info.port + self.api_handler.identify_btclock(address) + else: + wx.MessageBox("No service information available for selected device", "Error", wx.ICON_ERROR) + else: + wx.MessageBox("Please select a device to make an API call", "Error", wx.ICON_ERROR) + + def fetch_latest_release(self): + repo = "btclock/btclock_v3" + + filenames_to_download = ["lolin_s3_mini_213epd_firmware.bin", "btclock_rev_b_213epd_firmware.bin", "littlefs.bin"] + url = f"https://api.github.com/repos/{repo}/releases/latest" + try: + response = requests.get(url) + response.raise_for_status() + latest_release = response.json() + release_name = latest_release['tag_name'] + self.release_name = release_name + + + asset_url = None + asset_urls = [] + for asset in latest_release['assets']: + if asset['name'] in filenames_to_download: + asset_urls.append(asset['browser_download_url']) + if asset_urls: + for asset_url in asset_urls: + self.download_file(asset_url, release_name) + ref_url = f"https://api.github.com/repos/{repo}/git/ref/tags/{release_name}" + response = requests.get(ref_url) + response.raise_for_status() + ref_info = response.json() + self.commit_hash = ref_info["object"]["sha"] + self.fw_label.SetLabelText(f"Downloaded firmware version: {self.release_name}\nCommit: {self.commit_hash}") + + + else: + wx.CallAfter(self.SetStatusText, f"File {filenames_to_download} not found in latest release") + except requests.RequestException as e: + wx.CallAfter(self.SetStatusText, f"Error fetching release: {e}") + + + + def download_file(self, url, release_name): + local_filename = f"{release_name}_{url.split('/')[-1]}" + response = requests.get(url, stream=True) + total_length = response.headers.get('content-length') + + if os.path.exists(f"firmware/{local_filename}"): + wx.CallAfter(self.SetStatusText, f"{local_filename} is already downloaded") + return + + if total_length is None: + wx.CallAfter(self.SetStatusText, "No content length header") + else: + total_length = int(total_length) + chunk_size = 1024 + num_chunks = total_length // chunk_size + with open(f"firmware/{local_filename}", 'wb') as f: + for i, chunk in enumerate(response.iter_content(chunk_size=chunk_size)): + if chunk: + f.write(chunk) + f.flush() + progress = int((i / num_chunks) * 100) + wx.CallAfter(self.update_progress, progress) + + wx.CallAfter(self.update_progress, 100) + wx.CallAfter(self.SetStatusText, "Download completed") + + def OnAbout(self,e): + dlg = wx.MessageDialog( self, "An updater for BTClocks", "About BTClock OTA Updater", wx.OK) + dlg.ShowModal() + dlg.Destroy() + + def OnExit(self,e): + self.Close(True) \ No newline at end of file diff --git a/app/zeroconf_listener.py b/app/zeroconf_listener.py new file mode 100644 index 0000000..21204ea --- /dev/null +++ b/app/zeroconf_listener.py @@ -0,0 +1,29 @@ +import wx +from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange + +class ZeroconfListener: + release_name = "" + firmware_file = "" + + def __init__(self, update_callback): + self.update_callback = update_callback + # self.update_service = update_callback + self.services = {} + + def update_service(self, zc: Zeroconf, type: str, name: str) -> None: + if (name.startswith('btclock-')): + info = zc.get_service_info(type, name) + self.services[name] = info + wx.CallAfter(self.update_callback, type, name, "Added", info) + + def remove_service(self, zeroconf, type, name): + if name in self.services: + del self.services[name] + + wx.CallAfter(self.update_callback, type, name, "Removed") + + def add_service(self, zeroconf, type, name): + if (name.startswith('btclock-')): + info = zeroconf.get_service_info(type, name) + self.services[name] = info + wx.CallAfter(self.update_callback, type, name, "Added", info) \ No newline at end of file diff --git a/firmware/.gitkeep b/firmware/.gitkeep new file mode 100644 index 0000000..05c5808 --- /dev/null +++ b/firmware/.gitkeep @@ -0,0 +1,270 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,macos,windows,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,macos,windows,linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Python ### +# 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/ +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/ +cover/ + +# 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 +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .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 + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__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/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,macos,windows,linux +firmware/*.bin \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..111d6d1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Requests==2.31.0 +wxPython==4.2.1 +zeroconf==0.132.2 diff --git a/update-icon.icns b/update-icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..4766bf896c4d95b7f815b25bbad66a0d57ce81b2 GIT binary patch literal 75765 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 z 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 literal 0 HcmV?d00001 diff --git a/update-icon.png b/update-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..476363a093ef6945fb6428b7eaa9d4e4d8b55cc7 GIT binary patch literal 8511 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 literal 0 HcmV?d00001