/** * FlipperDroid — main application entry point * Handles GUI, input, lifecycle. Workers in fd_bridge.c. */ #include "fd_app.h" #include #include #define TAG "FlipperDroid" /* ---- GPIO pin table ---- */ static const struct { uint8_t id; const GpioPin* pin; const char* name; } gpio_defs[] = { {2, &gpio_ext_pa2, "A2"}, {3, &gpio_ext_pa3, "A3"}, {4, &gpio_ext_pa4, "A4"}, {6, &gpio_ext_pa6, "A6"}, {7, &gpio_ext_pa7, "A7"}, {13, &gpio_ext_pb2, "B2"}, {14, &gpio_ext_pb3, "B3"}, {15, &gpio_ext_pb13, "B13"}, {16, &gpio_ext_pb14, "B14"}, {17, &gpio_ext_pc0, "C0"}, {18, &gpio_ext_pc1, "C1"}, {19, &gpio_ext_pc3, "C3"}, }; #define GPIO_DEF_COUNT (sizeof(gpio_defs) / sizeof(gpio_defs[0])) /* ---- Logging ---- */ void fd_log(FdApp* app, const char* fmt, ...) { va_list args; va_start(args, fmt); char buf[FD_LOG_COLS + 1]; vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); furi_mutex_acquire(app->mutex, FuriWaitForever); uint8_t idx = (app->log.head + app->log.count) % FD_LOG_LINES; if(app->log.count < FD_LOG_LINES) { app->log.count++; } else { app->log.head = (app->log.head + 1) % FD_LOG_LINES; } strncpy(app->log.lines[idx], buf, FD_LOG_COLS); app->log.lines[idx][FD_LOG_COLS] = '\0'; furi_mutex_release(app->mutex); FURI_LOG_I(TAG, "%s", buf); } /* ---- Draw ---- */ void fd_draw_callback(Canvas* canvas, void* ctx) { FdApp* app = (FdApp*)ctx; furi_mutex_acquire(app->mutex, FuriWaitForever); canvas_clear(canvas); canvas_set_font(canvas, FontPrimary); switch(app->current_view) { case FdViewStatus: { /* Title bar */ canvas_draw_str(canvas, 0, 10, "FlipperDroid"); canvas_set_font(canvas, FontSecondary); /* Connection status */ if(app->connected) { canvas_draw_str(canvas, 72, 10, "[LINKED]"); } else { canvas_draw_str(canvas, 68, 10, "[WAITING]"); } /* Stats */ char buf[40]; canvas_draw_str(canvas, 0, 22, "Status:"); canvas_draw_str(canvas, 44, 22, app->connected ? "Connected" : "Listening on USB"); snprintf(buf, sizeof(buf), "RX: %lu TX: %lu ERR: %lu", app->rx_count, app->tx_count, app->err_count); canvas_draw_str(canvas, 0, 32, buf); /* Subsystem status */ snprintf(buf, sizeof(buf), "SubGHz: %s %lu Hz", app->subghz_rx_active ? "RX" : "idle", app->subghz_freq); canvas_draw_str(canvas, 0, 42, buf); snprintf(buf, sizeof(buf), "IR: %s GPIO: %u pins", app->ir_rx_active ? "RX" : "idle", app->gpio_count); canvas_draw_str(canvas, 0, 52, buf); /* Nav hint */ canvas_draw_str(canvas, 0, 63, " OK:Log Bk:Exit"); break; } case FdViewGpio: { canvas_draw_str(canvas, 0, 10, "GPIO Pins"); canvas_set_font(canvas, FontSecondary); for(uint8_t i = 0; i < app->gpio_count && i < 12; i++) { uint8_t row = i / 4; uint8_t col = i % 4; uint8_t x = col * 32; uint8_t y = 20 + row * 14; char buf[12]; FdGpioEntry* g = &app->gpio[i]; if(i == app->selected_gpio) { canvas_draw_box(canvas, x, y - 8, 30, 12); canvas_set_color(canvas, ColorWhite); } if(g->initialized) { snprintf(buf, sizeof(buf), "%s:%c", g->name, g->value ? 'H' : 'L'); } else { snprintf(buf, sizeof(buf), "%s:--", g->name); } canvas_draw_str(canvas, x + 1, y, buf); canvas_set_color(canvas, ColorBlack); } canvas_draw_str(canvas, 0, 63, "SG"); break; } case FdViewSubghz: { canvas_draw_str(canvas, 0, 10, "Sub-GHz"); canvas_set_font(canvas, FontSecondary); char buf[40]; snprintf(buf, sizeof(buf), "Freq: %lu Hz", app->subghz_freq); canvas_draw_str(canvas, 0, 22, buf); snprintf(buf, sizeof(buf), "RSSI: %.1f dBm", (double)app->subghz_last_rssi); canvas_draw_str(canvas, 0, 32, buf); canvas_draw_str(canvas, 0, 42, app->subghz_rx_active ? "State: RECEIVING" : "State: IDLE"); snprintf(buf, sizeof(buf), "Packets: %lu", app->rx_count); canvas_draw_str(canvas, 0, 52, buf); canvas_draw_str(canvas, 0, 63, "Log"); break; } case FdViewLog: { canvas_draw_str(canvas, 0, 10, "Log"); canvas_set_font(canvas, FontKeyboard); for(uint8_t i = 0; i < app->log.count && i < 6; i++) { uint8_t idx = (app->log.head + i) % FD_LOG_LINES; canvas_draw_str(canvas, 0, 20 + i * 8, app->log.lines[idx]); } canvas_set_font(canvas, FontSecondary); canvas_draw_str(canvas, 0, 63, "mutex); } /* ---- Input ---- */ void fd_input_callback(InputEvent* event, void* ctx) { FdApp* app = (FdApp*)ctx; furi_message_queue_put(app->input_queue, event, FuriWaitForever); } /* ---- Alloc / Free ---- */ FdApp* fd_app_alloc(void) { FdApp* app = malloc(sizeof(FdApp)); memset(app, 0, sizeof(FdApp)); app->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); app->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); app->gui = furi_record_open(RECORD_GUI); app->notifications = furi_record_open(RECORD_NOTIFICATION); app->current_view = FdViewStatus; app->running = true; /* Init GPIO table */ app->gpio_count = GPIO_DEF_COUNT; for(uint8_t i = 0; i < GPIO_DEF_COUNT; i++) { app->gpio[i].id = gpio_defs[i].id; app->gpio[i].pin = gpio_defs[i].pin; app->gpio[i].name = gpio_defs[i].name; app->gpio[i].initialized = false; app->gpio[i].output = false; app->gpio[i].value = false; } /* Create viewport */ app->view_port = view_port_alloc(); view_port_draw_callback_set(app->view_port, fd_draw_callback, app); view_port_input_callback_set(app->view_port, fd_input_callback, app); gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); return app; } void fd_app_free(FdApp* app) { /* Stop workers */ app->running = false; if(app->rx_thread) { furi_thread_join(app->rx_thread); furi_thread_free(app->rx_thread); } if(app->event_thread) { furi_thread_join(app->event_thread); furi_thread_free(app->event_thread); } /* Cleanup SubGHz */ if(app->subghz_rx_active) { furi_hal_subghz_idle(); app->subghz_rx_active = false; } /* Reset GPIO pins to analog (safe default) */ for(uint8_t i = 0; i < app->gpio_count; i++) { if(app->gpio[i].initialized) { furi_hal_gpio_init(app->gpio[i].pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow); } } /* Remove 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_message_queue_free(app->input_queue); furi_mutex_free(app->mutex); furi_record_close(RECORD_NOTIFICATION); furi_record_close(RECORD_GUI); free(app); } /* ---- Main loop ---- */ int32_t fd_app_run(FdApp* app) { fd_log(app, "Bridge starting..."); /* Launch RX worker — listens for commands on USB CDC */ app->rx_thread = furi_thread_alloc_ex("FdRx", 4096, fd_rx_worker, app); furi_thread_start(app->rx_thread); /* Launch event worker — pushes async events to phone */ app->event_thread = furi_thread_alloc_ex("FdEvt", 2048, fd_event_worker, app); furi_thread_start(app->event_thread); fd_log(app, "Listening on USB CDC..."); notification_message(app->notifications, &sequence_blink_start_blue); /* Input loop */ InputEvent event; while(app->running) { if(furi_message_queue_get(app->input_queue, &event, 100) == FuriStatusOk) { if(event.type != InputTypePress && event.type != InputTypeRepeat) continue; switch(event.key) { case InputKeyBack: app->running = false; break; case InputKeyRight: if(app->current_view == FdViewStatus) app->current_view = FdViewSubghz; else if(app->current_view == FdViewGpio) app->current_view = FdViewSubghz; else if(app->current_view == FdViewSubghz) app->current_view = FdViewLog; break; case InputKeyLeft: if(app->current_view == FdViewLog) app->current_view = FdViewSubghz; else if(app->current_view == FdViewSubghz) app->current_view = FdViewGpio; else if(app->current_view == FdViewGpio) app->current_view = FdViewStatus; break; case InputKeyOk: if(app->current_view == FdViewStatus) { app->current_view = FdViewLog; } else if(app->current_view == FdViewGpio) { /* Read selected GPIO pin */ uint8_t sel = app->selected_gpio; if(sel < app->gpio_count) { if(!app->gpio[sel].initialized) { furi_hal_gpio_init( app->gpio[sel].pin, GpioModeInput, GpioPullNo, GpioSpeedLow); app->gpio[sel].initialized = true; } app->gpio[sel].value = furi_hal_gpio_read(app->gpio[sel].pin); fd_log(app, "GPIO %s = %s", app->gpio[sel].name, app->gpio[sel].value ? "HIGH" : "LOW"); } } else if(app->current_view == FdViewSubghz) { /* Toggle SubGHz RX */ if(app->subghz_rx_active) { furi_hal_subghz_idle(); app->subghz_rx_active = false; fd_log(app, "SubGHz RX stopped"); } else if(app->subghz_freq > 0) { furi_hal_subghz_idle(); furi_hal_subghz_set_frequency(app->subghz_freq); furi_hal_subghz_rx(); app->subghz_rx_active = true; fd_log(app, "SubGHz RX on %lu Hz", app->subghz_freq); } } break; case InputKeyUp: if(app->current_view == FdViewGpio && app->selected_gpio > 0) app->selected_gpio--; break; case InputKeyDown: if(app->current_view == FdViewGpio && app->selected_gpio < app->gpio_count - 1) app->selected_gpio++; break; default: break; } view_port_update(app->view_port); } /* Periodic screen refresh */ view_port_update(app->view_port); } notification_message(app->notifications, &sequence_blink_stop); fd_log(app, "Bridge stopped"); return 0; } /* ---- Entry point ---- */ int32_t flipperdroid_app(void* p) { UNUSED(p); FdApp* app = fd_app_alloc(); fd_app_run(app); fd_app_free(app); return 0; }