diff --git a/build.sh b/build.sh index 6cd29cf..cf79d0a 100755 --- a/build.sh +++ b/build.sh @@ -21,7 +21,7 @@ set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -APPS=(bt_inspector ble_hid_host ble_sensor ble_tracker ble_gattfuzz ble_beacon ble_gattsrv subghz_scan) # app dirs under $HERE; BLE apps staged with libble/ +APPS=(bt_inspector ble_hid_host ble_sensor ble_tracker ble_gattfuzz ble_beacon ble_gattsrv subghz_scan subghz_spectrum) # app dirs under $HERE; BLE apps staged with libble/ APP_SRC="$HERE/bt_inspector" # used only for the BLE_API=min symbol scan FW_DIR="${FW_DIR:-$HERE/flipperzero-firmware}" FW_TAG="${FW_TAG:-1.4.3}" diff --git a/subghz_spectrum/application.fam b/subghz_spectrum/application.fam new file mode 100644 index 0000000..cc5a493 --- /dev/null +++ b/subghz_spectrum/application.fam @@ -0,0 +1,14 @@ +App( + appid="subghz_spectrum", + name="Sub-GHz Spectrum", + apptype=FlipperAppType.EXTERNAL, + entry_point="subghz_spectrum_app", + requires=["gui"], + stack_size=2 * 1024, + fap_category="Sub-GHz", + fap_icon="icon.png", + fap_version="1.0", + fap_description="Live RSSI spectrum and waterfall over a tunable window", + fap_author="andydixon", + fap_weburl="https://github.com/andydixon/flipper-bt-inspector", +) diff --git a/subghz_spectrum/icon.png b/subghz_spectrum/icon.png new file mode 100644 index 0000000..2d6f2fb Binary files /dev/null and b/subghz_spectrum/icon.png differ diff --git a/subghz_spectrum/subghz_spectrum.c b/subghz_spectrum/subghz_spectrum.c new file mode 100644 index 0000000..741de76 --- /dev/null +++ b/subghz_spectrum/subghz_spectrum.c @@ -0,0 +1,193 @@ +// Sub-GHz Spectrum: sweep a centre +/- span window, drawing a live spectrum bar +// graph and a scrolling waterfall of RSSI over time. Left/Right retune the +// centre, Up/Down change the span. Region-unlocked so any hardware-tunable +// centre works. +#include +#include +#include +#include +#include +#include +#include +#include + +#define COLS 128 +#define WF_ROWS 28 +#define SETTLE_US 3000 + +typedef struct { + uint32_t center, span; + float bins[COLS]; + uint8_t waterfall[WF_ROWS][COLS]; // 0..4 intensity, ring buffer by wf_head + int wf_head; + uint32_t peak_freq; + float peak_rssi; +} Model; + +typedef struct { + Gui* gui; + ViewPort* vp; + FuriMutex* mutex; + FuriMessageQueue* input; + Model m; + volatile bool active; +} App; + +static int32_t sweep_thread(void* ctx) { + App* app = ctx; + furi_hal_subghz_reset(); + furi_hal_subghz_idle(); + furi_hal_subghz_load_custom_preset(subghz_device_cc1101_preset_ook_650khz_async_regs); + + while(app->active) { + furi_mutex_acquire(app->mutex, FuriWaitForever); + uint32_t center = app->m.center, span = app->m.span; + furi_mutex_release(app->mutex); + uint32_t start = center - span / 2; + uint32_t step = span / COLS; + if(step == 0) step = 1; + float peak = -127.0f; + uint32_t peak_f = center; + uint8_t row[COLS]; + + for(int i = 0; i < COLS && app->active; i++) { + uint32_t f = start + (uint32_t)i * step; + float rssi = -127.0f; + if(furi_hal_subghz_is_frequency_valid(f)) { + furi_hal_subghz_idle(); + furi_hal_subghz_set_frequency_and_path(f); + furi_hal_subghz_flush_rx(); + furi_hal_subghz_rx(); + furi_delay_us(SETTLE_US); + rssi = furi_hal_subghz_get_rssi(); + } + furi_mutex_acquire(app->mutex, FuriWaitForever); + app->m.bins[i] = rssi; + furi_mutex_release(app->mutex); + float clamped = rssi < -110 ? -110 : rssi > -50 ? -50 : rssi; + row[i] = (uint8_t)((clamped + 110) / 60.0f * 4); + if(rssi > peak) { + peak = rssi; + peak_f = f; + } + } + furi_hal_subghz_idle(); + furi_mutex_acquire(app->mutex, FuriWaitForever); + memcpy(app->m.waterfall[app->m.wf_head], row, COLS); + app->m.wf_head = (app->m.wf_head + 1) % WF_ROWS; + app->m.peak_freq = peak_f; + app->m.peak_rssi = peak; + furi_mutex_release(app->mutex); + view_port_update(app->vp); + } + furi_hal_subghz_idle(); + furi_hal_subghz_sleep(); + return 0; +} + +static void draw_cb(Canvas* c, void* ctx) { + App* app = ctx; + furi_mutex_acquire(app->mutex, FuriWaitForever); + Model* m = &app->m; + char buf[48]; + canvas_clear(c); + canvas_set_font(c, FontSecondary); + snprintf( + buf, sizeof(buf), "%lu.%03lu MHz +-%lu kHz", (unsigned long)(m->center / 1000000), + (unsigned long)(m->center / 1000 % 1000), (unsigned long)(m->span / 2000)); + canvas_draw_str(c, 1, 8, buf); + snprintf( + buf, sizeof(buf), "pk %lu.%03lu %d", (unsigned long)(m->peak_freq / 1000000), + (unsigned long)(m->peak_freq / 1000 % 1000), (int)m->peak_rssi); + canvas_draw_str_aligned(c, 127, 8, AlignRight, AlignBottom, buf); + + // spectrum, top band 10..30 + for(int i = 0; i < COLS; i++) { + float r = m->bins[i]; + if(r < -110) r = -110; + if(r > -50) r = -50; + int h = (int)((r + 110) / 60.0f * 20); + canvas_draw_line(c, i, 30 - h, i, 30); + } + canvas_draw_line(c, 0, 32, 127, 32); + // waterfall, rows 34..62, newest at top + for(int r = 0; r < WF_ROWS; r++) { + int src = (m->wf_head - 1 - r + WF_ROWS * 2) % WF_ROWS; + int y = 34 + r; + if(y > 63) break; + for(int i = 0; i < COLS; i++) { + uint8_t v = m->waterfall[src][i]; + if(v >= 3) + canvas_draw_dot(c, i, y); + else if(v == 2 && (i % 2 == 0)) + canvas_draw_dot(c, i, y); + else if(v == 1 && (i % 4 == 0)) + canvas_draw_dot(c, i, y); + } + } + furi_mutex_release(app->mutex); +} + +static void input_cb(InputEvent* e, void* ctx) { + App* app = ctx; + furi_message_queue_put(app->input, e, FuriWaitForever); +} + +int32_t subghz_spectrum_app(void* p) { + UNUSED(p); + App* app = malloc(sizeof(App)); + memset(app, 0, sizeof(App)); + app->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + app->input = furi_message_queue_alloc(8, sizeof(InputEvent)); + app->m.center = 433920000; + app->m.span = 2000000; // +-1 MHz + + app->gui = furi_record_open(RECORD_GUI); + app->vp = view_port_alloc(); + view_port_draw_callback_set(app->vp, draw_cb, app); + view_port_input_callback_set(app->vp, input_cb, app); + gui_add_view_port(app->gui, app->vp, GuiLayerFullscreen); + + app->active = true; + FuriThread* th = furi_thread_alloc_ex("SubGhzSpec", 2048, sweep_thread, app); + furi_thread_start(th); + + bool run = true; + while(run) { + InputEvent e; + if(furi_message_queue_get(app->input, &e, FuriWaitForever) != FuriStatusOk) continue; + if(e.type != InputTypeShort && e.type != InputTypeRepeat) continue; + furi_mutex_acquire(app->mutex, FuriWaitForever); + switch(e.key) { + case InputKeyBack: + run = false; + break; + case InputKeyLeft: + app->m.center -= app->m.span / 4; + break; + case InputKeyRight: + app->m.center += app->m.span / 4; + break; + case InputKeyUp: + if(app->m.span < 8000000) app->m.span *= 2; + break; + case InputKeyDown: + if(app->m.span > 250000) app->m.span /= 2; + break; + default: + break; + } + furi_mutex_release(app->mutex); + } + + app->active = false; + furi_thread_join(th); + furi_thread_free(th); + gui_remove_view_port(app->gui, app->vp); + view_port_free(app->vp); + furi_record_close(RECORD_GUI); + furi_message_queue_free(app->input); + furi_mutex_free(app->mutex); + free(app); + return 0; +}