Initial commit — FlipperDroid v0.1.0-poc

KernelSU module + Flipper Zero FAP that bridges both devices into a
unified pentesting platform over USB CDC serial / BT rfcomm.

Android side: bridge daemon, WebUI (:8089), bind mount namespace
isolation stealth engine. Flipper side: proper FAP with 4-view GUI,
GPIO/SubGHz/IR/file command handlers, async event streaming.
This commit is contained in:
sssnake
2026-03-31 21:26:58 -07:00
commit be81a92d44
22 changed files with 4191 additions and 0 deletions

19
flipper/application.fam Normal file
View File

@@ -0,0 +1,19 @@
App(
appid="flipperdroid_bridge",
name="FlipperDroid",
apptype=FlipperAppType.EXTERNAL,
entry_point="flipperdroid_app",
requires=[
"gui",
"notification",
"storage",
],
stack_size=8 * 1024,
order=10,
fap_icon="assets/flipperdroid_10px.png",
fap_category="USB",
fap_icon_assets="assets",
fap_description="FlipperDroid Bridge — Fuse Flipper Zero with Android over USB. GPIO, SubGHz, IR, NFC, RFID, file transfer, CPU offload.",
fap_author="snake",
fap_version="0.1.0",
)

114
flipper/fd_app.h Normal file
View File

@@ -0,0 +1,114 @@
/**
* FlipperDroid — application state
*/
#pragma once
#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <gui/view_port.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <storage/storage.h>
#include "fd_protocol.h"
/* USB CDC channel — channel 0 is Flipper CLI, we use channel 1 */
#define FD_CDC_CHANNEL 0
/* App views */
typedef enum {
FdViewStatus,
FdViewGpio,
FdViewSubghz,
FdViewLog,
} FdView;
/* GPIO pin entry */
typedef struct {
uint8_t id;
const GpioPin* pin;
const char* name;
bool initialized;
bool output;
bool value;
} FdGpioEntry;
/* Ring buffer for log messages */
#define FD_LOG_LINES 8
#define FD_LOG_COLS 30
typedef struct {
char lines[FD_LOG_LINES][FD_LOG_COLS + 1];
uint8_t head;
uint8_t count;
} FdLogBuffer;
/* Main application state */
typedef struct {
/* Furi */
Gui* gui;
ViewPort* view_port;
FuriMessageQueue* input_queue;
NotificationApp* notifications;
FuriMutex* mutex;
/* Threads */
FuriThread* rx_thread;
FuriThread* event_thread;
bool running;
/* Connection */
bool connected;
uint32_t rx_count;
uint32_t tx_count;
uint32_t err_count;
uint32_t connect_tick;
/* View */
FdView current_view;
uint8_t selected_gpio;
/* SubGHz */
bool subghz_rx_active;
uint32_t subghz_freq;
float subghz_last_rssi;
/* IR */
bool ir_rx_active;
/* GPIO */
FdGpioEntry gpio[12];
uint8_t gpio_count;
/* Log */
FdLogBuffer log;
} FdApp;
/* Lifecycle */
FdApp* fd_app_alloc(void);
void fd_app_free(FdApp* app);
int32_t fd_app_run(FdApp* app);
/* GUI callbacks */
void fd_draw_callback(Canvas* canvas, void* ctx);
void fd_input_callback(InputEvent* event, void* ctx);
/* Log */
void fd_log(FdApp* app, const char* fmt, ...);
/* USB I/O */
void fd_usb_tx(const uint8_t* data, uint16_t len);
uint16_t fd_usb_rx(uint8_t* buf, uint16_t max_len, uint32_t timeout_ms);
/* Frame TX helpers */
void fd_send_ok(FdApp* app, const uint8_t* data, uint16_t len);
void fd_send_err(FdApp* app, FdError err, const char* msg);
void fd_send_event(FdApp* app, FdEvent ev, const uint8_t* data, uint16_t len);
/* Command dispatch */
void fd_handle_cmd(FdApp* app, uint8_t cmd, const uint8_t* payload, uint16_t len);
/* Workers */
int32_t fd_rx_worker(void* ctx);
int32_t fd_event_worker(void* ctx);

