diff --git a/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py b/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py index 52ad5555..a68b9d34 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.appstore/assets/appstore.py @@ -64,7 +64,10 @@ class AppStore(Activity): response = await DownloadManager.download_url(json_url) except Exception as e: print(f"Failed to download app index: {e}") - self.please_wait_label.set_text(f"Could not download app index from\n{json_url}\nError: {e}") + if DownloadManager.is_network_error(e): + self.please_wait_label.set_text(f"Network error - check your WiFi connection\nand try again.") + else: + self.please_wait_label.set_text(f"Could not download app index from\n{json_url}\nError: {e}") return print(f"Got response text: {response[0:20]}") try: @@ -203,6 +206,8 @@ class AppStore(Activity): response = await DownloadManager.download_url(details_url) except Exception as e: print(f"Could not download app details from {details_url}: {e}") + if DownloadManager.is_network_error(e): + print("Network error while fetching app details") return print(f"Got response text: {response[0:20]}") try: @@ -494,7 +499,10 @@ class AppDetail(Activity): self.progress_bar.set_value(90, True) except Exception as e: print(f"Download failed with exception: {e}") - self.install_label.set_text(f"Download failed") + if DownloadManager.is_network_error(e): + self.install_label.set_text(f"Network error - check WiFi") + else: + self.install_label.set_text(f"Download failed: {str(e)[:30]}") self.install_button.remove_state(lv.STATE.DISABLED) self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN) self.progress_bar.set_value(0, False) diff --git a/internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py b/internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py index 4ba0ccb3..cfde5528 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py @@ -212,7 +212,7 @@ class OSUpdate(Activity): except Exception as e: print(f"show_update_info got exception: {e}") # Check if this is a network connectivity error - if self.update_downloader._is_network_error(e): + if DownloadManager.is_network_error(e): # Network not available - wait for it to come back print("OSUpdate: Network error while checking for updates, waiting for WiFi") self.set_state(UpdateState.WAITING_WIFI) @@ -461,35 +461,6 @@ class UpdateDownloader: print("UpdateDownloader: Partition module not available, will simulate") self.simulate = True - def _is_network_error(self, exception): - """Check if exception is a network connectivity error that should trigger pause. - - Args: - exception: Exception to check - - Returns: - bool: True if this is a recoverable network error - """ - error_str = str(exception).lower() - error_repr = repr(exception).lower() - - # Check for common network error codes and messages - # -113 = ECONNABORTED (connection aborted) - # -104 = ECONNRESET (connection reset by peer) - # -110 = ETIMEDOUT (connection timed out) - # -118 = EHOSTUNREACH (no route to host) - # -202 = DNS/connection error (network not ready) - network_indicators = [ - '-113', '-104', '-110', '-118', '-202', # Error codes - 'econnaborted', 'econnreset', 'etimedout', 'ehostunreach', # Error names - 'connection reset', 'connection aborted', # Error messages - 'broken pipe', 'network unreachable', 'host unreachable', - 'failed to download chunk' # From download_manager OSError(-110) - ] - - return any(indicator in error_str or indicator in error_repr - for indicator in network_indicators) - def _setup_partition(self): """Initialize the OTA partition for writing.""" if not self.simulate and self._current_partition is None: @@ -678,7 +649,7 @@ class UpdateDownloader: result['bytes_written'] = self.bytes_written_so_far result['total_size'] = self.total_size_expected # Check if this is a network error that should trigger pause - elif self._is_network_error(e): + elif DownloadManager.is_network_error(e): print(f"UpdateDownloader: Network error ({e}), pausing download") # Clear buffer - we'll re-download this data on resume diff --git a/internal_filesystem/lib/mpos/battery_voltage.py b/internal_filesystem/lib/mpos/battery_voltage.py index 8308c355..f2039589 100644 --- a/internal_filesystem/lib/mpos/battery_voltage.py +++ b/internal_filesystem/lib/mpos/battery_voltage.py @@ -11,7 +11,7 @@ adc_pin = None _cached_raw_adc = None _last_read_time = 0 CACHE_DURATION_ADC1_MS = 30000 # 30 seconds (cheaper: no WiFi interference) -CACHE_DURATION_ADC2_MS = 300000 # 300 seconds (expensive: requires WiFi disable) +CACHE_DURATION_ADC2_MS = 600000 # 600 seconds (expensive: requires WiFi disable) #CACHE_DURATION_ADC2_MS = CACHE_DURATION_ADC1_MS # trigger frequent disconnections for debugging OSUpdate resume def _is_adc2_pin(pin): diff --git a/internal_filesystem/lib/mpos/net/download_manager.py b/internal_filesystem/lib/mpos/net/download_manager.py index 1f4a7f2a..d5d7dfab 100644 --- a/internal_filesystem/lib/mpos/net/download_manager.py +++ b/internal_filesystem/lib/mpos/net/download_manager.py @@ -14,6 +14,11 @@ Features: - Progress tracking with 2-decimal precision - Download speed reporting - Resume support via Range headers +- Network error detection utilities + +Utility Functions: + is_network_error(exception) - Check if error is recoverable network error + get_resume_position(outfile) - Get file size for resume support Example: from mpos import DownloadManager @@ -44,6 +49,19 @@ Example: "https://example.com/stream", chunk_callback=process_chunk ) + + # Error handling with retry + try: + await DownloadManager.download_url(url, outfile="/sdcard/file.bin") + except Exception as e: + if DownloadManager.is_network_error(e): + # Wait and retry with resume + await asyncio.sleep(2) + resume_from = DownloadManager.get_resume_position("/sdcard/file.bin") + headers = {'Range': f'bytes={resume_from}-'} if resume_from > 0 else None + await DownloadManager.download_url(url, outfile="/sdcard/file.bin", headers=headers) + else: + raise # Fatal error """ # Constants @@ -174,6 +192,75 @@ async def close_session(): _session_lock.release() +def is_network_error(exception): + """Check if exception is a recoverable network error. + + Recognizes common network error codes and messages that indicate + temporary connectivity issues that can be retried. + + Args: + exception: Exception to check + + Returns: + bool: True if this is a network error that can be retried + + Example: + try: + await DownloadManager.download_url(url) + except Exception as e: + if DownloadManager.is_network_error(e): + # Retry or pause + await asyncio.sleep(2) + # retry... + else: + # Fatal error + raise + """ + error_str = str(exception).lower() + error_repr = repr(exception).lower() + + # Common network error codes and messages + # -113 = ECONNABORTED (connection aborted) + # -104 = ECONNRESET (connection reset by peer) + # -110 = ETIMEDOUT (connection timed out) + # -118 = EHOSTUNREACH (no route to host) + # -202 = DNS/connection error (network not ready) + network_indicators = [ + '-113', '-104', '-110', '-118', '-202', # Error codes + 'econnaborted', 'econnreset', 'etimedout', 'ehostunreach', # Error names + 'connection reset', 'connection aborted', # Error messages + 'broken pipe', 'network unreachable', 'host unreachable', + 'failed to download chunk' # From download_manager OSError(-110) + ] + + return any(indicator in error_str or indicator in error_repr + for indicator in network_indicators) + + +def get_resume_position(outfile): + """Get the current size of a partially downloaded file. + + Useful for implementing resume functionality with Range headers. + + Args: + outfile: Path to file + + Returns: + int: File size in bytes, or 0 if file doesn't exist + + Example: + resume_from = DownloadManager.get_resume_position("/sdcard/file.bin") + if resume_from > 0: + headers = {'Range': f'bytes={resume_from}-'} + await DownloadManager.download_url(url, outfile=outfile, headers=headers) + """ + try: + import os + return os.stat(outfile)[6] # st_size + except OSError: + return 0 + + async def download_url(url, outfile=None, total_size=None, progress_callback=None, chunk_callback=None, headers=None, speed_callback=None):