Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b2b3e2b
fix(network): harden wifi connect wait, interface detection and profi…
gmmcosta15 Aug 25, 2026
a984589
fix(network): show live AP signal strength on main network page
gmmcosta15 Aug 25, 2026
190e0e2
fix(network): add reading set values and format keyboard for numeric …
gmmcosta15 Aug 25, 2026
ac73d51
feat(keyboard): full-screen numpad layout for numeric-only fields
gmmcosta15 Aug 25, 2026
c70049a
refactor(keyboard): match numpad styling to app UI and fix psk test f…
gmmcosta15 Aug 25, 2026
afbb3ed
fix(network): stop ethernet and wifi from disabling each other, re-ar…
gmmcosta15 Aug 25, 2026
a0df3f3
feat(network): enforce one active link at a time and move numpad dot …
gmmcosta15 Aug 25, 2026
da76dec
fix(network): persist ethernet off in the wired profile, move numpad …
gmmcosta15 Aug 25, 2026
cd49b35
fix(network): make route check intent-aware and match numpad 0 to the…
gmmcosta15 Aug 25, 2026
7ac1839
fix(network): persist ethernet-off intent when the device is already …
gmmcosta15 Aug 25, 2026
d340f5d
fix(network): retry signal map after re-detecting a stale wifi device…
gmmcosta15 Aug 26, 2026
e7edd70
chore(network): drop temporary ip_by_iface debug instrumentation
gmmcosta15 Aug 26, 2026
418e153
chore(network): drop per-poll debug logs and compress comments to one…
gmmcosta15 Aug 26, 2026
54aac20
feat(network): harden NetworkManager layer, cut complexity, expand RF…
gmmcosta15 Aug 26, 2026
2f63ce8
fix(network): arm loading guard before link changes
gmmcosta15 Aug 26, 2026
b40b525
fix(network): detect AP mode as hotspot, arm toggle guard before link…
gmmcosta15 Aug 26, 2026
f863e99
docs(network): add module and nested-function docstrings to network a…
gmmcosta15 Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions BlocksScreen/lib/network/manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Qt-facing NetworkManager facade: owns the worker thread and its signals."""

# pylint: disable=protected-access

import asyncio
Expand All @@ -17,7 +19,7 @@

logger = logging.getLogger(__name__)

_KEEPALIVE_POLL_MS: int = 300_000 # 5 minutes safety net for missed signals
_KEEPALIVE_POLL_MS: int = 300_000 # 5 minutes: safety net for missed signals


class NetworkManager(QObject):
Expand All @@ -27,9 +29,9 @@ class NetworkManager(QObject):
a ``NetworkManagerWorker`` that runs all D-Bus coroutines on its
dedicated asyncio thread.

Coroutines are submitted to ``worker._asyncio_loop`` the same loop
on which the D-Bus file-descriptor was registered so signal delivery
and async I/O always occur on the correct selector.
Coroutines are submitted to ``worker._asyncio_loop`` (the same loop the
D-Bus file-descriptor was registered on), so signal delivery and async
I/O always occur on the correct selector.

"""

Expand All @@ -41,6 +43,7 @@ class NetworkManager(QObject):
error_occurred = pyqtSignal(str, str)
reconnect_complete = pyqtSignal()
hotspot_config_updated = pyqtSignal(str, str, str)
network_password_loaded = pyqtSignal(str, str)

def __init__(self, parent: QObject | None = None) -> None:
"""Create the worker, wire all signals"""
Expand All @@ -55,7 +58,7 @@ def __init__(self, parent: QObject | None = None) -> None:
self._shutting_down: bool = False
self._worker_ready: bool = False

self._pending_futures: set["asyncio.Future"] = set()
self._pending_futures: set[asyncio.Future] = set()

self._worker = NetworkManagerWorker()

Expand All @@ -70,9 +73,10 @@ def __init__(self, parent: QObject | None = None) -> None:
self._worker.error_occurred.connect(self.error_occurred)
self._worker.hotspot_info_ready.connect(self._on_hotspot_info_ready)
self._worker.reconnect_complete.connect(self.reconnect_complete)
self._worker.network_password_loaded.connect(self.network_password_loaded)
self._worker.initialized.connect(self._on_worker_initialized)

