Uncluttered code
This commit is contained in:
parent
d356e089c9
commit
3eb23209e8
13 changed files with 564 additions and 416 deletions
2
app.py
2
app.py
|
@ -1,6 +1,6 @@
|
||||||
import wx
|
import wx
|
||||||
|
|
||||||
from app.gui import BTClockOTAUpdater
|
from app.main import BTClockOTAUpdater
|
||||||
|
|
||||||
app = wx.App(False)
|
app = wx.App(False)
|
||||||
frame = BTClockOTAUpdater(None, 'BTClock OTA updater')
|
frame = BTClockOTAUpdater(None, 'BTClock OTA updater')
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import time
|
import time
|
||||||
import requests
|
import requests
|
||||||
|
import json
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
|
|
||||||
class ApiHandler:
|
class ApiHandler:
|
||||||
def identify_btclock(self, address):
|
def identify_btclock(self, address):
|
||||||
self.make_api_call(address, "api/identify")
|
self.make_api_call(address, "api/identify")
|
||||||
|
@ -11,8 +13,12 @@ class ApiHandler:
|
||||||
ret = self.run_api_call(address, "fs_hash.txt")
|
ret = self.run_api_call(address, "fs_hash.txt")
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def get_settings(self, address):
|
||||||
|
ret = json.loads(self.run_api_call(address, "api/settings"))
|
||||||
|
return ret
|
||||||
|
|
||||||
def make_api_call(self, address, path):
|
def make_api_call(self, address, path):
|
||||||
thread = Thread(target=self.run_api_call, args=(address,path))
|
thread = Thread(target=self.run_api_call, args=(address, path))
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def run_api_call(self, address, path):
|
def run_api_call(self, address, path):
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
import esptool
|
|
||||||
import serial
|
|
||||||
|
|
||||||
class FwUpdate:
|
|
||||||
def get_serial_ports(self):
|
|
||||||
ports = serial.tools.list_ports.comports()
|
|
||||||
available_ports = []
|
|
||||||
for port, desc, hwid in sorted(ports):
|
|
||||||
available_ports.append((port, desc, hwid))
|
|
||||||
print(f"Port: {port}, Description: {desc}, Hardware ID: {hwid}")
|
|
||||||
return available_ports
|
|
||||||
|
|
||||||
def flash_firmware(port, baud, firmware_path):
|
|
||||||
try:
|
|
||||||
# Initialize the serial port
|
|
||||||
serial_port = serial.Serial(port, baud)
|
|
||||||
|
|
||||||
# Initialize the ESP32ROM with the serial port
|
|
||||||
esp = esptool.ESP32ROM(serial_port)
|
|
||||||
|
|
||||||
# Connect to the ESP32
|
|
||||||
esp.connect()
|
|
||||||
|
|
||||||
# Perform the flashing operation
|
|
||||||
esp.flash_file(firmware_path, offset=0x1000)
|
|
||||||
|
|
||||||
# Optionally, verify the flash
|
|
||||||
esp.verify_flash(firmware_path, offset=0x1000)
|
|
||||||
|
|
||||||
print("Firmware flashed successfully!")
|
|
||||||
|
|
||||||
except esptool.FatalError as e:
|
|
||||||
print(f"Failed to flash firmware: {e}")
|
|
||||||
finally:
|
|
||||||
# Ensure the serial port is closed
|
|
||||||
if serial_port.is_open:
|
|
||||||
serial_port.close()
|
|
96
app/fw_updater.py
Normal file
96
app/fw_updater.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
from threading import Thread
|
||||||
|
from app import espota
|
||||||
|
from app.espota import FLASH, SPIFFS
|
||||||
|
import esptool
|
||||||
|
import serial
|
||||||
|
import wx
|
||||||
|
|
||||||
|
|
||||||
|
class FwUpdater:
|
||||||
|
update_progress = None
|
||||||
|
currentlyUpdating = False
|
||||||
|
|
||||||
|
def __init__(self, update_progress):
|
||||||
|
self.update_progress = update_progress
|
||||||
|
|
||||||
|
def get_serial_ports(self):
|
||||||
|
ports = serial.tools.list_ports.comports()
|
||||||
|
available_ports = []
|
||||||
|
for port, desc, hwid in sorted(ports):
|
||||||
|
available_ports.append((port, desc, hwid))
|
||||||
|
print(f"Port: {port}, Description: {desc}, Hardware ID: {hwid}")
|
||||||
|
return available_ports
|
||||||
|
|
||||||
|
def run_fs_update(self, address, firmware_file, type):
|
||||||
|
global PROGRESS
|
||||||
|
PROGRESS = True
|
||||||
|
espota.PROGRESS = True
|
||||||
|
global TIMEOUT
|
||||||
|
TIMEOUT = 10
|
||||||
|
espota.TIMEOUT = 10
|
||||||
|
|
||||||
|
espota.serve(address, "0.0.0.0", 3232, random.randint(
|
||||||
|
10000, 60000), "", firmware_file, type, self.update_progress)
|
||||||
|
|
||||||
|
wx.CallAfter(self.update_progress, 1)
|
||||||
|
self.currentlyUpdating = False
|
||||||
|
# self.SetStatusText(f"Finished!")
|
||||||
|
|
||||||
|
def flash_firmware(port, baud, firmware_path):
|
||||||
|
try:
|
||||||
|
# Initialize the serial port
|
||||||
|
serial_port = serial.Serial(port, baud)
|
||||||
|
|
||||||
|
# Initialize the ESP32ROM with the serial port
|
||||||
|
esp = esptool.ESP32ROM(serial_port)
|
||||||
|
|
||||||
|
# Connect to the ESP32
|
||||||
|
esp.connect()
|
||||||
|
|
||||||
|
# Perform the flashing operation
|
||||||
|
esp.flash_file(firmware_path, offset=0x1000)
|
||||||
|
|
||||||
|
# Optionally, verify the flash
|
||||||
|
esp.verify_flash(firmware_path, offset=0x1000)
|
||||||
|
|
||||||
|
print("Firmware flashed successfully!")
|
||||||
|
|
||||||
|
except esptool.FatalError as e:
|
||||||
|
print(f"Failed to flash firmware: {e}")
|
||||||
|
finally:
|
||||||
|
# Ensure the serial port is closed
|
||||||
|
if serial_port.is_open:
|
||||||
|
serial_port.close()
|
||||||
|
|
||||||
|
def start_firmware_update(self, release_name, address, hw_rev):
|
||||||
|
# self.SetStatusText(f"Starting firmware update")
|
||||||
|
|
||||||
|
model_name = "lolin_s3_mini_213epd"
|
||||||
|
if (hw_rev == "REV_B_EPD_2_13"):
|
||||||
|
model_name = "btclock_rev_b_213epd"
|
||||||
|
|
||||||
|
local_filename = f"firmware/{
|
||||||
|
release_name}_{model_name}_firmware.bin"
|
||||||
|
|
||||||
|
self.updatingName = address
|
||||||
|
self.currentlyUpdating = True
|
||||||
|
|
||||||
|
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):
|
||||||
|
# Path to the firmware file
|
||||||
|
local_filename = f"firmware/{release_name}_littlefs.bin"
|
||||||
|
|
||||||
|
self.updatingName = address
|
||||||
|
self.currentlyUpdating = True
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
372
app/gui.py
372
app/gui.py
|
@ -1,372 +0,0 @@
|
||||||
import random
|
|
||||||
from threading import Thread
|
|
||||||
import threading
|
|
||||||
import serial
|
|
||||||
import wx
|
|
||||||
import wx.lib.mixins.listctrl
|
|
||||||
|
|
||||||
from zeroconf import ServiceBrowser, Zeroconf
|
|
||||||
import requests
|
|
||||||
import os
|
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
from app import espota
|
|
||||||
from app.api import ApiHandler
|
|
||||||
from app.fw_update import FwUpdate
|
|
||||||
from app.zeroconf_listener import ZeroconfListener
|
|
||||||
|
|
||||||
from app.espota import FLASH,SPIFFS
|
|
||||||
|
|
||||||
class SerialPortsComboBox(wx.ComboBox):
|
|
||||||
def __init__(self, parent, fw_update):
|
|
||||||
self.fw_update = fw_update
|
|
||||||
self.ports = serial.tools.list_ports.comports()
|
|
||||||
wx.ComboBox.__init__(self,parent, choices=[port.device for port in self.ports])
|
|
||||||
|
|
||||||
class DevicesPanel(wx.ListCtrl,wx.lib.mixins.listctrl.ColumnSorterMixin, wx.lib.mixins.listctrl.ListCtrlAutoWidthMixin,
|
|
||||||
):
|
|
||||||
def __init__(self, parent):
|
|
||||||
wx.ListCtrl.__init__(self, parent, style=wx.LC_REPORT)
|
|
||||||
self.column_headings = ["name", "Version", "SW Revision", "HW Revision", "IP", "FS Version"]
|
|
||||||
wx.lib.mixins.listctrl.ColumnSorterMixin.__init__(
|
|
||||||
self,
|
|
||||||
len(self.column_headings),
|
|
||||||
)
|
|
||||||
wx.lib.mixins.listctrl.ListCtrlAutoWidthMixin.__init__(self)
|
|
||||||
|
|
||||||
for column, heading in enumerate(self.column_headings):
|
|
||||||
self.AppendColumn(heading)
|
|
||||||
|
|
||||||
self.itemDataMap = {}
|
|
||||||
|
|
||||||
def OnSortOrderChanged(self):
|
|
||||||
"""Method to handle changes to the sort order"""
|
|
||||||
|
|
||||||
column, ascending = self.GetSortState()
|
|
||||||
self.ShowSortIndicator(column, ascending)
|
|
||||||
self.SortListItems(column, ascending)
|
|
||||||
|
|
||||||
def GetListCtrl(self):
|
|
||||||
"""Method required by the ColumnSorterMixin"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
class BTClockOTAUpdater(wx.Frame):
|
|
||||||
release_name = ""
|
|
||||||
commit_hash = ""
|
|
||||||
currentlyUpdating = False
|
|
||||||
updatingName = ""
|
|
||||||
|
|
||||||
def __init__(self, parent, title):
|
|
||||||
wx.Frame.__init__(self, parent, title=title, size=(800,500))
|
|
||||||
self.SetMinSize((800,500))
|
|
||||||
ubuntu_it = wx.Font(32, wx.FONTFAMILY_MODERN, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, faceName="Ubuntu")
|
|
||||||
# "Ubuntu-RI.ttf")
|
|
||||||
|
|
||||||
self.fw_update = FwUpdate()
|
|
||||||
|
|
||||||
panel = wx.Panel(self)
|
|
||||||
|
|
||||||
self.title = wx.StaticText(panel, label="BTClock OTA firmware updater")
|
|
||||||
self.title.SetFont(ubuntu_it)
|
|
||||||
vbox = wx.BoxSizer(wx.VERTICAL)
|
|
||||||
vbox.Add(self.title, 0, wx.EXPAND | wx.ALL, 20, 0)
|
|
||||||
|
|
||||||
# serialPorts = SerialPortsComboBox(panel, self.fw_update)
|
|
||||||
# vbox.Add(serialPorts, 0, wx.EXPAND | wx.ALL, 20, 0)
|
|
||||||
|
|
||||||
self.device_list = DevicesPanel(panel)
|
|
||||||
self.device_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_item_selected)
|
|
||||||
self.device_list.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.on_item_deselected)
|
|
||||||
|
|
||||||
vbox.Add(self.device_list, proportion = 2, flag=wx.EXPAND | wx.ALL, border = 20)
|
|
||||||
hbox = wx.BoxSizer(wx.HORIZONTAL)
|
|
||||||
bbox = wx.BoxSizer(wx.HORIZONTAL)
|
|
||||||
|
|
||||||
gs = wx.GridSizer(1, 4, 1, 1)
|
|
||||||
|
|
||||||
self.fw_label = wx.StaticText(panel, label=f"Checking latest version...")
|
|
||||||
self.update_button = wx.Button(panel, label="Update Firmware")
|
|
||||||
self.update_button.Bind(wx.EVT_BUTTON, self.on_click_update_firmware)
|
|
||||||
self.update_fs_button = wx.Button(panel, label="Update Filesystem")
|
|
||||||
self.update_fs_button.Bind(wx.EVT_BUTTON, self.on_click_update_fs)
|
|
||||||
|
|
||||||
self.identify_button = wx.Button(panel, label="Identify")
|
|
||||||
self.identify_button.Bind(wx.EVT_BUTTON, self.on_click_identify)
|
|
||||||
self.open_webif_button = wx.Button(panel, label="Open WebUI")
|
|
||||||
self.open_webif_button.Bind(wx.EVT_BUTTON, self.on_click_webui)
|
|
||||||
self.update_button.Disable()
|
|
||||||
self.update_fs_button.Disable()
|
|
||||||
self.identify_button.Disable()
|
|
||||||
self.open_webif_button.Disable()
|
|
||||||
|
|
||||||
hbox.Add(self.fw_label, 1, wx.EXPAND | wx.ALL, 5)
|
|
||||||
bbox.Add(self.update_button)
|
|
||||||
bbox.Add(self.update_fs_button)
|
|
||||||
bbox.Add(self.identify_button)
|
|
||||||
bbox.Add(self.open_webif_button)
|
|
||||||
|
|
||||||
hbox.AddStretchSpacer()
|
|
||||||
hbox.Add(bbox, 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)
|
|
||||||
|
|
||||||
panel.SetSizer(vbox)
|
|
||||||
|
|
||||||
filemenu= wx.Menu()
|
|
||||||
menuAbout = filemenu.Append(wx.ID_ABOUT, "&About"," Information about this program")
|
|
||||||
menuExit = filemenu.Append(wx.ID_EXIT,"E&xit"," Terminate the program")
|
|
||||||
|
|
||||||
menuBar = wx.MenuBar()
|
|
||||||
menuBar.Append(filemenu,"&File") # Adding the "filemenu" to the MenuBar
|
|
||||||
self.SetMenuBar(menuBar) # Adding the MenuBar to the Frame content.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.Bind(wx.EVT_MENU, self.OnAbout, menuAbout)
|
|
||||||
self.Bind(wx.EVT_MENU, self.OnExit, menuExit)
|
|
||||||
self.status_bar = self.CreateStatusBar(2)
|
|
||||||
# self.StatusBar.SetFieldsCount(2)
|
|
||||||
# self.StatusBar.SetStatusWidths(-3, -1)
|
|
||||||
self.Show(True)
|
|
||||||
|
|
||||||
self.zeroconf = Zeroconf()
|
|
||||||
self.listener = ZeroconfListener(self.on_zeroconf_state_change)
|
|
||||||
self.browser = ServiceBrowser(self.zeroconf, "_http._tcp.local.", self.listener)
|
|
||||||
self.api_handler = ApiHandler()
|
|
||||||
|
|
||||||
wx.CallAfter(self.fetch_latest_release)
|
|
||||||
|
|
||||||
def on_item_selected(self, event):
|
|
||||||
if self.release_name != "":
|
|
||||||
self.update_button.Enable()
|
|
||||||
self.update_fs_button.Enable()
|
|
||||||
self.identify_button.Enable()
|
|
||||||
self.open_webif_button.Enable()
|
|
||||||
|
|
||||||
def on_item_deselected(self, event):
|
|
||||||
if self.device_list.GetFirstSelected() == -1:
|
|
||||||
self.update_button.Disable()
|
|
||||||
self.update_fs_button.Disable()
|
|
||||||
self.identify_button.Disable()
|
|
||||||
self.open_webif_button.Disable()
|
|
||||||
|
|
||||||
def on_zeroconf_state_change(self, type, name, state, info):
|
|
||||||
index = self.device_list.FindItem(0, name)
|
|
||||||
|
|
||||||
if state == "Added":
|
|
||||||
if index == wx.NOT_FOUND:
|
|
||||||
index = self.device_list.InsertItem(self.device_list.GetItemCount(), type)
|
|
||||||
self.device_list.SetItem(index, 0, name)
|
|
||||||
self.device_list.SetItem(index, 1, info.properties.get(b"version").decode())
|
|
||||||
self.device_list.SetItem(index, 2, info.properties.get(b"rev").decode())
|
|
||||||
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, info.parsed_addresses()[0])
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.device_list.SetItem(index, 0, name)
|
|
||||||
self.device_list.SetItem(index, 1, info.properties.get(b"version").decode())
|
|
||||||
self.device_list.SetItem(index, 2, info.properties.get(b"rev").decode())
|
|
||||||
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, info.parsed_addresses()[0])
|
|
||||||
self.device_list.SetItem(index, 5, self.api_handler.check_fs_hash(info.parsed_addresses()[0]))
|
|
||||||
self.device_list.SetItemData(index, index)
|
|
||||||
self.device_list.itemDataMap[index] = [name, info.properties.get(b"version").decode(), info.properties.get(b"rev").decode(), info.properties.get(b"hw_rev").decode(), info.parsed_addresses()[0]]
|
|
||||||
for col in range(0, len(self.device_list.column_headings)):
|
|
||||||
self.device_list.SetColumnWidth(col, wx.LIST_AUTOSIZE_USEHEADER)
|
|
||||||
elif state == "Removed":
|
|
||||||
if index != wx.NOT_FOUND:
|
|
||||||
self.device_list.DeleteItem(index)
|
|
||||||
|
|
||||||
def on_click_update_firmware(self, event):
|
|
||||||
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 info:
|
|
||||||
address = info.parsed_addresses()[0] if info.parsed_addresses() else "N/A"
|
|
||||||
self.start_firmware_update(address, hw_rev)
|
|
||||||
else:
|
|
||||||
wx.MessageBox("No service information available for selected device", "Error", wx.ICON_ERROR)
|
|
||||||
else:
|
|
||||||
wx.MessageBox("Please select a device to update", "Error", wx.ICON_ERROR)
|
|
||||||
|
|
||||||
def on_click_webui(self, event):
|
|
||||||
selected_index = self.device_list.GetFirstSelected()
|
|
||||||
if selected_index != -1:
|
|
||||||
service_name = self.device_list.GetItemText(selected_index, 0)
|
|
||||||
info = self.listener.services.get(service_name)
|
|
||||||
if info:
|
|
||||||
address = info.parsed_addresses()[0] if info.parsed_addresses() else "N/A"
|
|
||||||
thread = threading.Thread(target=lambda: webbrowser.open(f"http://{address}"))
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
def run_fs_update(self, address, firmware_file, type):
|
|
||||||
global PROGRESS
|
|
||||||
PROGRESS = True
|
|
||||||
espota.PROGRESS = True
|
|
||||||
global TIMEOUT
|
|
||||||
TIMEOUT = 10
|
|
||||||
espota.TIMEOUT = 10
|
|
||||||
|
|
||||||
espota.serve(address, "0.0.0.0", 3232, random.randint(10000,60000), "", firmware_file, type, self.call_progress)
|
|
||||||
|
|
||||||
wx.CallAfter(self.update_progress, 100)
|
|
||||||
self.currentlyUpdating = False
|
|
||||||
self.SetStatusText(f"Finished!")
|
|
||||||
|
|
||||||
def call_progress(self, progress):
|
|
||||||
progressPerc = int(progress*100)
|
|
||||||
self.SetStatusText(f"{self.updatingName} - Progress: {progressPerc}%")
|
|
||||||
wx.CallAfter(self.update_progress, progress)
|
|
||||||
|
|
||||||
def update_progress(self, progress):
|
|
||||||
self.progress_bar.SetValue(int(progress*100))
|
|
||||||
wx.YieldIfNeeded()
|
|
||||||
|
|
||||||
def on_click_update_fs(self, event):
|
|
||||||
selected_index = self.device_list.GetFirstSelected()
|
|
||||||
if selected_index != -1:
|
|
||||||
service_name = self.device_list.GetItemText(selected_index, 0)
|
|
||||||
info = self.listener.services.get(service_name)
|
|
||||||
if self.currentlyUpdating:
|
|
||||||
wx.MessageBox("Please wait, already updating", "Error", wx.ICON_ERROR)
|
|
||||||
return
|
|
||||||
|
|
||||||
if info:
|
|
||||||
address = info.parsed_addresses()[0] if info.parsed_addresses() else "N/A"
|
|
||||||
self.start_fs_update(address)
|
|
||||||
else:
|
|
||||||
wx.MessageBox("No service information available for selected device", "Error", wx.ICON_ERROR)
|
|
||||||
else:
|
|
||||||
wx.MessageBox("Please select a device to update", "Error", wx.ICON_ERROR)
|
|
||||||
|
|
||||||
def start_firmware_update(self, address, hw_rev):
|
|
||||||
self.SetStatusText(f"Starting firmware update")
|
|
||||||
|
|
||||||
model_name = "lolin_s3_mini_213epd"
|
|
||||||
if (hw_rev == "REV_B_EPD_2_13"):
|
|
||||||
model_name = "btclock_rev_b_213epd"
|
|
||||||
|
|
||||||
local_filename = f"firmware/{self.release_name}_{model_name}_firmware.bin"
|
|
||||||
|
|
||||||
self.updatingName = address
|
|
||||||
self.currentlyUpdating = True
|
|
||||||
|
|
||||||
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, address):
|
|
||||||
# Path to the firmware file
|
|
||||||
self.SetStatusText(f"Starting filesystem update")
|
|
||||||
local_filename = f"firmware/{self.release_name}_littlefs.bin"
|
|
||||||
|
|
||||||
self.updatingName = address
|
|
||||||
self.currentlyUpdating = True
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
wx.CallAfter(self.update_progress, 100)
|
|
||||||
|
|
||||||
def on_click_identify(self, event):
|
|
||||||
selected_index = self.device_list.GetFirstSelected()
|
|
||||||
if selected_index != -1:
|
|
||||||
service_name = self.device_list.GetItemText(selected_index, 0)
|
|
||||||
info = self.listener.services.get(service_name)
|
|
||||||
if info:
|
|
||||||
address = info.parsed_addresses()[0] if info.parsed_addresses() else "N/A"
|
|
||||||
port = info.port
|
|
||||||
self.api_handler.identify_btclock(address)
|
|
||||||
else:
|
|
||||||
wx.MessageBox("No service information available for selected device", "Error", wx.ICON_ERROR)
|
|
||||||
else:
|
|
||||||
wx.MessageBox("Please select a device to make an API call", "Error", wx.ICON_ERROR)
|
|
||||||
|
|
||||||
def fetch_latest_release(self):
|
|
||||||
repo = "btclock/btclock_v3"
|
|
||||||
|
|
||||||
if not os.path.exists("firmware"):
|
|
||||||
os.makedirs("firmware")
|
|
||||||
filenames_to_download = ["lolin_s3_mini_213epd_firmware.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_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()
|
|
||||||
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.fw_label.SetLabelText(f"Downloaded firmware version: {self.release_name}\nCommit: {self.commit_hash}")
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
wx.CallAfter(self.SetStatusText, f"File {filenames_to_download} not found in latest release")
|
|
||||||
except requests.RequestException as e:
|
|
||||||
wx.CallAfter(self.SetStatusText, f"Error fetching release: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def download_file(self, url, release_name):
|
|
||||||
local_filename = f"{release_name}_{url.split('/')[-1]}"
|
|
||||||
response = requests.get(url, stream=True)
|
|
||||||
total_length = response.headers.get('content-length')
|
|
||||||
if not os.path.exists("firmware"):
|
|
||||||
os.makedirs("firmware")
|
|
||||||
if os.path.exists(f"firmware/{local_filename}"):
|
|
||||||
wx.CallAfter(self.SetStatusText, f"{local_filename} is already downloaded")
|
|
||||||
return
|
|
||||||
|
|
||||||
if total_length is None:
|
|
||||||
wx.CallAfter(self.SetStatusText, "No content length header")
|
|
||||||
else:
|
|
||||||
total_length = int(total_length)
|
|
||||||
chunk_size = 1024
|
|
||||||
num_chunks = total_length // chunk_size
|
|
||||||
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)
|
|
||||||
f.flush()
|
|
||||||
progress = int((i / num_chunks) * 100)
|
|
||||||
wx.CallAfter(self.update_progress, progress)
|
|
||||||
|
|
||||||
wx.CallAfter(self.update_progress, 100)
|
|
||||||
wx.CallAfter(self.SetStatusText, "Download completed")
|
|
||||||
|
|
||||||
def OnAbout(self,e):
|
|
||||||
dlg = wx.MessageDialog( self, "An updater for BTClocks", "About BTClock OTA Updater", wx.OK)
|
|
||||||
dlg.ShowModal()
|
|
||||||
dlg.Destroy()
|
|
||||||
|
|
||||||
def OnExit(self,e):
|
|
||||||
self.Close(False)
|
|
0
app/gui/__init__.py
Normal file
0
app/gui/__init__.py
Normal file
126
app/gui/action_button_panel.py
Normal file
126
app/gui/action_button_panel.py
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import threading
|
||||||
|
import webbrowser
|
||||||
|
from app.api import ApiHandler
|
||||||
|
from app.gui.devices_panel import DevicesPanel
|
||||||
|
from app.zeroconf_listener import ZeroconfListener
|
||||||
|
import wx
|
||||||
|
|
||||||
|
class ActionButtonPanel(wx.Panel):
|
||||||
|
currentlyUpdating = False
|
||||||
|
|
||||||
|
def __init__(self, parent:wx.Panel, parent_frame:wx.Frame, *args, **kwargs):
|
||||||
|
super(ActionButtonPanel, self).__init__(parent, *args, **kwargs)
|
||||||
|
|
||||||
|
self.parent = parent
|
||||||
|
self.parent_frame = parent_frame
|
||||||
|
self.api_handler:ApiHandler = parent_frame.api_handler
|
||||||
|
self.device_list:DevicesPanel = parent_frame.device_list
|
||||||
|
self.listener:ZeroconfListener = parent_frame.listener
|
||||||
|
|
||||||
|
self.device_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_item_selected)
|
||||||
|
self.device_list.Bind(wx.EVT_LIST_ITEM_DESELECTED,
|
||||||
|
self.on_item_deselected)
|
||||||
|
self.InitUI()
|
||||||
|
|
||||||
|
def InitUI(self):
|
||||||
|
sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
|
||||||
|
self.update_button = wx.Button(self, label="Update Firmware")
|
||||||
|
self.update_button.Bind(wx.EVT_BUTTON, self.on_click_update_firmware)
|
||||||
|
self.update_fs_button = wx.Button(self, label="Update Filesystem")
|
||||||
|
self.update_fs_button.Bind(wx.EVT_BUTTON, self.on_click_update_fs)
|
||||||
|
|
||||||
|
self.identify_button = wx.Button(self, label="Identify")
|
||||||
|
self.identify_button.Bind(wx.EVT_BUTTON, self.on_click_identify)
|
||||||
|
self.open_webif_button = wx.Button(self, label="Open WebUI")
|
||||||
|
self.open_webif_button.Bind(wx.EVT_BUTTON, self.on_click_webui)
|
||||||
|
self.update_button.Disable()
|
||||||
|
self.update_fs_button.Disable()
|
||||||
|
self.identify_button.Disable()
|
||||||
|
self.open_webif_button.Disable()
|
||||||
|
|
||||||
|
sizer.Add(self.update_button)
|
||||||
|
sizer.Add(self.update_fs_button)
|
||||||
|
sizer.Add(self.identify_button)
|
||||||
|
sizer.Add(self.open_webif_button)
|
||||||
|
|
||||||
|
self.SetSizer(sizer)
|
||||||
|
|
||||||
|
def on_click_update_firmware(self, event):
|
||||||
|
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 info:
|
||||||
|
address = info.parsed_addresses(
|
||||||
|
)[0] if info.parsed_addresses() else "N/A"
|
||||||
|
self.parent_frame.fw_updater.start_firmware_update(self.parent_frame.releaseChecker.release_name, address, hw_rev)
|
||||||
|
else:
|
||||||
|
wx.MessageBox(
|
||||||
|
"No service information available for selected device", "Error", wx.ICON_ERROR)
|
||||||
|
else:
|
||||||
|
wx.MessageBox("Please select a device to update",
|
||||||
|
"Error", wx.ICON_ERROR)
|
||||||
|
|
||||||
|
def on_click_webui(self, event):
|
||||||
|
selected_index = self.device_list.GetFirstSelected()
|
||||||
|
if selected_index != -1:
|
||||||
|
service_name = self.device_list.GetItemText(selected_index, 0)
|
||||||
|
info = self.listener.services.get(service_name)
|
||||||
|
if info:
|
||||||
|
address = info.parsed_addresses(
|
||||||
|
)[0] if info.parsed_addresses() else "N/A"
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=lambda: webbrowser.open(f"http://{address}"))
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def on_click_update_fs(self, event):
|
||||||
|
selected_index = self.device_list.GetFirstSelected()
|
||||||
|
if selected_index != -1:
|
||||||
|
service_name = self.device_list.GetItemText(selected_index, 0)
|
||||||
|
info = self.listener.services.get(service_name)
|
||||||
|
if self.currentlyUpdating:
|
||||||
|
wx.MessageBox("Please wait, already updating",
|
||||||
|
"Error", wx.ICON_ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
wx.MessageBox(
|
||||||
|
"No service information available for selected device", "Error", wx.ICON_ERROR)
|
||||||
|
else:
|
||||||
|
wx.MessageBox("Please select a device to update",
|
||||||
|
"Error", wx.ICON_ERROR)
|
||||||
|
def on_click_identify(self, event):
|
||||||
|
selected_index = self.device_list.GetFirstSelected()
|
||||||
|
if selected_index != -1:
|
||||||
|
service_name = self.device_list.GetItemText(selected_index, 0)
|
||||||
|
info = self.listener.services.get(service_name)
|
||||||
|
if info:
|
||||||
|
address = info.parsed_addresses(
|
||||||
|
)[0] if info.parsed_addresses() else "N/A"
|
||||||
|
port = info.port
|
||||||
|
self.api_handler.identify_btclock(address)
|
||||||
|
else:
|
||||||
|
wx.MessageBox(
|
||||||
|
"No service information available for selected device", "Error", wx.ICON_ERROR)
|
||||||
|
else:
|
||||||
|
wx.MessageBox(
|
||||||
|
"Please select a device to make an API call", "Error", wx.ICON_ERROR)
|
||||||
|
def on_item_selected(self, event):
|
||||||
|
self.update_button.Enable()
|
||||||
|
self.update_fs_button.Enable()
|
||||||
|
self.identify_button.Enable()
|
||||||
|
self.open_webif_button.Enable()
|
||||||
|
|
||||||
|
def on_item_deselected(self, event):
|
||||||
|
if self.device_list.GetFirstSelected() == -1:
|
||||||
|
self.update_button.Disable()
|
||||||
|
self.update_fs_button.Disable()
|
||||||
|
self.identify_button.Disable()
|
||||||
|
self.open_webif_button.Disable()
|
31
app/gui/devices_panel.py
Normal file
31
app/gui/devices_panel.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import wx
|
||||||
|
import wx.lib.mixins.listctrl
|
||||||
|
|
||||||
|
|
||||||
|
class DevicesPanel(wx.ListCtrl, wx.lib.mixins.listctrl.ColumnSorterMixin, wx.lib.mixins.listctrl.ListCtrlAutoWidthMixin,
|
||||||
|
):
|
||||||
|
def __init__(self, parent):
|
||||||
|
wx.ListCtrl.__init__(self, parent, style=wx.LC_REPORT)
|
||||||
|
self.column_headings = [
|
||||||
|
"Hostname", "Version", "FW commit", "HW Revision", "IP", "WebUI commit"]
|
||||||
|
wx.lib.mixins.listctrl.ColumnSorterMixin.__init__(
|
||||||
|
self,
|
||||||
|
len(self.column_headings),
|
||||||
|
)
|
||||||
|
wx.lib.mixins.listctrl.ListCtrlAutoWidthMixin.__init__(self)
|
||||||
|
|
||||||
|
for column, heading in enumerate(self.column_headings):
|
||||||
|
self.AppendColumn(heading)
|
||||||
|
|
||||||
|
self.itemDataMap = {}
|
||||||
|
|
||||||
|
def OnSortOrderChanged(self):
|
||||||
|
"""Method to handle changes to the sort order"""
|
||||||
|
|
||||||
|
column, ascending = self.GetSortState()
|
||||||
|
self.ShowSortIndicator(column, ascending)
|
||||||
|
self.SortListItems(column, ascending)
|
||||||
|
|
||||||
|
def GetListCtrl(self):
|
||||||
|
"""Method required by the ColumnSorterMixin"""
|
||||||
|
return self
|
167
app/main.py
Normal file
167
app/main.py
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from app.gui.action_button_panel import ActionButtonPanel
|
||||||
|
from app.release_checker import ReleaseChecker
|
||||||
|
import wx
|
||||||
|
|
||||||
|
from zeroconf import ServiceBrowser, Zeroconf
|
||||||
|
import os
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
from app import espota
|
||||||
|
from app.api import ApiHandler
|
||||||
|
from app.fw_updater import FwUpdater
|
||||||
|
from app.gui.devices_panel import DevicesPanel
|
||||||
|
from app.zeroconf_listener import ZeroconfListener
|
||||||
|
|
||||||
|
from app.espota import FLASH, SPIFFS
|
||||||
|
|
||||||
|
class SerialPortsComboBox(wx.ComboBox):
|
||||||
|
def __init__(self, parent, fw_update):
|
||||||
|
self.fw_update = fw_update
|
||||||
|
self.ports = serial.tools.list_ports.comports()
|
||||||
|
wx.ComboBox.__init__(self, parent, choices=[
|
||||||
|
port.device for port in self.ports])
|
||||||
|
|
||||||
|
|
||||||
|
class BTClockOTAUpdater(wx.Frame):
|
||||||
|
updatingName = ""
|
||||||
|
|
||||||
|
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()
|
||||||
|
self.listener = ZeroconfListener(self.on_zeroconf_state_change)
|
||||||
|
self.browser = ServiceBrowser(
|
||||||
|
self.zeroconf, "_http._tcp.local.", self.listener)
|
||||||
|
self.api_handler = ApiHandler()
|
||||||
|
self.fw_updater = FwUpdater(self.call_progress)
|
||||||
|
|
||||||
|
panel = wx.Panel(self)
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
panel.SetSizer(vbox)
|
||||||
|
|
||||||
|
self.setup_ui()
|
||||||
|
wx.CallAfter(self.fetch_latest_release_async)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
self.setup_menubar()
|
||||||
|
self.status_bar = self.CreateStatusBar(2)
|
||||||
|
self.Show(True)
|
||||||
|
self.Centre()
|
||||||
|
|
||||||
|
def setup_menubar(self):
|
||||||
|
filemenu = wx.Menu()
|
||||||
|
menuAbout = filemenu.Append(
|
||||||
|
wx.ID_ABOUT, "&About", " Information about this program")
|
||||||
|
menuExit = filemenu.Append(
|
||||||
|
wx.ID_EXIT, "E&xit", " Terminate the program")
|
||||||
|
|
||||||
|
menuBar = wx.MenuBar()
|
||||||
|
menuBar.Append(filemenu, "&File")
|
||||||
|
self.SetMenuBar(menuBar)
|
||||||
|
|
||||||
|
self.Bind(wx.EVT_MENU, self.OnAbout, menuAbout)
|
||||||
|
self.Bind(wx.EVT_MENU, self.OnExit, menuExit)
|
||||||
|
|
||||||
|
def on_zeroconf_state_change(self, type, name, state, info):
|
||||||
|
index = self.device_list.FindItem(0, name)
|
||||||
|
|
||||||
|
if state == "Added":
|
||||||
|
deviceSettings = self.api_handler.get_settings(
|
||||||
|
info.parsed_addresses()[0])
|
||||||
|
|
||||||
|
version = info.properties.get(b"rev").decode()
|
||||||
|
|
||||||
|
if 'gitTag' in deviceSettings:
|
||||||
|
version = deviceSettings["gitTag"]
|
||||||
|
|
||||||
|
fwHash = info.properties.get(b"rev").decode()[:7]
|
||||||
|
fsHash = deviceSettings['fsRev'][:7]
|
||||||
|
address = info.parsed_addresses()[0]
|
||||||
|
|
||||||
|
if index == wx.NOT_FOUND:
|
||||||
|
index = self.device_list.InsertItem(
|
||||||
|
self.device_list.GetItemCount(), type)
|
||||||
|
self.device_list.SetItem(index, 0, name)
|
||||||
|
self.device_list.SetItem(index, 1, version)
|
||||||
|
self.device_list.SetItem(index, 2, fwHash)
|
||||||
|
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)
|
||||||
|
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, 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)
|
||||||
|
elif state == "Removed":
|
||||||
|
if index != wx.NOT_FOUND:
|
||||||
|
self.device_list.DeleteItem(index)
|
||||||
|
|
||||||
|
def call_progress(self, progress):
|
||||||
|
progressPerc = int(progress*100)
|
||||||
|
self.SetStatusText(f"Progress: {progressPerc}%")
|
||||||
|
wx.CallAfter(self.update_progress, progress)
|
||||||
|
|
||||||
|
def update_progress(self, progress):
|
||||||
|
progressPerc = int(progress*100)
|
||||||
|
self.progress_bar.SetValue(progressPerc)
|
||||||
|
wx.YieldIfNeeded()
|
||||||
|
|
||||||
|
def fetch_latest_release_async(self):
|
||||||
|
# Start a new thread to execute fetch_latest_release
|
||||||
|
executor = concurrent.futures.ThreadPoolExecutor()
|
||||||
|
future = executor.submit(self.releaseChecker.fetch_latest_release)
|
||||||
|
future.add_done_callback(self.handle_latest_release)
|
||||||
|
|
||||||
|
def handle_latest_release(self, future):
|
||||||
|
try:
|
||||||
|
latest_release = future.result()
|
||||||
|
self.fw_label.SetLabelText(f"Downloaded firmware version: {
|
||||||
|
latest_release}\nCommit: {self.releaseChecker.commit_hash}")
|
||||||
|
except Exception as e:
|
||||||
|
self.fw_label.SetLabel(f"Error occurred: {str(e)}")
|
||||||
|
|
||||||
|
def OnAbout(self, e):
|
||||||
|
dlg = wx.MessageDialog(
|
||||||
|
self, "An updater for BTClocks", "About BTClock OTA Updater", wx.OK)
|
||||||
|
dlg.ShowModal()
|
||||||
|
dlg.Destroy()
|
||||||
|
|
||||||
|
def OnExit(self, e):
|
||||||
|
self.Close(False)
|
97
app/release_checker.py
Normal file
97
app/release_checker.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import wx
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from app.utils import keep_latest_versions
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseChecker:
|
||||||
|
'''Release Checker for firmware updates'''
|
||||||
|
release_name = ""
|
||||||
|
commit_hash = ""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.progress_callback: Callable[[int], None] = None
|
||||||
|
|
||||||
|
def fetch_latest_release(self):
|
||||||
|
'''Fetch latest firmware release from GitHub'''
|
||||||
|
repo = "btclock/btclock_v3"
|
||||||
|
|
||||||
|
if not os.path.exists("firmware"):
|
||||||
|
os.makedirs("firmware")
|
||||||
|
filenames_to_download = ["lolin_s3_mini_213epd_firmware.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_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()
|
||||||
|
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"]
|
||||||
|
|
||||||
|
return self.release_name
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ReleaseCheckerException(
|
||||||
|
f"File {filenames_to_download} not found in latest release")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise ReleaseCheckerException(
|
||||||
|
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]}"
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
total_length = response.headers.get('content-length')
|
||||||
|
if not os.path.exists("firmware"):
|
||||||
|
os.makedirs("firmware")
|
||||||
|
if os.path.exists(f"firmware/{local_filename}"):
|
||||||
|
return
|
||||||
|
|
||||||
|
keep_latest_versions('firmware', 2)
|
||||||
|
|
||||||
|
if total_length is None:
|
||||||
|
raise ReleaseCheckerException("No content length header")
|
||||||
|
else:
|
||||||
|
total_length = int(total_length)
|
||||||
|
chunk_size = 1024
|
||||||
|
num_chunks = total_length // chunk_size
|
||||||
|
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)
|
||||||
|
f.flush()
|
||||||
|
progress = int((i / num_chunks) * 100)
|
||||||
|
if callable(self.progress_callback):
|
||||||
|
self.progress_callback(progress)
|
||||||
|
|
||||||
|
if callable(self.progress_callback):
|
||||||
|
self.progress_callback(100)
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseCheckerException(Exception):
|
||||||
|
pass
|
32
app/utils.py
Normal file
32
app/utils.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
def count_versions(folder_path):
|
||||||
|
versions = set()
|
||||||
|
version_pattern = re.compile(r'^(\d+\.\d+\.\d+)')
|
||||||
|
for file_name in os.listdir(folder_path):
|
||||||
|
match = version_pattern.match(file_name)
|
||||||
|
if match:
|
||||||
|
versions.add(match.group(1))
|
||||||
|
return len(versions)
|
||||||
|
|
||||||
|
|
||||||
|
def keep_latest_versions(folder_path, num_versions_to_keep=2):
|
||||||
|
version_files = {}
|
||||||
|
version_pattern = re.compile(r'^(\d+\.\d+\.\d+)')
|
||||||
|
for file_name in os.listdir(folder_path):
|
||||||
|
match = version_pattern.match(file_name)
|
||||||
|
if match:
|
||||||
|
version = match.group(1)
|
||||||
|
if version not in version_files:
|
||||||
|
version_files[version] = []
|
||||||
|
version_files[version].append(file_name)
|
||||||
|
|
||||||
|
versions_sorted = sorted(version_files.keys(), reverse=True)
|
||||||
|
versions_to_remove = versions_sorted[num_versions_to_keep:]
|
||||||
|
|
||||||
|
for version in versions_to_remove:
|
||||||
|
for file_name in version_files[version]:
|
||||||
|
os.remove(os.path.join(folder_path, file_name))
|
|
@ -1,7 +1,9 @@
|
||||||
import wx
|
import wx
|
||||||
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange
|
from zeroconf import Zeroconf
|
||||||
|
|
||||||
|
|
||||||
class ZeroconfListener:
|
class ZeroconfListener:
|
||||||
|
'''Zeroconf Handler to find BTClocks in the network'''
|
||||||
release_name = ""
|
release_name = ""
|
||||||
firmware_file = ""
|
firmware_file = ""
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Requests==2.32.3
|
Requests==2.32.3
|
||||||
wxPython==4.2.1
|
wxPython==4.2.2a1.dev5670+a207b407
|
||||||
zeroconf==0.132.2
|
zeroconf==0.132.2
|
||||||
esptool==4.7.0
|
esptool==4.7.0
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
|
|
Loading…
Reference in a new issue