Files
andyandClaude Fable 5.1 99d65f674d Fix text running off screen in Sub-GHz and GATT Server screens
Sub-GHz Scanner: shorten the peak readout and the hint line. Sub-GHz Spectrum:
shorten the centre/span header and show only peak dBm on the right so the two
no longer overlap. GATT Server: put the service state on its own line (was
overlapping the title), shorten "Serial GATT service (RX/TX)", and cap the
last-received text so none of it exceeds 128 px.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEaD4ZeAfvWLBUxXp8G11r
2026-09-10 23:48:11 +01:00

194 lines
6.2 KiB
C

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2026 Andy Dixon <andy@dixon.cx>
// 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 <furi.h>
#include <furi_hal_subghz.h>
#include <furi_hal_region.h>
#include <gui/gui.h>
#include <gui/view_port.h>
#include <input/input.h>
#include <lib/subghz/devices/cc1101_configs.h>
#include <stdio.h>
#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 +-%luk", (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), "%ddBm", (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;
}