Skip to content

Commit 8ed5947

Browse files
Simplify OSUpdate by using ConnectivityManager
1 parent aa1b358 commit 8ed5947

File tree

2 files changed

+78
-168
lines changed

2 files changed

+78
-168
lines changed

internal_filesystem/builtin/apps/com.micropythonos.osupdate/assets/osupdate.py

Lines changed: 77 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import _thread
66

77
from mpos.apps import Activity
8-
from mpos import PackageManager
8+
from mpos import PackageManager, ConnectivityManager
99
import mpos.info
1010
import mpos.ui
1111

@@ -28,10 +28,10 @@ class OSUpdate(Activity):
2828
def __init__(self):
2929
super().__init__()
3030
# Initialize business logic components with dependency injection
31-
self.network_monitor = NetworkMonitor()
3231
self.update_checker = UpdateChecker()
33-
self.update_downloader = UpdateDownloader(network_monitor=self.network_monitor)
32+
self.update_downloader = UpdateDownloader()
3433
self.current_state = UpdateState.IDLE
34+
self.connectivity_manager = None # Will be initialized in onStart
3535

3636
def set_state(self, new_state):
3737
"""Change app state and update UI accordingly."""
@@ -79,16 +79,17 @@ def onCreate(self):
7979
self.setContentView(self.main_screen)
8080

8181
def onStart(self, screen):
82-
# Check wifi and either start update check or wait for wifi
83-
if not self.network_monitor.is_connected():
84-
self.set_state(UpdateState.WAITING_WIFI)
85-
# Start wifi monitoring in background
86-
_thread.stack_size(mpos.apps.good_stack_size())
87-
_thread.start_new_thread(self._wifi_wait_thread, ())
88-
else:
82+
# Get connectivity manager instance
83+
self.connectivity_manager = ConnectivityManager.get()
84+
85+
# Check if online and either start update check or wait for network
86+
if self.connectivity_manager.is_online():
8987
self.set_state(UpdateState.CHECKING_UPDATE)
90-
print("Showing update info...")
88+
print("OSUpdate: Online, checking for updates...")
9189
self.show_update_info()
90+
else:
91+
self.set_state(UpdateState.WAITING_WIFI)
92+
print("OSUpdate: Offline, waiting for network...")
9293

9394
def _update_ui_for_state(self):
9495
"""Update UI elements based on current state."""
@@ -108,32 +109,49 @@ def _update_ui_for_state(self):
108109
# Show "Check Again" button on errors
109110
self.check_again_button.remove_flag(lv.obj.FLAG.HIDDEN)
110111

111-
def _wifi_wait_thread(self):
112-
"""Background thread that waits for wifi connection."""
113-
print("OSUpdate: waiting for wifi...")
114-
check_interval = 5 # Check every 5 seconds
115-
max_wait_time = 300 # 5 minutes timeout
116-
elapsed = 0
117-
118-
while elapsed < max_wait_time and self.has_foreground():
119-
if self.network_monitor.is_connected():
120-
print("OSUpdate: wifi connected, checking for updates")
121-
# Switch to checking state and start update check
112+
def onResume(self, screen):
113+
"""Register for connectivity callbacks when app resumes."""
114+
super().onResume(screen)
115+
if self.connectivity_manager:
116+
self.connectivity_manager.register_callback(self.network_changed)
117+
# Check current state
118+
self.network_changed(self.connectivity_manager.is_online())
119+
120+
def onPause(self, screen):
121+
"""Unregister connectivity callbacks when app pauses."""
122+
if self.connectivity_manager:
123+
self.connectivity_manager.unregister_callback(self.network_changed)
124+
super().onPause(screen)
125+
126+
def network_changed(self, online):
127+
"""Callback when network connectivity changes.
128+
129+
Args:
130+
online: True if network is online, False if offline
131+
"""
132+
print(f"OSUpdate: network_changed, now: {'ONLINE' if online else 'OFFLINE'}")
133+
134+
if not online:
135+
# Went offline
136+
if self.current_state == UpdateState.DOWNLOADING:
137+
# Download will automatically pause due to connectivity check
138+
pass
139+
elif self.current_state == UpdateState.CHECKING_UPDATE:
140+
# Was checking for updates when network dropped
141+
self.update_ui_threadsafe_if_foreground(
142+
self.set_state, UpdateState.WAITING_WIFI
143+
)
144+
else:
145+
# Went online
146+
if self.current_state == UpdateState.WAITING_WIFI:
147+
# Was waiting for network, now can check for updates
122148
self.update_ui_threadsafe_if_foreground(
123149
self.set_state, UpdateState.CHECKING_UPDATE
124150
)
125151
self.show_update_info()
126-
return
127-
128-
time.sleep(check_interval)
129-
elapsed += check_interval
130-
131-
# Timeout or user navigated away
132-
if self.has_foreground():
133-
self.update_ui_threadsafe_if_foreground(
134-
self.status_label.set_text,
135-
"WiFi connection timeout.\nPlease check your network and restart the app."
136-
)
152+
elif self.current_state == UpdateState.DOWNLOAD_PAUSED:
153+
# Download was paused, will auto-resume in download thread
154+
pass
137155

