diff --git a/.gitignore b/.gitignore index 000754a..ea36021 100644 --- a/.gitignore +++ b/.gitignore @@ -266,4 +266,5 @@ $RECYCLE.BIN/ *.lnk # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,macos,windows,linux -firmware/*.bin \ No newline at end of file +firmware/*.bin +firmware/*.json \ No newline at end of file diff --git a/README.md b/README.md index 7ac1800..45208d8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # BTClock OTA Flasher interface +![Screenshot Windows](screenshot-win.webp) +![Screenshot Mac](screenshot-mac.webp) + ## Instructions - Make sure you have Python (tested with Python 3.12) - Run `pip3 install -r requirements.txt` @@ -17,7 +20,7 @@ pyinstaller --hidden-import zeroconf._utils.ipaddress --hidden-import zeroconf._ ### Windows ```` -pyinstaller.exe --hidden-import zeroconf._utils.ipaddress --hidden-import zeroconf._handlers.answers --hidden-import pyserial -n BTClockOTA --windowed --onefile app.py +pyinstaller.exe BTClockOTA.spec ```` ### Linux diff --git a/app/main.py b/app/main.py index 93383ec..3c0c173 100644 --- a/app/main.py +++ b/app/main.py @@ -17,6 +17,7 @@ from app.zeroconf_listener import ZeroconfListener from app.espota import FLASH, SPIFFS + class SerialPortsComboBox(wx.ComboBox): def __init__(self, parent, fw_update): self.fw_update = fw_update @@ -51,11 +52,11 @@ class BTClockOTAUpdater(wx.Frame): self.fw_label = wx.StaticText( panel, label=f"Fetching latest version from GitHub...") hbox.Add(self.fw_label, 1, wx.EXPAND | wx.ALL, 5) - + self.actionButtons = ActionButtonPanel( panel, self) hbox.AddStretchSpacer() - + hbox.Add(self.actionButtons, 2, wx.EXPAND | wx.ALL, 5) vbox.Add(hbox, 0, wx.EXPAND | wx.ALL, 20) @@ -95,12 +96,19 @@ class BTClockOTAUpdater(wx.Frame): info.parsed_addresses()[0]) version = info.properties.get(b"rev").decode() + fsHash = "Too old" + hwRev = "REV_A_EPD_2_13" if 'gitTag' in deviceSettings: version = deviceSettings["gitTag"] + if 'fsRev' in deviceSettings: + fsHash = deviceSettings['fsRev'][:7] + + if (info.properties.get(b"hw_rev") is not None): + hwRev = info.properties.get(b"hw_rev").decode() + fwHash = info.properties.get(b"rev").decode()[:7] - fsHash = deviceSettings['fsRev'][:7] address = info.parsed_addresses()[0] if index == wx.NOT_FOUND: @@ -109,23 +117,19 @@ class BTClockOTAUpdater(wx.Frame): self.device_list.SetItem(index, 0, name) self.device_list.SetItem(index, 1, version) self.device_list.SetItem(index, 2, fwHash) - 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, 3, hwRev) self.device_list.SetItem(index, 4, address) else: self.device_list.SetItem(index, 0, name) self.device_list.SetItem(index, 1, version) self.device_list.SetItem(index, 2, fwHash) - 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, 3, hwRev) self.device_list.SetItem(index, 4, address) self.device_list.SetItem(index, 5, fsHash) self.device_list.SetItemData(index, index) self.device_list.itemDataMap[index] = [ - name, version, fwHash, info.properties.get(b"hw_rev").decode(), address, fsHash] + name, version, fwHash, hwRev, address, fsHash] for col in range(0, len(self.device_list.column_headings)): self.device_list.SetColumnWidth( col, wx.LIST_AUTOSIZE_USEHEADER) diff --git a/app/release_checker.py b/app/release_checker.py index 0e4ec9e..dcc2d66 100644 --- a/app/release_checker.py +++ b/app/release_checker.py @@ -1,10 +1,15 @@ +import json import os import requests import wx from typing import Callable +from datetime import datetime, timedelta from app.utils import keep_latest_versions +CACHE_FILE = 'firmware/cache.json' +CACHE_DURATION = timedelta(minutes=30) + class ReleaseChecker: '''Release Checker for firmware updates''' @@ -14,53 +19,87 @@ class ReleaseChecker: def __init__(self): self.progress_callback: Callable[[int], None] = None + def load_cache(self): + '''Load cached data from file''' + if os.path.exists(CACHE_FILE): + with open(CACHE_FILE, 'r') as f: + return json.load(f) + return {} + + def save_cache(self, cache_data): + '''Save cache data to file''' + with open(CACHE_FILE, 'w') as f: + json.dump(cache_data, f) + def fetch_latest_release(self): '''Fetch latest firmware release from GitHub''' repo = "btclock/btclock_v3" + cache = self.load_cache() + now = datetime.now() if not os.path.exists("firmware"): os.makedirs("firmware") + + if 'latest_release' in cache and (now - datetime.fromisoformat(cache['latest_release']['timestamp'])) < CACHE_DURATION: + latest_release = cache['latest_release']['data'] + else: + url = f"https://api.github.com/repos/{repo}/releases/latest" + try: + response = requests.get(url) + response.raise_for_status() + latest_release = response.json() + cache['latest_release'] = { + 'data': latest_release, + 'timestamp': now.isoformat() + } + self.save_cache(cache) + except requests.RequestException as e: + raise ReleaseCheckerException( + f"Error fetching release: {e}") from e + + release_name = latest_release['tag_name'] + self.release_name = release_name + 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}" + asset_urls = [asset['browser_download_url'] + for asset in latest_release['assets'] if asset['name'] in filenames_to_download] + + 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}" + if ref_url in cache and (now - datetime.fromisoformat(cache[ref_url]['timestamp'])) < CACHE_DURATION: + commit_hash = cache[ref_url]['data'] + + else: response = requests.get(ref_url) response.raise_for_status() ref_info = response.json() - if (ref_info["object"]["type"] == "commit"): - self.commit_hash = ref_info["object"]["sha"] + if ref_info["object"]["type"] == "commit": + commit_hash = ref_info["object"]["sha"] else: tag_url = f"https://api.github.com/repos/{ - repo}/git/tags/{ref_info["object"]["sha"]}" + repo}/git/tags/{ref_info['object']['sha']}" response = requests.get(tag_url) response.raise_for_status() tag_info = response.json() - self.commit_hash = tag_info["object"]["sha"] + commit_hash = tag_info["object"]["sha"] + cache[ref_url] = { + 'data': commit_hash, + 'timestamp': now.isoformat() + } + self.save_cache(cache) - return self.release_name + self.commit_hash = commit_hash - else: - raise ReleaseCheckerException( - f"File {filenames_to_download} not found in latest release") - except requests.RequestException as e: + return self.release_name + else: raise ReleaseCheckerException( - f"Error fetching release: {e}") from e + f"File {filenames_to_download} not found in latest release") def download_file(self, url, release_name): '''Downloads Fimware Files''' diff --git a/screenshot-mac.webp b/screenshot-mac.webp new file mode 100644 index 0000000..c39631e Binary files /dev/null and b/screenshot-mac.webp differ diff --git a/screenshot-win.webp b/screenshot-win.webp new file mode 100644 index 0000000..6ddf75e Binary files /dev/null and b/screenshot-win.webp differ