Skip to content

Commit df486a5

Browse files
WifiService: connect to strongest networks first
1 parent 8b5e560 commit df486a5

File tree

4 files changed

+268
-153
lines changed

4 files changed

+268
-153
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ internal_filesystem/data
1111
internal_filesystem/sdcard
1212
internal_filesystem/tests
1313

14+
internal_filesystem_excluded/
15+
1416
# these tests contain actual NWC URLs:
1517
tests/manual_test_nwcwallet_alby.py
1618
tests/manual_test_nwcwallet_cashu.py
@@ -21,3 +23,6 @@ __pycache__/
2123
*$py.class
2224
*.so
2325
.Python
26+
27+
# these get created:
28+
c_mpos/c_mpos

internal_filesystem/lib/mpos/net/wifi_service.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class WifiService:
4646
def connect(network_module=None):
4747
"""
4848
Scan for available networks and connect to the first saved network found.
49+
Networks are tried in order of signal strength (strongest first).
4950
5051
Args:
5152
network_module: Network module for dependency injection (testing)
@@ -63,9 +64,14 @@ def connect(network_module=None):
6364
# Scan for available networks
6465
networks = wlan.scan()
6566

67+
# Sort networks by RSSI (signal strength) in descending order
68+
# RSSI is at index 3, higher values (less negative) = stronger signal
69+
networks = sorted(networks, key=lambda n: n[3], reverse=True)
70+
6671
for n in networks:
6772
ssid = n[0].decode()
68-
print(f"WifiService: Found network '{ssid}'")
73+
rssi = n[3]
74+
print(f"WifiService: Found network '{ssid}' (RSSI: {rssi} dBm)")
6975

7076
if ssid in WifiService.access_points:
7177
password = WifiService.access_points.get(ssid).get("password")

tests/test_wifi_service.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,3 +455,259 @@ def test_disconnect_desktop_mode(self):
455455
WifiService.disconnect(network_module=None)
456456

457457

458+
class TestWifiServiceRSSISorting(unittest.TestCase):
459+
"""Test RSSI-based network prioritization."""
460+
461+
def setUp(self):
462+
"""Set up test fixtures."""
463+
MockSharedPreferences.reset_all()
464+
WifiService.access_points = {}
465+
WifiService.wifi_busy = False
466+
467+
def tearDown(self):
468+
"""Clean up after tests."""
469+
WifiService.access_points = {}
470+
WifiService.wifi_busy = False
471+
MockSharedPreferences.reset_all()
472+
473+
def test_networks_sorted_by_rssi_strongest_first(self):
474+
"""Test that networks are sorted by RSSI with strongest first."""
475+
# Create mock networks with different RSSI values
476+
# Format: (ssid, bssid, channel, rssi, security, hidden)
477+
mock_network = MockNetwork(connected=False)
478+
mock_wlan = mock_network.WLAN(mock_network.STA_IF)
479+
480+
# Unsorted networks (weak, strong, medium)
481+
mock_wlan._scan_results = [
482+
(b'WeakNetwork', b'\xaa\xbb\xcc\xdd\xee\xff', 6, -85, 3, False),
483+
(b'StrongNetwork', b'\x11\x22\x33\x44\x55\x66', 1, -45, 3, False),
484+
(b'MediumNetwork', b'\x77\x88\x99\xaa\xbb\xcc', 11, -65, 3, False),
485+
]
486+
487+
# Configure all as saved networks
488+
WifiService.access_points = {
489+
'WeakNetwork': {'password': 'weak123'},
490+
'StrongNetwork': {'password': 'strong123'},
491+
'MediumNetwork': {'password': 'medium123'}
492+
}
493+
494+
# Track connection attempts
495+
connection_attempts = []
496+
497+
def mock_connect(ssid, password):
498+
connection_attempts.append(ssid)
499+
# Succeed on first attempt
500+
mock_wlan._connected = True
501+
502+
mock_wlan.connect = mock_connect
503+
504+
result = WifiService.connect(network_module=mock_network)
505+
506+
self.assertTrue(result)
507+
# Should try strongest first (-45 dBm)
508+
self.assertEqual(connection_attempts[0], 'StrongNetwork')
509+
# Should only try one (first succeeds)
510+
self.assertEqual(len(connection_attempts), 1)
511+
512+
def test_multiple_networks_tried_in_rssi_order(self):
513+
"""Test that multiple networks are tried in RSSI order when first fails."""
514+
mock_network = MockNetwork(connected=False)
515+
mock_wlan = mock_network.WLAN(mock_network.STA_IF)
516+
517+
# Three networks with different signal strengths
518+
mock_wlan._scan_results = [
519+
(b'BadNetwork1', b'\xaa\xbb\xcc\xdd\xee\xff', 1, -40, 3, False),
520+
(b'BadNetwork2', b'\x11\x22\x33\x44\x55\x66', 6, -50, 3, False),
521+
(b'GoodNetwork', b'\x77\x88\x99\xaa\xbb\xcc', 11, -60, 3, False),
522+
]
523+
524+
WifiService.access_points = {
525+
'BadNetwork1': {'password': 'pass1'},
526+
'BadNetwork2': {'password': 'pass2'},
527+
'GoodNetwork': {'password': 'pass3'}
528+
}
529+
530+
# Track attempts and make first two fail
531+
connection_attempts = []
532+
533+
def mock_connect(ssid, password):
534+
connection_attempts.append(ssid)
535+
# Only succeed on third attempt
536+
if len(connection_attempts) >= 3:
537+
mock_wlan._connected = True
538+
539+
mock_wlan.connect = mock_connect
540+
541+
result = WifiService.connect(network_module=mock_network)
542+
543+
self.assertTrue(result)
544+
# Verify order: strongest to weakest
545+
self.assertEqual(connection_attempts[0], 'BadNetwork1') # RSSI -40
546+
self.assertEqual(connection_attempts[1], 'BadNetwork2') # RSSI -50
547+
self.assertEqual(connection_attempts[2], 'GoodNetwork') # RSSI -60
548+
self.assertEqual(len(connection_attempts), 3)
549+
550+
def test_duplicate_ssid_strongest_tried_first(self):
551+
"""Test that with duplicate SSIDs, strongest signal is tried first."""
552+
mock_network = MockNetwork(connected=False)
553+
mock_wlan = mock_network.WLAN(mock_network.STA_IF)
554+
555+
# Real-world scenario: Multiple APs with same SSID
556+
mock_wlan._scan_results = [
557+
(b'MyNetwork', b'\xaa\xbb\xcc\xdd\xee\xff', 1, -70, 3, False),
558+
(b'MyNetwork', b'\x11\x22\x33\x44\x55\x66', 6, -50, 3, False), # Strongest
559+
(b'MyNetwork', b'\x77\x88\x99\xaa\xbb\xcc', 11, -85, 3, False),
560+
]
561+
562+
WifiService.access_points = {
563+
'MyNetwork': {'password': 'mypass123'}
564+
}
565+
566+
connection_attempts = []
567+
568+
def mock_connect(ssid, password):
569+
connection_attempts.append(ssid)
570+
# Succeed on first
571+
mock_wlan._connected = True
572+
573+
mock_wlan.connect = mock_connect
574+
575+
result = WifiService.connect(network_module=mock_network)
576+
577+
self.assertTrue(result)
578+
# Should only try once (first is strongest and succeeds)
579+
self.assertEqual(len(connection_attempts), 1)
580+
self.assertEqual(connection_attempts[0], 'MyNetwork')
581+
582+
def test_rssi_order_with_real_scan_data(self):
583+
"""Test with real scan data from actual ESP32 device."""
584+
mock_network = MockNetwork(connected=False)
585+
mock_wlan = mock_network.WLAN(mock_network.STA_IF)
586+
587+
# Real scan output from user's example
588+
mock_wlan._scan_results = [
589+
(b'Channel 8', b'\xde\xec^\x8f\x00A', 11, -47, 3, False),
590+
(b'Baptistus', b'\xd8\xec^\x8f\x00A', 11, -48, 7, False),
591+
(b'telenet-BD74DC9', b'TgQ>t\xe7', 11, -70, 3, False),
592+
(b'Galaxy S10+64bf', b'b\x19\xdf\xef\xb0\x8f', 11, -83, 3, False),
593+
(b'Najeeb\xe2\x80\x99s iPhone', b"F\x07'\xb8\x0b0", 6, -84, 7, False),
594+
(b'DIRECT-83-HP OfficeJet Pro 7740', b'\x1a`$dk\x83', 1, -87, 3, False),
595+
(b'Channel 8', b'\xde\xec^\xe1#w', 1, -91, 3, False),
596+
(b'Baptistus', b'\xd8\xec^\xe1#w', 1, -91, 7, False),
597+
(b'Proximus-Home-596457', b'\xf4\x05\x95\xf9A\xf1', 1, -93, 3, False),
598+
(b'Proximus-Home-596457', b'\xcc\x00\xf1j}\x94', 1, -93, 3, False),
599+
(b'BASE-9104320', b'4,\xc4\xe7\x01\xb7', 1, -94, 3, False),
600+
]
601+
602+
# Save several networks
603+
WifiService.access_points = {
604+
'Channel 8': {'password': 'pass1'},
605+
'Baptistus': {'password': 'pass2'},
606+
'telenet-BD74DC9': {'password': 'pass3'},
607+
'Galaxy S10+64bf': {'password': 'pass4'},
608+
}
609+
610+
# Track attempts and fail first to see ordering
611+
connection_attempts = []
612+
613+
def mock_connect(ssid, password):
614+
connection_attempts.append(ssid)
615+
# Succeed on second attempt
616+
if len(connection_attempts) >= 2:
617+
mock_wlan._connected = True
618+
619+
mock_wlan.connect = mock_connect
620+
621+
result = WifiService.connect(network_module=mock_network)
622+
623+
self.assertTrue(result)
624+
# Expected order: Channel 8 (-47), Baptistus (-48), telenet (-70), Galaxy (-83)
625+
self.assertEqual(connection_attempts[0], 'Channel 8')
626+
self.assertEqual(connection_attempts[1], 'Baptistus')
627+
self.assertEqual(len(connection_attempts), 2)
628+
629+
def test_sorting_preserves_network_data_integrity(self):
630+
"""Test that sorting doesn't corrupt or lose network data."""
631+
mock_network = MockNetwork(connected=False)
632+
mock_wlan = mock_network.WLAN(mock_network.STA_IF)
633+
634+
# Networks with various attributes
635+
mock_wlan._scan_results = [
636+
(b'Net3', b'\xaa\xaa\xaa\xaa\xaa\xaa', 11, -90, 3, False),
637+
(b'Net1', b'\xbb\xbb\xbb\xbb\xbb\xbb', 1, -40, 7, True), # Hidden
638+
(b'Net2', b'\xcc\xcc\xcc\xcc\xcc\xcc', 6, -60, 2, False),
639+
]
640+
641+
WifiService.access_points = {
642+
'Net1': {'password': 'p1'},
643+
'Net2': {'password': 'p2'},
644+
'Net3': {'password': 'p3'}
645+
}
646+
647+
# Track attempts to verify all are tried
648+
connection_attempts = []
649+
650+
def mock_connect(ssid, password):
651+
connection_attempts.append(ssid)
652+
# Never succeed, try all
653+
pass
654+
655+
mock_wlan.connect = mock_connect
656+
657+
result = WifiService.connect(network_module=mock_network)
658+
659+
self.assertFalse(result) # No connection succeeded
660+
# Verify all 3 were attempted in RSSI order
661+
self.assertEqual(len(connection_attempts), 3)
662+
self.assertEqual(connection_attempts[0], 'Net1') # RSSI -40
663+
self.assertEqual(connection_attempts[1], 'Net2') # RSSI -60
664+
self.assertEqual(connection_attempts[2], 'Net3') # RSSI -90
665+
666+
def test_no_saved_networks_in_scan(self):
667+
"""Test behavior when scan finds no saved networks."""
668+
mock_network = MockNetwork(connected=False)
669+
mock_wlan = mock_network.WLAN(mock_network.STA_IF)
670+
671+
mock_wlan._scan_results = [
672+
(b'UnknownNet1', b'\xaa\xbb\xcc\xdd\xee\xff', 1, -50, 3, False),
673+
(b'UnknownNet2', b'\x11\x22\x33\x44\x55\x66', 6, -60, 3, False),
674+
]
675+
676+
WifiService.access_points = {
677+
'SavedNetwork': {'password': 'pass123'}
678+
}
679+
680+
connection_attempts = []
681+
682+
def mock_connect(ssid, password):
683+
connection_attempts.append(ssid)
684+
685+
mock_wlan.connect = mock_connect
686+
687+
result = WifiService.connect(network_module=mock_network)
688+
689+
self.assertFalse(result)
690+
# No attempts should be made
691+
self.assertEqual(len(connection_attempts), 0)
692+
693+
def test_rssi_logging_shows_signal_strength(self):
694+
"""Test that RSSI value is logged during scan (for debugging)."""
695+
# This is more of a documentation test to verify the log format
696+
mock_network = MockNetwork(connected=False)
697+
mock_wlan = mock_network.WLAN(mock_network.STA_IF)
698+
699+
mock_wlan._scan_results = [
700+
(b'TestNet', b'\xaa\xbb\xcc\xdd\xee\xff', 1, -55, 3, False),
701+
]
702+
703+
WifiService.access_points = {
704+
'TestNet': {'password': 'pass'}
705+
}
706+
707+
# The connect method now logs "Found network 'TestNet' (RSSI: -55 dBm)"
708+
# This test just verifies it doesn't crash
709+
result = WifiService.connect(network_module=mock_network)
710+
# Since mock doesn't actually connect, this will likely be False
711+
# but the important part is the code runs without error
712+
713+

0 commit comments

Comments
 (0)