From a995b9e79a34ba3c59a893e15820793615f9a426 Mon Sep 17 00:00:00 2001 From: Alexander Poletaev Date: Fri, 30 Jan 2026 00:23:40 +0300 Subject: [PATCH] add scroll up/down flipper zero --- obd2_client/src/flipper/pages.py | 23 ++++++++++++++++++++--- obd2_client/src/flipper/server.py | 11 ++++++++++- obd2_client/src/main.py | 25 ++++++++++++++++++------- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/obd2_client/src/flipper/pages.py b/obd2_client/src/flipper/pages.py index b76f7ed..8b3de4f 100644 --- a/obd2_client/src/flipper/pages.py +++ b/obd2_client/src/flipper/pages.py @@ -5,9 +5,12 @@ from dataclasses import dataclass from enum import Enum import socket import os +import logging from .protocol import Page, PageType +_logger = logging.getLogger("obd2_client.pages") + class ActionID(Enum): """Action identifiers for menu items.""" @@ -31,7 +34,7 @@ class PageDefinition: class PageManager: """Manages page content and navigation.""" - MAX_VISIBLE_LINES = 5 # Flipper display limit + MAX_VISIBLE_LINES = 4 # Flipper display limit (5 lines overlap with footer) def __init__(self): self._pages: List[PageDefinition] = [] @@ -170,16 +173,21 @@ class PageManager: True if scroll occurred """ if self._current_index not in range(len(self._pages)): + _logger.debug(f"scroll_up: invalid page index {self._current_index}") return False page_def = self._pages[self._current_index] if page_def.page_type != PageType.INFO: + _logger.debug(f"scroll_up: page type is {page_def.page_type}, not INFO") return False current_offset = self._scroll_offsets.get(self._current_index, 0) + _logger.debug(f"scroll_up: current_offset={current_offset}") if current_offset > 0: self._scroll_offsets[self._current_index] = current_offset - 1 + _logger.debug(f"scroll_up: new offset={current_offset - 1}") return True + _logger.debug("scroll_up: already at top") return False def scroll_down(self) -> bool: @@ -189,20 +197,27 @@ class PageManager: True if scroll occurred """ if self._current_index not in range(len(self._pages)): + _logger.debug(f"scroll_down: invalid page index {self._current_index}") return False page_def = self._pages[self._current_index] if page_def.page_type != PageType.INFO: + _logger.debug(f"scroll_down: page type is {page_def.page_type}, not INFO") return False # Get full page content to know total lines page = page_def.generator(self) - max_offset = max(0, len(page.lines) - self.MAX_VISIBLE_LINES) + total_lines = len(page.lines) + max_offset = max(0, total_lines - self.MAX_VISIBLE_LINES) current_offset = self._scroll_offsets.get(self._current_index, 0) + _logger.debug(f"scroll_down: total_lines={total_lines}, max_offset={max_offset}, current_offset={current_offset}") + if current_offset < max_offset: self._scroll_offsets[self._current_index] = current_offset + 1 + _logger.debug(f"scroll_down: new offset={current_offset + 1}") return True + _logger.debug("scroll_down: already at bottom") return False def navigate_next(self) -> bool: @@ -318,6 +333,7 @@ class PageManager: if state: # Get all values - returns dict of PID -> PIDValue all_values = state.get_all() + _logger.debug(f"Live data: got {len(all_values)} PIDs from state") if all_values: # Priority order for display @@ -339,8 +355,9 @@ class PageManager: if not lines: lines = ["Waiting for data...", "", "Polling active"] else: - lines = ["No connection", "to OBD2", "", "Use Actions menu", "to reconnect"] + lines = ["No OBD2 connection", "", "Use Actions menu", "to reconnect"] + _logger.debug(f"Live data page: {len(lines)} lines") return Page(PageType.INFO, "Live Data", lines) def _generate_stats_page(self, mgr: "PageManager") -> Page: diff --git a/obd2_client/src/flipper/server.py b/obd2_client/src/flipper/server.py index 376de71..e88710e 100644 --- a/obd2_client/src/flipper/server.py +++ b/obd2_client/src/flipper/server.py @@ -199,12 +199,13 @@ class FlipperServer: def _handle_line(self, line: str) -> None: """Handle received line.""" + self._logger.debug(f"RX raw: '{line.strip()}'") cmd = FlipperProtocol.parse_command(line) if cmd is None: self._logger.debug(f"Unknown command: {line.strip()}") return - self._logger.debug(f"Received: {cmd.cmd_type.name}") + self._logger.info(f"CMD: {cmd.cmd_type.name}" + (f" param={cmd.param}" if cmd.param else "")) # INIT can be received at any time (reconnection support) if cmd.cmd_type == CommandType.INIT: @@ -310,13 +311,21 @@ class FlipperServer: def _handle_scroll_up(self) -> None: """Handle scroll up on info pages.""" + self._logger.debug(f"Scroll UP requested, page {self._page_manager.current_index}") if self._page_manager.scroll_up(): + self._logger.debug(f"Scroll UP success, offset now {self._page_manager._scroll_offsets.get(self._page_manager.current_index, 0)}") self._send_current_page() + else: + self._logger.debug("Scroll UP - already at top") def _handle_scroll_down(self) -> None: """Handle scroll down on info pages.""" + self._logger.debug(f"Scroll DOWN requested, page {self._page_manager.current_index}") if self._page_manager.scroll_down(): + self._logger.debug(f"Scroll DOWN success, offset now {self._page_manager._scroll_offsets.get(self._page_manager.current_index, 0)}") self._send_current_page() + else: + self._logger.debug("Scroll DOWN - already at bottom") def _handle_refresh(self) -> None: """Handle refresh request.""" diff --git a/obd2_client/src/main.py b/obd2_client/src/main.py index 0a417ef..f9c2a72 100644 --- a/obd2_client/src/main.py +++ b/obd2_client/src/main.py @@ -179,24 +179,29 @@ class OBD2Client: if monitor: self._monitor_loop() + else: + # Keep running without monitor display + self._logger.info("Running without monitor (use Ctrl+C to stop)") + while self._running: + time.sleep(1.0) except KeyboardInterrupt: pass finally: self._shutdown() - def _monitor_loop(self) -> None: + def _monitor_loop(self, clear_screen: bool = True) -> None: """Display live values in console.""" print("\n" + "=" * 50) print(" OBD2 Live Monitor (Ctrl+C to stop)") print("=" * 50 + "\n") while self._running: - self._clear_screen() - self._print_header() - self._print_values() - self._print_stats() - + if clear_screen: + self._clear_screen() + self._print_header() + self._print_values() + self._print_stats() time.sleep(0.5) def _clear_screen(self) -> None: @@ -314,6 +319,12 @@ def main(): help="Enable Flipper Zero server on specified serial port (e.g., /dev/serial0)", ) + parser.add_argument( + "--no-monitor", + action="store_true", + help="Disable live monitor display (useful with --debug)", + ) + parser.add_argument( "--debug", action="store_true", @@ -334,7 +345,7 @@ def main(): config.can.virtual = True client = OBD2Client(config, flipper_port=args.flipper) - client.run(scan_only=args.scan_only) + client.run(scan_only=args.scan_only, monitor=not args.no_monitor) if __name__ == "__main__":