Compare commits

...

20 commits
847264 ... main

Author SHA1 Message Date
c8c69a39b4 More workflow fixes 2024-12-31 12:41:11 +01:00
807d4d0585 Add ghcr authentication 2024-12-31 12:20:20 +01:00
cd5f999cda Use docker images directly instead of dind 2024-12-31 12:15:13 +01:00
3452a924f9 Fix workflow 2024-12-31 12:02:53 +01:00
86b4b50b99 add forgejo workflow 2024-12-31 12:01:50 +01:00
46da0c049b Update for new filenames 2024-12-31 11:52:58 +01:00
c820fb9421 Change endpoint to btclock git 2024-11-26 01:06:00 +01:00
7dfed6af6c Change API to rof.tools mirrors 2024-11-17 21:27:50 -06:00
3cd9fcef46 Add more exotic versions to HW detection 2024-09-21 18:51:41 +02:00
018b0431df Improve buttons, use application data dir for cache and downloads 2024-06-10 20:39:39 +02:00
504199a8c9 Add hidden imports 2024-06-10 15:43:08 +02:00
9fb7fb433f Add console control, bugfixes and windows debug build 2024-06-10 15:31:13 +02:00
6f53026c38 Add build linux to build all 2024-06-10 13:23:14 +02:00
e5bddaa8b1 Add linux build 2024-06-10 13:11:10 +02:00
fee2992e86 All in one file 2024-06-10 01:20:47 +02:00
a83c50ab41 Trying it different 2024-06-10 01:12:41 +02:00
4649feda74 Extension fix 2024-06-10 01:04:56 +02:00
7872142ea0 Checkout repo 2024-06-10 01:01:25 +02:00
f84e27eeaf Add extensions 2024-06-10 01:00:06 +02:00
1898bec5bb Improve workflow 2024-06-10 00:56:42 +02:00
15 changed files with 530 additions and 86 deletions

View 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

154
.github/workflows/build_all.yaml vendored Normal file
View file

@ -0,0 +1,154 @@
name: Build all artifacts and make release
on: workflow_dispatch
jobs:
build-macos:
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:
runs-on:
- ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Run Docker Container
run: |
docker run --rm \
--volume "${{ github.workspace }}:/src/" \
--env SPECFILE=./BTClockOTA.spec \
batonogov/pyinstaller-windows:latest
- name: Archive artifacts
uses: actions/upload-artifact@v4
with:
name: windows-artifacts
path: dist/
build-linux:
runs-on:
- ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Run Docker Container
run: |
docker run --rm \
--volume "${{ github.workspace }}:/src/" \
--env SPECFILE=./BTClockOTA.spec \
ghcr.io/btclock/pyinstaller-wxpython-linux:latest &&
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

39
.github/workflows/build_linux.yaml vendored Normal file
View file

@ -0,0 +1,39 @@
name: Build Linux artifacts
on: workflow_dispatch
jobs:
build-linux:
runs-on:
- ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Run Docker Container
run: |
docker run --rm \
--volume "${{ github.workspace }}:/src/" \
--env SPECFILE=./BTClockOTA.spec \
ghcr.io/btclock/pyinstaller-wxpython-linux:latest
- name: Archive artifacts
uses: actions/upload-artifact@v4
with:
name: linux-artifacts
path: dist/

View file

