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:
19
flipper/application.fam
Normal file
19
flipper/application.fam
Normal 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
114
flipper/fd_app.h
Normal 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
526
flipper/fd_bridge.c
Normal 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
90
flipper/fd_protocol.h
Normal 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
351
flipper/flipperdroid_app.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user