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.
This commit is contained in:
2026-09-11 14:08:30 +01:00
parent 7fe1972d28
commit c23380f2dd
4 changed files with 258 additions and 1 deletions
+1 -1
View File
@@ -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}"
+14
View File
@@ -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",
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

+243
View File
@@ -0,0 +1,243 @@
// Sub-GHz Scanner: sweep RSSI across a frequency range in fixed steps, show the
// strongest bins, and log any frequency whose RSSI crosses a threshold. Uses the
// region-unlocked firmware so the whole hardware-tunable range is available.
#include <furi.h>
#include <furi_hal_subghz.h>
#include <furi_hal_region.h>
#include <furi_hal_rtc.h>
#include <gui/gui.h>
#include <gui/view_port.h>
#include <input/input.h>
#include <storage/storage.h>
#include <lib/subghz/devices/cc1101_configs.h>
#include <stdio.h>
#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;
}