four: new wifi ui design (#37152)

* start

* start

* lil more

* add forget

* fix forget button scrolling

* push right a bit

* fix forget press

* add divider

* fix scroll panel

* better forget and overriding

* revert this

* check icon

* cursor merge conflict fix

* fix rounding and forget btn placement

* scroll indicator

* 65%

* calibrate

* try loading animation

* push to device

* top right

* bottom right

* no red

* top left

* bottom left

* down 2px

* WHY DOES NETWORK MANAGER KEEP CRASHING AHHH

* reduce round trip calls in update_networks

* clean up and combine getallaccesspoint and activeaccesspoint

* cmt

* animate big button over smoothly. super hacky, need to clean up

* animate

* remove old widgets and images

* remove status label, tune loading animation opac back

* connecting is a little buggy still

* add back missing network and don't pop

* some fixes

* "clean up"

* fix lag in animation

* fix adding saved connection to start

* remove saved network to start, divider

* animate up, over, and down

* revert for now

* remove fancy complex animation for now, sorry nick

* remove divider + clean up

* more clean up

* more clean up

* fix forget button press

* cmt

* tweak loading animation behavior

* new lock fix wifi

* rm old lock

* great catch by opus

* clean up

* debug

* fix touch events that are down -> up in one frame (why it only bugged on mici)

* clean up

* eager forgetting

* this SHOULD be full eager forget, more than i thought

* fix wifi slash positioning

* move forgotten networks after saved networks

* temp keep

* test on device

* fix

* see 65

* 5 best

* fix double render double brightness

* can click bottom right now

* disable touch while animating

* fix animation

* can scroll while animating, not tap

* not great yet

* clean up

* didn't work

* always update networks after activation

* stash

* move to update_state

* debug

* debug

* temp

* fix ip and metered flickering when updating at high freq (or rare race condition)

* fix

* if you give it less than 8 chars it never clears connecting

* lock no int

* better wrong password handling

* shake when wrong password

* nm set connecting when it connects on its own

* loading bottom right

* sort connecting first

* sort by unquantized to put strongest first

* clean up

* clean up nm

* clean up nm

* shorter

* fix crash

* 0.5s

* debug

* revert and try something else

* stash

* no

* rev

* use signals

* more

* not wrong password if ever connected after wrong

* similar to gnome shell, don't save connection that never successfully activated.

we do this by creating temporary memory connection with persist: volatile that deletes itself if failed, and then only write to disk when activated

* clean up

* cover all states

* clear if connecting too

* remove pritn

* might need this for CoxWifi

* whoops

* save last pass

* Revert "whoops"

This reverts commit 83a133955246ce32dcf119ededd8b01b3162a866.

* Revert "might need this for CoxWifi"

This reverts commit cddb8b35be152ed154462b188283f9d5a844583d.

* this may be less noisy for low strength networks, but less accurate as previous was reflecting nm state better

* Revert "this may be less noisy for low strength networks, but less accurate as previous was reflecting nm state better"

This reverts commit 740286c846556f32125a96bfe6ecf128300af0d8.

* race condition with volotile not removing conn fast enough/update networks not firing fast enough

* Revert "save last pass"

This reverts commit 7249a58a18b11487fd0370cee36e40a17f7ac521.

* revert some wifiman stuff to master

* not needed

* rm active ap

* remove old dead code

* do after

* always send forgotten callback so we can't be stuck in forgetting state forever

* reproduce race condition where connection removed signal takes a while to remove, then update networks keep is_saved true

* fix from merge

* nice, we can remove some eager code now for treating is_saved as not saved after forgot since it's live

* more

* rm

* simplify passed in callbacks

* clean up

* need this one check back for wrong password to hide forget for a split second

* opus says this is simpler 🤔

* Revert "opus says this is simpler 🤔"

This reverts commit 71472e5b383d7f2083d95ba1188070f41ae14775.

* another attempt

* Revert "another attempt"

This reverts commit 31f30babe656f9cad24399bc2196bb6e7ab79bbd.

* fix from merge

* some lcean up

* fix

* fixes to make work with new animation

* clean up

* this works too

* simplify loading animation behavior for now, revert wifi scan time

* clean up

* temporary fix

* stash

* Revert "stash"

This reverts commit 7471dbdc452807b33b4868a98dd8565681b2e44d.

* stash

* Revert "stash"

This reverts commit e0e5e6e861734320ce5dea5626086784577cb334.

* this check was because is_connected could have been stale from Network as the source

* nm can show connected/connecting to network with 0 aps for a while if strength is low, move out of range under those states

* stash

* Revert "stash"

This reverts commit 5ec3b454d54392523947f6477f551657d3863a6d.

* todo

* todo

* order

* don't need temporary fix anymore

* cmt

* order

* unused i
This commit is contained in:
Shane Smiskol
2026-02-22 07:08:48 -08:00
committed by GitHub
parent 31ac5a216d
commit 5f722d2c93
10 changed files with 279 additions and 316 deletions

View File

@@ -3,67 +3,78 @@ import numpy as np
import pyray as rl
from collections.abc import Callable
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.swaglog import cloudlog
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfirmationDialogV2
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR, LABEL_HORIZONTAL_PADDING, LABEL_VERTICAL_PADDING
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, WifiState, normalize_ssid
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, normalize_ssid
class LoadingAnimation(Widget):
def _render(self, _):
cx = int(self._rect.x + 70)
cy = int(self._rect.y + self._rect.height / 2 - 50)
HIDE_TIME = 4
y_mag = 20
anim_scale = 5
spacing = 28
def __init__(self):
super().__init__()
self._opacity_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
self._opacity_target = 1.0
self._hide_time = 0.0
def show_event(self):
self._opacity_target = 1.0
self._hide_time = rl.get_time()
def _render(self, _):
if rl.get_time() - self._hide_time > self.HIDE_TIME:
self._opacity_target = 0.0
self._opacity_filter.update(self._opacity_target)
if self._opacity_filter.x < 0.01:
return
cx = int(self._rect.x + self._rect.width / 2)
cy = int(self._rect.y + self._rect.height / 2)
y_mag = 7
anim_scale = 4
spacing = 14
for i in range(3):
x = cx - spacing + i * spacing
y = int(cy + min(math.sin((rl.get_time() - i * 0.2) * anim_scale) * y_mag, 0))
alpha = int(np.interp(cy - y, [0, y_mag], [255 * 0.45, 255 * 0.9]))
rl.draw_circle(x, y, 10, rl.Color(255, 255, 255, alpha))
alpha = int(np.interp(cy - y, [0, y_mag], [255 * 0.45, 255 * 0.9]) * self._opacity_filter.x)
rl.draw_circle(x, y, 5, rl.Color(255, 255, 255, alpha))
class WifiIcon(Widget):
def __init__(self):
def __init__(self, network: Network):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, 86, 64))
self.set_rect(rl.Rectangle(0, 0, 48 + 5, 36 + 5))
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 86, 64)
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 86, 64)
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 86, 64)
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 86, 64)
self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 22, 32)
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 48, 42)
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 48, 36)
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 48, 36)
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 48, 36)
self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 21, 27)
self._network: Network | None = None
self._network: Network = network
self._network_missing = False # if network disappeared from scan results
self._scale = 1.0
self._opacity = 1.0
def set_current_network(self, network: Network):
def update_network(self, network: Network):
self._network = network
def set_network_missing(self, missing: bool):
self._network_missing = missing
def set_scale(self, scale: float):
self._scale = scale
def set_opacity(self, opacity: float):
self._opacity = opacity
@staticmethod
def get_strength_icon_idx(strength: int) -> int:
return round(strength / 100 * 2)
def _render(self, _):
if self._network is None:
return
# Determine which wifi strength icon to use
strength = self.get_strength_icon_idx(self._network.strength)
if self._network_missing:
@@ -75,126 +86,186 @@ class WifiIcon(Widget):
else:
strength_icon = self._wifi_low_txt
tint = rl.Color(255, 255, 255, int(255 * self._opacity))
icon_x = int(self._rect.x + (self._rect.width - strength_icon.width * self._scale) // 2)
icon_y = int(self._rect.y + (self._rect.height - strength_icon.height * self._scale) // 2)
rl.draw_texture_ex(strength_icon, (icon_x, icon_y), 0.0, self._scale, tint)
rl.draw_texture_ex(strength_icon, (self._rect.x, self._rect.y + self._rect.height - strength_icon.height), 0.0, 1.0, rl.WHITE)
# Render lock icon at lower right of wifi icon if secured
if self._network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED):
lock_scale = self._scale * 1.1
lock_x = int(icon_x + 1 + strength_icon.width * self._scale - self._lock_txt.width * lock_scale / 2)
lock_y = int(icon_y + 1 + strength_icon.height * self._scale - self._lock_txt.height * lock_scale / 2)
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, lock_scale, tint)
lock_x = self._rect.x + self._rect.width - self._lock_txt.width
lock_y = self._rect.y + self._rect.height - self._lock_txt.height + 6
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE)
class WifiItem(BigDialogOptionButton):
LEFT_MARGIN = 20
class WifiButton(BigButton):
LABEL_PADDING = 98
LABEL_WIDTH = 402 - 98 - 28 # button width - left padding - right padding
SUB_LABEL_WIDTH = 402 - LABEL_HORIZONTAL_PADDING * 2
def __init__(self, network: Network, wifi_state_callback: Callable[[], WifiState]):
super().__init__(network.ssid)
self.set_rect(rl.Rectangle(0, 0, gui_app.width, self.HEIGHT))
self._selected_txt = gui_app.texture("icons_mici/settings/network/new/wifi_selected.png", 48, 96)
def __init__(self, network: Network, wifi_manager: WifiManager):
super().__init__(normalize_ssid(network.ssid), scroll=True)
self._network = network
self._wifi_state_callback = wifi_state_callback
self._wifi_icon = WifiIcon()
self._wifi_icon.set_current_network(network)
self._wifi_manager = wifi_manager
self._wifi_icon = WifiIcon(network)
self._forget_btn = ForgetButton(self._forget_network)
self._check_txt = gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 32, 32)
# Eager state (not sourced from Network)
self._network_missing = False
self._network_forgetting = False
self._wrong_password = False
self._shake_start: float | None = None
def update_network(self, network: Network):
self._network = network
self._wifi_icon.update_network(network)
# We can assume network is not missing if got new Network
self._network_missing = False
self._wifi_icon.set_network_missing(False)
if self._is_connected or self._is_connecting:
self._wrong_password = False
def _forget_network(self):
if self._network_forgetting:
return
self._network_forgetting = True
self._forget_btn.set_visible(False)
self._wifi_manager.forget_connection(self._network.ssid)
def on_forgotten(self):
self._network_forgetting = False
self._forget_btn.set_visible(True)
def set_network_missing(self, missing: bool):
self._network_missing = missing
self._wifi_icon.set_network_missing(missing)
def set_current_network(self, network: Network):
self._network = network
self._wifi_icon.set_current_network(network)
# reset if we see the network again
self.set_enabled(True)
self.set_network_missing(False)
def _render(self, _):
disabled_alpha = 0.35 if not self.enabled else 1.0
# connecting or connected
if self._wifi_state_callback().ssid == self._network.ssid:
selected_x = int(self._rect.x - self._selected_txt.width / 2)
selected_y = int(self._rect.y + (self._rect.height - self._selected_txt.height) / 2)
rl.draw_texture(self._selected_txt, selected_x, selected_y, rl.WHITE)
self._wifi_icon.set_opacity(disabled_alpha)
self._wifi_icon.set_scale((1.0 if self._selected else 0.65) * 0.7)
self._wifi_icon.render(rl.Rectangle(
self._rect.x + self.LEFT_MARGIN,
self._rect.y,
self.SELECTED_HEIGHT,
self._rect.height
))
if self._selected:
self._label.set_font_size(self.SELECTED_HEIGHT)
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9 * disabled_alpha)))
self._label.set_font_weight(FontWeight.DISPLAY)
else:
self._label.set_font_size(self.HEIGHT)
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58 * disabled_alpha)))
self._label.set_font_weight(FontWeight.DISPLAY_REGULAR)
label_offset = self.LEFT_MARGIN + self._wifi_icon.rect.width + 20
label_rect = rl.Rectangle(self._rect.x + label_offset, self._rect.y, self._rect.width - label_offset, self._rect.height)
self._label.set_text(normalize_ssid(self._network.ssid))
self._label.render(label_rect)
class ConnectButton(Widget):
def __init__(self):
super().__init__()
self._bg_txt = gui_app.texture("icons_mici/settings/network/new/connect_button.png", 410, 100)
self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/connect_button_pressed.png", 410, 100)
self._bg_full_txt = gui_app.texture("icons_mici/settings/network/new/full_connect_button.png", 520, 100)
self._bg_full_pressed_txt = gui_app.texture("icons_mici/settings/network/new/full_connect_button_pressed.png", 520, 100)
self._full: bool = False
self._label = UnifiedLabel("", 36, FontWeight.MEDIUM, rl.Color(255, 255, 255, int(255 * 0.9)),
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
def set_wrong_password(self):
self._wrong_password = True
self._shake_start = rl.get_time()
@property
def full(self) -> bool:
return self._full
def network(self) -> Network:
return self._network
def set_full(self, full: bool):
self._full = full
self.set_rect(rl.Rectangle(0, 0, 520 if self._full else 410, 100))
@property
def _show_forget_btn(self):
return (self._is_saved and not self._wrong_password) or self._is_connecting
def set_label(self, text: str):
self._label.set_text(text)
def _handle_mouse_release(self, mouse_pos: MousePos):
if self._show_forget_btn and rl.check_collision_point_rec(mouse_pos, self._forget_btn.rect):
return
super()._handle_mouse_release(mouse_pos)
def _render(self, _):
if self._full:
bg_txt = self._bg_full_pressed_txt if self.is_pressed and self.enabled else self._bg_full_txt
else:
bg_txt = self._bg_pressed_txt if self.is_pressed and self.enabled else self._bg_txt
def _get_label_font_size(self):
return 48
rl.draw_texture(bg_txt, int(self._rect.x), int(self._rect.y), rl.WHITE)
@property
def _shake_offset(self) -> float:
SHAKE_DURATION = 0.5
SHAKE_AMPLITUDE = 24.0
SHAKE_FREQUENCY = 32.0
t = rl.get_time() - (self._shake_start or 0.0)
if t > SHAKE_DURATION:
return 0.0
decay = 1.0 - t / SHAKE_DURATION
return decay * SHAKE_AMPLITUDE * math.sin(t * SHAKE_FREQUENCY)
self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9) if self.enabled else int(255 * 0.9 * 0.65)))
self._label.render(self._rect)
def set_position(self, x: float, y: float) -> None:
super().set_position(x + self._shake_offset, y)
def _draw_content(self, btn_y: float):
self._label.set_color(LABEL_COLOR)
label_rect = rl.Rectangle(self._rect.x + self.LABEL_PADDING, btn_y + LABEL_VERTICAL_PADDING,
self.LABEL_WIDTH, self._rect.height - LABEL_VERTICAL_PADDING * 2)
self._label.render(label_rect)
if self.value:
sub_label_x = self._rect.x + LABEL_HORIZONTAL_PADDING
label_y = btn_y + self._rect.height - LABEL_VERTICAL_PADDING
sub_label_w = self.SUB_LABEL_WIDTH - (self._forget_btn.rect.width if self._show_forget_btn else 0)
sub_label_height = self._sub_label.get_content_height(sub_label_w)
if self._is_connected and not self._network_forgetting:
check_y = int(label_y - sub_label_height + (sub_label_height - self._check_txt.height) / 2)
rl.draw_texture(self._check_txt, int(sub_label_x), check_y, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)))
sub_label_x += self._check_txt.width + 14
sub_label_rect = rl.Rectangle(sub_label_x, label_y - sub_label_height, sub_label_w, sub_label_height)
self._sub_label.render(sub_label_rect)
# Wifi icon
self._wifi_icon.render(rl.Rectangle(
self._rect.x + 30,
btn_y + 30,
self._wifi_icon.rect.width,
self._wifi_icon.rect.height,
))
# Forget button
if self._show_forget_btn:
self._forget_btn.render(rl.Rectangle(
self._rect.x + self._rect.width - self._forget_btn.rect.width,
btn_y + self._rect.height - self._forget_btn.rect.height,
self._forget_btn.rect.width,
self._forget_btn.rect.height,
))
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(lambda: touch_callback() and not self._forget_btn.is_pressed)
self._forget_btn.set_touch_valid_callback(touch_callback)
@property
def _is_saved(self):
return self._wifi_manager.is_connection_saved(self._network.ssid)
@property
def _is_connecting(self):
return self._wifi_manager.connecting_to_ssid == self._network.ssid
@property
def _is_connected(self):
return self._wifi_manager.connected_ssid == self._network.ssid
def _update_state(self):
if any((self._network_missing, self._is_connecting, self._is_connected, self._network_forgetting,
self._network.security_type == SecurityType.UNSUPPORTED)):
self.set_enabled(False)
self._sub_label.set_color(rl.Color(255, 255, 255, int(255 * 0.585)))
self._sub_label.set_font_weight(FontWeight.ROMAN)
if self._network_forgetting:
self.set_value("forgetting...")
elif self._is_connecting:
self.set_value("connecting...")
elif self._is_connected:
self.set_value("connected")
elif self._network_missing:
# after connecting/connected since NM will still attempt to connect/stay connected for a while
self.set_value("not in range")
else:
self.set_value("unsupported")
else: # saved, wrong password, or unknown
self.set_value("wrong password" if self._wrong_password else "connect")
self.set_enabled(True)
self._sub_label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
self._sub_label.set_font_weight(FontWeight.SEMI_BOLD)
class ForgetButton(Widget):
HORIZONTAL_MARGIN = 8
MARGIN = 12 # bottom and right
def __init__(self, forget_network: Callable):
super().__init__()
self._forget_network = forget_network
self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 100, 100)
self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 100, 100)
self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 35, 42)
self.set_rect(rl.Rectangle(0, 0, 100 + self.HORIZONTAL_MARGIN * 2, 100))
self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 84, 84)
self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 84, 84)
self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 29, 35)
self.set_rect(rl.Rectangle(0, 0, 84 + self.MARGIN * 2, 84 + self.MARGIN * 2))
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
@@ -204,150 +275,22 @@ class ForgetButton(Widget):
def _render(self, _):
bg_txt = self._bg_pressed_txt if self.is_pressed else self._bg_txt
rl.draw_texture(bg_txt, int(self._rect.x + self.HORIZONTAL_MARGIN), int(self._rect.y), rl.WHITE)
rl.draw_texture_ex(bg_txt, (self._rect.x + (self._rect.width - self._bg_txt.width) / 2,
self._rect.y + (self._rect.height - self._bg_txt.height) / 2), 0, 1.0, rl.WHITE)
trash_x = int(self._rect.x + (self._rect.width - self._trash_txt.width) // 2)
trash_y = int(self._rect.y + (self._rect.height - self._trash_txt.height) // 2)
rl.draw_texture(self._trash_txt, trash_x, trash_y, rl.WHITE)
trash_x = self._rect.x + (self._rect.width - self._trash_txt.width) / 2
trash_y = self._rect.y + (self._rect.height - self._trash_txt.height) / 2
rl.draw_texture_ex(self._trash_txt, (trash_x, trash_y), 0, 1.0, rl.WHITE)
class NetworkInfoPage(NavWidget):
def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable,
connecting_callback: Callable[[], str | None], connected_callback: Callable[[], str | None]):
super().__init__()
self._wifi_manager = wifi_manager
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self._wifi_icon = WifiIcon()
self._forget_btn = ForgetButton(lambda: forget_callback(self._network.ssid) if self._network is not None else None)
self._forget_btn.set_enabled(lambda: self.enabled) # for stack
self._connect_btn = ConnectButton()
self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None)
self._title = UnifiedLabel("", 64, FontWeight.DISPLAY, rl.Color(255, 255, 255, int(255 * 0.9)),
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, scroll=True)
self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
self.set_back_callback(gui_app.pop_widget)
# State
self._network: Network | None = None
self._connecting_callback = connecting_callback
self._connected_callback = connected_callback
def show_event(self):
super().show_event()
self._title.reset_scroll()
def update_networks(self, networks: dict[str, Network]):
# update current network from latest scan results
for ssid, network in networks.items():
if self._network is not None and ssid == self._network.ssid:
self.set_current_network(network)
break
else:
# network disappeared, close page
# TODO: pop_widgets_to, to close potentially open keyboard too
if gui_app.get_active_widget() == self:
gui_app.pop_widget()
def _update_state(self):
super()._update_state()
# TODO: remove? only left for potential compatibility with setup/updater
self._wifi_manager.process_callbacks()
if self._network is None:
return
self._connect_btn.set_full(not self._wifi_manager.is_connection_saved(self._network.ssid) and not self._is_connecting)
if self._is_connecting:
self._connect_btn.set_label("connecting...")
self._connect_btn.set_enabled(False)
elif self._is_connected:
self._connect_btn.set_label("connected")
self._connect_btn.set_enabled(False)
elif self._network.security_type == SecurityType.UNSUPPORTED:
self._connect_btn.set_label("connect")
self._connect_btn.set_enabled(False)
else: # saved or unknown
self._connect_btn.set_label("connect")
self._connect_btn.set_enabled(self.enabled)
self._title.set_text(normalize_ssid(self._network.ssid))
if self._network.security_type == SecurityType.OPEN:
self._subtitle.set_text("open")
elif self._network.security_type == SecurityType.UNSUPPORTED:
self._subtitle.set_text("unsupported")
else:
self._subtitle.set_text("secured")
def set_current_network(self, network: Network):
self._network = network
self._wifi_icon.set_current_network(network)
@property
def _is_connecting(self):
if self._network is None:
return False
is_connecting = self._connecting_callback() == self._network.ssid
return is_connecting
@property
def _is_connected(self):
if self._network is None:
return False
is_connected = self._connected_callback() == self._network.ssid
return is_connected
def _render(self, _):
self._wifi_icon.render(rl.Rectangle(
self._rect.x + 32,
self._rect.y + (self._rect.height - self._connect_btn.rect.height - self._wifi_icon.rect.height) / 2,
self._wifi_icon.rect.width,
self._wifi_icon.rect.height,
))
self._title.render(rl.Rectangle(
self._rect.x + self._wifi_icon.rect.width + 32 + 32,
self._rect.y + 32 - 16,
self._rect.width - (self._wifi_icon.rect.width + 32 + 32),
64,
))
self._subtitle.render(rl.Rectangle(
self._rect.x + self._wifi_icon.rect.width + 32 + 32,
self._rect.y + 32 + 64 - 16,
self._rect.width - (self._wifi_icon.rect.width + 32 + 32),
48,
))
self._connect_btn.render(rl.Rectangle(
self._rect.x + 8,
self._rect.y + self._rect.height - self._connect_btn.rect.height,
self._connect_btn.rect.width,
self._connect_btn.rect.height,
))
if not self._connect_btn.full:
self._forget_btn.render(rl.Rectangle(
self._rect.x + self._rect.width - self._forget_btn.rect.width,
self._rect.y + self._rect.height - self._forget_btn.rect.height,
self._forget_btn.rect.width,
self._forget_btn.rect.height,
))
class WifiUIMici(BigMultiOptionDialog):
class WifiUIMici(NavWidget):
def __init__(self, wifi_manager: WifiManager):
super().__init__([], None)
super().__init__()
# Set up back navigation
self.set_back_callback(gui_app.pop_widget)
self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, wifi_manager.forget_connection,
lambda: wifi_manager.connecting_to_ssid, lambda: wifi_manager.connected_ssid)
self._scroller = Scroller([])
self._loading_animation = LoadingAnimation()
@@ -356,12 +299,15 @@ class WifiUIMici(BigMultiOptionDialog):
self._wifi_manager.add_callbacks(
need_auth=self._on_need_auth,
forgotten=self._on_forgotten,
networks_updated=self._on_network_updated,
)
def show_event(self):
# Clear scroller items and update from latest scan results
super().show_event()
self._scroller.show_event()
self._loading_animation.show_event()
self._wifi_manager.set_active(True)
self._scroller.items.clear()
self._update_buttons()
@@ -373,43 +319,38 @@ class WifiUIMici(BigMultiOptionDialog):
def _on_network_updated(self, networks: list[Network]):
self._networks = {network.ssid: network for network in networks}
self._update_buttons()
self._network_info_page.update_networks(self._networks)
def _update_buttons(self):
# Only add new buttons to the end. Update existing buttons without re-sorting so user can freely scroll around
# Update existing buttons, add new ones to the end
existing = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)}
for network in self._networks.values():
network_button_idx = next((i for i, btn in enumerate(self._scroller.items) if btn.option == network.ssid), None)
if network_button_idx is not None:
# Update network on existing button
self._scroller.items[network_button_idx].set_current_network(network)
if network.ssid in existing:
existing[network.ssid].update_network(network)
else:
network_button = WifiItem(network, lambda: self._wifi_manager.wifi_state)
self._scroller.add_widget(network_button)
btn = WifiButton(network, self._wifi_manager)
btn.set_click_callback(lambda ssid=network.ssid: self._connect_to_network(ssid))
self._scroller.add_widget(btn)
# Move connecting/connected network to the start
connected_btn_idx = next((i for i, btn in enumerate(self._scroller.items) if self._wifi_manager.wifi_state.ssid == btn._network.ssid), None)
if connected_btn_idx is not None and connected_btn_idx > 0:
self._scroller.items.insert(0, self._scroller.items.pop(connected_btn_idx))
self._scroller._layout() # fixes selected style single frame stutter
# Disable networks no longer present
# Mark networks no longer in scan results (display handled by _update_state)
for btn in self._scroller.items:
if btn.option not in self._networks:
btn.set_enabled(False)
if isinstance(btn, WifiButton) and btn.network.ssid not in self._networks:
btn.set_network_missing(True)
# Move connecting/connected network to the front with animation
front_ssid = self._wifi_manager.wifi_state.ssid
front_btn_idx = next((i for i, btn in enumerate(self._scroller.items)
if isinstance(btn, WifiButton) and
btn.network.ssid == front_ssid), None) if front_ssid else None
if front_btn_idx is not None and front_btn_idx > 0:
self._scroller.move_item(front_btn_idx, 0)
def _connect_with_password(self, ssid: str, password: str):
self._wifi_manager.connect_to_network(ssid, password)
self._scroller.scroll_to(self._scroller.scroll_panel.get_offset(), smooth=True)
self._update_buttons()
def _on_option_selected(self, option: str):
super()._on_option_selected(option)
if option in self._networks:
self._network_info_page.set_current_network(self._networks[option])
gui_app.push_widget(self._network_info_page)
def _connect_to_network(self, ssid: str):
network = self._networks.get(ssid)
if network is None:
@@ -418,21 +359,46 @@ class WifiUIMici(BigMultiOptionDialog):
if self._wifi_manager.is_connection_saved(network.ssid):
self._wifi_manager.activate_connection(network.ssid)
self._update_buttons()
elif network.security_type == SecurityType.OPEN:
self._wifi_manager.connect_to_network(network.ssid, "")
self._update_buttons()
else:
self._on_need_auth(network.ssid, False)
return
self._scroller.scroll_to(self._scroller.scroll_panel.get_offset(), smooth=True)
self._update_buttons()
def _on_need_auth(self, ssid, incorrect_password=True):
hint = "wrong password..." if incorrect_password else "enter password..."
dlg = BigInputDialog(hint, "", minimum_length=8,
if incorrect_password:
for btn in self._scroller.items:
if isinstance(btn, WifiButton) and btn.network.ssid == ssid:
btn.set_wrong_password()
break
return
dlg = BigInputDialog("enter password...", "", minimum_length=8,
confirm_callback=lambda _password: self._connect_with_password(ssid, _password))
gui_app.push_widget(dlg)
def _render(self, _):
super()._render(_)
def _on_forgotten(self, ssid):
# For eager UI forget
for btn in self._scroller.items:
if isinstance(btn, WifiButton) and btn.network.ssid == ssid:
btn.on_forgotten()
if not self._networks:
self._loading_animation.render(self._rect)
def _update_state(self):
super()._update_state()
# Show loading animation near end
max_scroll = max(self._scroller.content_size - self._scroller.rect.width, 1)
progress = -self._scroller.scroll_panel.get_offset() / max_scroll
if progress > 0.8 or len(self._scroller.items) <= 1:
self._loading_animation.show_event()
def _render(self, _):
self._scroller.render(self._rect)
anim_w = 90
anim_x = self._rect.x + self._rect.width - anim_w
anim_y = self._rect.y + self._rect.height - 25 + 2
self._loading_animation.render(rl.Rectangle(anim_x, anim_y, anim_w, 20))

View File

@@ -26,6 +26,7 @@ class NMDeviceStateReason(IntEnum):
NO_SECRETS = 7
SUPPLICANT_DISCONNECT = 8
CONNECTION_REMOVED = 38
SSID_NOT_FOUND = 53
NEW_ACTIVATION = 60

View File

@@ -378,6 +378,9 @@ class WifiManager:
while len(state_q):
new_state, previous_state, change_reason = state_q.popleft().body
# TODO: Handle (FAILED, SSID_NOT_FOUND) and emit for ui to show error
# Happens when network drops off after starting connection
if new_state == NMDeviceState.DISCONNECTED:
if change_reason != NMDeviceStateReason.NEW_ACTIVATION:
# catches CONNECTION_REMOVED reason when connection is forgotten
@@ -414,8 +417,6 @@ class WifiManager:
elif new_state == NMDeviceState.ACTIVATED:
# Note that IP address from Ip4Config may not be propagated immediately and could take until the next scan results
self._update_networks()
wifi_state = replace(self._wifi_state, prev_ssid=None, status=ConnectStatus.CONNECTED)
conn_path, _ = self._get_active_wifi_connection(self._conn_monitor)
@@ -423,10 +424,12 @@ class WifiManager:
cloudlog.warning("Failed to get active wifi connection during ACTIVATED state")
self._wifi_state = wifi_state
self._enqueue_callbacks(self._activated)
self._update_networks()
else:
wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None)
self._wifi_state = wifi_state
self._enqueue_callbacks(self._activated)
self._update_networks()
# Persist volatile connections (created by AddAndActivateConnection2) to disk
conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE)