138156
def _get_user_friendly_error(self, error):
139157
"""Convert technical errors into user-friendly messages with guidance."""
@@ -299,13 +317,15 @@ def update_with_lvgl(self, url):
299317
)
300318

301319
# Wait for wifi to return
302-
check_interval = 5 # Check every 5 seconds
320+
# ConnectivityManager will notify us via callback when network returns
321+
print("OSUpdate: Waiting for network to return...")
322+
check_interval = 2 # Check every 2 seconds
303323
max_wait = 300 # 5 minutes timeout
304324
elapsed = 0
305325

306326
while elapsed < max_wait and self.has_foreground():
307-
if self.network_monitor.is_connected():
308-
print("OSUpdate: WiFi reconnected, resuming download")
327+
if self.connectivity_manager.is_online():
328+
print("OSUpdate: Network reconnected, resuming download")
309329
self.update_ui_threadsafe_if_foreground(
310330
self.set_state, UpdateState.DOWNLOADING
311331
)
@@ -315,15 +335,18 @@ def update_with_lvgl(self, url):
315335
elapsed += check_interval
316336

317337
if elapsed >= max_wait:
318-
# Timeout waiting for wifi
319-
msg = f"WiFi timeout during download.\n{bytes_written}/{total_size} bytes written.\nPress Update to retry."
338+
# Timeout waiting for network
339+
msg = f"Network timeout during download.\n{bytes_written}/{total_size} bytes written.\nPress 'Update OS' to retry."
320340
self.update_ui_threadsafe_if_foreground(self.status_label.set_text, msg)
321341
self.update_ui_threadsafe_if_foreground(
322342
self.install_button.remove_state, lv.STATE.DISABLED
323343
)
344+
self.update_ui_threadsafe_if_foreground(
345+
self.set_state, UpdateState.ERROR
346+
)
324347
return
325348

326-
# If we're here, wifi is back - continue to next iteration to resume
349+
# If we're here, network is back - continue to next iteration to resume
327350

328351
else:
329352
# Update failed with error (not pause)
@@ -378,57 +401,20 @@ class UpdateState:
378401
COMPLETED = "completed"
379402
ERROR = "error"
380403

381-
class NetworkMonitor:
382-
"""Monitors network connectivity status."""
383-
384-
def __init__(self, network_module=None):
385-
"""Initialize with optional dependency injection for testing.
386-
387-
Args:
388-
network_module: Network module (defaults to network if available)
389-
"""
390-
self.network_module = network_module
391-
if self.network_module is None:
392-
try:
393-
import network
394-
self.network_module = network
395-
except ImportError:
396-
# Desktop/simulation mode - no network module
397-
self.network_module = None
398-
399-
def is_connected(self):
400-
"""Check if WiFi is currently connected.
401-
402-
Returns:
403-
bool: True if connected, False otherwise
404-
"""
405-
if self.network_module is None:
406-
# No network module available (desktop mode)
407-
# Assume connected for testing purposes
408-
return True
409-
410-
try:
411-
wlan = self.network_module.WLAN(self.network_module.STA_IF)
412-
return wlan.isconnected()
413-
except Exception as e:
414-
print(f"NetworkMonitor: Error checking connection: {e}")
415-
return False
416-
417-
418404
class UpdateDownloader:
419405
"""Handles downloading and installing OS updates."""
420406

421-
def __init__(self, requests_module=None, partition_module=None, network_monitor=None):
407+
def __init__(self, requests_module=None, partition_module=None, connectivity_manager=None):
422408
"""Initialize with optional dependency injection for testing.
423409
424410
Args:
425411
requests_module: HTTP requests module (defaults to requests)
426412
partition_module: ESP32 Partition module (defaults to esp32.Partition if available)
427-
network_monitor: NetworkMonitor instance for checking wifi during download
413+
connectivity_manager: ConnectivityManager instance for checking network during download
428414
"""
429415
self.requests = requests_module if requests_module else requests
430416
self.partition_module = partition_module
431-
self.network_monitor = network_monitor
417+
self.connectivity_manager = connectivity_manager
432418
self.simulate = False
433419

