Compare commits
10 commits
Author | SHA1 | Date | |
---|---|---|---|
c8c69a39b4 | |||
807d4d0585 | |||
cd5f999cda | |||
3452a924f9 | |||
86b4b50b99 | |||
46da0c049b | |||
c820fb9421 | |||
7dfed6af6c | |||
3cd9fcef46 | |||
018b0431df |
7 changed files with 228 additions and 44 deletions
144
.forgejo/workflows/build_all.yaml
Normal file
144
.forgejo/workflows/build_all.yaml
Normal file
|
@ -0,0 +1,144 @@
|
|||
name: Build all artifacts and make release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build:
|
||||
description: 'Select build type'
|
||||
required: true
|
||||
default: 'all'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- mac
|
||||
- windows
|
||||
- linux
|
||||
|
||||
jobs:
|
||||
build-macos:
|
||||
if: ${{ github.event.inputs.build == 'all' || github.event.inputs.build == 'mac' }}
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Download universal2 Python
|
||||
run: |
|
||||
curl -o python.pkg https://www.python.org/ftp/python/3.12.4/python-3.12.4-macos11.pkg
|
||||
- name: Install Python
|
||||
run: |
|
||||
sudo installer -pkg python.pkg -target /
|
||||
- name: Add Python to PATH
|
||||
run: |
|
||||
echo "/Library/Frameworks/Python.framework/Versions/3.12/bin" >> $GITHUB_PATH
|
||||
- name: Verify Python installation
|
||||
run: |
|
||||
python3 --version
|
||||
pip3 --version
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip3 install --upgrade pip
|
||||
pip3 install pyinstaller
|
||||
pip3 install --no-cache cffi --no-binary :all:
|
||||
pip3 install --no-cache charset_normalizer --no-binary :all:
|
||||
pip3 install -U --pre -f https://wxpython.org/Phoenix/snapshot-builds/ wxPython
|
||||
pip3 install -r requirements.txt
|
||||
- name: Build with PyInstaller
|
||||
run: |
|
||||
pyinstaller BTClockOTA-universal.spec
|
||||
- name: Zip the app bundle
|
||||
run: |
|
||||
cd dist
|
||||
zip -r BTClockOTA-macos-universal2.zip BTClockOTA.app
|
||||
- name: Create DMG
|
||||
run: |
|
||||
mkdir dmg_temp
|
||||
cp -R dist/BTClockOTA.app dmg_temp/
|
||||
# Create the DMG file
|
||||
hdiutil create -volname "BTClockOTA" -srcfolder dmg_temp -ov -format UDZO "dist/BTClockOTA-universal.dmg"
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-artifacts
|
||||
path: |
|
||||
dist/*
|
||||
!dist/BTClockOTA.app
|
||||
build-windows:
|
||||
if: ${{ github.event.inputs.build == 'all' || github.event.inputs.build == 'windows' }}
|
||||
runs-on: docker-amd64
|
||||
container:
|
||||
image: batonogov/pyinstaller-windows:latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Build with PyInstaller
|
||||
run: |
|
||||
python -m PyInstaller $SPECFILE
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-artifacts
|
||||
path: dist/
|
||||
build-linux:
|
||||
if: ${{ github.event.inputs.build == 'all' || github.event.inputs.build == 'linux' }}
|
||||
runs-on: docker-amd64
|
||||
container:
|
||||
image: ghcr.io/btclock/pyinstaller-wxpython-linux:latest
|
||||
credentials:
|
||||
username: dsbaars
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Build with PyInstaller
|
||||
run: |
|
||||
python -m PyInstaller $SPECFILE &&
|
||||
mv dist/BTClockOTA dist/BTClockOTA-linux-amd64
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-artifacts
|
||||
path: dist/
|
||||
release:
|
||||
needs: [build-macos, build-windows, build-linux]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Get current block
|
||||
id: getBlockHeight
|
||||
run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT
|
||||
- name: Get Windows Artifacts
|
||||
if: ${{ always() }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-artifacts
|
||||
path: windows
|
||||
- name: Get macOS Artifacts
|
||||
if: ${{ always() }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: macos-artifacts
|
||||
path: macos
|
||||
- name: Get Linux Artifacts
|
||||
if: ${{ always() }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: linux-artifacts
|
||||
path: linux
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
|
||||
commit: main
|
||||
name: release-${{ steps.getBlockHeight.outputs.blockHeight }}
|
||||
artifacts: "macos/**/*.dmg,macos/**/*.zip,windows/**/*.exe,linux/*"
|
||||
allowUpdates: true
|
||||
makeLatest: true
|
11
app.py
11
app.py
|
@ -1,7 +1,8 @@
|
|||
from app.main import BTClockOTAUpdater
|
||||
import wx
|
||||
|
||||
from app.main import BTClockOTAUpdater
|
||||
|
||||
app = wx.App(False)
|
||||
frame = BTClockOTAUpdater(None, 'BTClock OTA updater')
|
||||
app.MainLoop()
|
||||
if __name__ == "__main__":
|
||||
app = wx.App(False)
|
||||
frame = BTClockOTAUpdater(None, 'BTClock OTA updater')
|
||||
|
||||
app.MainLoop()
|
||||
|
|
|
@ -7,6 +7,8 @@ import esptool
|
|||
import serial
|
||||
import wx
|
||||
|
||||
from app.utils import get_app_data_folder
|
||||
|
||||
|
||||
class FwUpdater:
|
||||
update_progress = None
|
||||
|
@ -68,11 +70,16 @@ class FwUpdater:
|
|||
def start_firmware_update(self, release_name, 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"
|
||||
hw_rev_to_model = {
|
||||
"REV_B_EPD_2_13": "btclock_rev_b_213epd",
|
||||
"REV_V8_EPD_2_13": "btclock_v8_213epd",
|
||||
"REV_A_EPD_2_9": "lolin_s3_mini_29epd"
|
||||
}
|
||||
|
||||
local_filename = f"firmware/{
|
||||
model_name = hw_rev_to_model.get(hw_rev, "lolin_s3_mini_213epd")
|
||||
|
||||
|
||||
local_filename = f"{get_app_data_folder()}/{
|
||||
release_name}_{model_name}_firmware.bin"
|
||||
|
||||
self.updatingName = address
|
||||
|
@ -86,18 +93,26 @@ class FwUpdater:
|
|||
address, os.path.abspath(local_filename), FLASH))
|
||||
thread.start()
|
||||
|
||||
def start_fs_update(self, release_name, address):
|
||||
def start_fs_update(self, release_name, address, hw_rev):
|
||||
hw_rev_to_model = {
|
||||
"REV_B_EPD_2_13": "littlefs_8MB",
|
||||
"REV_V8_EPD_2_13": "littlefs_16MB",
|
||||
"REV_A_EPD_2_9": "littlefs_4MB"
|
||||
}
|
||||
|
||||
# Path to the firmware file
|
||||
local_filename = f"firmware/{release_name}_littlefs.bin"
|
||||
local_filename = f"{get_app_data_folder()}/{release_name}_{hw_rev_to_model.get(hw_rev, "littlefs_4MB")}.bin"
|
||||
|
||||
self.updatingName = address
|
||||
self.currentlyUpdating = True
|
||||
|
||||
if self.event_cb is not None:
|
||||
self.event_cb("Starting WebUI update")
|
||||
self.event_cb(f"Starting WebUI update {local_filename}")
|
||||
|
||||
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()
|
||||
|
||||
else:
|
||||
if self.event_cb is not None:
|
||||
self.event_cb(f"Firmware file not found: {local_filename}")
|
||||
|
|
|
@ -27,7 +27,7 @@ class ActionButtonPanel(wx.Panel):
|
|||
|
||||
self.update_button = wx.Button(self, label="Update Firmware")
|
||||
self.update_button.Bind(wx.EVT_BUTTON, self.on_click_update_firmware)
|
||||
self.update_fs_button = wx.Button(self, label="Update Filesystem")
|
||||
self.update_fs_button = wx.Button(self, label="Update WebUI")
|
||||
self.update_fs_button.Bind(wx.EVT_BUTTON, self.on_click_update_fs)
|
||||
|
||||
self.identify_button = wx.Button(self, label="Identify")
|
||||
|
@ -80,6 +80,7 @@ class ActionButtonPanel(wx.Panel):
|
|||
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 self.currentlyUpdating:
|
||||
wx.MessageBox("Please wait, already updating",
|
||||
|
@ -89,7 +90,7 @@ class ActionButtonPanel(wx.Panel):
|
|||
if info:
|
||||
address = info.parsed_addresses(
|
||||
)[0] if info.parsed_addresses() else "N/A"
|
||||
self.parent_frame.fw_updater.start_fs_update(self.parent_frame.releaseChecker.release_name, address)
|
||||
self.parent_frame.fw_updater.start_fs_update(self.parent_frame.releaseChecker.release_name, address, hw_rev)
|
||||
else:
|
||||
wx.MessageBox(
|
||||
"No service information available for selected device", "Error", wx.ICON_ERROR)
|
||||
|
|
27
app/main.py
27
app/main.py
|
@ -1,5 +1,6 @@
|
|||
import concurrent.futures
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import serial
|
||||
from app.gui.action_button_panel import ActionButtonPanel
|
||||
|
@ -15,11 +16,14 @@ from app import espota
|
|||
from app.api import ApiHandler
|
||||
from app.fw_updater import FwUpdater
|
||||
from app.gui.devices_panel import DevicesPanel
|
||||
from app.utils import get_app_data_folder
|
||||
from app.zeroconf_listener import ZeroconfListener
|
||||
|
||||
from app.espota import FLASH, SPIFFS
|
||||
|
||||
|
||||
class BTClockOTAApp(wx.App):
|
||||
def OnInit(self):
|
||||
return True
|
||||
class RichTextCtrlHandler(logging.Handler):
|
||||
def __init__(self, ctrl):
|
||||
super().__init__()
|
||||
|
@ -27,7 +31,7 @@ class RichTextCtrlHandler(logging.Handler):
|
|||
|
||||
def emit(self, record):
|
||||
msg = self.format(record)
|
||||
wx.CallAfter(self.append_text, msg + '\n')
|
||||
wx.CallAfter(self.append_text, "\n" + msg)
|
||||
|
||||
def append_text(self, text):
|
||||
self.ctrl.AppendText(text)
|
||||
|
@ -46,6 +50,7 @@ class BTClockOTAUpdater(wx.Frame):
|
|||
|
||||
def __init__(self, parent, title):
|
||||
wx.Frame.__init__(self, parent, title=title, size=(800, 500))
|
||||
|
||||
self.SetMinSize((800, 500))
|
||||
self.releaseChecker = ReleaseChecker()
|
||||
self.zeroconf = Zeroconf()
|
||||
|
@ -62,7 +67,7 @@ class BTClockOTAUpdater(wx.Frame):
|
|||
self.log_ctrl.SetFont(monospace_font)
|
||||
|
||||
handler = RichTextCtrlHandler(self.log_ctrl)
|
||||
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
||||
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', '%H:%M:%S'))
|
||||
logging.getLogger().addHandler(handler)
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
@ -93,7 +98,7 @@ class BTClockOTAUpdater(wx.Frame):
|
|||
self.setup_ui()
|
||||
|
||||
wx.CallAfter(self.fetch_latest_release_async)
|
||||
|
||||
wx.YieldIfNeeded()
|
||||
def setup_ui(self):
|
||||
self.setup_menubar()
|
||||
self.status_bar = self.CreateStatusBar(2)
|
||||
|
@ -102,6 +107,8 @@ class BTClockOTAUpdater(wx.Frame):
|
|||
|
||||
def setup_menubar(self):
|
||||
filemenu = wx.Menu()
|
||||
menuOpenDownloadDir = filemenu.Append(
|
||||
wx.ID_OPEN, "&Open Download Dir", " Open the directory with firmware files and cache")
|
||||
menuAbout = filemenu.Append(
|
||||
wx.ID_ABOUT, "&About", " Information about this program")
|
||||
menuExit = filemenu.Append(
|
||||
|
@ -109,8 +116,9 @@ class BTClockOTAUpdater(wx.Frame):
|
|||
|
||||
menuBar = wx.MenuBar()
|
||||
menuBar.Append(filemenu, "&File")
|
||||
self.SetMenuBar(menuBar)
|
||||
|
||||
self.SetMenuBar(menuBar)
|
||||
self.Bind(wx.EVT_MENU, self.OnOpenDownloadFolder, menuOpenDownloadDir)
|
||||
self.Bind(wx.EVT_MENU, self.OnAbout, menuAbout)
|
||||
self.Bind(wx.EVT_MENU, self.OnExit, menuExit)
|
||||
|
||||
|
@ -175,6 +183,9 @@ class BTClockOTAUpdater(wx.Frame):
|
|||
|
||||
def fetch_latest_release_async(self):
|
||||
# Start a new thread to execute fetch_latest_release
|
||||
app_folder = get_app_data_folder()
|
||||
if not os.path.exists(app_folder):
|
||||
os.makedirs(app_folder)
|
||||
executor = concurrent.futures.ThreadPoolExecutor()
|
||||
future = executor.submit(self.releaseChecker.fetch_latest_release)
|
||||
future.add_done_callback(self.handle_latest_release)
|
||||
|
@ -186,7 +197,11 @@ class BTClockOTAUpdater(wx.Frame):
|
|||
latest_release}\nCommit: {self.releaseChecker.commit_hash}")
|
||||
except Exception as e:
|
||||
self.fw_label.SetLabel(f"Error occurred: {str(e)}")
|
||||
|
||||
traceback.print_tb(e.__traceback__)
|
||||
|
||||
def OnOpenDownloadFolder(self, e):
|
||||
wx.LaunchDefaultBrowser(get_app_data_folder())
|
||||
|
||||
def OnAbout(self, e):
|
||||
dlg = wx.MessageDialog(
|
||||
self, "An updater for BTClocks", "About BTClock OTA Updater", wx.OK)
|
||||
|
|
|
@ -6,11 +6,12 @@ import wx
|
|||
from typing import Callable
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.utils import keep_latest_versions
|
||||
from app.utils import get_app_data_folder, keep_latest_versions
|
||||
|
||||
CACHE_FILE = 'firmware/cache.json'
|
||||
CACHE_FILE = get_app_data_folder() + '/cache.json'
|
||||
CACHE_DURATION = timedelta(minutes=30)
|
||||
|
||||
LATEST_RELEASE_ENDPOINT = "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/tags"
|
||||
|
||||
class ReleaseChecker:
|
||||
'''Release Checker for firmware updates'''
|
||||
|
@ -38,13 +39,12 @@ class ReleaseChecker:
|
|||
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"
|
||||
# url = f"https://api.github.com/repos/{repo}/releases/latest"
|
||||
url = f"https://git.btclock.dev/api/v1/repos/{repo}/releases/latest"
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
|
@ -62,7 +62,10 @@ class ReleaseChecker:
|
|||
self.release_name = release_name
|
||||
|
||||
filenames_to_download = ["lolin_s3_mini_213epd_firmware.bin",
|
||||
"btclock_rev_b_213epd_firmware.bin", "littlefs.bin"]
|
||||
"lolin_s3_mini_29epd_firmware.bin",
|
||||
"btclock_v8_213epd_firmware.bin",
|
||||
"btclock_rev_b_213epd_firmware.bin",
|
||||
"littlefs_4MB.bin", "littlefs_8MB.bin", "littlefs_16MB.bin"]
|
||||
|
||||
asset_urls = [asset['browser_download_url']
|
||||
for asset in latest_release['assets'] if asset['name'] in filenames_to_download]
|
||||
|
@ -71,8 +74,9 @@ class ReleaseChecker:
|
|||
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}"
|
||||
ref_url = f"https://git.btclock.dev/api/v1/repos/{repo}/tags/{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']
|
||||
|
||||
|
@ -80,15 +84,8 @@ class ReleaseChecker:
|
|||
response = requests.get(ref_url)
|
||||
response.raise_for_status()
|
||||
ref_info = response.json()
|
||||
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']}"
|
||||
response = requests.get(tag_url)
|
||||
response.raise_for_status()
|
||||
tag_info = response.json()
|
||||
commit_hash = tag_info["object"]["sha"]
|
||||
commit_hash = ref_info["commit"]["sha"]
|
||||
|
||||
cache[ref_url] = {
|
||||
'data': commit_hash,
|
||||
'timestamp': now.isoformat()
|
||||
|
@ -106,14 +103,12 @@ class ReleaseChecker:
|
|||
'''Downloads Fimware Files'''
|
||||
local_filename = f"{release_name}_{url.split('/')[-1]}"
|
||||
|
||||
if not os.path.exists("firmware"):
|
||||
os.makedirs("firmware")
|
||||
if os.path.exists(f"firmware/{local_filename}"):
|
||||
if os.path.exists(f"{get_app_data_folder()}/{local_filename}"):
|
||||
return
|
||||
|
||||
response = requests.get(url, stream=True)
|
||||
total_length = response.headers.get('content-length')
|
||||
keep_latest_versions('firmware', 2)
|
||||
keep_latest_versions(get_app_data_folder(), 2)
|
||||
|
||||
if total_length is None:
|
||||
raise ReleaseCheckerException("No content length header")
|
||||
|
@ -121,7 +116,7 @@ class ReleaseChecker:
|
|||
total_length = int(total_length)
|
||||
chunk_size = 1024
|
||||
num_chunks = total_length // chunk_size
|
||||
with open(f"firmware/{local_filename}", 'wb') as f:
|
||||
with open(f"{get_app_data_folder()}/{local_filename}", 'wb') as f:
|
||||
for i, chunk in enumerate(response.iter_content(chunk_size=chunk_size)):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
|
|
15
app/utils.py
15
app/utils.py
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
import re
|
||||
import shutil
|
||||
import wx
|
||||
|
||||
|
||||
def count_versions(folder_path):
|
||||
|
@ -30,3 +30,16 @@ def keep_latest_versions(folder_path, num_versions_to_keep=2):
|
|||
for version in versions_to_remove:
|
||||
for file_name in version_files[version]:
|
||||
os.remove(os.path.join(folder_path, file_name))
|
||||
|
||||
def get_app_data_folder():
|
||||
app = wx.GetApp()
|
||||
if app is None:
|
||||
app = wx.App(False)
|
||||
standard_paths = wx.StandardPaths.Get()
|
||||
app_data_dir = standard_paths.GetAppDocumentsDir() + "/BTClockOTA"
|
||||
app.Destroy()
|
||||
return app_data_dir
|
||||
else:
|
||||
standard_paths = wx.StandardPaths.Get()
|
||||
app_data_dir = standard_paths.GetAppDocumentsDir() + "/BTClockOTA"
|
||||
return app_data_dir
|
Loading…
Add table
Reference in a new issue