Compare commits

..

22 commits
847257 ... 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
9b04535f24 Add zip 2024-06-10 00:14:50 +02:00
11b4ea3b44 Install wxPython prerelease and make it a DMG 2024-06-09 23:43:13 +02:00
15 changed files with 544 additions and 87 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

@ -29,24 +29,37 @@ jobs:
pip3 install pyinstaller pip3 install pyinstaller
pip3 install --no-cache cffi --no-binary :all: pip3 install --no-cache cffi --no-binary :all:
pip3 install --no-cache charset_normalizer --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 pip3 install -r requirements.txt
- name: Build with PyInstaller - name: Build with PyInstaller
run: | run: |
pyinstaller BTClockOTA-universal.spec pyinstaller BTClockOTA-universal.spec
- name: Get current block # - name: Get current block
id: getBlockHeight # id: getBlockHeight
run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT # run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT
- 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 - name: Archive artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: macos-artifacts name: macos-artifacts
path: dist/ path: |
- name: Create release dist/*
uses: ncipollo/release-action@v1 !dist/BTClockOTA.app
with: # - name: Create release
tag: ${{ steps.getBlockHeight.outputs.blockHeight }} # uses: ncipollo/release-action@v1
commit: main # with:
name: release-${{ steps.getBlockHeight.outputs.blockHeight }} # tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
artifacts: "dist/**" # commit: main
allowUpdates: true # name: release-${{ steps.getBlockHeight.outputs.blockHeight }}
makeLatest: true # artifacts: "dist/*.dmg,dist/*.zip"
# allowUpdates: true
# makeLatest: true

View file

@ -32,23 +32,23 @@ jobs:
run: | run: |
docker run --rm \ docker run --rm \
--volume "${{ github.workspace }}:/src/" \ --volume "${{ github.workspace }}:/src/" \
--env SPECFILE=./BTClockOTA.spec \ --env SPECFILE=./BTClockOTA-debug.spec \
batonogov/pyinstaller-windows:latest batonogov/pyinstaller-windows:latest
- name: Get current block # - name: Get current block
id: getBlockHeight # id: getBlockHeight
run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT # run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: windows-artifacts name: windows-artifacts
path: dist/ path: dist/
- name: Create release # - name: Create release
uses: ncipollo/release-action@v1 # uses: ncipollo/release-action@v1
with: # with:
tag: ${{ steps.getBlockHeight.outputs.blockHeight }} # tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
commit: main # commit: main
name: release-${{ steps.getBlockHeight.outputs.blockHeight }} # name: release-${{ steps.getBlockHeight.outputs.blockHeight }}
artifacts: 'dist/**' # artifacts: 'dist/**'
allowUpdates: true # allowUpdates: true
removeArtifacts: true # removeArtifacts: true
makeLatest: 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=[], binaries=[],
datas=[], datas=[],
hiddenimports=['zeroconf._utils.ipaddress', hiddenimports=['zeroconf._utils.ipaddress',
'zeroconf._handlers.answers', 'pyserial', 'wx'], 'zeroconf._handlers.answers', 'pyserial', 'wx', 'wx._xml'],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},
runtime_hooks=[], runtime_hooks=[],

View file

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

5
app.py
View file

@ -1,7 +1,8 @@
from app.main import BTClockOTAUpdater
import wx import wx
from app.main import BTClockOTAUpdater if __name__ == "__main__":
app = wx.App(False) app = wx.App(False)
frame = BTClockOTAUpdater(None, 'BTClock OTA updater') frame = BTClockOTAUpdater(None, 'BTClock OTA updater')
app.MainLoop() app.MainLoop()

View file

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

View file

@ -7,13 +7,16 @@ import esptool
import serial import serial
import wx import wx
from app.utils import get_app_data_folder
class FwUpdater: class FwUpdater:
update_progress = None update_progress = None
currentlyUpdating = False currentlyUpdating = False
def __init__(self, update_progress): def __init__(self, update_progress, event_cb):
self.update_progress = update_progress self.update_progress = update_progress
self.event_cb = event_cb
def get_serial_ports(self): def get_serial_ports(self):
ports = serial.tools.list_ports.comports() ports = serial.tools.list_ports.comports()
@ -67,30 +70,49 @@ class FwUpdater:
def start_firmware_update(self, release_name, address, hw_rev): def start_firmware_update(self, release_name, address, hw_rev):
# self.SetStatusText(f"Starting firmware update") # self.SetStatusText(f"Starting firmware update")
model_name = "lolin_s3_mini_213epd" hw_rev_to_model = {
if (hw_rev == "REV_B_EPD_2_13"): "REV_B_EPD_2_13": "btclock_rev_b_213epd",
model_name = "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" release_name}_{model_name}_firmware.bin"
self.updatingName = address self.updatingName = address
self.currentlyUpdating = True 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)): if os.path.exists(os.path.abspath(local_filename)):
thread = Thread(target=self.run_fs_update, args=( thread = Thread(target=self.run_fs_update, args=(
address, os.path.abspath(local_filename), FLASH)) address, os.path.abspath(local_filename), FLASH))
thread.start() 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 # 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.updatingName = address
self.currentlyUpdating = True 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)): if os.path.exists(os.path.abspath(local_filename)):
thread = Thread(target=self.run_fs_update, args=( thread = Thread(target=self.run_fs_update, args=(
address, os.path.abspath(local_filename), SPIFFS)) address, os.path.abspath(local_filename), SPIFFS))
thread.start() 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 = wx.Button(self, label="Update Firmware")
self.update_button.Bind(wx.EVT_BUTTON, self.on_click_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.update_fs_button.Bind(wx.EVT_BUTTON, self.on_click_update_fs)
self.identify_button = wx.Button(self, label="Identify") self.identify_button = wx.Button(self, label="Identify")
@ -80,6 +80,7 @@ class ActionButtonPanel(wx.Panel):
selected_index = self.device_list.GetFirstSelected() selected_index = self.device_list.GetFirstSelected()
if selected_index != -1: if selected_index != -1:
service_name = self.device_list.GetItemText(selected_index, 0) 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) info = self.listener.services.get(service_name)
if self.currentlyUpdating: if self.currentlyUpdating:
wx.MessageBox("Please wait, already updating", wx.MessageBox("Please wait, already updating",
@ -89,7 +90,7 @@ class ActionButtonPanel(wx.Panel):
if info: if info:
address = info.parsed_addresses( address = info.parsed_addresses(
)[0] if info.parsed_addresses() else "N/A" )[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: else:
wx.MessageBox( wx.MessageBox(
"No service information available for selected device", "Error", wx.ICON_ERROR) "No service information available for selected device", "Error", wx.ICON_ERROR)

View file

@ -1,9 +1,12 @@
import concurrent.futures import concurrent.futures
import logging
import traceback
import serial import serial
from app.gui.action_button_panel import ActionButtonPanel from app.gui.action_button_panel import ActionButtonPanel
from app.release_checker import ReleaseChecker from app.release_checker import ReleaseChecker
import wx import wx
import wx.richtext as rt
from zeroconf import ServiceBrowser, Zeroconf from zeroconf import ServiceBrowser, Zeroconf
import os import os
@ -13,10 +16,26 @@ from app import espota
from app.api import ApiHandler from app.api import ApiHandler
from app.fw_updater import FwUpdater from app.fw_updater import FwUpdater
from app.gui.devices_panel import DevicesPanel from app.gui.devices_panel import DevicesPanel
from app.utils import get_app_data_folder
from app.zeroconf_listener import ZeroconfListener from app.zeroconf_listener import ZeroconfListener
from app.espota import FLASH, SPIFFS 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): class SerialPortsComboBox(wx.ComboBox):
def __init__(self, parent, fw_update): def __init__(self, parent, fw_update):
@ -31,6 +50,7 @@ class BTClockOTAUpdater(wx.Frame):
def __init__(self, parent, title): def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(800, 500)) wx.Frame.__init__(self, parent, title=title, size=(800, 500))
self.SetMinSize((800, 500)) self.SetMinSize((800, 500))
self.releaseChecker = ReleaseChecker() self.releaseChecker = ReleaseChecker()
self.zeroconf = Zeroconf() self.zeroconf = Zeroconf()
@ -38,13 +58,23 @@ class BTClockOTAUpdater(wx.Frame):
self.browser = ServiceBrowser( self.browser = ServiceBrowser(
self.zeroconf, "_http._tcp.local.", self.listener) self.zeroconf, "_http._tcp.local.", self.listener)
self.api_handler = ApiHandler() self.api_handler = ApiHandler()
self.fw_updater = FwUpdater(self.call_progress) self.fw_updater = FwUpdater(self.call_progress, self.SetStatusText)
panel = wx.Panel(self) 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) self.device_list = DevicesPanel(panel)
vbox = wx.BoxSizer(wx.VERTICAL) vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(self.device_list, proportion=2, vbox.Add(self.device_list, proportion=2,
flag=wx.EXPAND | wx.ALL, border=20) flag=wx.EXPAND | wx.ALL, border=20)
hbox = wx.BoxSizer(wx.HORIZONTAL) hbox = wx.BoxSizer(wx.HORIZONTAL)
@ -62,12 +92,13 @@ class BTClockOTAUpdater(wx.Frame):
self.progress_bar = wx.Gauge(panel, range=100) self.progress_bar = wx.Gauge(panel, range=100)
vbox.Add(self.progress_bar, 0, wx.EXPAND | wx.ALL, 20) 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) panel.SetSizer(vbox)
self.setup_ui() self.setup_ui()
wx.CallAfter(self.fetch_latest_release_async)
wx.CallAfter(self.fetch_latest_release_async)
wx.YieldIfNeeded()
def setup_ui(self): def setup_ui(self):
self.setup_menubar() self.setup_menubar()
self.status_bar = self.CreateStatusBar(2) self.status_bar = self.CreateStatusBar(2)
@ -76,6 +107,8 @@ class BTClockOTAUpdater(wx.Frame):
def setup_menubar(self): def setup_menubar(self):
filemenu = wx.Menu() filemenu = wx.Menu()
menuOpenDownloadDir = filemenu.Append(
wx.ID_OPEN, "&Open Download Dir", " Open the directory with firmware files and cache")
menuAbout = filemenu.Append( menuAbout = filemenu.Append(
wx.ID_ABOUT, "&About", " Information about this program") wx.ID_ABOUT, "&About", " Information about this program")
menuExit = filemenu.Append( menuExit = filemenu.Append(
@ -83,8 +116,9 @@ class BTClockOTAUpdater(wx.Frame):
menuBar = wx.MenuBar() menuBar = wx.MenuBar()
menuBar.Append(filemenu, "&File") 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.OnAbout, menuAbout)
self.Bind(wx.EVT_MENU, self.OnExit, menuExit) self.Bind(wx.EVT_MENU, self.OnExit, menuExit)
@ -149,6 +183,9 @@ class BTClockOTAUpdater(wx.Frame):
def fetch_latest_release_async(self): def fetch_latest_release_async(self):
# Start a new thread to execute fetch_latest_release # 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() executor = concurrent.futures.ThreadPoolExecutor()
future = executor.submit(self.releaseChecker.fetch_latest_release) future = executor.submit(self.releaseChecker.fetch_latest_release)
future.add_done_callback(self.handle_latest_release) future.add_done_callback(self.handle_latest_release)
@ -160,6 +197,10 @@ class BTClockOTAUpdater(wx.Frame):
latest_release}\nCommit: {self.releaseChecker.commit_hash}") latest_release}\nCommit: {self.releaseChecker.commit_hash}")
except Exception as e: except Exception as e:
self.fw_label.SetLabel(f"Error occurred: {str(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): def OnAbout(self, e):
dlg = wx.MessageDialog( dlg = wx.MessageDialog(

View file

@ -1,15 +1,17 @@
import json import json
import logging
import os import os
import requests import requests
import wx import wx
from typing import Callable from typing import Callable
from datetime import datetime, timedelta 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) CACHE_DURATION = timedelta(minutes=30)
LATEST_RELEASE_ENDPOINT = "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/tags"
class ReleaseChecker: class ReleaseChecker:
'''Release Checker for firmware updates''' '''Release Checker for firmware updates'''
@ -37,13 +39,12 @@ class ReleaseChecker:
cache = self.load_cache() cache = self.load_cache()
now = datetime.now() 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: if 'latest_release' in cache and (now - datetime.fromisoformat(cache['latest_release']['timestamp'])) < CACHE_DURATION:
latest_release = cache['latest_release']['data'] latest_release = cache['latest_release']['data']
else: 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: try:
response = requests.get(url) response = requests.get(url)
response.raise_for_status() response.raise_for_status()
@ -61,7 +62,10 @@ class ReleaseChecker:
self.release_name = release_name self.release_name = release_name
filenames_to_download = ["lolin_s3_mini_213epd_firmware.bin", 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'] asset_urls = [asset['browser_download_url']
for asset in latest_release['assets'] if asset['name'] in filenames_to_download] 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: for asset_url in asset_urls:
self.download_file(asset_url, release_name) self.download_file(asset_url, release_name)
ref_url = f"https://api.github.com/repos/{ ref_url = f"https://git.btclock.dev/api/v1/repos/{repo}/tags/{release_name}"
repo}/git/ref/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: if ref_url in cache and (now - datetime.fromisoformat(cache[ref_url]['timestamp'])) < CACHE_DURATION:
commit_hash = cache[ref_url]['data'] commit_hash = cache[ref_url]['data']
@ -79,15 +84,8 @@ class ReleaseChecker:
response = requests.get(ref_url) response = requests.get(ref_url)
response.raise_for_status() response.raise_for_status()
ref_info = response.json() ref_info = response.json()
if ref_info["object"]["type"] == "commit": commit_hash = ref_info["commit"]["sha"]
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"]
cache[ref_url] = { cache[ref_url] = {
'data': commit_hash, 'data': commit_hash,
'timestamp': now.isoformat() 'timestamp': now.isoformat()
@ -104,14 +102,13 @@ class ReleaseChecker:
def download_file(self, url, release_name): def download_file(self, url, release_name):
'''Downloads Fimware Files''' '''Downloads Fimware Files'''
local_filename = f"{release_name}_{url.split('/')[-1]}" 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"{get_app_data_folder()}/{local_filename}"):
if not os.path.exists("firmware"):
os.makedirs("firmware")
if os.path.exists(f"firmware/{local_filename}"):
return 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: if total_length is None:
raise ReleaseCheckerException("No content length header") raise ReleaseCheckerException("No content length header")
@ -119,7 +116,7 @@ class ReleaseChecker:
total_length = int(total_length) total_length = int(total_length)
chunk_size = 1024 chunk_size = 1024
num_chunks = total_length // chunk_size 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)): for i, chunk in enumerate(response.iter_content(chunk_size=chunk_size)):
if chunk: if chunk:
f.write(chunk) f.write(chunk)

View file

@ -1,6 +1,6 @@
import os import os
import re import re
import shutil import wx
def count_versions(folder_path): 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 version in versions_to_remove:
for file_name in version_files[version]: for file_name in version_files[version]:
os.remove(os.path.join(folder_path, file_name)) 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