View File

@@ -15,6 +15,7 @@ ANIMATION_SCALE = 0.6
MOVE_LIFT = 20
MOVE_OVERLAY_ALPHA = 0.65
SCROLL_RC = 0.15
EDGE_SHADOW_WIDTH = 20
@@ -78,7 +79,7 @@ class Scroller(Widget):
self._reset_scroll_at_show = True
self._scrolling_to: float | None = None
self._scroll_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
self._scroll_filter = FirstOrderFilter(0.0, SCROLL_RC, 1 / gui_app.target_fps)
self._zoom_filter = FirstOrderFilter(1.0, 0.2, 1 / gui_app.target_fps)
self._zoom_out_t: float = 0.0
@@ -134,6 +135,10 @@ class Scroller(Widget):
def items(self) -> list[Widget]:
return self._items
@property
def content_size(self) -> float:
return self._content_size
def add_widget(self, item: Widget) -> None:
self._items.append(item)
item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled and self._scrolling_to is None
@@ -159,7 +164,7 @@ class Scroller(Widget):
if self._scrolling_to is not None and (self.scroll_panel.state == ScrollState.PRESSED or self.scroll_panel.state == ScrollState.MANUAL_SCROLL):
self._scrolling_to = None
if self._scrolling_to is not None:
if self._scrolling_to is not None and len(self._pending_lift) == 0:
self._scroll_filter.update(self._scrolling_to)
self.scroll_panel.set_offset(self._scroll_filter.x)
@@ -230,14 +235,17 @@ class Scroller(Widget):
# store original position in content space of all affected widgets to animate from
for idx in range(min(from_idx, to_idx), max(from_idx, to_idx) + 1):
affected_item = self._items[idx]
self._move_animations[affected_item] = FirstOrderFilter(affected_item.rect.x - self._scroll_offset, 0.15, 1 / gui_app.target_fps)
self._move_animations[affected_item] = FirstOrderFilter(affected_item.rect.x - self._scroll_offset, SCROLL_RC, 1 / gui_app.target_fps)
self._pending_move.add(affected_item)
# lift only src widget to make it more clear which one is moving
self._move_lift[item] = FirstOrderFilter(0.0, 0.15, 1 / gui_app.target_fps)
self._move_lift[item] = FirstOrderFilter(0.0, SCROLL_RC, 1 / gui_app.target_fps)
self._pending_lift.add(item)
def _do_move_animation(self, item: Widget, target_x: float, target_y: float) -> tuple[float, float]:
# wait a frame before moving so we match potential pending scroll animation
can_start_move = len(self._pending_lift) == 0
if item in self._move_lift:
lift_filter = self._move_lift[item]
@@ -260,7 +268,7 @@ class Scroller(Widget):
# compare/update in content space to match filter
content_x = target_x - self._scroll_offset
if len(self._pending_lift) == 0:
if can_start_move:
move_filter.update(content_x)
# drop when close to target