diff --git a/.gitignore b/.gitignore index 0437344..fb7ac51 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,4 @@ can_edge.log* config.json .cursor/ +CLAUDE.md diff --git a/can_sniffer/src/config.py b/can_sniffer/src/config.py index 53dd93a..a408550 100644 --- a/can_sniffer/src/config.py +++ b/can_sniffer/src/config.py @@ -156,11 +156,34 @@ class LoggingConfig(BaseModel): ) +class FlipperConfig(BaseModel): + """Конфигурация Flipper Zero UART.""" + + model_config = {"extra": "ignore"} + + enabled: bool = Field( + default=False, + description="Включить отправку статистики на Flipper Zero" + ) + device: str = Field( + default="/dev/ttyAMA0", + description="UART устройство для подключения Flipper Zero" + ) + baudrate: int = Field( + default=115200, + description="Скорость UART (бод)" + ) + send_interval: float = Field( + default=1.0, + description="Интервал отправки статистики (секунды)" + ) + + class GeneralConfig(BaseModel): """Общие настройки.""" - + model_config = {"extra": "ignore"} - + buffer_size: int = Field( default=10000, description="Размер буфера для данных" @@ -196,6 +219,7 @@ class Config(BaseSettings): can: CanConfig = Field(default_factory=CanConfig) storage: StorageConfig = Field(default_factory=StorageConfig) postgresql: PostgreSQLConfig = Field(default_factory=PostgreSQLConfig) + flipper: FlipperConfig = Field(default_factory=FlipperConfig) logging: LoggingConfig = Field(default_factory=LoggingConfig) general: GeneralConfig = Field(default_factory=GeneralConfig) diff --git a/can_sniffer/src/handlers/__init__.py b/can_sniffer/src/handlers/__init__.py index 6c6b784..033a9d9 100644 --- a/can_sniffer/src/handlers/__init__.py +++ b/can_sniffer/src/handlers/__init__.py @@ -7,10 +7,12 @@ from .base import BaseHandler from .storage_handler import StorageHandler from .postgresql_handler import PostgreSQLHandler +from .flipper_handler import FlipperHandler __all__ = [ 'BaseHandler', 'StorageHandler', 'PostgreSQLHandler', + 'FlipperHandler', ] diff --git a/can_sniffer/src/handlers/flipper_handler.py b/can_sniffer/src/handlers/flipper_handler.py new file mode 100644 index 0000000..06eae2d --- /dev/null +++ b/can_sniffer/src/handlers/flipper_handler.py @@ -0,0 +1,293 @@ +""" +Flipper Zero UART Handler. + +Sends CAN sniffer statistics to Flipper Zero via UART. +Provides real-time monitoring on Flipper Zero display. +""" + +import socket +import threading +import time +from typing import Dict, Any, List, Optional + +from handlers.base import BaseHandler +from can_frame import CANFrame +from config import config +from logger import get_logger + +logger = get_logger(__name__) + + +def get_ip_address() -> str: + """ + Get the primary IP address of this device. + + Returns: + IP address string or "0.0.0.0" if unable to determine + """ + try: + # Create a socket to determine the outgoing IP + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(0.1) + # Connect to a public address (doesn't actually send data) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except Exception: + pass + + # Fallback: try to get any non-localhost IP + try: + hostname = socket.gethostname() + ip = socket.gethostbyname(hostname) + if ip and not ip.startswith("127."): + return ip + except Exception: + pass + + return "0.0.0.0" + + +class FlipperHandler(BaseHandler): + """ + Handler that sends statistics to Flipper Zero via UART. + + UART Configuration: + - Device: /dev/ttyAMA0 (or configured device) + - Baud: 115200 + - Format: 8N1 + + Protocol: + Sends text line: STATS:ip=,total=,pending=,processed=\n + """ + + def __init__(self, enabled: Optional[bool] = None): + """ + Initialize Flipper handler. + + Args: + enabled: Whether handler is enabled. If None, reads from config. + """ + # Check config for enabled status + if enabled is None: + enabled = getattr(config, "flipper", None) is not None + if enabled: + enabled = getattr(config.flipper, "enabled", False) + + super().__init__(name="flipper_handler", enabled=enabled) + + self.serial_port: Optional[Any] = None + self.device = "/dev/ttyAMA0" + self.baudrate = 115200 + self.send_interval = 1.0 # Send stats every 1 second + + # Load config if available + if hasattr(config, "flipper"): + flipper_cfg = config.flipper + self.device = getattr(flipper_cfg, "device", self.device) + self.baudrate = getattr(flipper_cfg, "baudrate", self.baudrate) + self.send_interval = getattr(flipper_cfg, "send_interval", self.send_interval) + + # Statistics + self._stats_lock = threading.Lock() + self._total_frames = 0 + self._pending_frames = 0 + self._processed_frames = 0 + self._sent_count = 0 + self._error_count = 0 + + # Background sender thread + self._sender_thread: Optional[threading.Thread] = None + self._running = False + + # IP address cache + self._ip_address = "0.0.0.0" + self._last_ip_check = 0 + + def initialize(self) -> bool: + """ + Initialize UART connection to Flipper Zero. + + Returns: + True if initialization successful + """ + try: + import serial + + self.serial_port = serial.Serial( + port=self.device, + baudrate=self.baudrate, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=0.1, + ) + + # Get initial IP address + self._ip_address = get_ip_address() + self._last_ip_check = time.time() + + self._initialized = True + self.logger.info( + f"Flipper handler initialized on {self.device} @ {self.baudrate} baud" + ) + return True + + except ImportError: + self.logger.error("pyserial not installed. Run: pip install pyserial") + return False + except Exception as e: + self.logger.error(f"Failed to initialize Flipper UART: {e}") + return False + + def start(self) -> None: + """Start the background sender thread.""" + if self._running: + return + + self._running = True + self._sender_thread = threading.Thread( + target=self._sender_loop, name="FlipperSender", daemon=True + ) + self._sender_thread.start() + self.logger.info("Flipper sender thread started") + + def _sender_loop(self) -> None: + """Background loop that sends stats periodically.""" + while self._running: + try: + self._send_stats() + except Exception as e: + self.logger.debug(f"Error sending stats to Flipper: {e}") + with self._stats_lock: + self._error_count += 1 + + time.sleep(self.send_interval) + + def _send_stats(self) -> None: + """Send current statistics to Flipper Zero.""" + if not self.serial_port or not self.serial_port.is_open: + return + + # Refresh IP address every 60 seconds + current_time = time.time() + if current_time - self._last_ip_check > 60: + self._ip_address = get_ip_address() + self._last_ip_check = current_time + + with self._stats_lock: + total = self._total_frames + pending = self._pending_frames + processed = self._processed_frames + + # Build stats message + message = f"STATS:ip={self._ip_address},total={total},pending={pending},processed={processed}\n" + + try: + self.serial_port.write(message.encode("utf-8")) + self.serial_port.flush() + + with self._stats_lock: + self._sent_count += 1 + + except Exception as e: + self.logger.debug(f"UART write error: {e}") + with self._stats_lock: + self._error_count += 1 + + def handle(self, frame: CANFrame) -> bool: + """ + Handle a single CAN frame. + + Updates frame counters for statistics. + + Args: + frame: CANFrame to handle + + Returns: + True (always succeeds, just updates counters) + """ + with self._stats_lock: + self._total_frames += 1 + self._pending_frames += 1 + return True + + def handle_batch(self, frames: List[CANFrame]) -> int: + """ + Handle a batch of CAN frames. + + Args: + frames: List of CANFrame objects + + Returns: + Number of frames processed (all of them) + """ + count = len(frames) + with self._stats_lock: + self._total_frames += count + # After batch processing, frames are processed + self._processed_frames += count + # Reduce pending by batch count + self._pending_frames = max(0, self._pending_frames - count) + return count + + def update_pending(self, pending_count: int) -> None: + """ + Update pending frame count. + + Called externally to sync with actual queue size. + + Args: + pending_count: Current number of pending frames + """ + with self._stats_lock: + self._pending_frames = pending_count + + def flush(self) -> None: + """Flush - send immediate stats update.""" + try: + self._send_stats() + except Exception as e: + self.logger.debug(f"Error in flush: {e}") + + def shutdown(self) -> None: + """Shutdown the handler.""" + self.logger.info("Shutting down Flipper handler...") + + self._running = False + + if self._sender_thread and self._sender_thread.is_alive(): + self._sender_thread.join(timeout=2.0) + + if self.serial_port and self.serial_port.is_open: + try: + # Send final "disconnected" message + self.serial_port.write(b"STATS:ip=---,total=0,pending=0,processed=0\n") + self.serial_port.flush() + self.serial_port.close() + except Exception as e: + self.logger.debug(f"Error closing serial port: {e}") + + self._initialized = False + self.logger.info("Flipper handler stopped") + + def get_stats(self) -> Dict[str, Any]: + """ + Get handler statistics. + + Returns: + Dictionary with handler stats + """ + with self._stats_lock: + return { + "total_frames": self._total_frames, + "pending_frames": self._pending_frames, + "processed_frames": self._processed_frames, + "sent_count": self._sent_count, + "error_count": self._error_count, + "device": self.device, + "baudrate": self.baudrate, + "connected": self.serial_port.is_open if self.serial_port else False, + "ip_address": self._ip_address, + } diff --git a/can_sniffer/src/socket_can/message_processor.py b/can_sniffer/src/socket_can/message_processor.py index 096a562..34c1fdb 100644 --- a/can_sniffer/src/socket_can/message_processor.py +++ b/can_sniffer/src/socket_can/message_processor.py @@ -14,7 +14,7 @@ from typing import Optional, Dict, Any, List from logger import get_logger from config import config from can_frame import CANFrame -from handlers import BaseHandler, StorageHandler, PostgreSQLHandler +from handlers import BaseHandler, StorageHandler, PostgreSQLHandler, FlipperHandler logger = get_logger(__name__) @@ -87,18 +87,21 @@ class MessageProcessor: def _create_default_handlers(self) -> List[BaseHandler]: """ Создание обработчиков по умолчанию из конфигурации. - + Returns: Список обработчиков """ handlers = [] - + # Storage handler всегда включен handlers.append(StorageHandler(enabled=True)) - + # PostgreSQL handler зависит от конфигурации handlers.append(PostgreSQLHandler(enabled=None)) # None = из config - + + # Flipper Zero handler зависит от конфигурации + handlers.append(FlipperHandler(enabled=None)) # None = из config + return handlers def _init_handlers(self, handlers: List[BaseHandler]) -> None: @@ -347,9 +350,9 @@ class MessageProcessor: self.running = True - # Запускаем специальные обработчики (например, PostgreSQL forwarder) + # Запускаем специальные обработчики (например, PostgreSQL forwarder, Flipper sender) for handler in self.handlers: - if isinstance(handler, PostgreSQLHandler) and handler.is_initialized(): + if isinstance(handler, (PostgreSQLHandler, FlipperHandler)) and handler.is_initialized(): try: handler.start() except Exception as e: diff --git a/flip_monitor/README.md b/flip_monitor/README.md new file mode 100644 index 0000000..c935517 --- /dev/null +++ b/flip_monitor/README.md @@ -0,0 +1,170 @@ +# CAN Monitor for Flipper Zero + +Flipper Zero application for monitoring CAN sniffer statistics from Raspberry Pi 5 via UART. + +## Features + +- Real-time display of CAN sniffer statistics +- Shows: IP address, total frames, pending frames, processed frames +- Connection status indicator +- Compatible with Unleashed firmware (0.84e and later) + +## Hardware Connection + +### Wiring (RPI5 <-> Flipper Zero) + +``` +RPI5 GPIO Flipper Zero GPIO +----------- ----------------- +TX (GPIO 14) --> RX (Pin 14) +RX (GPIO 15) <-- TX (Pin 13) +GND --> GND +``` + +**Note:** Cross TX/RX connections (RPI TX -> Flipper RX, RPI RX -> Flipper TX) + +### Flipper Zero GPIO Pinout + +``` +Pin 13 = TX (USART) +Pin 14 = RX (USART) +Pin 8/11/18 = GND +``` + +### RPI5 GPIO (using /dev/ttyAMA0) + +``` +GPIO 14 = TX (Pin 8) +GPIO 15 = RX (Pin 10) +GND = Pin 6, 9, 14, 20, 25, 30, 34, 39 +``` + +## Building the Application + +### Prerequisites + +1. Clone Flipper Zero firmware: +```bash +git clone --recursive https://github.com/DarkFlippers/unleashed-firmware.git +cd unleashed-firmware +``` + +2. Copy the `flip_monitor` folder to `applications_user/`: +```bash +cp -r /path/to/carpibord/flip_monitor applications_user/can_monitor +``` + +3. Create icon (10x10 PNG, 1-bit): +```bash +# Create icons/can_monitor.png (10x10 pixels, black & white) +# You can use any image editor or online tool +``` + +### Build + +```bash +# Build the FAP +./fbt fap_can_monitor + +# Or build all external apps +./fbt fap_dist +``` + +The compiled `.fap` file will be in `build/f7-firmware-D/.extapps/can_monitor.fap` + +### Install + +Copy the `.fap` file to your Flipper Zero SD card: +``` +SD Card/apps/GPIO/can_monitor.fap +``` + +## RPI5 Configuration + +### 1. Enable UART + +Add to `/boot/config.txt`: +``` +enable_uart=1 +dtoverlay=uart0 +``` + +Reboot after changes. + +### 2. Install pyserial + +```bash +pip install pyserial +``` + +### 3. Configure CAN Sniffer + +Add to `can_sniffer/config.json`: +```json +{ + "flipper": { + "enabled": true, + "device": "/dev/ttyAMA0", + "baudrate": 115200, + "send_interval": 1.0 + } +} +``` + +Or use environment variables: +```bash +export CAN_SNIFFER_FLIPPER__ENABLED=true +export CAN_SNIFFER_FLIPPER__DEVICE=/dev/ttyAMA0 +``` + +### 4. Run CAN Sniffer + +```bash +cd can_sniffer/src +python main.py +``` + +## Protocol + +The RPI5 sends text-based statistics over UART: + +``` +STATS:ip=192.168.1.100,total=12345,pending=100,processed=12245\n +``` + +Fields: +- `ip` - RPI5 IP address +- `total` - Total CAN frames received +- `pending` - Frames in processing queue +- `processed` - Successfully processed frames + +## Troubleshooting + +### No connection + +1. Check wiring (TX/RX crossed correctly) +2. Verify UART is enabled on RPI5: `ls -la /dev/ttyAMA0` +3. Check config: `flipper.enabled = true` +4. Test UART manually: +```bash +# On RPI5 +echo "STATS:ip=test,total=1,pending=0,processed=1" > /dev/ttyAMA0 +``` + +### Permission denied on /dev/ttyAMA0 + +Add user to dialout group: +```bash +sudo usermod -a -G dialout $USER +# Then logout and login again +``` + +### Flipper shows "Waiting..." + +- Stats are sent every 1 second (configurable) +- Connection timeout is 5 seconds +- Check if CAN sniffer is running + +## License + +MIT License diff --git a/flip_monitor/application.fam b/flip_monitor/application.fam new file mode 100644 index 0000000..03f0e1a --- /dev/null +++ b/flip_monitor/application.fam @@ -0,0 +1,11 @@ +App( + appid="can_monitor", + name="CAN Monitor", + apptype=FlipperAppType.EXTERNAL, + entry_point="can_monitor_app", + stack_size=2 * 1024, + fap_category="GPIO", + fap_author="carpibord", + fap_version="1.0", + fap_description="CAN Sniffer monitor via UART from RPI5", +) diff --git a/flip_monitor/can_monitor.c b/flip_monitor/can_monitor.c new file mode 100644 index 0000000..13341ac --- /dev/null +++ b/flip_monitor/can_monitor.c @@ -0,0 +1,327 @@ +/** + * CAN Monitor for Flipper Zero + * + * Receives CAN sniffer statistics from RPI5 via UART. + * Displays: IP address, total frames, pending frames, processed frames. + * + * UART Configuration: + * - TX: GPIO 13 (pin 13) + * - RX: GPIO 14 (pin 14) + * - Baud: 115200 + * - 8N1 + * + * Protocol: Text-based + * Format: STATS:ip=,total=,pending=,processed=\n + * Example: STATS:ip=192.168.1.100,total=12345,pending=100,processed=12245\n + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#define TAG "CANMonitor" + +// UART configuration +#define UART_CH FuriHalSerialIdUsart +#define UART_BAUD 115200 + +// Buffer sizes +#define RX_BUFFER_SIZE 256 +#define IP_BUFFER_SIZE 32 + +// Statistics structure +typedef struct { + char ip_address[IP_BUFFER_SIZE]; + uint32_t total_frames; + uint32_t pending_frames; + uint32_t processed_frames; + bool connected; + uint32_t last_update_tick; +} CanStats; + +// Application context +typedef struct { + Gui* gui; + ViewPort* view_port; + FuriMessageQueue* event_queue; + FuriMutex* mutex; + + FuriHalSerialHandle* serial_handle; + FuriStreamBuffer* rx_stream; + FuriThread* rx_thread; + + CanStats stats; + bool running; +} CanMonitorApp; + +// Event types +typedef enum { + EventTypeKey, + EventTypeStats, +} EventType; + +typedef struct { + EventType type; + InputEvent input; +} CanMonitorEvent; + +// Parse statistics from received line +static bool parse_stats_line(const char* line, CanStats* stats) { + // Format: STATS:ip=192.168.1.100,total=12345,pending=100,processed=12245 + if(strncmp(line, "STATS:", 6) != 0) { + return false; + } + + const char* data = line + 6; + + // Parse ip= + const char* ip_start = strstr(data, "ip="); + if(ip_start) { + ip_start += 3; + const char* ip_end = strchr(ip_start, ','); + if(ip_end) { + size_t len = ip_end - ip_start; + if(len < IP_BUFFER_SIZE) { + strncpy(stats->ip_address, ip_start, len); + stats->ip_address[len] = '\0'; + } + } + } + + // Parse total= + const char* total_start = strstr(data, "total="); + if(total_start) { + stats->total_frames = strtoul(total_start + 6, NULL, 10); + } + + // Parse pending= + const char* pending_start = strstr(data, "pending="); + if(pending_start) { + stats->pending_frames = strtoul(pending_start + 8, NULL, 10); + } + + // Parse processed= + const char* processed_start = strstr(data, "processed="); + if(processed_start) { + stats->processed_frames = strtoul(processed_start + 10, NULL, 10); + } + + stats->connected = true; + stats->last_update_tick = furi_get_tick(); + + return true; +} + +// UART receive callback +static void uart_rx_callback( + FuriHalSerialHandle* handle, + FuriHalSerialRxEvent event, + void* context) { + CanMonitorApp* app = context; + UNUSED(handle); + + if(event == FuriHalSerialRxEventData) { + uint8_t data = furi_hal_serial_async_rx(handle); + furi_stream_buffer_send(app->rx_stream, &data, 1, 0); + } +} + +// UART receive thread +static int32_t uart_rx_thread(void* context) { + CanMonitorApp* app = context; + char rx_buffer[RX_BUFFER_SIZE]; + size_t rx_index = 0; + + while(app->running) { + uint8_t data; + size_t len = furi_stream_buffer_receive(app->rx_stream, &data, 1, 100); + + if(len > 0) { + if(data == '\n' || data == '\r') { + if(rx_index > 0) { + rx_buffer[rx_index] = '\0'; + + // Parse the received line + furi_mutex_acquire(app->mutex, FuriWaitForever); + if(parse_stats_line(rx_buffer, &app->stats)) { + // Notify view to redraw + view_port_update(app->view_port); + } + furi_mutex_release(app->mutex); + + rx_index = 0; + } + } else if(rx_index < RX_BUFFER_SIZE - 1) { + rx_buffer[rx_index++] = data; + } + } + + // Check connection timeout (5 seconds) + furi_mutex_acquire(app->mutex, FuriWaitForever); + if(app->stats.connected && + (furi_get_tick() - app->stats.last_update_tick) > 5000) { + app->stats.connected = false; + view_port_update(app->view_port); + } + furi_mutex_release(app->mutex); + } + + return 0; +} + +// Draw callback +static void draw_callback(Canvas* canvas, void* context) { + CanMonitorApp* app = context; + + furi_mutex_acquire(app->mutex, FuriWaitForever); + + canvas_clear(canvas); + canvas_set_font(canvas, FontPrimary); + + // Title + canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, "CAN Monitor"); + + canvas_set_font(canvas, FontSecondary); + + // Connection status + if(app->stats.connected) { + canvas_draw_str(canvas, 0, 14, "Status: Connected"); + } else { + canvas_draw_str(canvas, 0, 14, "Status: Waiting..."); + } + + // IP Address + char buf[64]; + if(strlen(app->stats.ip_address) > 0) { + snprintf(buf, sizeof(buf), "IP: %s", app->stats.ip_address); + } else { + snprintf(buf, sizeof(buf), "IP: ---"); + } + canvas_draw_str(canvas, 0, 26, buf); + + // Total frames + snprintf(buf, sizeof(buf), "Total: %lu", (unsigned long)app->stats.total_frames); + canvas_draw_str(canvas, 0, 38, buf); + + // Pending frames + snprintf(buf, sizeof(buf), "Pending: %lu", (unsigned long)app->stats.pending_frames); + canvas_draw_str(canvas, 0, 50, buf); + + // Processed frames + snprintf(buf, sizeof(buf), "Processed: %lu", (unsigned long)app->stats.processed_frames); + canvas_draw_str(canvas, 0, 62, buf); + + furi_mutex_release(app->mutex); +} + +// Input callback +static void input_callback(InputEvent* input_event, void* context) { + CanMonitorApp* app = context; + CanMonitorEvent event = {.type = EventTypeKey, .input = *input_event}; + furi_message_queue_put(app->event_queue, &event, FuriWaitForever); +} + +// Initialize application +static CanMonitorApp* can_monitor_app_alloc(void) { + CanMonitorApp* app = malloc(sizeof(CanMonitorApp)); + memset(app, 0, sizeof(CanMonitorApp)); + + app->running = true; + app->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + app->event_queue = furi_message_queue_alloc(8, sizeof(CanMonitorEvent)); + + // Initialize stats + memset(&app->stats, 0, sizeof(CanStats)); + strcpy(app->stats.ip_address, ""); + + // Disable expansion protocol to use UART + Expansion* expansion = furi_record_open(RECORD_EXPANSION); + expansion_disable(expansion); + furi_record_close(RECORD_EXPANSION); + + // Initialize UART + app->rx_stream = furi_stream_buffer_alloc(RX_BUFFER_SIZE, 1); + app->serial_handle = furi_hal_serial_control_acquire(UART_CH); + furi_hal_serial_init(app->serial_handle, UART_BAUD); + furi_hal_serial_async_rx_start(app->serial_handle, uart_rx_callback, app, false); + + // Start RX thread + app->rx_thread = furi_thread_alloc_ex("CANMonitorRx", 1024, uart_rx_thread, app); + furi_thread_start(app->rx_thread); + + // Initialize GUI + app->gui = furi_record_open(RECORD_GUI); + app->view_port = view_port_alloc(); + view_port_draw_callback_set(app->view_port, draw_callback, app); + view_port_input_callback_set(app->view_port, input_callback, app); + gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); + + return app; +} + +// Free application +static void can_monitor_app_free(CanMonitorApp* app) { + // Stop RX thread + app->running = false; + furi_thread_join(app->rx_thread); + furi_thread_free(app->rx_thread); + + // Deinitialize UART + furi_hal_serial_async_rx_stop(app->serial_handle); + furi_hal_serial_deinit(app->serial_handle); + furi_hal_serial_control_release(app->serial_handle); + furi_stream_buffer_free(app->rx_stream); + + // Re-enable expansion protocol + Expansion* expansion = furi_record_open(RECORD_EXPANSION); + expansion_enable(expansion); + furi_record_close(RECORD_EXPANSION); + + // Free GUI + view_port_enabled_set(app->view_port, false); + gui_remove_view_port(app->gui, app->view_port); + view_port_free(app->view_port); + furi_record_close(RECORD_GUI); + + furi_message_queue_free(app->event_queue); + furi_mutex_free(app->mutex); + + free(app); +} + +// Main application entry point +int32_t can_monitor_app(void* p) { + UNUSED(p); + + CanMonitorApp* app = can_monitor_app_alloc(); + + CanMonitorEvent event; + bool running = true; + + // Main event loop + while(running) { + if(furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) { + if(event.type == EventTypeKey) { + if(event.input.type == InputTypeShort || + event.input.type == InputTypeLong) { + if(event.input.key == InputKeyBack) { + running = false; + } + } + } + } + } + + can_monitor_app_free(app); + + return 0; +}