From c23380f2ddea9811bbe8c8db44db622f53f48bfd Mon Sep 17 00:00:00 2001 From: andy Date: Fri, 11 Sep 2026 14:08:30 +0100 Subject: [PATCH] Add Sub-GHz Scanner app Sweeps RSSI across the CC1101 bands in fixed steps, shows a spectrum, and logs frequencies above a threshold. Uses the region-unlocked firmware and an exported CC1101 preset. --- build.sh | 2 +- subghz_scan/application.fam | 14 +++ subghz_scan/icon.png | Bin 0 -> 95 bytes subghz_scan/subghz_scan.c | 243 ++++++++++++++++++++++++++++++++++++ 4 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 subghz_scan/application.fam create mode 100644 subghz_scan/icon.png create mode 100644 subghz_scan/subghz_scan.c diff --git a/build.sh b/build.sh index 02b7835..6cd29cf 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) # 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) # 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_scan/application.fam b/subghz_scan/application.fam new file mode 100644 index 0000000..a93e84a --- /dev/null +++ b/subghz_scan/application.fam @@ -0,0 +1,14 @@ +App( + appid="subghz_scan", + name="Sub-GHz Scanner", + apptype=FlipperAppType.EXTERNAL, + entry_point="subghz_scan_app", + requires=["gui"], + stack_size=2 * 1024, + fap_category="Sub-GHz", + fap_icon="icon.png", + fap_version="1.0", + fap_description="Sweep RSSI across the full CC1101 range, log active frequencies", + fap_author="andydixon", + fap_weburl="https://github.com/andydixon/flipper-bt-inspector", +) diff --git a/subghz_scan/icon.png b/subghz_scan/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2d6f2fbc8f428fe0b6c579297e80bee719714064 GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2VkYHF5IUx~9sd>6MhE&{2{=v_}^Z%g=GjkK? rvI8nYO`T0s4qY)y2>mVZse2`cq5H7+*7zpl8jwy;S3j3^P6 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_DIR EXT_PATH("apps_data/subghz_scan") +#define STEP_HZ 250000u +#define SETTLE_US 3500 +#define THRESH_DBM (-90.0f) + +// Three CC1101 hardware bands (the region unlock removes the regulatory gate; +// these are the PLL limits and remain). +typedef struct { + uint32_t start, end; +} Band; +static const Band bands[] = { + {300000000, 348000000}, + {387000000, 464000000}, + {779000000, 928000000}, +}; + +typedef struct { + uint32_t peak_freq; + float peak_rssi; + uint32_t cur_freq; + float bins[128]; // one column per screen x + int nbins; + uint32_t range_start, range_end; + bool running, paused; +} Model; + +typedef struct { + Gui* gui; + ViewPort* vp; + FuriMutex* mutex; + FuriMessageQueue* input; + Storage* storage; + File* log; + Model m; + int band_idx; // -1 = all bands + volatile bool active; +} App; + +static void log_line(App* app, const char* fmt, ...) { + if(!app->log) return; + DateTime dt; + furi_hal_rtc_get_datetime(&dt); + FuriString* s = furi_string_alloc_printf( + "[%02u:%02u:%02u] ", dt.hour, dt.minute, dt.second); + va_list va; + va_start(va, fmt); + furi_string_cat_vprintf(s, fmt, va); + va_end(va); + furi_string_cat_str(s, "\n"); + storage_file_write(app->log, furi_string_get_cstr(s), furi_string_size(s)); + storage_file_sync(app->log); + furi_string_free(s); +} + +static int32_t scan_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); + bool paused = app->m.paused; + uint32_t rs = app->m.range_start, re = app->m.range_end; + furi_mutex_release(app->mutex); + if(paused) { + furi_delay_ms(100); + continue; + } + + int nb = (int)((re - rs) / STEP_HZ) + 1; + if(nb > 128) nb = 128; + uint32_t step = (re - rs) / (nb > 1 ? nb - 1 : 1); + uint32_t peak_f = rs; + float peak = -127.0f; + for(int i = 0; i < nb && app->active; i++) { + uint32_t f = rs + (uint32_t)i * step; + if(!furi_hal_subghz_is_frequency_valid(f)) continue; + 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); + float rssi = furi_hal_subghz_get_rssi(); + furi_mutex_acquire(app->mutex, FuriWaitForever); + app->m.bins[i] = rssi; + app->m.nbins = nb; + app->m.cur_freq = f; + furi_mutex_release(app->mutex); + if(rssi > peak) { + peak = rssi; + peak_f = f; + } + if(rssi > THRESH_DBM) log_line(app, "%lu Hz %.0f dBm", (unsigned long)f, (double)rssi); + } + furi_hal_subghz_idle(); + furi_mutex_acquire(app->mutex, FuriWaitForever); + 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, FontPrimary); + canvas_draw_str(c, 2, 9, "Sub-GHz Scanner"); + canvas_set_font(c, FontSecondary); + if(m->paused) canvas_draw_str_aligned(c, 126, 9, AlignRight, AlignBottom, "PAUSED"); + canvas_draw_line(c, 0, 11, 127, 11); + + snprintf( + buf, sizeof(buf), "%lu.%03lu-%lu.%03lu MHz", (unsigned long)(m->range_start / 1000000), + (unsigned long)(m->range_start / 1000 % 1000), (unsigned long)(m->range_end / 1000000), + (unsigned long)(m->range_end / 1000 % 1000)); + canvas_draw_str(c, 2, 20, buf); + snprintf( + buf, sizeof(buf), "peak %lu.%03lu MHz %d dBm", (unsigned long)(m->peak_freq / 1000000), + (unsigned long)(m->peak_freq / 1000 % 1000), (int)m->peak_rssi); + canvas_draw_str(c, 2, 29, buf); + + // spectrum bars, rssi -110..-40 mapped to 0..24 px + int gy = 32, gh = 24; + for(int i = 0; i < m->nbins && i < 128; i++) { + float r = m->bins[i]; + if(r < -110) r = -110; + if(r > -40) r = -40; + int h = (int)((r + 110) / 70.0f * gh); + canvas_draw_line(c, i, gy + gh - h, i, gy + gh); + } + canvas_draw_str(c, 2, 63, "OK pause <>band Back exit"); + furi_mutex_release(app->mutex); +} + +static void input_cb(InputEvent* e, void* ctx) { + App* app = ctx; + furi_message_queue_put(app->input, e, FuriWaitForever); +} + +static void set_range(App* app) { + furi_mutex_acquire(app->mutex, FuriWaitForever); + if(app->band_idx < 0) { + app->m.range_start = bands[0].start; + app->m.range_end = bands[COUNT_OF(bands) - 1].end; + } else { + app->m.range_start = bands[app->band_idx].start; + app->m.range_end = bands[app->band_idx].end; + } + app->m.nbins = 0; + furi_mutex_release(app->mutex); +} + +int32_t subghz_scan_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->storage = furi_record_open(RECORD_STORAGE); + app->band_idx = -1; + set_range(app); + + storage_simply_mkdir(app->storage, LOG_DIR); + DateTime dt; + furi_hal_rtc_get_datetime(&dt); + FuriString* path = furi_string_alloc_printf( + LOG_DIR "/scan_%02u%02u%02u.log", dt.hour, dt.minute, dt.second); + app->log = storage_file_alloc(app->storage); + if(!storage_file_open(app->log, furi_string_get_cstr(path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + storage_file_free(app->log); + app->log = NULL; + } + furi_string_free(path); + + 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("SubGhzScan", 2048, scan_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) continue; + if(e.key == InputKeyBack) { + run = false; + } else if(e.key == InputKeyOk) { + furi_mutex_acquire(app->mutex, FuriWaitForever); + app->m.paused = !app->m.paused; + furi_mutex_release(app->mutex); + } else if(e.key == InputKeyRight) { + app->band_idx = app->band_idx + 1 >= (int)COUNT_OF(bands) ? -1 : app->band_idx + 1; + set_range(app); + } else if(e.key == InputKeyLeft) { + app->band_idx = app->band_idx - 1 < -1 ? (int)COUNT_OF(bands) - 1 : app->band_idx - 1; + set_range(app); + } + view_port_update(app->vp); + } + + 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); + if(app->log) { + storage_file_close(app->log); + storage_file_free(app->log); + } + furi_record_close(RECORD_STORAGE); + furi_message_queue_free(app->input); + furi_mutex_free(app->mutex); + free(app); + return 0; +}