434420
# Download state for pause/resume
@@ -514,9 +500,18 @@ def download_and_install(self, url, progress_callback=None, should_continue_call
514500
response.close()
515501
return result
516502

517-
# Check wifi connection (if monitoring enabled)
518-
if self.network_monitor and not self.network_monitor.is_connected():
519-
print("UpdateDownloader: WiFi lost, pausing download")
503+
# Check network connection (if monitoring enabled)
504+
if self.connectivity_manager:
505+
is_online = self.connectivity_manager.is_online()
506+
elif ConnectivityManager._instance:
507+
# Use global instance if available
508+
is_online = ConnectivityManager._instance.is_online()
509+
else:
510+
# No connectivity checking available
511+
is_online = True
512+
513+
if not is_online:
514+
print("UpdateDownloader: Network lost, pausing download")
520515
self.is_paused = True
521516
self.bytes_written_so_far = bytes_written
522517
result['paused'] = True

tests/test_osupdate.py

Lines changed: 1 addition & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -39,92 +39,7 @@ def set_boot(self):
3939
# Import the actual classes we're testing
4040
# Tests run from internal_filesystem/, so we add the assets directory to path
4141
sys.path.append('builtin/apps/com.micropythonos.osupdate/assets')
42-
from osupdate import NetworkMonitor, UpdateChecker, UpdateDownloader, round_up_to_multiple
43-
44-
45-
class TestNetworkMonitor(unittest.TestCase):
46-
"""Test NetworkMonitor class."""
47-
48-
def test_is_connected_with_connected_network(self):
49-
"""Test that is_connected returns True when network is connected."""
50-
mock_network = MockNetwork(connected=True)
51-
monitor = NetworkMonitor(network_module=mock_network)
52-
53-
self.assertTrue(monitor.is_connected())
54-
55-
def test_is_connected_with_disconnected_network(self):
56-
"""Test that is_connected returns False when network is disconnected."""
57-
mock_network = MockNetwork(connected=False)
58-
monitor = NetworkMonitor(network_module=mock_network)
59-
60-
self.assertFalse(monitor.is_connected())
61-
62-
def test_is_connected_without_network_module(self):
63-
"""Test that is_connected returns True when no network module (desktop mode)."""
64-
monitor = NetworkMonitor(network_module=None)
65-
66-
# Should return True (assume connected) in desktop mode
67-
self.assertTrue(monitor.is_connected())
68-
69-
def test_is_connected_with_exception(self):
70-
"""Test that is_connected returns False when WLAN raises exception."""
71-
class BadNetwork:
72-
STA_IF = 0
73-
def WLAN(self, interface):
74-
raise Exception("WLAN error")
75-
76-
monitor = NetworkMonitor(network_module=BadNetwork())
77-
78-
self.assertFalse(monitor.is_connected())
79-
80-
def test_network_state_change_detection(self):
81-
"""Test detecting network state changes."""
82-
mock_network = MockNetwork(connected=True)
83-
monitor = NetworkMonitor(network_module=mock_network)
84-
85-
# Initially connected
86-
self.assertTrue(monitor.is_connected())
87-
88-
# Disconnect
89-
mock_network.set_connected(False)
90-
self.assertFalse(monitor.is_connected())
91-
92-
# Reconnect
93-
mock_network.set_connected(True)
94-
self.assertTrue(monitor.is_connected())
95-
96-
def test_multiple_checks_when_connected(self):
97-
"""Test that multiple checks return consistent results."""
98-
mock_network = MockNetwork(connected=True)
99-
monitor = NetworkMonitor(network_module=mock_network)
100-
101-
# Multiple checks should all return True
102-
for _ in range(5):
103-
self.assertTrue(monitor.is_connected())
104-
105-
def test_wlan_with_different_interface_types(self):
106-
"""Test that correct interface type is used."""
107-
class NetworkWithInterface:
108-
STA_IF = 0
109-
CALLED_WITH = None
110-
111-
class MockWLAN:
112-
def __init__(self, interface):
113-
NetworkWithInterface.CALLED_WITH = interface
114-
self._connected = True
115-
116-
def isconnected(self):
117-
return self._connected
118-
119-
def WLAN(self, interface):
120-
return self.MockWLAN(interface)
121-
122-
network = NetworkWithInterface()
123-
monitor = NetworkMonitor(network_module=network)
124-
monitor.is_connected()
125-
126-
# Should have been called with STA_IF
127-
self.assertEqual(NetworkWithInterface.CALLED_WITH, 0)
42+
from osupdate import UpdateChecker, UpdateDownloader, round_up_to_multiple
12843

12944

13045
class TestUpdateChecker(unittest.TestCase):

0 commit comments

Comments
 (0)