Compare commits

..

No commits in common. "main" and "847258" have entirely different histories.
main ... 847258

15 changed files with 88 additions and 538 deletions

View file

@ -1,144 +0,0 @@
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

View file

@ -1,154 +0,0 @@
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

View file

@ -1,39 +0,0 @@
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,32 +34,26 @@ jobs:
- 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 - name: Create DMG
run: | run: |
mkdir dmg_temp mkdir dmg_temp
cp -R dist/BTClockOTA.app dmg_temp/ cp -R dist/BTClockOTA.app dmg_temp/
# Create the DMG file # Create the DMG file
hdiutil create -volname "BTClockOTA" -srcfolder dmg_temp -ov -format UDZO "dist/BTClockOTA-universal.dmg" hdiutil create -volname "BTClockOTA" -srcfolder dmg_temp -ov -format UDZO "dist/BTClockOTA.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: | path: dist/BTClockOTA.dmg
dist/* - name: Create release
!dist/BTClockOTA.app uses: ncipollo/release-action@v1
# - name: Create release with:
# uses: ncipollo/release-action@v1 tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
# with: commit: main
# tag: ${{ steps.getBlockHeight.outputs.blockHeight }} name: release-${{ steps.getBlockHeight.outputs.blockHeight }}
# commit: main artifacts: "dist/*.dmg"
# name: release-${{ steps.getBlockHeight.outputs.blockHeight }} allowUpdates: true
# artifacts: "dist/*.dmg,dist/*.zip" makeLatest: true
# 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-debug.spec \ --env SPECFILE=./BTClockOTA.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

View file

@ -1,39 +0,0 @@
# -*- 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', 'wx._xml'], 'zeroconf._handlers.answers', 'pyserial', 'wx'],
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', 'wx._xml'], hiddenimports=['zeroconf._utils.ipaddress', 'zeroconf._handlers.answers', 'pyserial', 'wx'],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},
runtime_hooks=[], runtime_hooks=[],

11
app.py
View file

@ -1,8 +1,7 @@
from app.main import BTClockOTAUpdater
import wx import wx
if __name__ == "__main__": from app.main import BTClockOTAUpdater
app = wx.App(False)
frame = BTClockOTAUpdater(None, 'BTClock OTA updater') app = wx.App(False)
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
logging.info(msg) sys.stderr.write(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,7 +111,8 @@ 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
logging.info("failed\n") sys.stderr.write("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
@ -120,8 +121,11 @@ 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
# logging.info(".") sys.stderr.write(".")
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
@ -173,7 +177,8 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
if PROGRESS: if PROGRESS:
progress_handler(0) progress_handler(0)
else: else:
logging.info("Uploading") sys.stderr.write("Uploading")
sys.stderr.flush()
offset = 0 offset = 0
while True: while True:
chunk = f.read(1024) chunk = f.read(1024)
@ -187,6 +192,7 @@ 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
@ -196,6 +202,7 @@ 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,16 +7,13 @@ 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, event_cb): def __init__(self, update_progress):
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()
@ -70,49 +67,30 @@ 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")
hw_rev_to_model = { model_name = "lolin_s3_mini_213epd"
"REV_B_EPD_2_13": "btclock_rev_b_213epd", if (hw_rev == "REV_B_EPD_2_13"):
"REV_V8_EPD_2_13": "btclock_v8_213epd", model_name = "btclock_rev_b_213epd"
"REV_A_EPD_2_9": "lolin_s3_mini_29epd"
}
model_name = hw_rev_to_model.get(hw_rev, "lolin_s3_mini_213epd") local_filename = f"firmware/{
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, hw_rev): def start_fs_update(self, release_name, address):
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"{get_app_data_folder()}/{release_name}_{hw_rev_to_model.get(hw_rev, "littlefs_4MB")}.bin" local_filename = f"firmware/{release_name}_littlefs.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 WebUI") self.update_fs_button = wx.Button(self, label="Update Filesystem")
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,7 +80,6 @@ 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",
@ -90,7 +89,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, hw_rev) self.parent_frame.fw_updater.start_fs_update(self.parent_frame.releaseChecker.release_name, address)
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,12 +1,9 @@
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
@ -16,27 +13,11 @@ 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):
self.fw_update = fw_update self.fw_update = fw_update
@ -50,7 +31,6 @@ 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()
@ -58,23 +38,13 @@ 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.SetStatusText) self.fw_updater = FwUpdater(self.call_progress)
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)
@ -92,13 +62,12 @@ 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)
@ -107,8 +76,6 @@ 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(
@ -116,9 +83,8 @@ 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)
@ -183,9 +149,6 @@ 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)
@ -197,11 +160,7 @@ 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(
self, "An updater for BTClocks", "About BTClock OTA Updater", wx.OK) self, "An updater for BTClocks", "About BTClock OTA Updater", wx.OK)

View file

@ -1,17 +1,15 @@
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 get_app_data_folder, keep_latest_versions from app.utils import keep_latest_versions
CACHE_FILE = get_app_data_folder() + '/cache.json' CACHE_FILE = 'firmware/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'''
@ -39,12 +37,13 @@ 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()
@ -62,10 +61,7 @@ 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",
"lolin_s3_mini_29epd_firmware.bin", "btclock_rev_b_213epd_firmware.bin", "littlefs.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]
@ -74,9 +70,8 @@ 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://git.btclock.dev/api/v1/repos/{repo}/tags/{release_name}" ref_url = f"https://api.github.com/repos/{
#ref_url = f"https://api.github.com/repos/{ repo}/git/ref/tags/{release_name}"
# 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']
@ -84,8 +79,15 @@ 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()
commit_hash = ref_info["commit"]["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']}"
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()
@ -102,13 +104,14 @@ 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]}"
if os.path.exists(f"{get_app_data_folder()}/{local_filename}"):
return
response = requests.get(url, stream=True) response = requests.get(url, stream=True)
total_length = response.headers.get('content-length') total_length = response.headers.get('content-length')
keep_latest_versions(get_app_data_folder(), 2) if not os.path.exists("firmware"):
os.makedirs("firmware")
if os.path.exists(f"firmware/{local_filename}"):
return
keep_latest_versions('firmware', 2)
if total_length is None: if total_length is None:
raise ReleaseCheckerException("No content length header") raise ReleaseCheckerException("No content length header")
@ -116,7 +119,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"{get_app_data_folder()}/{local_filename}", 'wb') as f: with open(f"firmware/{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 wx import shutil
def count_versions(folder_path): def count_versions(folder_path):
@ -30,16 +30,3 @@ 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