diff --git a/.forgejo/workflows/build_all.yaml b/.forgejo/workflows/build_all.yaml deleted file mode 100644 index dfdf59e..0000000 --- a/.forgejo/workflows/build_all.yaml +++ /dev/null @@ -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 diff --git a/.github/workflows/build_all.yaml b/.github/workflows/build_all.yaml deleted file mode 100644 index 2fe9f3b..0000000 --- a/.github/workflows/build_all.yaml +++ /dev/null @@ -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 diff --git a/.github/workflows/build_windows.yaml b/.github/workflows/build_artifacts.yaml similarity index 62% rename from .github/workflows/build_windows.yaml rename to .github/workflows/build_artifacts.yaml index 2525f74..22f5206 100644 --- a/.github/workflows/build_windows.yaml +++ b/.github/workflows/build_artifacts.yaml @@ -1,4 +1,4 @@ -name: Build Windows artifacts and make release +name: Build artifacts and make release on: workflow_dispatch @@ -32,23 +32,23 @@ jobs: run: | docker run --rm \ --volume "${{ github.workspace }}:/src/" \ - --env SPECFILE=./BTClockOTA-debug.spec \ + --env SPECFILE=./BTClockOTA.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 \ No newline at end of file + - 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 \ No newline at end of file diff --git a/.github/workflows/build_linux.yaml b/.github/workflows/build_linux.yaml deleted file mode 100644 index 3f907c6..0000000 --- a/.github/workflows/build_linux.yaml +++ /dev/null @@ -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/ diff --git a/.github/workflows/build_macos.yaml b/.github/workflows/build_macos.yaml deleted file mode 100644 index 1d07fa9..0000000 --- a/.github/workflows/build_macos.yaml +++ /dev/null @@ -1,65 +0,0 @@ -name: Build macOS 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: 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 - 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 - # - 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 diff --git a/.gitignore b/.gitignore index ea36021..000754a 100644 --- a/.gitignore +++ b/.gitignore @@ -266,5 +266,4 @@ $RECYCLE.BIN/ *.lnk # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,macos,windows,linux -firmware/*.bin -firmware/*.json \ No newline at end of file +firmware/*.bin \ No newline at end of file diff --git a/BTClockOTA-debug.spec b/BTClockOTA-debug.spec deleted file mode 100644 index 4c9cbbf..0000000 --- a/BTClockOTA-debug.spec +++ /dev/null @@ -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'], -) diff --git a/BTClockOTA-universal.spec b/BTClockOTA-universal.spec deleted file mode 100644 index e3d1c07..0000000 --- a/BTClockOTA-universal.spec +++ /dev/null @@ -1,45 +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=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=False, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch="universal2", - codesign_identity=None, - entitlements_file=None, - icon=['update-icon.ico'], -) - -app = BUNDLE(exe, - name='BTClockOTA.app', - icon='update-icon.icns', - bundle_identifier=None) diff --git a/BTClockOTA.spec b/BTClockOTA.spec index 0f3984b..1aec7f3 100644 --- a/BTClockOTA.spec +++ b/BTClockOTA.spec @@ -6,7 +6,7 @@ a = Analysis( pathex=[], binaries=[], datas=[], - hiddenimports=['zeroconf._utils.ipaddress', 'zeroconf._handlers.answers', 'pyserial', 'wx', 'wx._xml'], + hiddenimports=['zeroconf._utils.ipaddress', 'zeroconf._handlers.answers', 'pyserial', 'wx'], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/README.md b/README.md index 45208d8..7ac1800 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # BTClock OTA Flasher interface -![Screenshot Windows](screenshot-win.webp) -![Screenshot Mac](screenshot-mac.webp) - ## Instructions - Make sure you have Python (tested with Python 3.12) - Run `pip3 install -r requirements.txt` @@ -20,7 +17,7 @@ pyinstaller --hidden-import zeroconf._utils.ipaddress --hidden-import zeroconf._ ### Windows ```` -pyinstaller.exe BTClockOTA.spec +pyinstaller.exe --hidden-import zeroconf._utils.ipaddress --hidden-import zeroconf._handlers.answers --hidden-import pyserial -n BTClockOTA --windowed --onefile app.py ```` ### Linux diff --git a/app.py b/app.py index e82a0c9..7e92065 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,7 @@ -from app.main import BTClockOTAUpdater import wx -if __name__ == "__main__": - app = wx.App(False) - frame = BTClockOTAUpdater(None, 'BTClock OTA updater') - - app.MainLoop() +from app.main import BTClockOTAUpdater + +app = wx.App(False) +frame = BTClockOTAUpdater(None, 'BTClock OTA updater') +app.MainLoop() \ No newline at end of file diff --git a/app/espota.py b/app/espota.py index a55e5cf..487f8ed 100644 --- a/app/espota.py +++ b/app/espota.py @@ -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 - logging.info(msg) - + sys.stderr.write(msg) + sys.stderr.flush() while inv_tries < 10: inv_tries += 1 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: sent = sock2.sendto(message.encode(), remote_address) # noqa: F841 except: # noqa: E722 - logging.info("failed\n") + sys.stderr.write("failed\n") + sys.stderr.flush() sock2.close() logging.error("Host %s Not Found", remote_addr) return 1 @@ -120,8 +121,11 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename, data = sock2.recv(37).decode() break except: # noqa: E722 -# logging.info(".") + sys.stderr.write(".") + sys.stderr.flush() sock2.close() + sys.stderr.write("\n") + sys.stderr.flush() if inv_tries == 10: logging.error("No response from the ESP") return 1 @@ -173,7 +177,8 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename, if PROGRESS: progress_handler(0) else: - logging.info("Uploading") + sys.stderr.write("Uploading") + sys.stderr.flush() offset = 0 while True: chunk = f.read(1024) @@ -187,6 +192,7 @@ 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 @@ -196,6 +202,7 @@ 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: diff --git a/app/fw_updater.py b/app/fw_updater.py index 6ac6b97..1ca37af 100644 --- a/app/fw_updater.py +++ b/app/fw_updater.py @@ -7,16 +7,13 @@ 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, event_cb): + def __init__(self, update_progress): self.update_progress = update_progress - self.event_cb = event_cb def get_serial_ports(self): ports = serial.tools.list_ports.comports() @@ -70,49 +67,30 @@ class FwUpdater: def start_firmware_update(self, release_name, address, hw_rev): # self.SetStatusText(f"Starting firmware update") - 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" - } + model_name = "lolin_s3_mini_213epd" + if (hw_rev == "REV_B_EPD_2_13"): + model_name = "btclock_rev_b_213epd" - model_name = hw_rev_to_model.get(hw_rev, "lolin_s3_mini_213epd") - - - local_filename = f"{get_app_data_folder()}/{ + local_filename = f"firmware/{ 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, 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" - } - + def start_fs_update(self, release_name, address): # 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.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}") + diff --git a/app/gui/action_button_panel.py b/app/gui/action_button_panel.py index 34e66e2..5c28669 100644 --- a/app/gui/action_button_panel.py +++ b/app/gui/action_button_panel.py @@ -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 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.identify_button = wx.Button(self, label="Identify") @@ -80,7 +80,6 @@ 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", @@ -90,7 +89,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, hw_rev) + self.parent_frame.fw_updater.start_fs_update(self.parent_frame.releaseChecker.release_name, address) else: wx.MessageBox( "No service information available for selected device", "Error", wx.ICON_ERROR) diff --git a/app/main.py b/app/main.py index 719dcde..93383ec 100644 --- a/app/main.py +++ b/app/main.py @@ -1,12 +1,9 @@ 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 @@ -16,27 +13,10 @@ 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 @@ -50,7 +30,6 @@ 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() @@ -58,23 +37,13 @@ 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.SetStatusText) - + self.fw_updater = FwUpdater(self.call_progress) 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) @@ -82,23 +51,22 @@ class BTClockOTAUpdater(wx.Frame): self.fw_label = wx.StaticText( panel, label=f"Fetching latest version from GitHub...") hbox.Add(self.fw_label, 1, wx.EXPAND | wx.ALL, 5) - + self.actionButtons = ActionButtonPanel( panel, self) hbox.AddStretchSpacer() - + hbox.Add(self.actionButtons, 2, wx.EXPAND | wx.ALL, 5) vbox.Add(hbox, 0, wx.EXPAND | wx.ALL, 20) 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() + self.setup_ui() wx.CallAfter(self.fetch_latest_release_async) - wx.YieldIfNeeded() + def setup_ui(self): self.setup_menubar() self.status_bar = self.CreateStatusBar(2) @@ -107,8 +75,6 @@ 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( @@ -116,9 +82,8 @@ class BTClockOTAUpdater(wx.Frame): menuBar = wx.MenuBar() menuBar.Append(filemenu, "&File") - 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) @@ -130,19 +95,12 @@ class BTClockOTAUpdater(wx.Frame): info.parsed_addresses()[0]) version = info.properties.get(b"rev").decode() - fsHash = "Too old" - hwRev = "REV_A_EPD_2_13" if 'gitTag' in deviceSettings: version = deviceSettings["gitTag"] - if 'fsRev' in deviceSettings: - fsHash = deviceSettings['fsRev'][:7] - - if (info.properties.get(b"hw_rev") is not None): - hwRev = info.properties.get(b"hw_rev").decode() - fwHash = info.properties.get(b"rev").decode()[:7] + fsHash = deviceSettings['fsRev'][:7] address = info.parsed_addresses()[0] if index == wx.NOT_FOUND: @@ -151,19 +109,23 @@ class BTClockOTAUpdater(wx.Frame): self.device_list.SetItem(index, 0, name) self.device_list.SetItem(index, 1, version) self.device_list.SetItem(index, 2, fwHash) - self.device_list.SetItem(index, 3, hwRev) + if (info.properties.get(b"hw_rev") is not None): + self.device_list.SetItem( + index, 3, info.properties.get(b"hw_rev").decode()) self.device_list.SetItem(index, 4, address) else: self.device_list.SetItem(index, 0, name) self.device_list.SetItem(index, 1, version) self.device_list.SetItem(index, 2, fwHash) - self.device_list.SetItem(index, 3, hwRev) + if (info.properties.get(b"hw_rev").decode()): + self.device_list.SetItem( + index, 3, info.properties.get(b"hw_rev").decode()) self.device_list.SetItem(index, 4, address) self.device_list.SetItem(index, 5, fsHash) self.device_list.SetItemData(index, index) self.device_list.itemDataMap[index] = [ - name, version, fwHash, hwRev, address, fsHash] + name, version, fwHash, info.properties.get(b"hw_rev").decode(), address, fsHash] for col in range(0, len(self.device_list.column_headings)): self.device_list.SetColumnWidth( col, wx.LIST_AUTOSIZE_USEHEADER) @@ -183,9 +145,6 @@ 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) @@ -197,11 +156,7 @@ 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) diff --git a/app/release_checker.py b/app/release_checker.py index 64b0f79..0e4ec9e 100644 --- a/app/release_checker.py +++ b/app/release_checker.py @@ -1,17 +1,10 @@ -import json -import logging import os import requests import wx from typing import Callable -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_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''' @@ -21,94 +14,65 @@ class ReleaseChecker: def __init__(self): self.progress_callback: Callable[[int], None] = None - def load_cache(self): - '''Load cached data from file''' - if os.path.exists(CACHE_FILE): - with open(CACHE_FILE, 'r') as f: - return json.load(f) - return {} - - def save_cache(self, cache_data): - '''Save cache data to file''' - with open(CACHE_FILE, 'w') as f: - json.dump(cache_data, f) - def fetch_latest_release(self): '''Fetch latest firmware release from GitHub''' repo = "btclock/btclock_v3" - cache = self.load_cache() - now = datetime.now() - - - if '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://git.btclock.dev/api/v1/repos/{repo}/releases/latest" - try: - response = requests.get(url) - response.raise_for_status() - latest_release = response.json() - cache['latest_release'] = { - 'data': latest_release, - 'timestamp': now.isoformat() - } - self.save_cache(cache) - except requests.RequestException as e: - raise ReleaseCheckerException( - f"Error fetching release: {e}") from e - - release_name = latest_release['tag_name'] - self.release_name = release_name + if not os.path.exists("firmware"): + os.makedirs("firmware") filenames_to_download = ["lolin_s3_mini_213epd_firmware.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"] + "btclock_rev_b_213epd_firmware.bin", "littlefs.bin"] + url = f"https://api.github.com/repos/{repo}/releases/latest" + try: + response = requests.get(url) + response.raise_for_status() + latest_release = response.json() + release_name = latest_release['tag_name'] + self.release_name = release_name - asset_urls = [asset['browser_download_url'] - for asset in latest_release['assets'] if asset['name'] in filenames_to_download] - - if asset_urls: - for asset_url in asset_urls: - self.download_file(asset_url, release_name) - - ref_url = f"https://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'] - - else: + asset_url = None + asset_urls = [] + for asset in latest_release['assets']: + if asset['name'] in filenames_to_download: + asset_urls.append(asset['browser_download_url']) + if asset_urls: + for asset_url in asset_urls: + self.download_file(asset_url, release_name) + ref_url = f"https://api.github.com/repos/{ + repo}/git/ref/tags/{release_name}" response = requests.get(ref_url) response.raise_for_status() ref_info = response.json() - commit_hash = ref_info["commit"]["sha"] - - cache[ref_url] = { - 'data': commit_hash, - 'timestamp': now.isoformat() - } - self.save_cache(cache) + if (ref_info["object"]["type"] == "commit"): + self.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() + self.commit_hash = tag_info["object"]["sha"] - self.commit_hash = commit_hash + return self.release_name - return self.release_name - else: + else: + raise ReleaseCheckerException( + f"File {filenames_to_download} not found in latest release") + except requests.RequestException as e: raise ReleaseCheckerException( - f"File {filenames_to_download} not found in latest release") + f"Error fetching release: {e}") from e def download_file(self, url, release_name): '''Downloads Fimware Files''' 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) 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: raise ReleaseCheckerException("No content length header") @@ -116,7 +80,7 @@ class ReleaseChecker: total_length = int(total_length) chunk_size = 1024 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)): if chunk: f.write(chunk) diff --git a/app/utils.py b/app/utils.py index 82559c7..8b79405 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,6 +1,6 @@ import os import re -import wx +import shutil 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 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 \ No newline at end of file diff --git a/screenshot-mac.webp b/screenshot-mac.webp deleted file mode 100644 index c39631e..0000000 Binary files a/screenshot-mac.webp and /dev/null differ diff --git a/screenshot-win.webp b/screenshot-win.webp deleted file mode 100644 index 6ddf75e..0000000 Binary files a/screenshot-win.webp and /dev/null differ