526
flipper/fd_bridge.c Normal file
View File

@@ -0,0 +1,526 @@
/**
* FlipperDroid Bridge — protocol handlers and USB I/O
*
* RX worker reads framed commands from USB CDC, dispatches to handlers.
* Event worker pushes async events (SubGHz RX, GPIO, buttons) to phone.
* All hardware access happens here.
*/
#include "fd_app.h"
#include <furi_hal_power.h>
#include <furi_hal_infrared.h>
#include <infrared.h>
#include <string.h>
#include <stdio.h>
#define TAG "FdBridge"
/* ---- USB CDC I/O ---- */
void fd_usb_tx(const uint8_t* data, uint16_t len) {
furi_hal_cdc_send(FD_CDC_CHANNEL, (uint8_t*)data, len);
}
uint16_t fd_usb_rx(uint8_t* buf, uint16_t max_len, uint32_t timeout_ms) {
uint32_t start = furi_get_tick();
uint16_t total = 0;
while(total < max_len) {
int32_t n = furi_hal_cdc_receive(FD_CDC_CHANNEL, buf + total, max_len - total);
if(n > 0) total += n;
if(furi_get_tick() - start > timeout_ms) break;
if(total >= max_len) break;
furi_delay_ms(1);
}
return total;
}
/* ---- Frame TX ---- */
static void fd_tx_frame(FdApp* app, uint8_t resp_cmd, const uint8_t* payload, uint16_t plen) {
uint16_t frame_len = 1 + plen; /* cmd + payload */
uint8_t hdr[5] = {
FD_MAGIC_HI, FD_MAGIC_LO,
(frame_len >> 8) & 0xFF, frame_len & 0xFF,
resp_cmd
};
/* CRC over [len_hi, len_lo, cmd, payload...] */
uint8_t crc_in[2 + 1 + FD_MAX_PAYLOAD];
crc_in[0] = hdr[2];
crc_in[1] = hdr[3];
crc_in[2] = resp_cmd;
if(payload && plen > 0) memcpy(crc_in + 3, payload, plen);
uint8_t crc = fd_crc8(crc_in, 3 + plen);
furi_mutex_acquire(app->mutex, FuriWaitForever);
fd_usb_tx(hdr, 5);
if(payload && plen > 0) fd_usb_tx(payload, plen);
fd_usb_tx(&crc, 1);
app->tx_count++;
furi_mutex_release(app->mutex);
}
void fd_send_ok(FdApp* app, const uint8_t* data, uint16_t len) {
fd_tx_frame(app, FD_RESP_OK, data, len);
}
void fd_send_err(FdApp* app, FdError err, const char* msg) {
uint16_t mlen = msg ? strlen(msg) : 0;
uint8_t buf[1 + 256];
buf[0] = (uint8_t)err;
if(msg && mlen > 0) memcpy(buf + 1, msg, mlen > 255 ? 255 : mlen);
fd_tx_frame(app, FD_RESP_ERR, buf, 1 + (mlen > 255 ? 255 : mlen));
app->err_count++;
}
void fd_send_event(FdApp* app, FdEvent ev, const uint8_t* data, uint16_t len) {
fd_tx_frame(app, (uint8_t)ev, data, len);
}
/* ---- GPIO helpers ---- */
static FdGpioEntry* fd_find_gpio(FdApp* app, uint8_t pin_id) {
for(uint8_t i = 0; i < app->gpio_count; i++) {
if(app->gpio[i].id == pin_id) return &app->gpio[i];
}
return NULL;
}
/* ---- Command handlers ---- */
static void fd_cmd_system(FdApp* app, uint8_t cmd, const uint8_t* p, uint16_t len) {
UNUSED(p);
UNUSED(len);
switch(cmd) {
case FdCmdPing:
fd_send_ok(app, (const uint8_t*)"PONG", 4);
fd_log(app, "PING -> PONG");
if(!app->connected) {
app->connected = true;
app->connect_tick = furi_get_tick();
notification_message(app->notifications, &sequence_blink_start_green);
fd_log(app, "Android connected!");
}
break;
case FdCmdVersion: {
char buf[64];
int n = snprintf(buf, sizeof(buf), "%s v%s (%s)",
FD_DEVICE_NAME, FD_VERSION,
furi_hal_version_get_name_ptr() ? furi_hal_version_get_name_ptr() : "Flipper");
fd_send_ok(app, (const uint8_t*)buf, n);
break;
}
case FdCmdCapabilities: {
uint8_t caps = FD_CAP_GPIO | FD_CAP_SUBGHZ | FD_CAP_IR | FD_CAP_CPU;
fd_send_ok(app, &caps, 1);
break;
}
case FdCmdStatus: {
uint8_t st[9];
st[0] = furi_hal_power_get_pct();
int16_t temp = (int16_t)(furi_hal_power_get_battery_temperature(FuriHalPowerICFuelGauge) * 10);
st[1] = (temp >> 8) & 0xFF;
st[2] = temp & 0xFF;
uint32_t up = (furi_get_tick() - app->connect_tick) / 1000;
st[3] = (up >> 24) & 0xFF;
st[4] = (up >> 16) & 0xFF;
st[5] = (up >> 8) & 0xFF;
st[6] = up & 0xFF;
st[7] = app->subghz_rx_active ? 1 : 0;
st[8] = app->ir_rx_active ? 1 : 0;
fd_send_ok(app, st, 9);
break;
}
default:
fd_send_err(app, FdErrUnknownCmd, "bad sys cmd");
break;
}
}
static void fd_cmd_gpio(FdApp* app, uint8_t cmd, const uint8_t* p, uint16_t len) {
switch(cmd) {
case FdCmdGpioInit: {
if(len < 2) { fd_send_err(app, FdErrInvalidParams, "need pin,mode"); return; }
FdGpioEntry* g = fd_find_gpio(app, p[0]);
if(!g) { fd_send_err(app, FdErrInvalidParams, "bad pin"); return; }
switch(p[1]) {
case 0:
furi_hal_gpio_init(g->pin, GpioModeInput, GpioPullNo, GpioSpeedLow);
g->output = false;
break;
case 1:
furi_hal_gpio_init(g->pin, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
g->output = true;
break;
case 2:
furi_hal_gpio_init(g->pin, GpioModeOutputOpenDrain, GpioPullNo, GpioSpeedVeryHigh);
g->output = true;
break;
case 3:
furi_hal_gpio_init(g->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
g->output = false;
break;
default:
fd_send_err(app, FdErrInvalidParams, "bad mode");
return;
}
g->initialized = true;
fd_send_ok(app, NULL, 0);
fd_log(app, "GPIO %s init mode %u", g->name, p[1]);
break;
}
case FdCmdGpioWrite: {
if(len < 2) { fd_send_err(app, FdErrInvalidParams, "need pin,val"); return; }
FdGpioEntry* g = fd_find_gpio(app, p[0]);
if(!g) { fd_send_err(app, FdErrInvalidParams, "bad pin"); return; }
if(!g->initialized) { fd_send_err(app, FdErrDisabled, "pin not init"); return; }
furi_hal_gpio_write(g->pin, p[1] ? true : false);
g->value = p[1] ? true : false;
fd_send_ok(app, NULL, 0);
break;
}
case FdCmdGpioRead: {
if(len < 1) { fd_send_err(app, FdErrInvalidParams, "need pin"); return; }
FdGpioEntry* g = fd_find_gpio(app, p[0]);
if(!g) { fd_send_err(app, FdErrInvalidParams, "bad pin"); return; }
if(!g->initialized) {
/* Auto-init as input for convenience */
furi_hal_gpio_init(g->pin, GpioModeInput, GpioPullNo, GpioSpeedLow);
g->initialized = true;
g->output = false;
}
uint8_t val = furi_hal_gpio_read(g->pin) ? 1 : 0;
g->value = val;
fd_send_ok(app, &val, 1);
break;
}
default:
fd_send_err(app, FdErrNotSupported, "gpio cmd n/a");
break;
}
}
static void fd_cmd_subghz(FdApp* app, uint8_t cmd, const uint8_t* p, uint16_t len) {
switch(cmd) {
case FdCmdSubghzSetFreq: {
if(len < 4) { fd_send_err(app, FdErrInvalidParams, "need 4B freq"); return; }
uint32_t freq = ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | p[3];
bool valid = (freq >= 300000000 && freq <= 348000000) ||
(freq >= 387000000 && freq <= 464000000) ||
(freq >= 779000000 && freq <= 928000000);
if(!valid) { fd_send_err(app, FdErrInvalidParams, "bad freq range"); return; }
app->subghz_freq = freq;
furi_hal_subghz_idle();
furi_hal_subghz_set_frequency(freq);
fd_send_ok(app, NULL, 0);
fd_log(app, "SubGHz freq=%lu", freq);
break;
}
case FdCmdSubghzTx: {
if(len < 1) { fd_send_err(app, FdErrInvalidParams, "need data"); return; }
if(app->subghz_freq == 0) { fd_send_err(app, FdErrInvalidParams, "set freq first"); return; }
furi_hal_subghz_idle();
furi_hal_subghz_set_frequency(app->subghz_freq);
furi_hal_subghz_write_packet(p, len);
furi_hal_subghz_tx();
furi_delay_ms(100);
furi_hal_subghz_idle();
fd_send_ok(app, NULL, 0);
fd_log(app, "SubGHz TX %u bytes", len);
break;
}
case FdCmdSubghzRxStart: {
if(app->subghz_freq == 0) { fd_send_err(app, FdErrInvalidParams, "set freq first"); return; }
furi_hal_subghz_idle();
furi_hal_subghz_set_frequency(app->subghz_freq);
furi_hal_subghz_rx();
app->subghz_rx_active = true;
fd_send_ok(app, NULL, 0);
fd_log(app, "SubGHz RX start");
break;
}
case FdCmdSubghzRxStop:
furi_hal_subghz_idle();
app->subghz_rx_active = false;
fd_send_ok(app, NULL, 0);
fd_log(app, "SubGHz RX stop");
break;
case FdCmdSubghzGetRssi: {
float rssi = furi_hal_subghz_get_rssi();
app->subghz_last_rssi = rssi;
int16_t ri = (int16_t)(rssi * 10);
uint8_t rb[2] = {(ri >> 8) & 0xFF, ri & 0xFF};
fd_send_ok(app, rb, 2);
break;
}
default:
fd_send_err(app, FdErrNotSupported, "subghz cmd n/a");
break;
}
}
static void fd_cmd_ir(FdApp* app, uint8_t cmd, const uint8_t* p, uint16_t len) {
switch(cmd) {
case FdCmdIrTx: {
if(len < 9) { fd_send_err(app, FdErrInvalidParams, "need 9B"); return; }
uint32_t addr = ((uint32_t)p[1] << 24) | ((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 8) | p[4];
uint32_t command = ((uint32_t)p[5] << 24) | ((uint32_t)p[6] << 16) |
((uint32_t)p[7] << 8) | p[8];
InfraredMessage msg = {
.protocol = p[0],
.address = addr,
.command = command,
.repeat = false,
};
infrared_send(&msg, 1);
fd_send_ok(app, NULL, 0);
fd_log(app, "IR TX proto=%u", p[0]);
break;
}
case FdCmdIrRxStart:
app->ir_rx_active = true;
fd_send_ok(app, NULL, 0);
fd_log(app, "IR RX start");
break;
case FdCmdIrRxStop:
app->ir_rx_active = false;
fd_send_ok(app, NULL, 0);
fd_log(app, "IR RX stop");
break;
default:
fd_send_err(app, FdErrNotSupported, "ir cmd n/a");
break;
}
}
static void fd_cmd_file(FdApp* app, uint8_t cmd, const uint8_t* p, uint16_t len) {
Storage* storage = furi_record_open(RECORD_STORAGE);
switch(cmd) {
case FdCmdFileList: {
char path[256] = "/ext";
if(len > 0 && len < sizeof(path)) {
memcpy(path, p, len);
path[len] = '\0';
}
File* dir = storage_file_alloc(storage);
if(!storage_dir_open(dir, path)) {
fd_send_err(app, FdErrHardware, "open dir fail");
storage_file_free(dir);
break;
}
char name[128];
FileInfo info;
uint8_t resp[FD_MAX_PAYLOAD];
uint16_t pos = 0;
while(storage_dir_read(dir, &info, name, sizeof(name))) {
uint16_t nlen = strlen(name);
if(pos + nlen + 3 > sizeof(resp)) break;
resp[pos++] = (info.flags & FSF_DIRECTORY) ? 'd' : 'f';
resp[pos++] = ',';
memcpy(resp + pos, name, nlen);
pos += nlen;
resp[pos++] = '\n';
}
storage_dir_close(dir);
storage_file_free(dir);
fd_send_ok(app, resp, pos);
fd_log(app, "ls %s -> %u entries", path, pos);
break;
}
case FdCmdFileRead: {
if(len < 1) { fd_send_err(app, FdErrInvalidParams, "need path"); break; }
char path[256];
uint16_t pl = (len < 255) ? len : 255;
memcpy(path, p, pl);
path[pl] = '\0';
File* file = storage_file_alloc(storage);
if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) {
fd_send_err(app, FdErrHardware, "open fail");
storage_file_free(file);
break;
}
uint8_t buf[FD_MAX_PAYLOAD];
uint16_t rd = storage_file_read(file, buf, sizeof(buf));
storage_file_close(file);
storage_file_free(file);
fd_send_ok(app, buf, rd);
fd_log(app, "read %s %uB", path, rd);
break;
}
case FdCmdFileWrite: {
if(len < 3) { fd_send_err(app, FdErrInvalidParams, "need plen+path+data"); break; }
uint16_t plen_f = ((uint16_t)p[0] << 8) | p[1];
if(2 + plen_f > len) { fd_send_err(app, FdErrInvalidParams, "bad plen"); break; }
char path[256];
uint16_t cpy = (plen_f < 255) ? plen_f : 255;
memcpy(path, p + 2, cpy);
path[cpy] = '\0';
File* file = storage_file_alloc(storage);
if(!storage_file_open(file, path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
fd_send_err(app, FdErrHardware, "create fail");
storage_file_free(file);
break;
}
uint16_t dlen = len - 2 - plen_f;
storage_file_write(file, p + 2 + plen_f, dlen);
storage_file_close(file);
storage_file_free(file);
fd_send_ok(app, NULL, 0);
fd_log(app, "write %s %uB", path, dlen);
break;
}
case FdCmdFileDelete: {
if(len < 1) { fd_send_err(app, FdErrInvalidParams, "need path"); break; }
char path[256];
uint16_t pl = (len < 255) ? len : 255;
memcpy(path, p, pl);
path[pl] = '\0';
if(storage_simply_remove(storage, path))
fd_send_ok(app, NULL, 0);
else
fd_send_err(app, FdErrHardware, "delete fail");
break;
}
default:
fd_send_err(app, FdErrUnknownCmd, "bad file cmd");
break;
}
furi_record_close(RECORD_STORAGE);
}
/* ---- Command dispatch ---- */
void fd_handle_cmd(FdApp* app, uint8_t cmd, const uint8_t* payload, uint16_t len) {
FURI_LOG_D(TAG, "CMD 0x%02X len=%u", cmd, len);
app->rx_count++;
if(cmd <= 0x04) fd_cmd_system(app, cmd, payload, len);
else if(cmd <= 0x15) fd_cmd_gpio(app, cmd, payload, len);
else if(cmd <= 0x26) fd_cmd_subghz(app, cmd, payload, len);
else if(cmd >= 0x50 && cmd <= 0x54) fd_cmd_ir(app, cmd, payload, len);
else if(cmd >= 0x90 && cmd <= 0x93) fd_cmd_file(app, cmd, payload, len);
else fd_send_err(app, FdErrUnknownCmd, "unknown");
}
/* ---- RX worker ---- */
int32_t fd_rx_worker(void* ctx) {
FdApp* app = (FdApp*)ctx;
FURI_LOG_I(TAG, "RX worker started");
while(app->running) {
/* Scan for magic bytes */
uint8_t magic[2];
if(fd_usb_rx(magic, 2, 100) < 2) continue;
if(magic[0] != FD_MAGIC_HI || magic[1] != FD_MAGIC_LO) continue;
/* Read frame length */
uint8_t lb[2];
if(fd_usb_rx(lb, 2, 500) < 2) continue;
uint16_t flen = ((uint16_t)lb[0] << 8) | lb[1];
if(flen > FD_MAX_PAYLOAD + 1) {
FURI_LOG_W(TAG, "Frame too big: %u", flen);
continue;
}
/* Read cmd + payload + crc */
uint8_t frame[FD_MAX_PAYLOAD + 2];
uint16_t got = fd_usb_rx(frame, flen + 1, 1000);
if(got < flen + 1) {
FURI_LOG_W(TAG, "Short frame: %u/%u", got, flen + 1);
continue;
}
/* Verify CRC */
uint8_t crc_in[2 + FD_MAX_PAYLOAD + 1];
crc_in[0] = lb[0];
crc_in[1] = lb[1];
memcpy(crc_in + 2, frame, flen);
uint8_t expected = fd_crc8(crc_in, 2 + flen);
if(expected != frame[flen]) {
FURI_LOG_W(TAG, "CRC fail: %02X vs %02X", frame[flen], expected);
continue;
}
/* Dispatch */
fd_handle_cmd(app, frame[0], frame + 1, flen - 1);
notification_message(app->notifications, &sequence_blink_cyan_10);
}
FURI_LOG_I(TAG, "RX worker stopped");
return 0;
}
/* ---- Event worker ---- */
int32_t fd_event_worker(void* ctx) {
FdApp* app = (FdApp*)ctx;
FURI_LOG_I(TAG, "Event worker started");
while(app->running) {
/* SubGHz RX check */
if(app->subghz_rx_active) {
if(furi_hal_subghz_is_rx_data_crc_valid()) {
uint8_t pkt[64];
furi_hal_subghz_read_packet(pkt, sizeof(pkt));
float rssi = furi_hal_subghz_get_rssi();
app->subghz_last_rssi = rssi;
/* Pack: data(64) + rssi_x10(2) + freq(4) */
uint8_t ev[70];
memcpy(ev, pkt, 64);
int16_t ri = (int16_t)(rssi * 10);
ev[64] = (ri >> 8) & 0xFF;
ev[65] = ri & 0xFF;
ev[66] = (app->subghz_freq >> 24) & 0xFF;
ev[67] = (app->subghz_freq >> 16) & 0xFF;
ev[68] = (app->subghz_freq >> 8) & 0xFF;
ev[69] = app->subghz_freq & 0xFF;
fd_send_event(app, FdEventSubghzRx, ev, 70);
notification_message(app->notifications, &sequence_blink_green_10);
fd_log(app, "SubGHz RX rssi=%.1f", (double)rssi);
}
}
furi_delay_ms(10);
}
FURI_LOG_I(TAG, "Event worker stopped");
return 0;
}

90
flipper/fd_protocol.h Normal file
View File

@@ -0,0 +1,90 @@
/**
* FlipperDroid Bridge Protocol v0.1
* Shared definitions between all source files.
*/
#pragma once
#include <stdint.h>
#include <stdbool.h>
/* Wire format: [MAGIC_HI][MAGIC_LO][LEN_HI][LEN_LO][CMD][PAYLOAD...][CRC8] */
#define FD_MAGIC_HI 0xFD
#define FD_MAGIC_LO 0x01
#define FD_MAX_PAYLOAD 2048
#define FD_VERSION "0.1.0"
#define FD_DEVICE_NAME "FlipperDroid"
/* ---- Commands (Phone -> Flipper) ---- */
typedef enum {
FdCmdPing = 0x01,
FdCmdVersion = 0x02,
FdCmdCapabilities = 0x03,
FdCmdStatus = 0x04,
FdCmdGpioInit = 0x10,
FdCmdGpioWrite = 0x11,
FdCmdGpioRead = 0x12,
FdCmdGpioPwm = 0x13,
FdCmdGpioAdcRead = 0x14,
FdCmdSubghzSetFreq = 0x20,
FdCmdSubghzTx = 0x21,
FdCmdSubghzRxStart = 0x22,
FdCmdSubghzRxStop = 0x23,
FdCmdSubghzGetRssi = 0x24,
FdCmdIrTx = 0x50,
FdCmdIrTxRaw = 0x51,
FdCmdIrRxStart = 0x52,
FdCmdIrRxStop = 0x53,
FdCmdFileList = 0x90,
FdCmdFileRead = 0x91,
FdCmdFileWrite = 0x92,
FdCmdFileDelete = 0x93,
} FdCommand;
/* ---- Events (Flipper -> Phone, async push) ---- */
typedef enum {
FdEventGpioIrq = 0xA0,
FdEventSubghzRx = 0xA1,
FdEventIrRx = 0xA2,
FdEventButton = 0xA4,
FdEventCpuReq = 0xA5,
} FdEvent;
/* Response / Error */
#define FD_RESP_OK 0xFE
#define FD_RESP_ERR 0xFF
typedef enum {
FdErrUnknownCmd = 0x01,
FdErrInvalidParams = 0x02,
FdErrDisabled = 0x03,
FdErrHardware = 0x04,
FdErrBusy = 0x05,
FdErrTimeout = 0x06,
FdErrNotSupported = 0x07,
} FdError;
/* Capability bitmask reported by 0x03 */
#define FD_CAP_GPIO (1 << 0)
#define FD_CAP_SUBGHZ (1 << 1)
#define FD_CAP_RFID (1 << 2)
#define FD_CAP_NFC (1 << 3)
#define FD_CAP_IR (1 << 4)
#define FD_CAP_IBUTTON (1 << 5)
#define FD_CAP_BADUSB (1 << 6)
#define FD_CAP_CPU (1 << 7)
/* CRC8 Dallas/Maxim 0x31 */
static inline uint8_t fd_crc8(const uint8_t* data, uint16_t len) {
uint8_t crc = 0;
for(uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for(uint8_t j = 0; j < 8; j++)
crc = (crc & 0x80) ? ((crc << 1) ^ 0x31) : (crc << 1);
}
return crc;
}

351
flipper/flipperdroid_app.c Normal file
View File

@@ -0,0 +1,351 @@
/**
* FlipperDroid — main application entry point
* Handles GUI, input, lifecycle. Workers in fd_bridge.c.
*/
#include "fd_app.h"
#include <stdio.h>
#include <stdarg.h>
#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, "<GPIO SubGHz> 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, "<Stat ^v:Sel OK:Read >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, "<GPIO OK:RX Tog >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, "<SubGHz Bk:Exit");
break;
}
}
furi_mutex_release(app->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;
}