Files
ProtoPirate/scenes/protopirate_scene_timing_tuner.c
2026-02-11 09:29:36 +01:00

725 lines
23 KiB
C

// scenes/protopirate_scene_timing_tuner.c
#include "../protopirate_app_i.h"
#ifdef ENABLE_TIMING_TUNER_SCENE
#include "../protocols/protocol_items.h"
#include <gui/elements.h>
#include <math.h>
#define TAG "ProtoPirateTimingTuner"
#define MAX_TIMING_SAMPLES 512
#define VISIBLE_LINES 6
#define LINE_HEIGHT 9
#define SUBGHZ_RAW_THRESHOLD_MIN -90.0f
typedef struct {
// Ring buffer for timing capture
int32_t samples[MAX_TIMING_SAMPLES];
size_t write_idx; // Next write position
size_t sample_count; // Total samples captured (may exceed MAX)
bool buffer_wrapped; // True if we've wrapped around
// Timing statistics
size_t short_count;
size_t long_count;
// Calculated stats
int32_t avg_short;
int32_t avg_long;
int32_t min_short;
int32_t max_short;
int32_t min_long;
int32_t max_long;
// Protocol match info
const char* matched_protocol;
const ProtoPirateProtocolTiming* timing_info;
// State
bool is_receiving;
bool has_match;
uint16_t animation_frame;
float rssi;
uint8_t scroll_offset;
uint8_t total_lines;
// App reference for callback
ProtoPirateApp* app;
} TimingTunerContext;
static TimingTunerContext* g_timing_ctx = NULL;
static void calculate_timing_stats(TimingTunerContext* ctx) {
size_t num_samples = ctx->buffer_wrapped ? MAX_TIMING_SAMPLES : ctx->write_idx;
if(num_samples < 10) {
FURI_LOG_W(TAG, "Not enough samples: %zu", num_samples);
return;
}
FURI_LOG_I(
TAG,
"Analyzing %zu samples (total captured: %zu, wrapped: %d)",
num_samples,
ctx->sample_count,
ctx->buffer_wrapped);
ctx->short_count = 0;
ctx->long_count = 0;
ctx->min_short = INT32_MAX;
ctx->max_short = 0;
ctx->min_long = INT32_MAX;
ctx->max_long = 0;
int64_t short_sum = 0;
int64_t long_sum = 0;
int32_t threshold;
int32_t min_valid;
int32_t max_valid;
if(ctx->timing_info) {
int32_t te_short = (int32_t)ctx->timing_info->te_short;
int32_t te_long = (int32_t)ctx->timing_info->te_long;
int32_t te_delta = (int32_t)ctx->timing_info->te_delta;
// Threshold halfway between expected short and long
threshold = (te_short + te_long) / 2;
// Valid pulse range - use 2x delta as bounds
min_valid = te_short - (te_delta * 2);
if(min_valid < 100) min_valid = 100;
max_valid = te_long + (te_delta * 2);
FURI_LOG_I(
TAG, "Protocol timing: threshold=%ld valid=%ld-%ld", threshold, min_valid, max_valid);
} else {
// No protocol info - use reasonable defaults
threshold = 400;
min_valid = 100;
max_valid = 1200;
FURI_LOG_I(
TAG,
"No protocol ref, using defaults: threshold=%ld valid=%ld-%ld",
threshold,
min_valid,
max_valid);
}
// Analyze all samples in the ring buffer
for(size_t i = 0; i < num_samples; i++) {
int32_t dur = ctx->samples[i];
if(dur < 0) dur = -dur;
// Filter out noise and gaps
if(dur < min_valid || dur > max_valid) continue;
if(dur < threshold) {
short_sum += dur;
ctx->short_count++;
if(dur < ctx->min_short) ctx->min_short = dur;
if(dur > ctx->max_short) ctx->max_short = dur;
} else {
long_sum += dur;
ctx->long_count++;
if(dur < ctx->min_long) ctx->min_long = dur;
if(dur > ctx->max_long) ctx->max_long = dur;
}
}
// Calculate averages
if(ctx->short_count > 0) {
ctx->avg_short = (int32_t)(short_sum / (int64_t)ctx->short_count);
} else {
ctx->avg_short = 0;
ctx->min_short = 0;
ctx->max_short = 0;
}
if(ctx->long_count > 0) {
ctx->avg_long = (int32_t)(long_sum / (int64_t)ctx->long_count);
} else {
ctx->avg_long = 0;
ctx->min_long = 0;
ctx->max_long = 0;
}
// Log results
FURI_LOG_I(
TAG,
"MEASURED SHORT: avg=%ld min=%ld max=%ld n=%zu",
ctx->avg_short,
ctx->min_short,
ctx->max_short,
ctx->short_count);
FURI_LOG_I(
TAG,
"MEASURED LONG: avg=%ld min=%ld max=%ld n=%zu",
ctx->avg_long,
ctx->min_long,
ctx->max_long,
ctx->long_count);
if(ctx->timing_info && ctx->short_count > 0 && ctx->long_count > 0) {
#ifndef REMOVE_LOGS
int32_t short_diff = ctx->avg_short - (int32_t)ctx->timing_info->te_short;
int32_t long_diff = ctx->avg_long - (int32_t)ctx->timing_info->te_long;
FURI_LOG_I(
TAG,
"DIFFERENCE: short=%+ld long=%+ld (tolerance +/-%lu)",
short_diff,
long_diff,
ctx->timing_info->te_delta);
#endif
}
}
static void timing_tuner_draw_listening(Canvas* canvas, TimingTunerContext* ctx) {
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, "Timing Tuner");
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(canvas, 64, 18, AlignCenter, AlignTop, "Listening for signals...");
int wave_y = 38;
ctx->animation_frame++;
for(int x = 0; x < 128; x++) {
float phase = (float)(x + ctx->animation_frame * 3) * 0.15f;
int y_offset = (int)(sinf(phase) * 8.0f);
canvas_draw_dot(canvas, x, wave_y + y_offset);
}
{
//RSSI Signal Bar
uint8_t spacer = 0;
for(uint8_t i = 1; i < (uint8_t)(ctx->rssi - SUBGHZ_RAW_THRESHOLD_MIN); i++) {
if(i % 5) {
uint8_t j = 46 + i + spacer;
canvas_draw_dot(canvas, j, 58);
canvas_draw_dot(canvas, j + 1, 59);
canvas_draw_dot(canvas, j, 60);
} else
spacer++;
}
}
canvas_set_font(canvas, FontSecondary);
char rssi_str[24];
snprintf(rssi_str, sizeof(rssi_str), "%.0f", (double)ctx->rssi);
canvas_draw_str_aligned(canvas, 127, 62, AlignRight, AlignBottom, rssi_str);
elements_button_left(canvas, "Config");
}
// Get a specific line of content
static bool
get_result_line(TimingTunerContext* ctx, uint8_t line_idx, char* buf, size_t buf_size) {
int32_t short_diff = 0;
int32_t long_diff = 0;
bool short_ok = false;
bool long_ok = false;
bool short_exact = false;
bool long_exact = false;
int32_t short_jitter = 0;
int32_t long_jitter = 0;
if(ctx->timing_info) {
short_diff = ctx->avg_short - (int32_t)ctx->timing_info->te_short;
long_diff = ctx->avg_long - (int32_t)ctx->timing_info->te_long;
short_ok = (ctx->short_count > 0) &&
(abs(short_diff) <= (int32_t)ctx->timing_info->te_delta);
long_ok = (ctx->long_count > 0) && (abs(long_diff) <= (int32_t)ctx->timing_info->te_delta);
short_exact = (abs(short_diff) <= 15);
long_exact = (abs(long_diff) <= 15);
}
short_jitter = ctx->max_short - ctx->min_short;
long_jitter = ctx->max_long - ctx->min_long;
if(ctx->timing_info) {
switch(line_idx) {
case 0:
snprintf(buf, buf_size, "PROTOCOL DEFINITION:");
return true;
case 1:
snprintf(buf, buf_size, " Short: %lu us", ctx->timing_info->te_short);
return true;
case 2:
snprintf(buf, buf_size, " Long: %lu us", ctx->timing_info->te_long);
return true;
case 3:
snprintf(buf, buf_size, " Tolerance: +/-%lu us", ctx->timing_info->te_delta);
return true;
case 4:
buf[0] = '\0';
return true;
case 5:
snprintf(buf, buf_size, "RECEIVED SIGNAL:");
return true;
case 6:
snprintf(buf, buf_size, " Short Avg: %ld us", ctx->avg_short);
return true;
case 7:
snprintf(buf, buf_size, " Short Min: %ld us", ctx->min_short);
return true;
case 8:
snprintf(buf, buf_size, " Short Max: %ld us", ctx->max_short);
return true;
case 9:
snprintf(buf, buf_size, " Short Samples: %zu", ctx->short_count);
return true;
case 10:
snprintf(buf, buf_size, " Long Avg: %ld us", ctx->avg_long);
return true;
case 11:
snprintf(buf, buf_size, " Long Min: %ld us", ctx->min_long);
return true;
case 12:
snprintf(buf, buf_size, " Long Max: %ld us", ctx->max_long);
return true;
case 13:
snprintf(buf, buf_size, " Long Samples: %zu", ctx->long_count);
return true;
case 14:
buf[0] = '\0';
return true;
case 15:
snprintf(buf, buf_size, "ANALYSIS:");
return true;
case 16:
snprintf(buf, buf_size, " Short Diff: %+ld us", short_diff);
return true;
case 17:
snprintf(buf, buf_size, " Long Diff: %+ld us", long_diff);
return true;
case 18:
snprintf(buf, buf_size, " Short Jitter: %ld us", short_jitter);
return true;
case 19:
snprintf(buf, buf_size, " Long Jitter: %ld us", long_jitter);
return true;
case 20:
buf[0] = '\0';
return true;
case 21:
snprintf(buf, buf_size, "CONCLUSION:");
return true;
case 22:
if(short_exact) {
snprintf(buf, buf_size, " Short: EXCELLENT");
} else if(short_ok) {
snprintf(buf, buf_size, " Short: OK (%+ld)", short_diff);
} else if(short_diff > 0) {
snprintf(buf, buf_size, " Short: HIGH by %ld", short_diff);
} else {
snprintf(buf, buf_size, " Short: LOW by %ld", -short_diff);
}
return true;
case 23:
if(long_exact) {
snprintf(buf, buf_size, " Long: EXCELLENT");
} else if(long_ok) {
snprintf(buf, buf_size, " Long: OK (%+ld)", long_diff);
} else if(long_diff > 0) {
snprintf(buf, buf_size, " Long: HIGH by %ld", long_diff);
} else {
snprintf(buf, buf_size, " Long: LOW by %ld", -long_diff);
}
return true;
case 24:
buf[0] = '\0';
return true;
case 25:
if(short_exact && long_exact) {
snprintf(buf, buf_size, "Timing matches fob!");
} else if(short_ok && long_ok) {
snprintf(buf, buf_size, "Within tolerance.");
} else {
snprintf(buf, buf_size, "NEEDS ADJUSTMENT:");
}
return true;
case 26:
if(short_exact && long_exact) {
snprintf(buf, buf_size, "No changes needed.");
} else if(short_ok && long_ok) {
snprintf(buf, buf_size, "Consider fine-tuning.");
} else if(!short_ok && !long_ok) {
snprintf(buf, buf_size, "te_short=%ld", ctx->avg_short);
} else if(!short_ok) {
snprintf(buf, buf_size, "Set te_short=%ld", ctx->avg_short);
} else {
snprintf(buf, buf_size, "Set te_long=%ld", ctx->avg_long);
}
return true;
case 27:
if(!short_ok && !long_ok) {
snprintf(buf, buf_size, "te_long=%ld", ctx->avg_long);
} else {
buf[0] = '\0';
}
return true;
case 28:
buf[0] = '\0';
return true;
case 29:
snprintf(buf, buf_size, "OK:Retry <:Config");
return true;
default:
return false;
}
} else {
// No timing reference
switch(line_idx) {
case 0:
snprintf(buf, buf_size, "NO PROTOCOL REFERENCE");
return true;
case 1:
buf[0] = '\0';
return true;
case 2:
snprintf(buf, buf_size, "RECEIVED SIGNAL:");
return true;
case 3:
snprintf(buf, buf_size, " Short Avg: %ld us", ctx->avg_short);
return true;
case 4:
snprintf(buf, buf_size, " Short Min: %ld us", ctx->min_short);
return true;
case 5:
snprintf(buf, buf_size, " Short Max: %ld us", ctx->max_short);
return true;
case 6:
snprintf(buf, buf_size, " Short Samples: %zu", ctx->short_count);
return true;
case 7:
snprintf(buf, buf_size, " Long Avg: %ld us", ctx->avg_long);
return true;
case 8:
snprintf(buf, buf_size, " Long Min: %ld us", ctx->min_long);
return true;
case 9:
snprintf(buf, buf_size, " Long Max: %ld us", ctx->max_long);
return true;
case 10:
snprintf(buf, buf_size, " Long Samples: %zu", ctx->long_count);
return true;
case 11:
buf[0] = '\0';
return true;
case 12:
snprintf(buf, buf_size, "Short Jitter: %ld us", short_jitter);
return true;
case 13:
snprintf(buf, buf_size, "Long Jitter: %ld us", long_jitter);
return true;
case 14:
buf[0] = '\0';
return true;
case 15:
snprintf(buf, buf_size, "Add to protocol_items.c");
return true;
case 16:
buf[0] = '\0';
return true;
case 17:
snprintf(buf, buf_size, "OK:Retry <:Config");
return true;
default:
return false;
}
}
}
static uint8_t count_result_lines(TimingTunerContext* ctx) {
if(ctx->timing_info) {
return 30; // Lines 0-29
} else {
return 18; // Lines 0-17
}
}
static void timing_tuner_draw_results(Canvas* canvas, TimingTunerContext* ctx) {
char line_buf[32];
uint8_t total_lines = count_result_lines(ctx);
ctx->total_lines = total_lines;
uint8_t max_scroll = 0;
if(total_lines > VISIBLE_LINES) {
max_scroll = total_lines - VISIBLE_LINES;
}
if(ctx->scroll_offset > max_scroll) {
ctx->scroll_offset = max_scroll;
}
// Draw header (protocol name)
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 64, 0, AlignCenter, AlignTop, ctx->matched_protocol);
// Draw content lines
canvas_set_font(canvas, FontSecondary);
uint8_t y = 10;
for(uint8_t i = 0; i < VISIBLE_LINES; i++) {
uint8_t line_idx = ctx->scroll_offset + i;
if(get_result_line(ctx, line_idx, line_buf, sizeof(line_buf))) {
canvas_draw_str(canvas, 1, y + LINE_HEIGHT, line_buf);
}
y += LINE_HEIGHT;
}
// Scroll indicators
if(total_lines > VISIBLE_LINES) {
if(ctx->scroll_offset > 0) {
canvas_draw_str_aligned(canvas, 126, 14, AlignRight, AlignTop, "^");
}
if(ctx->scroll_offset < max_scroll) {
canvas_draw_str_aligned(canvas, 126, 62, AlignRight, AlignBottom, "v");
}
}
}
static void timing_tuner_draw_callback(Canvas* canvas, void* context) {
UNUSED(context);
TimingTunerContext* ctx = g_timing_ctx;
if(!ctx) return;
canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
if(!ctx->has_match) {
timing_tuner_draw_listening(canvas, ctx);
} else {
timing_tuner_draw_results(canvas, ctx);
}
}
static bool timing_tuner_input_callback(InputEvent* event, void* context) {
ProtoPirateApp* app = context;
bool consumed = false;
if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
switch(event->key) {
case InputKeyBack:
if(event->type == InputTypeShort) {
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
consumed = true;
}
break;
case InputKeyOk:
if(event->type == InputTypeShort && g_timing_ctx && g_timing_ctx->has_match) {
g_timing_ctx->has_match = false;
g_timing_ctx->write_idx = 0;
g_timing_ctx->sample_count = 0;
g_timing_ctx->buffer_wrapped = false;
g_timing_ctx->timing_info = NULL;
g_timing_ctx->scroll_offset = 0;
consumed = true;
}
break;
case InputKeyLeft:
if(event->type == InputTypeShort) {
view_dispatcher_send_custom_event(app->view_dispatcher, 1);
consumed = true;
}
break;
case InputKeyUp:
if(g_timing_ctx && g_timing_ctx->has_match && g_timing_ctx->scroll_offset > 0) {
g_timing_ctx->scroll_offset--;
consumed = true;
}
break;
case InputKeyDown:
if(g_timing_ctx && g_timing_ctx->has_match) {
uint8_t max_scroll = 0;
if(g_timing_ctx->total_lines > VISIBLE_LINES) {
max_scroll = g_timing_ctx->total_lines - VISIBLE_LINES;
}
if(g_timing_ctx->scroll_offset < max_scroll) {
g_timing_ctx->scroll_offset++;
}
consumed = true;
}
break;
default:
break;
}
}
return consumed;
}
static void timing_tuner_rx_callback(
SubGhzReceiver* receiver,
SubGhzProtocolDecoderBase* decoder_base,
void* context) {
UNUSED(receiver);
ProtoPirateApp* app = context;
TimingTunerContext* ctx = g_timing_ctx;
if(!ctx || ctx->has_match) return;
const char* protocol_name = decoder_base->protocol->name;
ctx->matched_protocol = protocol_name;
FURI_LOG_I(TAG, "Matched protocol: %s", protocol_name);
ctx->timing_info = protopirate_get_protocol_timing(protocol_name);
if(ctx->timing_info) {
FURI_LOG_I(
TAG,
"Found timing for %s: short=%lu, long=%lu, delta=%lu",
ctx->timing_info->name,
ctx->timing_info->te_short,
ctx->timing_info->te_long,
ctx->timing_info->te_delta);
} else {
FURI_LOG_W(TAG, "No timing info found for protocol: %s", protocol_name);
}
calculate_timing_stats(ctx);
ctx->has_match = true;
ctx->scroll_offset = 0;
notification_message(app->notifications, &sequence_success);
}
static void timing_tuner_pair_callback(void* context, bool level, uint32_t duration) {
UNUSED(context);
TimingTunerContext* ctx = g_timing_ctx;
if(ctx && !ctx->has_match) {
// Ring buffer - always write, wrap around if needed
ctx->samples[ctx->write_idx] = level ? (int32_t)duration : -(int32_t)duration;
ctx->write_idx++;
ctx->sample_count++;
if(ctx->write_idx >= MAX_TIMING_SAMPLES) {
ctx->write_idx = 0;
ctx->buffer_wrapped = true;
}
}
if(ctx && ctx->app && ctx->app->txrx && ctx->app->txrx->receiver) {
subghz_receiver_decode(ctx->app->txrx->receiver, level, duration);
}
}
void protopirate_scene_timing_tuner_on_enter(void* context) {
furi_check(context);
ProtoPirateApp* app = context;
FURI_LOG_I(TAG, "Entering Timing Tuner");
g_timing_ctx = malloc(sizeof(TimingTunerContext));
if(!g_timing_ctx) {
FURI_LOG_E(TAG, "Failed to allocate timing tuner context");
notification_message(app->notifications, &sequence_error);
scene_manager_previous_scene(app->scene_manager);
return;
}
memset(g_timing_ctx, 0, sizeof(TimingTunerContext));
g_timing_ctx->is_receiving = false;
g_timing_ctx->has_match = false;
g_timing_ctx->rssi = -127.0f;
g_timing_ctx->timing_info = NULL;
g_timing_ctx->scroll_offset = 0;
g_timing_ctx->total_lines = 0;
g_timing_ctx->write_idx = 0;
g_timing_ctx->sample_count = 0;
g_timing_ctx->buffer_wrapped = false;
g_timing_ctx->app = app;
view_set_draw_callback(app->view_about, timing_tuner_draw_callback);
view_set_input_callback(app->view_about, timing_tuner_input_callback);
view_set_context(app->view_about, app);
// Allocate worker
if(!app->txrx->worker) {
app->txrx->worker = subghz_worker_alloc();
if(!app->txrx->worker) {
FURI_LOG_E(TAG, "Failed to allocate worker!");
return;
}
// Set up worker callbacks
subghz_worker_set_overrun_callback(
app->txrx->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset);
subghz_worker_set_pair_callback(
app->txrx->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
subghz_worker_set_context(app->txrx->worker, app->txrx->receiver);
}
subghz_receiver_set_rx_callback(app->txrx->receiver, timing_tuner_rx_callback, app);
subghz_worker_set_pair_callback(
app->txrx->worker, (SubGhzWorkerPairCallback)timing_tuner_pair_callback);
protopirate_begin(app, app->txrx->preset->data);
protopirate_rx(app, app->txrx->preset->frequency);
g_timing_ctx->is_receiving = true;
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewAbout);
}
bool protopirate_scene_timing_tuner_on_event(void* context, SceneManagerEvent event) {
ProtoPirateApp* app = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == 0) {
scene_manager_previous_scene(app->scene_manager);
consumed = true;
} else if(event.event == 1) {
if(g_timing_ctx && g_timing_ctx->is_receiving) {
protopirate_rx_end(app);
g_timing_ctx->is_receiving = false;
}
scene_manager_next_scene(app->scene_manager, ProtoPirateSceneReceiverConfig);
consumed = true;
}
} else if(event.type == SceneManagerEventTypeTick) {
if(g_timing_ctx && g_timing_ctx->is_receiving && !g_timing_ctx->has_match) {
if(app->txrx->radio_device) {
g_timing_ctx->rssi = subghz_devices_get_rssi(app->txrx->radio_device);
}
// Blink the light like the SubGHZ app
notification_message(app->notifications, &sequence_blink_cyan_10);
}
view_commit_model(app->view_about, true);
consumed = true;
}
return consumed;
}
void protopirate_scene_timing_tuner_on_exit(void* context) {
ProtoPirateApp* app = context;
FURI_LOG_I(TAG, "Exiting Timing Tuner");
if(g_timing_ctx && g_timing_ctx->is_receiving) {
protopirate_rx_end(app);
}
subghz_worker_set_pair_callback(
app->txrx->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
if(app->txrx->worker) {
FURI_LOG_D(TAG, "Freeing worker %p", app->txrx->worker);
subghz_worker_free(app->txrx->worker);
app->txrx->worker = NULL;
} else {
FURI_LOG_D(TAG, "Worker was NULL, skipping free");
}
view_set_draw_callback(app->view_about, NULL);
view_set_input_callback(app->view_about, NULL);
if(g_timing_ctx) {
free(g_timing_ctx);
g_timing_ctx = NULL;
}
}
#endif