/** * 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 #include #include #include #include #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; }