@ -34,9 +34,9 @@ jobs:
- name: Build with PyInstaller
run: |
pyinstaller BTClockOTA-universal.spec
- name: Get current block
id: getBlockHeight
run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT
# - name: Get current block
# id: getBlockHeight
# run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT
- name: Zip the app bundle
run: |
cd dist
@ -54,12 +54,12 @@ jobs:
path: |
dist/*
!dist/BTClockOTA.app
- name: Create release
uses: ncipollo/release-action@v1
with:
tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
commit: main
name: release-${{ steps.getBlockHeight.outputs.blockHeight }}
artifacts: "dist/*.dmg,dist/*.zip"
allowUpdates: true
makeLatest: true
# - name: Create release
# uses: ncipollo/release-action@v1
# with:
# tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
# commit: main
# name: release-${{ steps.getBlockHeight.outputs.blockHeight }}
# artifacts: "dist/*.dmg,dist/*.zip"
# allowUpdates: true
# makeLatest: true

View file

@ -32,23 +32,23 @@ jobs:
run: |
docker run --rm \
--volume "${{ github.workspace }}:/src/" \
--env SPECFILE=./BTClockOTA.spec \
--env SPECFILE=./BTClockOTA-debug.spec \
batonogov/pyinstaller-windows:latest
- name: Get current block
id: getBlockHeight
run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT
# - name: Get current block
# id: getBlockHeight
# run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT
- name: Archive artifacts
uses: actions/upload-artifact@v4
with:
name: windows-artifacts
path: dist/
- name: Create release
uses: ncipollo/release-action@v1
with:
tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
commit: main
name: release-${{ steps.getBlockHeight.outputs.blockHeight }}
artifacts: 'dist/**'
allowUpdates: true
removeArtifacts: true
makeLatest: true
# - name: Create release
# uses: ncipollo/release-action@v1
# with:
# tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
# commit: main
# name: release-${{ steps.getBlockHeight.outputs.blockHeight }}
# artifacts: 'dist/**'
# allowUpdates: true
# removeArtifacts: true
# makeLatest: true

39
BTClockOTA-debug.spec Normal file
View file

@ -0,0 +1,39 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['app.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=['zeroconf._utils.ipaddress', 'zeroconf._handlers.answers', 'pyserial', 'wx', 'wx._xml'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='BTClockOTA-debug',
debug=True,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['update-icon.ico'],
)

View file

@ -7,7 +7,7 @@ a = Analysis(
binaries=[],
datas=[],
hiddenimports=['zeroconf._utils.ipaddress',
'zeroconf._handlers.answers', 'pyserial', 'wx'],
'zeroconf._handlers.answers', 'pyserial', 'wx', 'wx._xml'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],

View file

@ -6,7 +6,7 @@ a = Analysis(
pathex=[],
binaries=[],
datas=[],
hiddenimports=['zeroconf._utils.ipaddress', 'zeroconf._handlers.answers', 'pyserial', 'wx'],
hiddenimports=['zeroconf._utils.ipaddress', 'zeroconf._handlers.answers', 'pyserial', 'wx', 'wx._xml'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],

11
app.py
View file

@ -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()

View file

@ -102,8 +102,8 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
inv_tries = 0
data = ""
msg = "Sending invitation to %s " % remote_addr
sys.stderr.write(msg)
sys.stderr.flush()
logging.info(msg)
while inv_tries < 10:
inv_tries += 1
sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@ -111,8 +111,7 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
try:
sent = sock2.sendto(message.encode(), remote_address) # noqa: F841
except: # noqa: E722
sys.stderr.write("failed\n")
sys.stderr.flush()
logging.info("failed\n")
sock2.close()
logging.error("Host %s Not Found", remote_addr)
return 1
@ -121,11 +120,8 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
data = sock2.recv(37).decode()
break
except: # noqa: E722
sys.stderr.write(".")
sys.stderr.flush()
# logging.info(".")
sock2.close()
sys.stderr.write("\n")
sys.stderr.flush()
if inv_tries == 10:
logging.error("No response from the ESP")
return 1
@ -177,8 +173,7 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
if PROGRESS:
progress_handler(0)
else:
sys.stderr.write("Uploading")
sys.stderr.flush()
logging.info("Uploading")
offset = 0
while True:
chunk = f.read(1024)
@ -192,7 +187,6 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
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
@ -202,7 +196,6 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
connection.close()
return 0
sys.stderr.write("\n")
logging.info("Waiting for result...")
count = 0
while count < 5:

View file

@ -7,13 +7,16 @@ import esptool
import serial
import wx
from app.utils import get_app_data_folder
class FwUpdater:
update_progress = None
currentlyUpdating = False
def __init__(self, update_progress):
def __init__(self, update_progress, event_cb):
self.update_progress = update_progress
self.event_cb = event_cb
def get_serial_ports(self):
ports = serial.tools.list_ports.comports()
@ -67,30 +70,49 @@ 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
self.currentlyUpdating = True
if self.event_cb is not None:
self.event_cb("Starting Firmware update")
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, 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(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}")

View file

@ -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)

View file

@ -1,9 +1,12 @@
import concurrent.futures
import logging
import traceback
import serial
from app.gui.action_button_panel import ActionButtonPanel
from app.release_checker import ReleaseChecker
import wx
import wx.richtext as rt
from zeroconf import ServiceBrowser, Zeroconf
import os
@ -13,11 +16,27 @@ 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__()
self.ctrl = ctrl
def emit(self, record):
msg = self.format(record)
wx.CallAfter(self.append_text, "\n" + msg)
def append_text(self, text):
self.ctrl.AppendText(text)
self.ctrl.ShowPosition(self.ctrl.GetLastPosition())
class SerialPortsComboBox(wx.ComboBox):
def __init__(self, parent, fw_update):
self.fw_update = fw_update
@ -31,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()
@ -38,13 +58,23 @@ class BTClockOTAUpdater(wx.Frame):
self.browser = ServiceBrowser(
self.zeroconf, "_http._tcp.local.", self.listener)
self.api_handler = ApiHandler()
self.fw_updater = FwUpdater(self.call_progress)
self.fw_updater = FwUpdater(self.call_progress, self.SetStatusText)
panel = wx.Panel(self)
self.log_ctrl = rt.RichTextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2)
monospace_font = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
self.log_ctrl.SetFont(monospace_font)
handler = RichTextCtrlHandler(self.log_ctrl)
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', '%H:%M:%S'))
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.DEBUG)
self.device_list = DevicesPanel(panel)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(self.device_list, proportion=2,
flag=wx.EXPAND | wx.ALL, border=20)
hbox = wx.BoxSizer(wx.HORIZONTAL)
@ -62,12 +92,13 @@ class BTClockOTAUpdater(wx.Frame):
self.progress_bar = wx.Gauge(panel, range=100)
vbox.Add(self.progress_bar, 0, wx.EXPAND | wx.ALL, 20)
vbox.Add(self.log_ctrl, 1, flag=wx.EXPAND | wx.ALL, border=20)
panel.SetSizer(vbox)
self.setup_ui()
wx.CallAfter(self.fetch_latest_release_async)
wx.CallAfter(self.fetch_latest_release_async)
wx.YieldIfNeeded()
def setup_ui(self):
self.setup_menubar()
self.status_bar = self.CreateStatusBar(2)
@ -76,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(
@ -83,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)
@ -149,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)
@ -160,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)

View file

@ -1,15 +1,17 @@
import json
import logging
import os
import requests
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'''
@ -37,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()
@ -61,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]
@ -70,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']
@ -79,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()
@ -104,14 +102,13 @@ class ReleaseChecker:
def download_file(self, url, release_name):
'''Downloads Fimware Files'''
local_filename = f"{release_name}_{url.split('/')[-1]}"
response = requests.get(url, stream=True)
total_length = response.headers.get('content-length')
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
keep_latest_versions('firmware', 2)
response = requests.get(url, stream=True)
total_length = response.headers.get('content-length')
keep_latest_versions(get_app_data_folder(), 2)
if total_length is None:
raise ReleaseCheckerException("No content length header")
@ -119,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)

View file

@ -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