Add flash over serial functionality

This commit is contained in:
Djuri Baars 2025-01-05 16:26:28 +01:00
parent c8c69a39b4
commit 63fe5a92b2
No known key found for this signature in database
GPG key ID: 61B9B2DDE5AA3AC1
16 changed files with 623 additions and 83 deletions

View file

@ -11,12 +11,19 @@ from app.utils import get_app_data_folder
class FwUpdater: class FwUpdater:
update_progress = None
currentlyUpdating = False
def __init__(self, update_progress, event_cb): def __init__(self, update_progress, event_cb):
self.update_progress = update_progress self.update_progress = update_progress
self.event_cb = event_cb self.event_cb = event_cb
self.currentlyUpdating = False
self.release_name = None
def call_progress(self, progress):
if callable(self.update_progress):
wx.CallAfter(self.update_progress, progress)
def call_event(self, message):
if callable(self.event_cb):
wx.CallAfter(self.event_cb, message)
def get_serial_ports(self): def get_serial_ports(self):
ports = serial.tools.list_ports.comports() ports = serial.tools.list_ports.comports()
@ -35,41 +42,112 @@ class FwUpdater:
espota.TIMEOUT = 10 espota.TIMEOUT = 10
espota.serve(address, "0.0.0.0", 3232, random.randint( espota.serve(address, "0.0.0.0", 3232, random.randint(
10000, 60000), "", firmware_file, type, self.update_progress) 10000, 60000), "", firmware_file, type, self.call_progress)
wx.CallAfter(self.update_progress, 1) wx.CallAfter(self.call_progress, 1)
self.currentlyUpdating = False self.currentlyUpdating = False
# self.SetStatusText(f"Finished!")
def flash_firmware(port, baud, firmware_path): def flash_firmware(self, port, firmware_path, hw_rev, preserve_nvs=False, baud=460800):
try: try:
# Initialize the serial port self.call_event("Starting serial flash...")
serial_port = serial.Serial(port, baud) self.call_progress(0)
# Initialize the ESP32ROM with the serial port # Determine flash size and partition table based on hardware revision
esp = esptool.ESP32ROM(serial_port) flash_size = "4MB"
partition_suffix = ""
littlefs_size = "4MB"
littlefs_offset = "0x380000" # For 4MB flash
# Connect to the ESP32 if hw_rev == "REV_B_EPD_2_13":
esp.connect() flash_size = "8MB"
partition_suffix = "_8mb"
littlefs_size = "8MB"
littlefs_offset = "0x700000" # After app0 (0x10000 + 0x370000) and app1 (0x370000)
elif hw_rev == "REV_V8_EPD_2_13":
flash_size = "16MB"
partition_suffix = "_16mb"
littlefs_size = "16MB"
littlefs_offset = "0xDF0000" # After app0 (0x10000 + 0x6F0000) and app1 (0x6F0000)
# Perform the flashing operation # Get paths to required files
esp.flash_file(firmware_path, offset=0x1000) bootloader_path = os.path.join("partition_tables", f"bootloader{partition_suffix}.bin")
partition_table_path = os.path.join("partition_tables", f"partitions{partition_suffix}.bin")
boot_app0_path = os.path.join("partition_tables", "boot_app0.bin")
ota_data_path = os.path.join("partition_tables", "ota_data_initial.bin")
littlefs_path = os.path.join(get_app_data_folder(), f"{self.release_name}_littlefs_{littlefs_size}.bin")
# Optionally, verify the flash # Verify all required files exist
esp.verify_flash(firmware_path, offset=0x1000) required_files = [bootloader_path, partition_table_path, boot_app0_path, ota_data_path, firmware_path, littlefs_path]
for file_path in required_files:
if not os.path.exists(file_path):
raise Exception(f"Required file not found: {file_path}")
print("Firmware flashed successfully!") # Common command parameters
common_args = [
'--chip', 'esp32s3',
'--port', port,
'--baud', str(baud),
'--before', 'default_reset',
'--after', 'hard_reset'
]
except esptool.FatalError as e: import logging
print(f"Failed to flash firmware: {e}") logger = logging.getLogger()
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): # First, handle erasing
# self.SetStatusText(f"Starting firmware update") try:
if not preserve_nvs:
# Erase entire flash
logger.info("Erasing flash...")
esptool.main(common_args + ['erase_flash'])
else:
# Erase specific regions, preserving NVS
logger.info("Preserve NVS...")
self.call_progress(0.3) # 30% progress after erase
except Exception as e:
logger.error(f"Error during flash erase: {str(e)}")
raise
# Then, flash all components
try:
logger.info("Writing firmware...")
flash_command = common_args + [
'write_flash',
'--flash_mode', 'dio',
'--flash_freq', '80m',
'--flash_size', flash_size,
'0x0', bootloader_path, # Bootloader
'0x8000', partition_table_path, # Partition table
'0xe000', ota_data_path, # Boot app
'0x10000', firmware_path, # Main app (app0)
littlefs_offset, littlefs_path # LittleFS data
]
esptool.main(flash_command)
self.call_progress(1.0) # 100% progress after flash
self.call_event("Firmware flashed successfully!")
self.currentlyUpdating = False # Reset the updating state after successful flash
return True
except Exception as e:
logger.error(f"Error during firmware write: {str(e)}")
raise
except Exception as e:
self.call_event(f"Failed to flash firmware: {str(e)}")
self.call_progress(0) # Reset progress bar
self.currentlyUpdating = False # Reset the updating state on error
return False
def start_serial_firmware_update(self, release_name, port, hw_rev, preserve_nvs=False):
if self.currentlyUpdating:
self.call_event("Another update is in progress")
return False
self.release_name = release_name # Store release name for littlefs path
hw_rev_to_model = { hw_rev_to_model = {
"REV_B_EPD_2_13": "btclock_rev_b_213epd", "REV_B_EPD_2_13": "btclock_rev_b_213epd",
"REV_V8_EPD_2_13": "btclock_v8_213epd", "REV_V8_EPD_2_13": "btclock_v8_213epd",
@ -77,16 +155,38 @@ class FwUpdater:
} }
model_name = hw_rev_to_model.get(hw_rev, "lolin_s3_mini_213epd") model_name = hw_rev_to_model.get(hw_rev, "lolin_s3_mini_213epd")
local_filename = f"{get_app_data_folder()}/{release_name}_{model_name}_firmware.bin"
self.currentlyUpdating = True
local_filename = f"{get_app_data_folder()}/{ if not os.path.exists(os.path.abspath(local_filename)):
release_name}_{model_name}_firmware.bin" self.call_event(f"Firmware file not found: {local_filename}")
self.currentlyUpdating = False
return False
try:
thread = Thread(target=self.flash_firmware, args=(port, os.path.abspath(local_filename), hw_rev, preserve_nvs))
thread.start()
return True
except Exception as e:
self.call_event(f"Failed to start serial update: {str(e)}")
self.currentlyUpdating = False
return False
def start_firmware_update(self, release_name, address, hw_rev):
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 = hw_rev_to_model.get(hw_rev, "lolin_s3_mini_213epd")
local_filename = f"{get_app_data_folder()}/{release_name}_{model_name}_firmware.bin"
self.updatingName = address self.updatingName = address
self.currentlyUpdating = True self.currentlyUpdating = True
if self.event_cb is not None: self.call_event("Starting Firmware update")
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=(
@ -106,13 +206,51 @@ class FwUpdater:
self.updatingName = address self.updatingName = address
self.currentlyUpdating = True self.currentlyUpdating = True
if self.event_cb is not None: self.call_event(f"Starting WebUI update {local_filename}")
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: else:
if self.event_cb is not None: self.call_event(f"Firmware file not found: {local_filename}")
self.event_cb(f"Firmware file not found: {local_filename}") self.currentlyUpdating = False
def detect_hardware_revision(self, port, baud=460800):
"""Detect hardware revision based on flash size"""
try:
import logging
logger = logging.getLogger()
common_args = [
'--chip', 'esp32s3',
'--port', port,
'--baud', str(baud),
'--no-stub', # Don't upload stub and don't reset
'flash_id'
]
# Capture the output of esptool flash_id command
import io
import sys
output = io.StringIO()
old_stdout = sys.stdout
sys.stdout = output
try:
esptool.main(common_args)
flash_id_output = output.getvalue()
finally:
sys.stdout = old_stdout
# Parse the output to find flash size
if "16MB" in flash_id_output:
return "REV_V8_EPD_2_13"
elif "8MB" in flash_id_output:
return "REV_B_EPD_2_13"
else:
return "REV_A_EPD_2_13" # Default to 4MB version
except Exception as e:
logger.error(f"Error detecting hardware revision: {str(e)}")
return None

View file

@ -0,0 +1,125 @@
import wx
from app.gui.serial_monitor import SerialMonitor
class SerialFlashDialog(wx.Dialog):
def __init__(self, parent, fw_updater, release_name):
super().__init__(parent, title="Flash via Serial", size=(400, 300))
self.fw_updater = fw_updater
self.release_name = release_name
panel = wx.Panel(self)
vbox = wx.BoxSizer(wx.VERTICAL)
# Port selection
hbox1 = wx.BoxSizer(wx.HORIZONTAL)
hbox1.Add(wx.StaticText(panel, label="Port:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.port_choice = wx.Choice(panel, choices=[])
hbox1.Add(self.port_choice, 1)
# Refresh button
refresh_btn = wx.Button(panel, label="Refresh")
refresh_btn.Bind(wx.EVT_BUTTON, self.on_refresh)
hbox1.Add(refresh_btn, 0, wx.LEFT, 5)
# Serial Monitor button
monitor_btn = wx.Button(panel, label="Monitor")
monitor_btn.Bind(wx.EVT_BUTTON, self.on_monitor)
hbox1.Add(monitor_btn, 0, wx.LEFT, 5)
vbox.Add(hbox1, 0, wx.EXPAND | wx.ALL, 5)
# Hardware revision
hbox2 = wx.BoxSizer(wx.HORIZONTAL)
hbox2.Add(wx.StaticText(panel, label="Hardware:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.hw_choice = wx.Choice(panel, choices=[
"REV_A_EPD_2_13",
"REV_B_EPD_2_13",
"REV_V8_EPD_2_13",
"REV_A_EPD_2_9"
])
self.hw_choice.SetSelection(0)
hbox2.Add(self.hw_choice, 1)
# Auto-detect button
detect_btn = wx.Button(panel, label="Auto-detect")
detect_btn.Bind(wx.EVT_BUTTON, self.on_detect)
hbox2.Add(detect_btn, 0, wx.LEFT, 5)
vbox.Add(hbox2, 0, wx.EXPAND | wx.ALL, 5)
# Preserve NVS checkbox
self.preserve_nvs = wx.CheckBox(panel, label="Preserve NVS (keep settings)")
vbox.Add(self.preserve_nvs, 0, wx.EXPAND | wx.ALL, 5)
# Buttons
button_sizer = wx.BoxSizer(wx.HORIZONTAL)
flash_button = wx.Button(panel, wx.ID_OK, "Flash")
cancel_button = wx.Button(panel, wx.ID_CANCEL, "Cancel")
button_sizer.Add(flash_button, 0, wx.ALL, 5)
button_sizer.Add(cancel_button, 0, wx.ALL, 5)
vbox.Add(button_sizer, 0, wx.ALIGN_CENTER | wx.ALL, 5)
panel.SetSizer(vbox)
self.refresh_ports()
def refresh_ports(self):
ports = self.fw_updater.get_serial_ports()
self.port_choice.Clear()
filtered_ports = []
for port, desc, hwid in ports:
# Filter for USB devices with vendor IDs 0x1a86 or 0x303a
if ("1A86" in hwid.upper() or "303A" in hwid.upper()):
filtered_ports.append(port)
self.port_choice.Append(f"{port} - {desc}")
if self.port_choice.GetCount() > 0:
self.port_choice.SetSelection(0)
def on_refresh(self, event):
self.refresh_ports()
def on_detect(self, event):
if self.port_choice.GetSelection() == wx.NOT_FOUND:
wx.MessageBox("Please select a port first", "Error", wx.OK | wx.ICON_ERROR)
return
port = self.get_selected_port()
if not port:
return
# Show busy cursor
wx.BeginBusyCursor()
try:
revision = self.fw_updater.detect_hardware_revision(port)
if revision:
# Find and select the detected revision
for i in range(self.hw_choice.GetCount()):
if self.hw_choice.GetString(i) == revision:
self.hw_choice.SetSelection(i)
break
finally:
wx.EndBusyCursor()
def on_monitor(self, event):
if self.port_choice.GetSelection() == wx.NOT_FOUND:
wx.MessageBox("Please select a port first", "Error", wx.OK | wx.ICON_ERROR)
return
port = self.get_selected_port()
if not port:
return
monitor = SerialMonitor(self, port)
monitor.Show()
def get_selected_port(self):
if self.port_choice.GetSelection() == wx.NOT_FOUND:
return None
return self.port_choice.GetString(self.port_choice.GetSelection()).split(" - ")[0]
def get_selected_revision(self):
return self.hw_choice.GetString(self.hw_choice.GetSelection())
def get_preserve_nvs(self):
return self.preserve_nvs.GetValue()

126
app/gui/serial_monitor.py Normal file
View file

@ -0,0 +1,126 @@
import wx
import serial
import threading
import time
class SerialMonitor(wx.Dialog):
def __init__(self, parent, port):
super().__init__(parent, title=f"Serial Monitor - {port}", size=(600, 400))
self.port = port
self.serial = None
self.running = False
self._destroyed = False
# Create UI elements
panel = wx.Panel(self)
vbox = wx.BoxSizer(wx.VERTICAL)
# Text area for output
self.text_area = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL)
vbox.Add(self.text_area, 1, wx.EXPAND | wx.ALL, 5)
# Input area
hbox = wx.BoxSizer(wx.HORIZONTAL)
self.input_field = wx.TextCtrl(panel, style=wx.TE_PROCESS_ENTER)
self.input_field.Bind(wx.EVT_TEXT_ENTER, self.on_send)
hbox.Add(self.input_field, 1, wx.EXPAND | wx.RIGHT, 5)
# Send button
send_button = wx.Button(panel, label="Send")
send_button.Bind(wx.EVT_BUTTON, self.on_send)
hbox.Add(send_button, 0)
vbox.Add(hbox, 0, wx.EXPAND | wx.ALL, 5)
# Baud rate selector
hbox2 = wx.BoxSizer(wx.HORIZONTAL)
hbox2.Add(wx.StaticText(panel, label="Baud Rate:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.baud_selector = wx.Choice(panel, choices=["9600", "115200"])
self.baud_selector.SetSelection(1) # Default to 115200
self.baud_selector.Bind(wx.EVT_CHOICE, self.on_baud_change)
hbox2.Add(self.baud_selector, 0)
# Clear button
clear_button = wx.Button(panel, label="Clear")
clear_button.Bind(wx.EVT_BUTTON, self.on_clear)
hbox2.Add(clear_button, 0, wx.LEFT, 5)
vbox.Add(hbox2, 0, wx.EXPAND | wx.ALL, 5)
panel.SetSizer(vbox)
# Bind close event
self.Bind(wx.EVT_CLOSE, self.on_close)
self.start_monitor()
def start_monitor(self):
try:
baud = int(self.baud_selector.GetString(self.baud_selector.GetSelection()))
self.serial = serial.Serial(self.port, baud, timeout=0.1)
self.running = True
self.monitor_thread = threading.Thread(target=self.read_serial)
self.monitor_thread.daemon = True
self.monitor_thread.start()
except Exception as e:
wx.MessageBox(f"Error opening port: {str(e)}", "Error", wx.OK | wx.ICON_ERROR)
self.Close()
def read_serial(self):
while self.running:
if self.serial and self.serial.is_open:
try:
if self.serial.in_waiting:
data = self.serial.read(self.serial.in_waiting)
if data and not self._destroyed:
wx.CallAfter(self.append_text, data.decode('utf-8', errors='replace'))
except Exception as e:
if not self._destroyed:
wx.CallAfter(self.append_text, f"\nError reading from port: {str(e)}\n")
break
time.sleep(0.1)
def append_text(self, text):
if not self._destroyed and self and self.text_area:
try:
self.text_area.AppendText(text)
except:
pass # Ignore any errors if the window is being destroyed
def on_send(self, event):
if self.serial and self.serial.is_open and not self._destroyed:
text = self.input_field.GetValue() + '\n'
try:
self.serial.write(text.encode())
self.input_field.SetValue("")
except Exception as e:
if not self._destroyed:
wx.MessageBox(f"Error sending data: {str(e)}", "Error", wx.OK | wx.ICON_ERROR)
def on_baud_change(self, event):
if self.serial and not self._destroyed:
try:
self.serial.close()
self.start_monitor()
except Exception as e:
if not self._destroyed:
wx.MessageBox(f"Error changing baud rate: {str(e)}", "Error", wx.OK | wx.ICON_ERROR)
def on_clear(self, event):
if not self._destroyed and self.text_area:
self.text_area.SetValue("")
def on_close(self, event):
self.running = False
self._destroyed = True
if self.serial:
try:
self.serial.close()
except:
pass # Ignore any errors during cleanup
event.Skip()
def Destroy(self):
self._destroyed = True
super().Destroy()

View file

@ -1,12 +1,16 @@
import concurrent.futures import concurrent.futures
import logging import logging
import traceback import traceback
import sys
from datetime import datetime
import serial import serial
from app.gui.action_button_panel import ActionButtonPanel from app.gui.action_button_panel import ActionButtonPanel
from app.gui.serial_flash_dialog import SerialFlashDialog
from app.release_checker import ReleaseChecker from app.release_checker import ReleaseChecker
import wx import wx
import wx.richtext as rt import wx.richtext as rt
from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
from zeroconf import ServiceBrowser, Zeroconf from zeroconf import ServiceBrowser, Zeroconf
import os import os
@ -21,33 +25,113 @@ from app.zeroconf_listener import ZeroconfListener
from app.espota import FLASH, SPIFFS from app.espota import FLASH, SPIFFS
class BTClockOTAApp(wx.App): # Type: ignore[attr-defined]
def OnInit(self): # The above comment tells the linter to ignore attribute-defined errors for this file
return True
class RichTextCtrlHandler(logging.Handler): class LogRedirector:
def __init__(self, logger, level):
self.logger = logger
self.level = level
self.buffer = ""
def write(self, text):
self.buffer += text
while '\n' in self.buffer:
line, self.buffer = self.buffer.split('\n', 1)
if line.strip(): # Only log non-empty lines
self.logger.log(self.level, line.rstrip())
def flush(self):
if self.buffer:
self.logger.log(self.level, self.buffer.rstrip())
self.buffer = ""
class LogListCtrl(wx.ListCtrl, ListCtrlAutoWidthMixin): # type: ignore[misc]
def __init__(self, parent):
wx.ListCtrl.__init__(self, parent, style=wx.LC_REPORT | wx.LC_VIRTUAL | wx.BORDER_NONE) # type: ignore[attr-defined]
ListCtrlAutoWidthMixin.__init__(self)
self.log_entries = []
# Add columns
self.InsertColumn(0, "Time", width=80)
self.InsertColumn(1, "Level", width=70)
self.InsertColumn(2, "Message", width=600)
# Set up virtual list
self.SetItemCount(0)
# Bind events
self.Bind(wx.EVT_LIST_CACHE_HINT, self.OnCacheHint)
def OnGetItemText(self, item, col):
try:
if item < len(self.log_entries):
entry = self.log_entries[item]
if col == 0:
return str(entry['time'])
elif col == 1:
return str(entry['level'])
else:
return str(entry['message'])
except Exception:
pass
return ""
def OnCacheHint(self, evt):
evt.Skip()
def append_log(self, time, level, message):
try:
# Convert float timestamp to string if needed
if isinstance(time, (float, int)):
time = datetime.fromtimestamp(time).strftime('%H:%M:%S')
elif isinstance(time, datetime):
time = time.strftime('%H:%M:%S')
elif not isinstance(time, str):
time = str(time)
self.log_entries.append({
'time': time,
'level': level,
'message': message
})
self.SetItemCount(len(self.log_entries))
# Ensure the last item is visible
if len(self.log_entries) > 0:
self.EnsureVisible(len(self.log_entries) - 1)
except Exception:
pass
class LogListHandler(logging.Handler):
def __init__(self, ctrl): def __init__(self, ctrl):
super().__init__() super().__init__()
self.ctrl = ctrl self.ctrl = ctrl
def emit(self, record): def emit(self, record):
msg = self.format(record) try:
wx.CallAfter(self.append_text, "\n" + msg) # Get time from the record
if hasattr(record, 'asctime'):
time = record.asctime.split()[3] # Get HH:MM:SS part
else:
from datetime import datetime
time = datetime.fromtimestamp(record.created).strftime('%H:%M:%S')
def append_text(self, text): level = record.levelname
self.ctrl.AppendText(text) # Format the message with its arguments
self.ctrl.ShowPosition(self.ctrl.GetLastPosition()) try:
msg = record.getMessage()
class SerialPortsComboBox(wx.ComboBox): except Exception:
def __init__(self, parent, fw_update): msg = str(record.msg)
self.fw_update = fw_update wx.CallAfter(self.ctrl.append_log, time, level, msg)
self.ports = serial.tools.list_ports.comports() except Exception:
wx.ComboBox.__init__(self, parent, choices=[ self.handleError(record)
port.device for port in self.ports])
class BTClockOTAApp(wx.App):
def OnInit(self):
return True
class BTClockOTAUpdater(wx.Frame): class BTClockOTAUpdater(wx.Frame):
updatingName = ""
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))
@ -60,17 +144,33 @@ class BTClockOTAUpdater(wx.Frame):
self.api_handler = ApiHandler() self.api_handler = ApiHandler()
self.fw_updater = FwUpdater(self.call_progress, self.SetStatusText) self.fw_updater = FwUpdater(self.call_progress, self.SetStatusText)
panel = wx.Panel(self) panel = wx.Panel(self)
self.log_ctrl = rt.RichTextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2)
monospace_font = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
self.log_ctrl.SetFont(monospace_font)
handler = RichTextCtrlHandler(self.log_ctrl) # Create log list control
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', '%H:%M:%S')) self.log_ctrl = LogListCtrl(panel)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.DEBUG)
# Set up logging to capture all output
handler = LogListHandler(self.log_ctrl)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', '%H:%M:%S')
handler.setFormatter(formatter)
# Get the root logger and remove any existing handlers
root_logger = logging.getLogger()
for h in root_logger.handlers[:]:
root_logger.removeHandler(h)
# Add our handler and set level to DEBUG to capture everything
root_logger.addHandler(handler)
root_logger.setLevel(logging.DEBUG)
# Also capture esptool output
esptool_logger = logging.getLogger('esptool')
esptool_logger.addHandler(handler)
esptool_logger.setLevel(logging.DEBUG)
# Redirect stdout and stderr to the log
sys.stdout = LogRedirector(root_logger, logging.INFO)
sys.stderr = LogRedirector(root_logger, logging.ERROR)
self.device_list = DevicesPanel(panel) self.device_list = DevicesPanel(panel)
@ -81,13 +181,13 @@ class BTClockOTAUpdater(wx.Frame):
self.fw_label = wx.StaticText( self.fw_label = wx.StaticText(
panel, label=f"Fetching latest version from GitHub...") panel, label=f"Fetching latest version from GitHub...")
hbox.Add(self.fw_label, 1, wx.EXPAND | wx.ALL, 5) hbox.Add(self.fw_label, 1, wx.EXPAND | wx.ALL, 5)
self.actionButtons = ActionButtonPanel( self.actionButtons = ActionButtonPanel(
panel, self) panel, self)
hbox.AddStretchSpacer() hbox.AddStretchSpacer()
hbox.Add(self.actionButtons, 2, wx.EXPAND | wx.ALL, 5) hbox.Add(self.actionButtons, 2, wx.EXPAND | wx.ALL, 5)
vbox.Add(hbox, 0, wx.EXPAND | wx.ALL, 20) vbox.Add(hbox, 0, wx.EXPAND | wx.ALL, 20)
self.progress_bar = wx.Gauge(panel, range=100) self.progress_bar = wx.Gauge(panel, range=100)
@ -99,6 +199,7 @@ class BTClockOTAUpdater(wx.Frame):
wx.CallAfter(self.fetch_latest_release_async) wx.CallAfter(self.fetch_latest_release_async)
wx.YieldIfNeeded() 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)
@ -109,6 +210,9 @@ class BTClockOTAUpdater(wx.Frame):
filemenu = wx.Menu() filemenu = wx.Menu()
menuOpenDownloadDir = filemenu.Append( menuOpenDownloadDir = filemenu.Append(
wx.ID_OPEN, "&Open Download Dir", " Open the directory with firmware files and cache") wx.ID_OPEN, "&Open Download Dir", " Open the directory with firmware files and cache")
menuFlashSerial = filemenu.Append(
wx.ID_ANY, "Flash via &Serial...", " Flash firmware using serial connection")
filemenu.AppendSeparator()
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(
@ -119,6 +223,7 @@ class BTClockOTAUpdater(wx.Frame):
self.SetMenuBar(menuBar) self.SetMenuBar(menuBar)
self.Bind(wx.EVT_MENU, self.OnOpenDownloadFolder, menuOpenDownloadDir) self.Bind(wx.EVT_MENU, self.OnOpenDownloadFolder, menuOpenDownloadDir)
self.Bind(wx.EVT_MENU, self.on_serial_flash, menuFlashSerial)
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)
@ -192,9 +297,9 @@ class BTClockOTAUpdater(wx.Frame):
def handle_latest_release(self, future): def handle_latest_release(self, future):
try: try:
latest_release = future.result() self.latest_release = future.result() # Store the result
self.fw_label.SetLabelText(f"Downloaded firmware version: { self.fw_label.SetLabelText(f"Downloaded firmware version: {
latest_release}\nCommit: {self.releaseChecker.commit_hash}") self.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__) traceback.print_tb(e.__traceback__)
@ -210,3 +315,22 @@ class BTClockOTAUpdater(wx.Frame):
def OnExit(self, e): def OnExit(self, e):
self.Close(False) self.Close(False)
def on_serial_flash(self, event):
if not hasattr(self, 'latest_release'):
wx.MessageBox("Please wait for firmware to be downloaded", "Error", wx.OK | wx.ICON_ERROR)
return
dlg = SerialFlashDialog(self, self.fw_updater, self.latest_release)
if dlg.ShowModal() == wx.ID_OK:
port = dlg.get_selected_port()
if port:
hw_rev = dlg.get_selected_revision()
preserve_nvs = dlg.get_preserve_nvs()
self.fw_updater.start_serial_firmware_update(
self.latest_release,
port,
hw_rev,
preserve_nvs
)
dlg.Destroy()

View file

@ -19,7 +19,17 @@ class ReleaseChecker:
commit_hash = "" commit_hash = ""
def __init__(self): def __init__(self):
self.progress_callback: Callable[[int], None] = None self._progress_callback: Callable[[int], None] | None = None
@property
def progress_callback(self) -> Callable[[int], None] | None:
return self._progress_callback
@progress_callback.setter
def progress_callback(self, callback: Callable[[int], None] | None) -> None:
if callback is not None and not callable(callback):
raise TypeError("progress_callback must be callable or None")
self._progress_callback = callback
def load_cache(self): def load_cache(self):
'''Load cached data from file''' '''Load cached data from file'''
@ -39,11 +49,9 @@ class ReleaseChecker:
cache = self.load_cache() cache = self.load_cache()
now = datetime.now() now = datetime.now()
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://git.btclock.dev/api/v1/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)
@ -75,11 +83,8 @@ class ReleaseChecker:
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://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: 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']
else: else:
response = requests.get(ref_url) response = requests.get(ref_url)
response.raise_for_status() response.raise_for_status()
@ -121,12 +126,12 @@ class ReleaseChecker:
if chunk: if chunk:
f.write(chunk) f.write(chunk)
f.flush() f.flush()
progress = int((i / num_chunks) * 100) if self._progress_callback is not None:
if callable(self.progress_callback): progress = int((i / num_chunks) * 100)
self.progress_callback(progress) self._progress_callback(progress)
if callable(self.progress_callback): if self._progress_callback is not None:
self.progress_callback(100) self._progress_callback(100)
class ReleaseCheckerException(Exception): class ReleaseCheckerException(Exception):

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1b8000,
app1, app, ota_1, , 0x1b8000,
spiffs, data, spiffs, , 0x66C00,
coredump, data, coredump,, 0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x1b8000
5 app1 app ota_1 0x1b8000
6 spiffs data spiffs 0x66C00
7 coredump data coredump 0x10000

View file

@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x6F0000,
app1, app, ota_1, , 0x6F0000,
spiffs, data, spiffs, , 0x200000,
coredump, data, coredump,, 0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x6F0000
5 app1 app ota_1 0x6F0000
6 spiffs data spiffs 0x200000
7 coredump data coredump 0x10000

View file

@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x370000,
app1, app, ota_1, , 0x370000,
spiffs, data, spiffs, , 0xCD000,
coredump, data, coredump,, 0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x370000
5 app1 app ota_1 0x370000
6 spiffs data spiffs 0xCD000
7 coredump data coredump 0x10000

Binary file not shown.

Binary file not shown.

Binary file not shown.