mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
OSUpdate app: show download speed
DownloadManager: add support for download speed
This commit is contained in:
@@ -20,6 +20,7 @@ class OSUpdate(Activity):
|
||||
main_screen = None
|
||||
progress_label = None
|
||||
progress_bar = None
|
||||
speed_label = None
|
||||
|
||||
# State management
|
||||
current_state = None
|
||||
@@ -249,7 +250,12 @@ class OSUpdate(Activity):
|
||||
|
||||
self.progress_label = lv.label(self.main_screen)
|
||||
self.progress_label.set_text("OS Update: 0.00%")
|
||||
self.progress_label.align(lv.ALIGN.CENTER, 0, 0)
|
||||
self.progress_label.align(lv.ALIGN.CENTER, 0, -15)
|
||||
|
||||
self.speed_label = lv.label(self.main_screen)
|
||||
self.speed_label.set_text("Speed: -- KB/s")
|
||||
self.speed_label.align(lv.ALIGN.CENTER, 0, 10)
|
||||
|
||||
self.progress_bar = lv.bar(self.main_screen)
|
||||
self.progress_bar.set_size(200, 20)
|
||||
self.progress_bar.align(lv.ALIGN.BOTTOM_MID, 0, -50)
|
||||
@@ -273,14 +279,36 @@ class OSUpdate(Activity):
|
||||
self.schedule_show_update_info()
|
||||
|
||||
async def async_progress_callback(self, percent):
|
||||
"""Async progress callback for DownloadManager."""
|
||||
print(f"OTA Update: {percent:.1f}%")
|
||||
"""Async progress callback for DownloadManager.
|
||||
|
||||
Args:
|
||||
percent: Progress percentage with 2 decimal places (0.00 - 100.00)
|
||||
"""
|
||||
print(f"OTA Update: {percent:.2f}%")
|
||||
# UI updates are safe from async context in MicroPythonOS (runs on main thread)
|
||||
if self.has_foreground():
|
||||
self.progress_bar.set_value(int(percent), True)
|
||||
self.progress_label.set_text(f"OTA Update: {percent:.2f}%")
|
||||
await TaskManager.sleep_ms(50)
|
||||
|
||||
async def async_speed_callback(self, bytes_per_second):
|
||||
"""Async speed callback for DownloadManager.
|
||||
|
||||
Args:
|
||||
bytes_per_second: Download speed in bytes per second
|
||||
"""
|
||||
# Convert to human-readable format
|
||||
if bytes_per_second >= 1024 * 1024:
|
||||
speed_str = f"{bytes_per_second / (1024 * 1024):.1f} MB/s"
|
||||
elif bytes_per_second >= 1024:
|
||||
speed_str = f"{bytes_per_second / 1024:.1f} KB/s"
|
||||
else:
|
||||
speed_str = f"{bytes_per_second:.0f} B/s"
|
||||
|
||||
print(f"Download speed: {speed_str}")
|
||||
if self.has_foreground() and self.speed_label:
|
||||
self.speed_label.set_text(f"Speed: {speed_str}")
|
||||
|
||||
async def perform_update(self):
|
||||
"""Download and install update using async patterns.
|
||||
|
||||
@@ -295,6 +323,7 @@ class OSUpdate(Activity):
|
||||
result = await self.update_downloader.download_and_install(
|
||||
url,
|
||||
progress_callback=self.async_progress_callback,
|
||||
speed_callback=self.async_speed_callback,
|
||||
should_continue_callback=self.has_foreground
|
||||
)
|
||||
|
||||
@@ -531,7 +560,7 @@ class UpdateDownloader:
|
||||
percent = (self.bytes_written_so_far / self.total_size_expected) * 100
|
||||
await self._progress_callback(min(percent, 100.0))
|
||||
|
||||
async def download_and_install(self, url, progress_callback=None, should_continue_callback=None):
|
||||
async def download_and_install(self, url, progress_callback=None, speed_callback=None, should_continue_callback=None):
|
||||
"""Download firmware and install to OTA partition using async DownloadManager.
|
||||
|
||||
Supports pause/resume on wifi loss using HTTP Range headers.
|
||||
@@ -539,7 +568,9 @@ class UpdateDownloader:
|
||||
Args:
|
||||
url: URL to download firmware from
|
||||
progress_callback: Optional async callback function(percent: float)
|
||||
Called by DownloadManager with progress 0-100
|
||||
Called by DownloadManager with progress 0.00-100.00 (2 decimal places)
|
||||
speed_callback: Optional async callback function(bytes_per_second: float)
|
||||
Called periodically with download speed
|
||||
should_continue_callback: Optional callback function() -> bool
|
||||
Returns False to cancel download
|
||||
|
||||
@@ -595,12 +626,13 @@ class UpdateDownloader:
|
||||
self.total_size_expected = 0
|
||||
|
||||
# Download with streaming chunk callback
|
||||
# Progress is reported by DownloadManager via progress_callback
|
||||
# Progress and speed are reported by DownloadManager via callbacks
|
||||
print(f"UpdateDownloader: Starting async download from {url}")
|
||||
success = await dm.download_url(
|
||||
url,
|
||||
chunk_callback=chunk_handler,
|
||||
progress_callback=progress_callback, # Let DownloadManager handle progress
|
||||
speed_callback=speed_callback, # Let DownloadManager handle speed
|
||||
headers=headers
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ Features:
|
||||
- Automatic session lifecycle management
|
||||
- Thread-safe session access
|
||||
- Retry logic (3 attempts per chunk, 10s timeout)
|
||||
- Progress tracking
|
||||
- Progress tracking with 2-decimal precision
|
||||
- Download speed reporting
|
||||
- Resume support via Range headers
|
||||
|
||||
Example:
|
||||
@@ -20,14 +21,18 @@ Example:
|
||||
# Download to memory
|
||||
data = await DownloadManager.download_url("https://api.example.com/data.json")
|
||||
|
||||
# Download to file with progress
|
||||
async def progress(pct):
|
||||
print(f"{pct}%")
|
||||
# Download to file with progress and speed
|
||||
async def on_progress(pct):
|
||||
print(f"{pct:.2f}%") # e.g., "45.67%"
|
||||
|
||||
async def on_speed(speed_bps):
|
||||
print(f"{speed_bps / 1024:.1f} KB/s")
|
||||
|
||||
success = await DownloadManager.download_url(
|
||||
"https://example.com/file.bin",
|
||||
outfile="/sdcard/file.bin",
|
||||
progress_callback=progress
|
||||
progress_callback=on_progress,
|
||||
speed_callback=on_speed
|
||||
)
|
||||
|
||||
# Stream processing
|
||||
@@ -46,6 +51,7 @@ _DEFAULT_CHUNK_SIZE = 1024 # 1KB chunks
|
||||
_DEFAULT_TOTAL_SIZE = 100 * 1024 # 100KB default if Content-Length missing
|
||||
_MAX_RETRIES = 3 # Retry attempts per chunk
|
||||
_CHUNK_TIMEOUT_SECONDS = 10 # Timeout per chunk read
|
||||
_SPEED_UPDATE_INTERVAL_MS = 1000 # Update speed every 1 second
|
||||
|
||||
# Module-level state (singleton pattern)
|
||||
_session = None
|
||||
@@ -169,7 +175,8 @@ async def close_session():
|
||||
|
||||
|
||||
async def download_url(url, outfile=None, total_size=None,
|
||||
progress_callback=None, chunk_callback=None, headers=None):
|
||||
progress_callback=None, chunk_callback=None, headers=None,
|
||||
speed_callback=None):
|
||||
"""Download a URL with flexible output modes.
|
||||
|
||||
This async download function can be used in 3 ways:
|
||||
@@ -182,11 +189,14 @@ async def download_url(url, outfile=None, total_size=None,
|
||||
outfile (str, optional): Path to write file. If None, returns bytes.
|
||||
total_size (int, optional): Expected size in bytes for progress tracking.
|
||||
If None, uses Content-Length header or defaults to 100KB.
|
||||
progress_callback (coroutine, optional): async def callback(percent: int)
|
||||
Called with progress 0-100.
|
||||
progress_callback (coroutine, optional): async def callback(percent: float)
|
||||
Called with progress 0.00-100.00 (2 decimal places).
|
||||
Only called when progress changes by at least 0.01%.
|
||||
chunk_callback (coroutine, optional): async def callback(chunk: bytes)
|
||||
Called for each chunk. Cannot use with outfile.
|
||||
headers (dict, optional): HTTP headers (e.g., {'Range': 'bytes=1000-'})
|
||||
speed_callback (coroutine, optional): async def callback(bytes_per_second: float)
|
||||
Called periodically (every ~1 second) with download speed.
|
||||
|
||||
Returns:
|
||||
bytes: Downloaded content (if outfile and chunk_callback are None)
|
||||
@@ -199,14 +209,18 @@ async def download_url(url, outfile=None, total_size=None,
|
||||
# Download to memory
|
||||
data = await DownloadManager.download_url("https://example.com/file.json")
|
||||
|
||||
# Download to file with progress
|
||||
# Download to file with progress and speed
|
||||
async def on_progress(percent):
|
||||
print(f"Progress: {percent}%")
|
||||
print(f"Progress: {percent:.2f}%")
|
||||
|
||||
async def on_speed(bps):
|
||||
print(f"Speed: {bps / 1024:.1f} KB/s")
|
||||
|
||||
success = await DownloadManager.download_url(
|
||||
"https://example.com/large.bin",
|
||||
outfile="/sdcard/large.bin",
|
||||
progress_callback=on_progress
|
||||
progress_callback=on_progress,
|
||||
speed_callback=on_speed
|
||||
)
|
||||
|
||||
# Stream processing
|
||||
@@ -282,6 +296,18 @@ async def download_url(url, outfile=None, total_size=None,
|
||||
chunks = []
|
||||
partial_size = 0
|
||||
chunk_size = _DEFAULT_CHUNK_SIZE
|
||||
|
||||
# Progress tracking with 2-decimal precision
|
||||
last_progress_pct = -1.0 # Track last reported progress to avoid duplicates
|
||||
|
||||
# Speed tracking
|
||||
speed_bytes_since_last_update = 0
|
||||
speed_last_update_time = None
|
||||
try:
|
||||
import time
|
||||
speed_last_update_time = time.ticks_ms()
|
||||
except ImportError:
|
||||
pass # time module not available
|
||||
|
||||
print(f"DownloadManager: {'Writing to ' + outfile if outfile else 'Downloading'} {total_size} bytes in chunks of size {chunk_size}")
|
||||
|
||||
@@ -317,12 +343,31 @@ async def download_url(url, outfile=None, total_size=None,
|
||||
else:
|
||||
chunks.append(chunk_data)
|
||||
|
||||
# Report progress
|
||||
partial_size += len(chunk_data)
|
||||
progress_pct = round((partial_size * 100) / int(total_size))
|
||||
print(f"DownloadManager: Progress: {partial_size} / {total_size} bytes = {progress_pct}%")
|
||||
if progress_callback:
|
||||
# Track bytes for speed calculation
|
||||
chunk_len = len(chunk_data)
|
||||
partial_size += chunk_len
|
||||
speed_bytes_since_last_update += chunk_len
|
||||
|
||||
# Report progress with 2-decimal precision
|
||||
# Only call callback if progress changed by at least 0.01%
|
||||
progress_pct = round((partial_size * 100) / int(total_size), 2)
|
||||
if progress_callback and progress_pct != last_progress_pct:
|
||||
print(f"DownloadManager: Progress: {partial_size} / {total_size} bytes = {progress_pct:.2f}%")
|
||||
await progress_callback(progress_pct)
|
||||
last_progress_pct = progress_pct
|
||||
|
||||
# Report speed periodically
|
||||
if speed_callback and speed_last_update_time is not None:
|
||||
import time
|
||||
current_time = time.ticks_ms()
|
||||
elapsed_ms = time.ticks_diff(current_time, speed_last_update_time)
|
||||
if elapsed_ms >= _SPEED_UPDATE_INTERVAL_MS:
|
||||
# Calculate bytes per second
|
||||
bytes_per_second = (speed_bytes_since_last_update * 1000) / elapsed_ms
|
||||
await speed_callback(bytes_per_second)
|
||||
# Reset for next interval
|
||||
speed_bytes_since_last_update = 0
|
||||
speed_last_update_time = current_time
|
||||
else:
|
||||
# Chunk is None, download complete
|
||||
print(f"DownloadManager: Finished downloading {url}")
|
||||
|
||||
@@ -699,15 +699,18 @@ class MockDownloadManager:
|
||||
self.url_received = None
|
||||
self.call_history = []
|
||||
self.chunk_size = 1024 # Default chunk size for streaming
|
||||
self.simulated_speed_bps = 100 * 1024 # 100 KB/s default simulated speed
|
||||
|
||||
async def download_url(self, url, outfile=None, total_size=None,
|
||||
progress_callback=None, chunk_callback=None, headers=None):
|
||||
progress_callback=None, chunk_callback=None, headers=None,
|
||||
speed_callback=None):
|
||||
"""
|
||||
Mock async download with flexible output modes.
|
||||
|
||||
Simulates the real DownloadManager behavior including:
|
||||
- Streaming chunks via chunk_callback
|
||||
- Progress reporting via progress_callback (based on total size)
|
||||
- Progress reporting via progress_callback with 2-decimal precision
|
||||
- Speed reporting via speed_callback
|
||||
- Network failure simulation
|
||||
|
||||
Args:
|
||||
@@ -715,8 +718,11 @@ class MockDownloadManager:
|
||||
outfile: Path to write file (optional)
|
||||
total_size: Expected size for progress tracking (optional)
|
||||
progress_callback: Async callback for progress updates (optional)
|
||||
Called with percent as float with 2 decimal places (0.00-100.00)
|
||||
chunk_callback: Async callback for streaming chunks (optional)
|
||||
headers: HTTP headers dict (optional)
|
||||
speed_callback: Async callback for speed updates (optional)
|
||||
Called with bytes_per_second as float
|
||||
|
||||
Returns:
|
||||
bytes: Downloaded content (if outfile and chunk_callback are None)
|
||||
@@ -732,7 +738,8 @@ class MockDownloadManager:
|
||||
'total_size': total_size,
|
||||
'headers': headers,
|
||||
'has_progress_callback': progress_callback is not None,
|
||||
'has_chunk_callback': chunk_callback is not None
|
||||
'has_chunk_callback': chunk_callback is not None,
|
||||
'has_speed_callback': speed_callback is not None
|
||||
})
|
||||
|
||||
if self.should_fail:
|
||||
@@ -751,6 +758,13 @@ class MockDownloadManager:
|
||||
|
||||
# Use provided total_size or actual data size for progress calculation
|
||||
effective_total_size = total_size if total_size else total_data_size
|
||||
|
||||
# Track progress to avoid duplicate callbacks
|
||||
last_progress_pct = -1.0
|
||||
|
||||
# Track speed reporting (simulate every ~1000 bytes for testing)
|
||||
bytes_since_speed_update = 0
|
||||
speed_update_threshold = 1000
|
||||
|
||||
while bytes_sent < total_data_size:
|
||||
# Check if we should simulate network failure
|
||||
@@ -768,11 +782,20 @@ class MockDownloadManager:
|
||||
chunks.append(chunk)
|
||||
|
||||
bytes_sent += len(chunk)
|
||||
bytes_since_speed_update += len(chunk)
|
||||
|
||||
# Report progress (like real DownloadManager does)
|
||||
# Report progress with 2-decimal precision (like real DownloadManager)
|
||||
# Only call callback if progress changed by at least 0.01%
|
||||
if progress_callback and effective_total_size > 0:
|
||||
percent = round((bytes_sent * 100) / effective_total_size)
|
||||
await progress_callback(percent)
|
||||
percent = round((bytes_sent * 100) / effective_total_size, 2)
|
||||
if percent != last_progress_pct:
|
||||
await progress_callback(percent)
|
||||
last_progress_pct = percent
|
||||
|
||||
# Report speed periodically
|
||||
if speed_callback and bytes_since_speed_update >= speed_update_threshold:
|
||||
await speed_callback(self.simulated_speed_bps)
|
||||
bytes_since_speed_update = 0
|
||||
|
||||
# Return based on mode
|
||||
if outfile or chunk_callback:
|
||||
|
||||
Reference in New Issue
Block a user