# Keepalive timer safety net for any missed D-Bus signals.
# Keepalive timer: safety net for any missed D-Bus signals.
self._keepalive_timer = QTimer(self)
self._keepalive_timer.setInterval(_KEEPALIVE_POLL_MS)
self._keepalive_timer.timeout.connect(self._on_keepalive_tick)
Expand All @@ -96,7 +100,7 @@ def _schedule(self, coro: "asyncio.Coroutine") -> None:
future.add_done_callback(self._pending_futures.discard)
else:
logger.debug(
"Dropping early coroutine loop not yet running: %s",
"Dropping early coroutine, loop not yet running: %s",
coro.__qualname__,
)
coro.close()
Expand All @@ -114,7 +118,7 @@ def _on_worker_initialized(self) -> None:
return
self._worker_ready = True
logger.info(
"Worker initialised starting keepalive (every %d ms)",
"Worker initialised: starting keepalive (every %d ms)",
_KEEPALIVE_POLL_MS,
)
self._keepalive_timer.start()
Expand Down Expand Up @@ -168,9 +172,11 @@ def _on_networks_scanned(self, networks: list) -> None:

@pyqtSlot(list)
def _on_saved_networks_loaded(self, networks: list) -> None:
"""Cache saved profiles, rebuild lowercase lookup map, and re-emit."""
"""Cache saved profiles, rebuild lowercase lookup map, and re-emit if changed."""
if self._shutting_down:
return
if networks == self._cached_saved:
return
self._cached_saved = networks
self._saved_network_map = {n.ssid.lower(): n for n in networks}
self.saved_networks_loaded.emit(networks)
Expand All @@ -185,7 +191,7 @@ def _on_hotspot_info_ready(self, ssid: str, password: str, security: str) -> Non

@pyqtSlot()
def _on_keepalive_tick(self) -> None:
"""Safety-net refresh runs every 5 min to catch any missed signals."""
"""Safety-net refresh: runs every 5 min to catch any missed signals."""
if self._shutting_down:
return
self._schedule(self._worker._async_get_current_state())
Expand Down Expand Up @@ -247,6 +253,10 @@ def update_network( # nosec B107
"""Update the password and/or autoconnect priority for a saved profile."""
self._schedule(self._worker._async_update_network(ssid, password, priority))

def get_network_password(self, ssid: str) -> None:
"""Ask NM for a saved profile's psk; answered by network_password_loaded."""
self._schedule(self._worker._async_get_network_password(ssid))

def set_wifi_enabled(self, enabled: bool) -> None:
"""Enable or disable the Wi-Fi radio."""
self._schedule(self._worker._async_set_wifi_enabled(enabled))
Expand All @@ -273,7 +283,7 @@ def update_hotspot_config(
new_password: str,
security: str = "wpa-psk",
) -> None:
"""Change hotspot name/password/security cleans up old profiles."""
"""Change hotspot name/password/security: cleans up old profiles."""
self._schedule(
self._worker._async_update_hotspot_config(
old_ssid, new_ssid, new_password, security
Expand Down Expand Up @@ -346,17 +356,17 @@ def saved_networks(self) -> list[SavedNetwork]:

@property
def hotspot_ssid(self) -> str:
"""Hotspot SSID read from main-thread cache (thread-safe)."""
"""Hotspot SSID: read from main-thread cache (thread-safe)."""
return self._cached_hotspot_ssid

@property
def hotspot_password(self) -> str:
"""Hotspot password read from main-thread cache (thread-safe)."""
"""Hotspot password: read from main-thread cache (thread-safe)."""
return self._cached_hotspot_password

@property
def hotspot_security(self) -> str:
"""Hotspot security type always 'wpa-psk' (WPA2-PSK, thread-safe)."""
"""Hotspot security type: always 'wpa-psk' (WPA2-PSK, thread-safe)."""
return self._cached_hotspot_security

def get_network_info(self, ssid: str) -> NetworkInfo | None:
Expand Down
10 changes: 7 additions & 3 deletions BlocksScreen/lib/network/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ class NetworkStatus(IntEnum):
``NetworkInfo.is_open`` (derived from ``security_type``) instead.
"""

DISCOVERED = 0 # Seen in scan, not saved protected security
OPEN = 1 # Seen in scan, not saved open (no passphrase)
DISCOVERED = 0 # Seen in scan, not saved: protected security
OPEN = 1 # Seen in scan, not saved: open (no passphrase)
SAVED = 2 # Profile saved on this device
ACTIVE = 3 # Currently connected
HIDDEN = 4 # Hidden-network placeholder
Expand Down Expand Up @@ -237,6 +237,10 @@ class SavedNetwork:
signal_strength: int = 0
timestamp: int = 0 # Unix time of last successful activation
is_dhcp: bool = True # True = auto (DHCP), False = manual (static IP)
ip_address: str = "" # static IPv4 config, all empty while on DHCP
netmask: str = ""
gateway: str = ""
dns_servers: tuple[str, ...] = ()


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -287,7 +291,7 @@ class HotspotSecurity(str, Enum):
"""

WPA1 = "wpa1"
WPA2_PSK = "wpa-psk" # WPA2-PSK (CCMP) default
WPA2_PSK = "wpa-psk" # WPA2-PSK (CCMP): default

@classmethod
def is_valid(cls, value: str) -> bool:
Expand Down
Loading