fix PageManager
This commit is contained in:
@@ -84,7 +84,14 @@ class PageManager:
|
||||
],
|
||||
))
|
||||
|
||||
# Page 4: Confirm (dynamic)
|
||||
# Page 4: UPS Status (if available)
|
||||
self._pages.append(PageDefinition(
|
||||
page_type=PageType.INFO,
|
||||
title="UPS Status",
|
||||
generator=self._generate_ups_page,
|
||||
))
|
||||
|
||||
# Page 5: Confirm (dynamic)
|
||||
self._pages.append(PageDefinition(
|
||||
page_type=PageType.CONFIRM,
|
||||
title="Confirm",
|
||||
@@ -110,15 +117,42 @@ class PageManager:
|
||||
"""
|
||||
self._data_providers[name] = provider
|
||||
|
||||
def set_action_handler(self, action_id: ActionID, handler: Callable[[], bool]) -> None:
|
||||
def set_action_handler(self, action_id: ActionID | str, handler: Callable[[], bool]) -> None:
|
||||
"""Register an action handler.
|
||||
|
||||
Args:
|
||||
action_id: Action identifier
|
||||
action_id: Action identifier (ActionID enum or string)
|
||||
handler: Callable that executes action, returns True on success
|
||||
"""
|
||||
self._action_handlers[action_id] = handler
|
||||
|
||||
def add_page(self, page: Any) -> None:
|
||||
"""Add a custom page to the page manager.
|
||||
|
||||
Args:
|
||||
page: Page object with get_content(), is_enabled(), name, title attributes
|
||||
"""
|
||||
# Create a PageDefinition wrapper for custom page
|
||||
def generator(mgr: "PageManager") -> Page:
|
||||
content = page.get_content()
|
||||
return Page(
|
||||
page_type=PageType(content.page_type.value),
|
||||
title=content.title,
|
||||
lines=content.lines,
|
||||
actions=content.actions,
|
||||
selected=content.selected,
|
||||
)
|
||||
|
||||
# Insert before the confirm page (last page)
|
||||
page_def = PageDefinition(
|
||||
page_type=PageType.INFO,
|
||||
title=page.title,
|
||||
generator=generator,
|
||||
)
|
||||
# Insert at position -1 (before confirm page)
|
||||
self._pages.insert(len(self._pages) - 1, page_def)
|
||||
_logger.info(f"Added custom page: {page.name}")
|
||||
|
||||
def get_data(self, name: str, default: Any = None) -> Any:
|
||||
"""Get data from a provider.
|
||||
|
||||
@@ -435,6 +469,40 @@ class PageManager:
|
||||
]
|
||||
return Page(PageType.MENU, "Actions", actions=actions)
|
||||
|
||||
def _generate_ups_page(self, mgr: "PageManager") -> Page:
|
||||
"""Generate UPS status page."""
|
||||
ups_data = mgr.get_data("ups")
|
||||
|
||||
if ups_data is None:
|
||||
lines = ["UPS not available", "", "Check I2C connection"]
|
||||
return Page(PageType.INFO, "UPS Status", lines)
|
||||
|
||||
# Battery bar visualization
|
||||
capacity = ups_data.capacity if hasattr(ups_data, 'capacity') else 0
|
||||
filled = int(capacity / 16.67) # 6 segments
|
||||
empty = 6 - filled
|
||||
bar = "[" + "=" * filled + " " * empty + "]"
|
||||
|
||||
lines = [
|
||||
f"Bat: {capacity:.1f}% {bar}",
|
||||
f"Voltage: {ups_data.voltage:.2f}V",
|
||||
]
|
||||
|
||||
if hasattr(ups_data, 'power_loss') and ups_data.power_loss:
|
||||
lines.append("Power: BATTERY MODE")
|
||||
if capacity < 15:
|
||||
lines.append("!! CRITICAL !!")
|
||||
elif capacity < 25:
|
||||
lines.append("! LOW BATTERY !")
|
||||
else:
|
||||
status = "Charging" if getattr(ups_data, 'is_charging', False) else "Full"
|
||||
lines.append(f"Status: {status}")
|
||||
input_v = getattr(ups_data, 'input_voltage', 0)
|
||||
if input_v > 0:
|
||||
lines.append(f"Input: {input_v:.2f}V")
|
||||
|
||||
return Page(PageType.INFO, "UPS Status", lines)
|
||||
|
||||
def _generate_confirm_page(self, mgr: "PageManager") -> Page:
|
||||
"""Generate confirmation page."""
|
||||
if mgr._pending_action == ActionID.REBOOT_SYSTEM:
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
Flipper Zero UI Pages.
|
||||
|
||||
Each page represents a screen on the Flipper Zero display.
|
||||
"""
|
||||
|
||||
from .base import BasePage, InfoPage, MenuPage, ConfirmPage
|
||||
from .ups_status import UPSStatusPage
|
||||
|
||||
__all__ = [
|
||||
"BasePage",
|
||||
"InfoPage",
|
||||
"MenuPage",
|
||||
"ConfirmPage",
|
||||
"UPSStatusPage",
|
||||
]
|
||||
@@ -1,312 +0,0 @@
|
||||
"""
|
||||
Base Page Interface for Flipper Zero UI.
|
||||
|
||||
All pages must inherit from BasePage and implement required methods.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, Callable, List
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class PageType(Enum):
|
||||
"""Types of pages for different UI layouts."""
|
||||
INFO = "info"
|
||||
MENU = "menu"
|
||||
CONFIRM = "confirm"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PageContent:
|
||||
"""
|
||||
Content structure for a page to be displayed on Flipper Zero.
|
||||
|
||||
Attributes:
|
||||
page_type: Type of page (info, menu, confirm)
|
||||
title: Page title (max ~20 chars for display)
|
||||
lines: Content lines to display (max 4-5 lines)
|
||||
actions: Available actions/menu items (for menu type)
|
||||
selected: Currently selected item index (for menu type)
|
||||
icon: Optional icon identifier for the page
|
||||
"""
|
||||
page_type: PageType
|
||||
title: str
|
||||
lines: List[str] = field(default_factory=list)
|
||||
actions: List[str] = field(default_factory=list)
|
||||
selected: int = 0
|
||||
icon: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
"""Truncate title if too long."""
|
||||
if len(self.title) > 20:
|
||||
self.title = self.title[:17] + "..."
|
||||
|
||||
|
||||
class BasePage(ABC):
|
||||
"""
|
||||
Abstract base class for Flipper Zero pages.
|
||||
|
||||
Each page represents a screen on Flipper Zero display.
|
||||
Pages can be informational (read-only), interactive menus,
|
||||
or confirmation dialogs.
|
||||
|
||||
Attributes:
|
||||
name: Unique identifier for the page
|
||||
title: Display title (shown in header)
|
||||
icon: Icon identifier for visual representation
|
||||
enabled: Whether the page is active
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
title: str,
|
||||
icon: str = "",
|
||||
enabled: bool = True,
|
||||
):
|
||||
"""
|
||||
Initialize base page.
|
||||
|
||||
Args:
|
||||
name: Unique page identifier
|
||||
title: Display title
|
||||
icon: Icon identifier
|
||||
enabled: Whether page is enabled
|
||||
"""
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.enabled = enabled
|
||||
self._selected_index = 0
|
||||
self._scroll_offset = 0
|
||||
self._pending_action: Optional[str] = None
|
||||
|
||||
@abstractmethod
|
||||
def get_content(self) -> PageContent:
|
||||
"""
|
||||
Get current page content for display.
|
||||
|
||||
Returns:
|
||||
PageContent object with type, title, lines, and actions
|
||||
"""
|
||||
pass
|
||||
|
||||
def handle_select(self, index: int) -> Optional[str]:
|
||||
"""
|
||||
Handle menu item selection.
|
||||
|
||||
Called when user presses OK on a menu item.
|
||||
|
||||
Args:
|
||||
index: Selected item index
|
||||
|
||||
Returns:
|
||||
Optional result message to display, or None
|
||||
"""
|
||||
self._selected_index = index
|
||||
return None
|
||||
|
||||
def handle_confirm(self) -> Optional[str]:
|
||||
"""
|
||||
Handle confirmation action.
|
||||
|
||||
Called when user confirms a pending action.
|
||||
|
||||
Returns:
|
||||
Optional result message, or None
|
||||
"""
|
||||
return None
|
||||
|
||||
def handle_cancel(self) -> Optional[str]:
|
||||
"""
|
||||
Handle cancel action.
|
||||
|
||||
Called when user cancels a pending action.
|
||||
|
||||
Returns:
|
||||
Optional result message, or None
|
||||
"""
|
||||
self._pending_action = None
|
||||
return None
|
||||
|
||||
def handle_scroll(self, direction: str) -> None:
|
||||
"""
|
||||
Handle scroll up/down.
|
||||
|
||||
Args:
|
||||
direction: "up" or "down"
|
||||
"""
|
||||
if direction == "up":
|
||||
self._scroll_offset = max(0, self._scroll_offset - 1)
|
||||
elif direction == "down":
|
||||
self._scroll_offset += 1
|
||||
|
||||
def get_selected_index(self) -> int:
|
||||
"""Get currently selected index for menu pages."""
|
||||
return self._selected_index
|
||||
|
||||
def set_selected_index(self, index: int) -> None:
|
||||
"""Set selected index for menu pages."""
|
||||
self._selected_index = index
|
||||
|
||||
def get_scroll_offset(self) -> int:
|
||||
"""Get current scroll offset."""
|
||||
return self._scroll_offset
|
||||
|
||||
def reset_scroll(self) -> None:
|
||||
"""Reset scroll position to top."""
|
||||
self._scroll_offset = 0
|
||||
|
||||
def has_pending_action(self) -> bool:
|
||||
"""Check if there's a pending action requiring confirmation."""
|
||||
return self._pending_action is not None
|
||||
|
||||
def get_pending_action(self) -> Optional[str]:
|
||||
"""Get pending action name."""
|
||||
return self._pending_action
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
"""Check if page is enabled."""
|
||||
return self.enabled
|
||||
|
||||
def on_enter(self) -> None:
|
||||
"""Called when user navigates to this page."""
|
||||
pass
|
||||
|
||||
def on_leave(self) -> None:
|
||||
"""Called when user navigates away from this page."""
|
||||
pass
|
||||
|
||||
|
||||
class InfoPage(BasePage):
|
||||
"""
|
||||
Base class for information-only pages.
|
||||
|
||||
Info pages display read-only data that updates periodically.
|
||||
They have no interactive elements.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
title: str,
|
||||
icon: str = "",
|
||||
enabled: bool = True,
|
||||
):
|
||||
super().__init__(name, title, icon, enabled)
|
||||
|
||||
@abstractmethod
|
||||
def get_lines(self) -> List[str]:
|
||||
"""
|
||||
Get content lines for display.
|
||||
|
||||
Returns:
|
||||
List of strings to display (max 4-5 lines)
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_content(self) -> PageContent:
|
||||
"""Get page content as INFO type."""
|
||||
return PageContent(
|
||||
page_type=PageType.INFO,
|
||||
title=self.title,
|
||||
lines=self.get_lines(),
|
||||
icon=self.icon,
|
||||
)
|
||||
|
||||
|
||||
class MenuPage(BasePage):
|
||||
"""
|
||||
Base class for menu pages.
|
||||
|
||||
Menu pages display selectable items that can trigger actions.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
title: str,
|
||||
icon: str = "",
|
||||
enabled: bool = True,
|
||||
):
|
||||
super().__init__(name, title, icon, enabled)
|
||||
self._menu_items: List[tuple[str, Callable[[], Optional[str]]]] = []
|
||||
|
||||
def add_item(self, label: str, action: Callable[[], Optional[str]]) -> None:
|
||||
"""
|
||||
Add menu item.
|
||||
|
||||
Args:
|
||||
label: Display label for the item
|
||||
action: Callback function when item is selected
|
||||
"""
|
||||
self._menu_items.append((label, action))
|
||||
|
||||
def clear_items(self) -> None:
|
||||
"""Clear all menu items."""
|
||||
self._menu_items.clear()
|
||||
self._selected_index = 0
|
||||
|
||||
def get_content(self) -> PageContent:
|
||||
"""Get page content as MENU type."""
|
||||
labels = [item[0] for item in self._menu_items]
|
||||
return PageContent(
|
||||
page_type=PageType.MENU,
|
||||
title=self.title,
|
||||
lines=[],
|
||||
actions=labels,
|
||||
selected=self._selected_index,
|
||||
icon=self.icon,
|
||||
)
|
||||
|
||||
def handle_select(self, index: int) -> Optional[str]:
|
||||
"""Execute action for selected menu item."""
|
||||
self._selected_index = index
|
||||
if 0 <= index < len(self._menu_items):
|
||||
_, action = self._menu_items[index]
|
||||
return action()
|
||||
return None
|
||||
|
||||
|
||||
class ConfirmPage(BasePage):
|
||||
"""
|
||||
Confirmation dialog page.
|
||||
|
||||
Used to confirm dangerous actions like shutdown or reboot.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
title: str,
|
||||
message: str,
|
||||
on_confirm: Callable[[], Optional[str]],
|
||||
on_cancel: Optional[Callable[[], Optional[str]]] = None,
|
||||
icon: str = "",
|
||||
):
|
||||
super().__init__(name, title, icon, enabled=True)
|
||||
self.message = message
|
||||
self._on_confirm = on_confirm
|
||||
self._on_cancel = on_cancel
|
||||
|
||||
def get_content(self) -> PageContent:
|
||||
"""Get page content as CONFIRM type."""
|
||||
return PageContent(
|
||||
page_type=PageType.CONFIRM,
|
||||
title=self.title,
|
||||
lines=[self.message],
|
||||
actions=["Yes", "No"],
|
||||
selected=1, # Default to "No" for safety
|
||||
icon=self.icon,
|
||||
)
|
||||
|
||||
def handle_confirm(self) -> Optional[str]:
|
||||
"""Execute confirm action."""
|
||||
return self._on_confirm()
|
||||
|
||||
def handle_cancel(self) -> Optional[str]:
|
||||
"""Execute cancel action."""
|
||||
if self._on_cancel:
|
||||
return self._on_cancel()
|
||||
return None
|
||||
@@ -1,94 +0,0 @@
|
||||
"""
|
||||
UPS Status Page.
|
||||
|
||||
Displays X120x UPS battery and power status on Flipper Zero.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from .base import InfoPage
|
||||
from ..providers.ups_provider import UPSProvider
|
||||
|
||||
|
||||
class UPSStatusPage(InfoPage):
|
||||
"""
|
||||
Page displaying UPS (X120x) status.
|
||||
|
||||
Shows:
|
||||
- Battery percentage and voltage
|
||||
- Charging status
|
||||
- Power loss indicator
|
||||
- Input voltage
|
||||
|
||||
Visual battery bar for quick status check.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="ups_status",
|
||||
title="UPS Status",
|
||||
icon="battery",
|
||||
)
|
||||
self._provider = UPSProvider()
|
||||
|
||||
def get_lines(self) -> List[str]:
|
||||
"""Get UPS status lines for display."""
|
||||
if not self._provider.is_available():
|
||||
return [
|
||||
"UPS not available",
|
||||
self._provider.get_last_error() or "Check I2C connection",
|
||||
]
|
||||
|
||||
# Force refresh to get fresh data
|
||||
self._provider.refresh()
|
||||
data = self._provider.get_data()
|
||||
|
||||
# Battery bar visualization [=====]
|
||||
battery_bar = self._get_battery_bar(data.capacity)
|
||||
|
||||
lines = [
|
||||
f"Bat: {data.capacity:.1f}% {battery_bar}",
|
||||
f"Voltage: {data.voltage:.2f}V",
|
||||
]
|
||||
|
||||
# Charging/power status
|
||||
if data.power_loss:
|
||||
lines.append("Power: BATTERY MODE")
|
||||
if data.capacity < 15:
|
||||
lines.append("!! CRITICAL !!")
|
||||
elif data.capacity < 25:
|
||||
lines.append("! LOW BATTERY !")
|
||||
else:
|
||||
status = "Charging" if data.is_charging else "Full"
|
||||
lines.append(f"Status: {status}")
|
||||
if data.input_voltage > 0:
|
||||
lines.append(f"Input: {data.input_voltage:.2f}V")
|
||||
|
||||
return lines
|
||||
|
||||
def _get_battery_bar(self, percent: float) -> str:
|
||||
"""
|
||||
Create ASCII battery bar.
|
||||
|
||||
Args:
|
||||
percent: Battery percentage (0-100)
|
||||
|
||||
Returns:
|
||||
Battery visualization string like "[==== ]"
|
||||
"""
|
||||
filled = int(percent / 16.67) # 6 segments
|
||||
empty = 6 - filled
|
||||
bar = "[" + "=" * filled + " " * empty + "]"
|
||||
return bar
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
"""Check if page should be shown."""
|
||||
return self.enabled and self._provider.is_available()
|
||||
|
||||
def get_provider(self) -> UPSProvider:
|
||||
"""Get the UPS provider instance."""
|
||||
return self._provider
|
||||
|
||||
def on_enter(self) -> None:
|
||||
"""Refresh data when page is shown."""
|
||||
self._provider.refresh()
|
||||
@@ -18,7 +18,7 @@ from .vehicle.poller import VehiclePoller
|
||||
from .flipper.server import FlipperServer
|
||||
from .flipper.pages import ActionID
|
||||
from .handlers import HandlerPipeline, StorageHandler, PostgreSQLHandler, OBD2Reading
|
||||
from .flipper.pages.ups_status import UPSStatusPage
|
||||
from .flipper.providers.ups_provider import UPSProvider
|
||||
|
||||
|
||||
class OBD2Client:
|
||||
@@ -138,12 +138,12 @@ class OBD2Client:
|
||||
# Add UPS status page if enabled
|
||||
if self.config.flipper.show_ups:
|
||||
try:
|
||||
ups_page = UPSStatusPage()
|
||||
if ups_page.is_enabled():
|
||||
pm.add_page(ups_page)
|
||||
self._logger.info("UPS status page added")
|
||||
ups_provider = UPSProvider()
|
||||
if ups_provider.is_available():
|
||||
pm.set_data_provider("ups", lambda: ups_provider.get_data())
|
||||
self._logger.info("UPS provider registered")
|
||||
except Exception as e:
|
||||
self._logger.debug(f"UPS page not available: {e}")
|
||||
self._logger.debug(f"UPS not available: {e}")
|
||||
|
||||
# Action handlers
|
||||
pm.set_action_handler(ActionID.RECONNECT_OBD, self._action_reconnect_obd)
|
||||
|
||||
Reference in New Issue
Block a user