diff --git a/Applications/UberGuidoZ/smart_meter_monitor/README.md b/Applications/UberGuidoZ/smart_meter_monitor/README.md new file mode 100644 index 000000000..aaa56d355 --- /dev/null +++ b/Applications/UberGuidoZ/smart_meter_monitor/README.md @@ -0,0 +1,148 @@ +# Smart Meter Monitor - Flipper Zero FAP v1.11 + +By: UberGuidoZ | https://github.com/UberGuidoZ/Flipper + +Code: [Applications/UberGuidoZ/smart_meter_monitor/Source](https://github.com/UberGuidoZ/Flipper/tree/main/Applications/UberGuidoZ/smart_meter_monitor/Source) + +Monitors the 433 MHz, 868 MHz, and 915 MHz bands for smart meter +radio transmissions using the Flipper Zero's built-in CC1101 +sub-GHz radio. Displays live RSSI as a scrolling waveform, +detects signal bursts, and tracks transmission intervals. + +--- + +## What It Does + +Most North American electric and gas meters use the ERT (Encoder +Receiver Transmitter) protocol at 915 MHz, broadcasting +unencrypted SCM (Standard Consumption Message) packets every +30-90 seconds. European meters typically use 868 MHz. Some water +meters use 433 MHz. + +This app monitors RSSI (signal strength) in real time. When a +meter transmits, a spike appears in the waveform and is counted +as a detection event. The app tracks how often transmissions +occur, giving you the broadcast interval of meters in range. + +No packet decoding is performed - this is energy detection only. + +--- + +## Display + +``` +Meter Monitor 915MHz 500ms +[RSSI waveform - 128 samples, newest on right] +[dashed threshold line across waveform] +-90dBm Cnt:003 +Int: 47s Avg: 52s +``` + +- Waveform bars show RSSI from -120 dBm (bottom) to -40 dBm (top) +- Dashed line marks the burst detection threshold +- Cnt: total detected transmissions since last reset +- Int: interval between last two detections (seconds) +- Avg: exponential moving average of all intervals + +When fewer than 2 bursts have been detected, line 2 shows the +current threshold and "---" for interval. + +--- + +## Controls + +| Button | Action | +|-------------|----------------------------------------| +| OK (short) | Pause / resume capture (II indicator) | +| OK (long) | Reset stats and waveform history | +| UP | Raise threshold (less sensitive) | +| DOWN | Lower threshold (more sensitive) | +| LEFT | Previous frequency (cycle 433/868/915) | +| RIGHT | Next frequency | +| BACK | Stop radio, return to menu | + +--- + +## Frequencies + +| Frequency | Region | Meter type | +|------------|----------------|----------------------| +| 433.92 MHz | Worldwide | Some water meters | +| 868.30 MHz | Europe | Electric / gas | +| 915.00 MHz | North America | Electric / gas (ERT) | + +Change the frequency on the fly with LEFT/RIGHT while monitoring. +NOTE: Changing frequency resets stats and restarts the radio. + +--- + +## Sample Rates and History Window + +| Rate | History at 128 samples | +|--------|------------------------| +| 100 ms | 13 seconds | +| 250 ms | 32 seconds | +| 500 ms | 64 seconds (default) | +| 1 s | 128 seconds (~2 min) | +| 2 s | 256 seconds (~4 min) | + +UP/DOWN cycles the sample rate while monitoring. + +--- + +## Threshold Adjustment + +The dashed line on the waveform marks the burst detection +threshold. Adjust with UP/DOWN in 5 dBm steps (-110 to -60 dBm). + +Default: -90 dBm. Start here and watch the ambient RSSI noise +floor. Raise the threshold until it sits clearly above the noise +floor but below the meter transmission spikes. + +Typical noise floor: -100 to -105 dBm +Typical meter transmission RSSI: -60 to -90 dBm (distance-dependent) + +--- + +## Building + +```sh +pip install --upgrade ufbt +ufbt update +cd smart_meter_monitor +ufbt launch +``` + +Output: `dist/smart_meter_monitor.fap` +Install to: `/ext/apps/Sub-GHz/smart_meter_monitor.fap` + +--- + +## File Structure + +``` +smart_meter_monitor/ + application.fam - ufbt app manifest + smart_meter.h - shared types, constants, app struct + smart_meter_app.c - entry point, alloc/free, scene handler table + scene_splash.c - splash screen (3s auto-advance) + scene_menu.c - main menu + scene_freq.c - frequency selector submenu + scene_monitor.c - CC1101 capture, waveform, burst detection + scene_instructions.c - 4-page on-screen reference + scene_about.c - about screen + README.md - this file +``` + +--- + +## Notes + +- The app uses the CC1101 HAL directly. Do not launch while the + built-in Sub-GHz app is actively scanning. +- Burst detection uses hysteresis: 2 consecutive samples above + threshold to enter, 3 consecutive samples below to exit. This + prevents spurious counts from momentary noise spikes. +- The ERT protocol broadcasts every 30-90 seconds depending on + meter model and utility configuration. Allow 2-3 minutes of + monitoring before drawing conclusions from interval data. \ No newline at end of file diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Release/smart_meter_monitor.fap b/Applications/UberGuidoZ/smart_meter_monitor/Release/smart_meter_monitor.fap new file mode 100644 index 000000000..b50a04ea9 Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Release/smart_meter_monitor.fap differ diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/01 Name.png b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/01 Name.png new file mode 100644 index 000000000..81b1a934d Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/01 Name.png differ diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/02 About.png b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/02 About.png new file mode 100644 index 000000000..3c6fdd2f0 Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/02 About.png differ diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/03 Main.png b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/03 Main.png new file mode 100644 index 000000000..ac2a489d4 Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/03 Main.png differ diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/04 Instructions 1.png b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/04 Instructions 1.png new file mode 100644 index 000000000..3f902f86f Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/04 Instructions 1.png differ diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/05 Instructions 2.png b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/05 Instructions 2.png new file mode 100644 index 000000000..b5fad2f53 Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/05 Instructions 2.png differ diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/06 Instructions 3.png b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/06 Instructions 3.png new file mode 100644 index 000000000..1100e0e41 Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/06 Instructions 3.png differ diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/07 Instructions 4.png b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/07 Instructions 4.png new file mode 100644 index 000000000..fc6a0c729 Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/07 Instructions 4.png differ diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/08 Monitoring.png b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/08 Monitoring.png new file mode 100644 index 000000000..7a7b50a47 Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/08 Monitoring.png differ diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/09 Detecting.png b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/09 Detecting.png new file mode 100644 index 000000000..a498f8318 Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Screenshots/09 Detecting.png differ diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Source/application.fam b/Applications/UberGuidoZ/smart_meter_monitor/Source/application.fam new file mode 100644 index 000000000..3e9c01efa --- /dev/null +++ b/Applications/UberGuidoZ/smart_meter_monitor/Source/application.fam @@ -0,0 +1,11 @@ +App( + appid="smart_meter_monitor", + name="Smart Meter Monitor", + apptype=FlipperAppType.EXTERNAL, + entry_point="smart_meter_app", + requires=["gui", "notification"], + stack_size=3 * 1024, + fap_category="Sub-GHz", + fap_version=(1, 11), + fap_icon="smart_meter_icon.png", +) \ No newline at end of file diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_about.c b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_about.c new file mode 100644 index 000000000..0795a6cea --- /dev/null +++ b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_about.c @@ -0,0 +1,66 @@ +// #################################################### +// # scene_about.c - about screen # +// # By: UberGuidoZ | https://github.com/UberGuidoZ/ # +// # v1.11 # +// #################################################### + +#include "smart_meter.h" + +static void sm_about_draw_cb(Canvas* canvas, void* model) { + UNUSED(model); + canvas_clear(canvas); + + // ---------------------------------------------------------- + // Box around title block measured at runtime for exact + // centering. Identical geometry to the splash screen. + // ---------------------------------------------------------- + canvas_set_font(canvas, FontPrimary); + size_t w1 = canvas_string_width(canvas, "Smart Meter"); + size_t w2 = canvas_string_width(canvas, "Monitor"); + size_t box_w = (w1 > w2 ? w1 : w2) + 16U; + int32_t box_x = (int32_t)((128U - box_w) / 2U); + if(box_x < 1) box_x = 1; + canvas_draw_frame(canvas, box_x, 8, (uint8_t)box_w, 32); + canvas_draw_str(canvas, (int32_t)((128U - w1) / 2U), 22, "Smart Meter"); + canvas_draw_str(canvas, (int32_t)((128U - w2) / 2U), 34, "Monitor"); + + // ---------------------------------------------------------- + // Separator and byline. + // ---------------------------------------------------------- + canvas_draw_line(canvas, 10, 44, 118, 44); + canvas_set_font(canvas, FontKeyboard); + canvas_draw_str(canvas, 7, 54, "v1.11 by UberGuidoZ"); + canvas_draw_str(canvas, 1, 62, "github.com/UberGuidoZ"); +} + +static bool sm_about_input_cb(InputEvent* event, void* context) { + UNUSED(event); + UNUSED(context); + return false; +} + +void scene_about_setup_view(SmartMeterApp* app) { + furi_check(app != NULL); + + app->view_about = view_alloc(); + furi_check(app->view_about != NULL); + + view_set_draw_callback(app->view_about, sm_about_draw_cb); + view_set_input_callback(app->view_about, sm_about_input_cb); +} + +void scene_about_on_enter(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + view_dispatcher_switch_to_view(app->view_dispatcher, SmViewAbout); +} + +bool scene_about_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return false; +} + +void scene_about_on_exit(void* context) { + UNUSED(context); +} \ No newline at end of file diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_freq.c b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_freq.c new file mode 100644 index 000000000..cf821b94a --- /dev/null +++ b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_freq.c @@ -0,0 +1,50 @@ +// #################################################### +// # scene_freq.c - frequency selector submenu # +// # By: UberGuidoZ | https://github.com/UberGuidoZ/ # +// # v1.11 # +// #################################################### + +#include "smart_meter.h" + +// Frequencies must stay in sync with k_frequencies[] in scene_monitor.c. +static const char* const k_freq_menu_labels[SM_FREQ_COUNT] = { + "433.92 MHz (some water)", + "868.30 MHz (EU meters)", + "915.00 MHz (US meters)", +}; + +static void sm_freq_cb(void* context, uint32_t index) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + app->freq_index = (uint8_t)index; + // Pop back to the menu after selection. + scene_manager_previous_scene(app->scene_manager); +} + +void scene_freq_on_enter(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + submenu_reset(app->submenu_freq); + submenu_set_header(app->submenu_freq, "Select Frequency"); + + for(uint8_t i = 0U; i < SM_FREQ_COUNT; i++) { + submenu_add_item(app->submenu_freq, k_freq_menu_labels[i], i, sm_freq_cb, app); + } + // Highlight the currently selected frequency. + submenu_set_selected_item(app->submenu_freq, app->freq_index); + + view_dispatcher_switch_to_view(app->view_dispatcher, SmViewFreq); +} + +bool scene_freq_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return false; +} + +void scene_freq_on_exit(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + submenu_reset(app->submenu_freq); +} \ No newline at end of file diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_instructions.c b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_instructions.c new file mode 100644 index 000000000..14d16e292 --- /dev/null +++ b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_instructions.c @@ -0,0 +1,189 @@ +// ###################################################### +// # scene_instructions.c - 4-page on-screen reference # +// # By: UberGuidoZ | https://github.com/UberGuidoZ/ # +// # v1.11 # +// ###################################################### +// +// Page 1: About Smart Meters +// Page 2: Frequencies +// Page 3: Display Guide +// Page 4: Controls +// +// LEFT - previous page +// RIGHT - next page +// BACK - return to menu +// +// ############################################################## + +#include "smart_meter.h" +#include +#include + +static const char* const k_page_titles[SM_INSTR_PAGES] = { + "About Smart Meters", + "Frequencies", + "Display Guide", + "Controls", +}; + +// Max 21 chars per line (FontKeyboard at 6px/char = 126px) +static const char* const k_page_body[SM_INSTR_PAGES][SM_INSTR_BODY_LINES] = { + // Page 0: About Smart Meters + { + "Meters broadcast ERT", + "packets every 30-60s", + "CC1101 detects RSSI", + "spikes from each TX", + "No decoding needed", + }, + // Page 1: Frequencies + { + "433MHz some water", + "868.30MHz EU electric", + "915.00MHz US electric", + "LEFT/RIGHT to change", + "Changing freq resets", + }, + // Page 2: Display Guide + { + "Bars = RSSI strength", + "Dashed line=threshold", + "Spike = transmission", + "C=count /m=per min", + "~SCM=brief INT?=noise", + }, + // Page 3: Controls + { + "OK pause/resume", + "OK hold reset stats", + "UP/DOWN threshold", + "Hold U/D rate change", + "L/R frequency", + }, +}; + +// ============================================================ +// Draw callback +// ============================================================ +static void sm_instructions_draw_cb(Canvas* canvas, void* model) { + SmInstrModel* m = (SmInstrModel*)model; + if(m == NULL) return; + + uint8_t page = m->page; + if(page >= SM_INSTR_PAGES) page = 0U; + + canvas_clear(canvas); + + // ---------------------------------------------------------- + // Title bar with page indicator right-aligned (same baseline). + // ---------------------------------------------------------- + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 1, 7, k_page_titles[page]); + + char indicator[8]; + snprintf(indicator, sizeof(indicator), "%d/%d", (int)(page + 1U), (int)SM_INSTR_PAGES); + size_t iw = canvas_string_width(canvas, indicator); + canvas_draw_str(canvas, (int32_t)(127U - iw), 7, indicator); + + canvas_draw_line(canvas, 0, 9, 127, 9); + + // ---------------------------------------------------------- + // Body text: 5 lines, 10px spacing (8px glyph + 2px gap). + // Lines at baselines y=19,29,39,49,59. + // ---------------------------------------------------------- + canvas_set_font(canvas, FontKeyboard); + int32_t y = 19; + for(uint8_t i = 0U; i < SM_INSTR_BODY_LINES; i++) { + if(k_page_body[page][i] != NULL && k_page_body[page][i][0] != '\0') { + canvas_draw_str(canvas, 1, y, k_page_body[page][i]); + } + y += 10; + } +} + +// ============================================================ +// Input callback +// ============================================================ +static bool sm_instructions_input_cb(InputEvent* event, void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + if(event->type == InputTypeShort) { + switch(event->key) { + case InputKeyLeft: + view_dispatcher_send_custom_event( + app->view_dispatcher, SmEventInstrPageLeft); + return true; + case InputKeyRight: + view_dispatcher_send_custom_event( + app->view_dispatcher, SmEventInstrPageRight); + return true; + default: + break; + } + } + return false; +} + +// ============================================================ +// View setup +// ============================================================ +void scene_instructions_setup_view(SmartMeterApp* app) { + furi_check(app != NULL); + + app->view_instructions = view_alloc(); + furi_check(app->view_instructions != NULL); + + view_set_draw_callback(app->view_instructions, sm_instructions_draw_cb); + view_set_input_callback(app->view_instructions, sm_instructions_input_cb); + view_set_context(app->view_instructions, app); + + view_allocate_model( + app->view_instructions, ViewModelTypeLockFree, sizeof(SmInstrModel)); + + SmInstrModel* m = view_get_model(app->view_instructions); + if(m != NULL) m->page = 0U; + view_commit_model(app->view_instructions, false); +} + +// ============================================================ +// Scene handlers +// ============================================================ +void scene_instructions_on_enter(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + SmInstrModel* m = view_get_model(app->view_instructions); + if(m != NULL) m->page = 0U; + view_commit_model(app->view_instructions, false); + + view_dispatcher_switch_to_view(app->view_dispatcher, SmViewInstructions); +} + +bool scene_instructions_on_event(void* context, SceneManagerEvent event) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + if(event.type != SceneManagerEventTypeCustom) return false; + + SmInstrModel* m = view_get_model(app->view_instructions); + if(m == NULL) return false; + + switch(event.event) { + case SmEventInstrPageLeft: + if(m->page > 0U) m->page--; + view_commit_model(app->view_instructions, true); + return true; + case SmEventInstrPageRight: + if(m->page < SM_INSTR_PAGES - 1U) m->page++; + view_commit_model(app->view_instructions, true); + return true; + default: + break; + } + return false; +} + +void scene_instructions_on_exit(void* context) { + UNUSED(context); +} \ No newline at end of file diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_menu.c b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_menu.c new file mode 100644 index 000000000..1ac10263d --- /dev/null +++ b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_menu.c @@ -0,0 +1,61 @@ +// #################################################### +// # scene_menu.c - main menu scene # +// # By: UberGuidoZ | https://github.com/UberGuidoZ/ # +// # v1.11 # +// #################################################### + +#include "smart_meter.h" + +static void sm_menu_cb(void* context, uint32_t index) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void scene_menu_on_enter(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + submenu_reset(app->submenu_menu); + submenu_set_header(app->submenu_menu, "Smart Meter Monitor"); + + submenu_add_item(app->submenu_menu, "Start Monitor", SmEventMenuStart, sm_menu_cb, app); + submenu_add_item(app->submenu_menu, "Instructions", SmEventMenuInstr, sm_menu_cb, app); + submenu_add_item(app->submenu_menu, "About", SmEventMenuAbout, sm_menu_cb, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, SmViewMenu); +} + +bool scene_menu_on_event(void* context, SceneManagerEvent event) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + // BACK on the root menu exits the app entirely. + if(event.type == SceneManagerEventTypeBack) { + view_dispatcher_stop(app->view_dispatcher); + return true; + } + + if(event.type == SceneManagerEventTypeCustom) { + switch(event.event) { + case SmEventMenuStart: + scene_manager_next_scene(app->scene_manager, SmSceneMonitor); + return true; + case SmEventMenuInstr: + scene_manager_next_scene(app->scene_manager, SmSceneInstructions); + return true; + case SmEventMenuAbout: + scene_manager_next_scene(app->scene_manager, SmSceneAbout); + return true; + default: + break; + } + } + return false; +} + +void scene_menu_on_exit(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + submenu_reset(app->submenu_menu); +} \ No newline at end of file diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_monitor.c b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_monitor.c new file mode 100644 index 000000000..fb5da5368 --- /dev/null +++ b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_monitor.c @@ -0,0 +1,488 @@ +// ############################################################ +// # scene_monitor.c - CC1101 capture, waveform, burst stats # +// # By: UberGuidoZ | https://github.com/UberGuidoZ/ # +// # v1.11 # +// ############################################################ +// +// The CC1101 is polled by a FuriTimer at the selected sample +// rate. RSSI values fill a 128-sample circular buffer that maps +// 1:1 to the 128px-wide waveform (newest = rightmost pixel). +// A dashed threshold line across the waveform marks the burst +// detection level, adjustable in 5 dBm steps with UP/DOWN. +// +// CONTROLS: +// OK (short) - reset stats (count, intervals) +// OK (long) - pause / resume capture +// UP - raise detection threshold (less sensitive) +// DOWN - lower detection threshold (more sensitive) +// LEFT - cycle to previous frequency +// RIGHT - cycle to next frequency +// BACK - stop radio, return to menu +// +// ############################################################## + +#include "smart_meter.h" +#include +#include + +// ============================================================ +// Frequency and sample rate tables +// ============================================================ +static const uint32_t k_frequencies[SM_FREQ_COUNT] = { + 433920000U, // 433.92 MHz - some water meters + 868300000U, // 868.30 MHz - EU electric / gas + 915000000U, // 915.00 MHz - US electric / gas (ERT/SCM) +}; + +static const char* const k_freq_labels[SM_FREQ_COUNT] = { + "433MHz", + "868MHz", + "915MHz", +}; + +static const uint32_t k_sample_rates[SM_RATE_COUNT] = { + 100U, // 100 ms - 13s history + 250U, // 250 ms - 32s history + 500U, // 500 ms - 64s history (default) + 1000U, // 1000 ms - 128s history + 2000U, // 2000 ms - 256s history +}; + +static const char* const k_rate_labels[SM_RATE_COUNT] = { + "100ms", + "250ms", + "500ms", + "1s", + "2s", +}; + +// ============================================================ +// Radio helpers +// ============================================================ + +static void sm_radio_start(SmartMeterApp* app) { + furi_check(app != NULL); + if(app->radio_running) return; + + furi_hal_subghz_reset(); + furi_hal_subghz_set_frequency_and_path(k_frequencies[app->freq_index]); + furi_hal_subghz_rx(); + app->radio_running = true; +} + +static void sm_radio_stop(SmartMeterApp* app) { + furi_check(app != NULL); + if(!app->radio_running) return; + + furi_hal_subghz_idle(); + furi_hal_subghz_sleep(); + app->radio_running = false; +} + +// Restart radio on frequency change without clearing stats. +static void sm_radio_restart(SmartMeterApp* app) { + sm_radio_stop(app); + sm_radio_start(app); +} + +// ============================================================ +// Stats reset (does not touch the RSSI history or radio) +// ============================================================ +static void sm_reset_stats(SmartMeterApp* app) { + furi_check(app != NULL); + furi_mutex_acquire(app->mutex, FuriWaitForever); + app->burst_count = 0U; + app->last_burst_tick = 0U; + app->last_interval_ms = 0U; + app->avg_interval_ms = 0U; + app->burst_ts_head = 0U; + app->burst_ts_count = 0U; + app->tx_per_min = 0U; + app->burst_enter_tick = 0U; + app->burst_sample_count = 0U; + app->last_burst_samples = 0U; + app->in_burst = false; + app->burst_above = 0U; + app->burst_below = 0U; + // Clear the RSSI history so the waveform starts fresh. + memset(app->rssi_history, (uint8_t)SM_RSSI_MIN_DBM, sizeof(app->rssi_history)); + app->rssi_head = 0U; + app->rssi_count = 0U; + furi_mutex_release(app->mutex); +} + +// ============================================================ +// Timer callback (FreeRTOS timer service task) +// ============================================================ +static void sm_sample_timer_cb(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + if(furi_mutex_acquire(app->mutex, 0U) != FuriStatusOk) return; + + if(app->radio_running && app->monitoring) { + // Read RSSI from CC1101. + float rssi_f = furi_hal_subghz_get_rssi(); + int8_t rssi; + if(rssi_f <= (float)SM_RSSI_MIN_DBM) rssi = (int8_t)SM_RSSI_MIN_DBM; + else if(rssi_f >= (float)SM_RSSI_MAX_DBM) rssi = (int8_t)SM_RSSI_MAX_DBM; + else rssi = (int8_t)rssi_f; + + app->rssi_current = rssi; + + // Write into circular buffer. + app->rssi_history[app->rssi_head] = rssi; + app->rssi_head = (app->rssi_head + 1U) % SM_HISTORY_SIZE; + if(app->rssi_count < SM_HISTORY_SIZE) app->rssi_count++; + + // Burst detection with hysteresis. + bool above = (rssi > app->threshold_dbm); + + if(!app->in_burst) { + // Not currently in a burst - look for entry condition. + if(above) { + app->burst_above++; + if(app->burst_above >= SM_BURST_ENTER_SAMPLES) { + app->in_burst = true; + app->burst_below = 0U; + app->burst_sample_count = app->burst_above; + app->burst_enter_tick = furi_get_tick(); + } + } else { + app->burst_above = 0U; + } + } else { + // In a burst - count samples and look for exit condition. + if(above) { + app->burst_sample_count++; + app->burst_below = 0U; + } else { + app->burst_below++; + if(app->burst_below >= SM_BURST_EXIT_SAMPLES) { + // Burst ended - record all stats. + app->in_burst = false; + app->burst_above = 0U; + app->last_burst_samples = app->burst_sample_count; + app->burst_sample_count = 0U; + + uint32_t now = furi_get_tick(); + + // Interval tracking. + if(app->burst_count > 0U && app->last_burst_tick > 0U) { + app->last_interval_ms = now - app->last_burst_tick; + if(app->avg_interval_ms == 0U) { + app->avg_interval_ms = app->last_interval_ms; + } else { + app->avg_interval_ms = + (app->avg_interval_ms * 3U + app->last_interval_ms) / 4U; + } + } + app->last_burst_tick = now; + app->burst_count++; + + // Tx/min: store timestamp in circular buffer. + app->burst_timestamps[app->burst_ts_head] = now; + app->burst_ts_head = + (uint8_t)((app->burst_ts_head + 1U) % 30U); + if(app->burst_ts_count < 30U) app->burst_ts_count++; + + // Count entries within the last 60 seconds. + uint8_t tpm = 0U; + for(uint8_t t = 0U; t < app->burst_ts_count; t++) { + if((now - app->burst_timestamps[t]) <= 60000U) tpm++; + } + app->tx_per_min = tpm; + + notification_message(app->notifications, &sequence_blink_green_10); + } + } + } + } + + furi_mutex_release(app->mutex); + view_commit_model(app->view_monitor, true); +} + +// ============================================================ +// Draw callback (GUI thread) +// ============================================================ +static void sm_monitor_draw_cb(Canvas* canvas, void* model) { + SmMonitorModel* m = (SmMonitorModel*)model; + if(m == NULL || m->app == NULL) return; + SmartMeterApp* app = m->app; + + // Always clear so stale pixels never persist on mutex timeout. + canvas_clear(canvas); + + if(furi_mutex_acquire(app->mutex, 25U) != FuriStatusOk) return; + + // ---------------------------------------------------------- + // Header bar + // ---------------------------------------------------------- + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 1, 7, "SMM"); + + // Right side: frequency, rate, and paused indicator. + char hdr[24]; + snprintf(hdr, sizeof(hdr), "%s %s%s", + k_freq_labels[app->freq_index], + k_rate_labels[app->rate_index], + app->monitoring ? "" : " II"); + size_t hw = canvas_string_width(canvas, hdr); + canvas_draw_str(canvas, (int32_t)(SM_SCREEN_W - 1U - hw), 7, hdr); + + // Separator + canvas_draw_line(canvas, 0, (int32_t)(SM_HEADER_H - 1U), + (int32_t)(SM_SCREEN_W - 1U), (int32_t)(SM_HEADER_H - 1U)); + + // ---------------------------------------------------------- + // RSSI waveform (bar graph, newest = rightmost column) + // ---------------------------------------------------------- + uint32_t count = app->rssi_count; + uint32_t head = app->rssi_head; + uint32_t show = count < SM_WAVE_W ? count : SM_WAVE_W; + uint32_t oldest_idx = count < SM_HISTORY_SIZE ? 0U : head; + + for(uint32_t s = 0U; s < show; s++) { + uint32_t x = SM_WAVE_W - show + s; + uint32_t sample_idx = (oldest_idx + s) % SM_HISTORY_SIZE; + int8_t rssi = app->rssi_history[sample_idx]; + + int32_t h = ((int32_t)rssi - SM_RSSI_MIN_DBM) * (int32_t)SM_WAVE_H / SM_RSSI_RANGE; + if(h < 0) h = 0; + if(h > (int32_t)SM_WAVE_H) h = (int32_t)SM_WAVE_H; + + if(h > 0) { + canvas_draw_line( + canvas, + (int32_t)x, (int32_t)SM_WAVE_Y_BOT, + (int32_t)x, (int32_t)SM_WAVE_Y_BOT - h + 1); + } + } + + // ---------------------------------------------------------- + // Threshold line (2-on, 2-off dashed across full width) + // ---------------------------------------------------------- + int32_t thr_h = ((int32_t)app->threshold_dbm - SM_RSSI_MIN_DBM) * + (int32_t)SM_WAVE_H / SM_RSSI_RANGE; + if(thr_h < 0) thr_h = 0; + if(thr_h > (int32_t)SM_WAVE_H) thr_h = (int32_t)SM_WAVE_H; + int32_t thr_y = (int32_t)SM_WAVE_Y_BOT - thr_h; + for(int32_t x = 0; x < (int32_t)SM_SCREEN_W; x += 4) { + canvas_draw_dot(canvas, x, thr_y); + canvas_draw_dot(canvas, x + 1, thr_y); + } + + // ---------------------------------------------------------- + // Stats line 1: RSSI, burst count, tx/min + // ---------------------------------------------------------- + canvas_set_font(canvas, FontKeyboard); + char s1[20]; + snprintf(s1, sizeof(s1), "%4ddBm C:%03lu %2u/m", + (int)app->rssi_current, + (unsigned long)app->burst_count, + (unsigned)app->tx_per_min); + canvas_draw_str(canvas, 0, (int32_t)SM_STATS1_Y, s1); + + // ---------------------------------------------------------- + // Stats line 2: intervals + SCM framing hint + // + // SCM heuristic: brief above-threshold events (1-2 samples) + // are consistent with short meter packets. Sustained events + // (4+ samples at any poll rate) suggest interference. + // This is energy-based estimation only, not actual decoding. + // ---------------------------------------------------------- + char s2[28]; + if(app->burst_count < 2U) { + snprintf(s2, sizeof(s2), "Thr:%3ddBm", (int)app->threshold_dbm); + } else { + uint32_t lst_s = app->last_interval_ms / 1000U; + uint32_t avg_s = app->avg_interval_ms / 1000U; + if(lst_s > 999U) lst_s = 999U; + if(avg_s > 999U) avg_s = 999U; + + const char* hint = ""; + if(app->last_burst_samples <= 2U) { + hint = " ~SCM"; // brief: consistent with meter packet + } else if(app->last_burst_samples >= 4U) { + hint = " INT?"; // sustained: likely interference + } + + snprintf(s2, sizeof(s2), "I:%3lus A:%3lus%s", + (unsigned long)lst_s, + (unsigned long)avg_s, + hint); + } + canvas_draw_str(canvas, 0, (int32_t)SM_STATS2_Y, s2); + + furi_mutex_release(app->mutex); +} + +// ============================================================ +// Input callback (GUI thread) +// ============================================================ +static bool sm_monitor_input_cb(InputEvent* event, void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + if(event->type == InputTypeShort) { + switch(event->key) { + case InputKeyOk: + // Skip toggle if a long press was just consumed for reset. + if(app->ok_long_consumed) { + app->ok_long_consumed = false; + return true; + } + view_dispatcher_send_custom_event( + app->view_dispatcher, SmEventMonitorToggle); + return true; + case InputKeyUp: + view_dispatcher_send_custom_event( + app->view_dispatcher, SmEventMonitorThreshUp); + return true; + case InputKeyDown: + view_dispatcher_send_custom_event( + app->view_dispatcher, SmEventMonitorThreshDown); + return true; + case InputKeyLeft: + view_dispatcher_send_custom_event( + app->view_dispatcher, SmEventMonitorFreqPrev); + return true; + case InputKeyRight: + view_dispatcher_send_custom_event( + app->view_dispatcher, SmEventMonitorFreqNext); + return true; + default: + break; + } + } else if(event->type == InputTypeLong) { + switch(event->key) { + case InputKeyOk: + app->ok_long_consumed = true; + view_dispatcher_send_custom_event( + app->view_dispatcher, SmEventMonitorReset); + return true; + case InputKeyUp: + view_dispatcher_send_custom_event( + app->view_dispatcher, SmEventMonitorRateUp); + return true; + case InputKeyDown: + view_dispatcher_send_custom_event( + app->view_dispatcher, SmEventMonitorRateDown); + return true; + default: + break; + } + } + + return false; +} + +// ============================================================ +// View setup (called once from sm_alloc) +// ============================================================ +void scene_monitor_setup_view(SmartMeterApp* app) { + furi_check(app != NULL); + + app->view_monitor = view_alloc(); + furi_check(app->view_monitor != NULL); + + view_set_draw_callback(app->view_monitor, sm_monitor_draw_cb); + view_set_input_callback(app->view_monitor, sm_monitor_input_cb); + view_set_context(app->view_monitor, app); + + view_allocate_model( + app->view_monitor, ViewModelTypeLockFree, sizeof(SmMonitorModel)); + + SmMonitorModel* m = view_get_model(app->view_monitor); + if(m != NULL) m->app = app; + view_commit_model(app->view_monitor, false); + + // Allocate the sample timer here so the callback symbol stays local. + app->sample_timer = furi_timer_alloc(sm_sample_timer_cb, FuriTimerTypePeriodic, app); + furi_check(app->sample_timer != NULL); +} + +// ============================================================ +// Scene handlers +// ============================================================ +void scene_monitor_on_enter(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + sm_reset_stats(app); + sm_radio_start(app); + furi_timer_start(app->sample_timer, k_sample_rates[app->rate_index]); + + view_dispatcher_switch_to_view(app->view_dispatcher, SmViewMonitor); +} + +bool scene_monitor_on_event(void* context, SceneManagerEvent event) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + if(event.type != SceneManagerEventTypeCustom) return false; + + switch(event.event) { + + case SmEventMonitorToggle: + app->monitoring = !app->monitoring; + return true; + + case SmEventMonitorReset: + sm_reset_stats(app); + return true; + + case SmEventMonitorThreshUp: + if(app->threshold_dbm < (int8_t)SM_THRESHOLD_MAX) { + app->threshold_dbm += (int8_t)SM_THRESHOLD_STEP; + } + return true; + + case SmEventMonitorThreshDown: + if(app->threshold_dbm > (int8_t)SM_THRESHOLD_MIN) { + app->threshold_dbm -= (int8_t)SM_THRESHOLD_STEP; + } + return true; + + case SmEventMonitorFreqNext: + app->freq_index = (uint8_t)((app->freq_index + 1U) % SM_FREQ_COUNT); + sm_radio_restart(app); + sm_reset_stats(app); + return true; + + case SmEventMonitorFreqPrev: + app->freq_index = (uint8_t)( + app->freq_index > 0U ? app->freq_index - 1U : SM_FREQ_COUNT - 1U); + sm_radio_restart(app); + sm_reset_stats(app); + return true; + + case SmEventMonitorRateUp: + if(app->rate_index < SM_RATE_COUNT - 1U) { + app->rate_index++; + furi_timer_stop(app->sample_timer); + furi_timer_start(app->sample_timer, k_sample_rates[app->rate_index]); + } + return true; + + case SmEventMonitorRateDown: + if(app->rate_index > 0U) { + app->rate_index--; + furi_timer_stop(app->sample_timer); + furi_timer_start(app->sample_timer, k_sample_rates[app->rate_index]); + } + return true; + + default: + break; + } + return false; +} + +void scene_monitor_on_exit(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + furi_timer_stop(app->sample_timer); + sm_radio_stop(app); +} \ No newline at end of file diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_splash.c b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_splash.c new file mode 100644 index 000000000..cbd9389a6 --- /dev/null +++ b/Applications/UberGuidoZ/smart_meter_monitor/Source/scene_splash.c @@ -0,0 +1,114 @@ +// #################################################### +// # scene_splash.c - title / splash screen # +// # By: UberGuidoZ | https://github.com/UberGuidoZ/ # +// # v1.11 # +// #################################################### + +#include "smart_meter.h" + +// ============================================================ +// Splash timer callback +// ============================================================ +static void sm_splash_timer_cb(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + view_dispatcher_send_custom_event(app->view_dispatcher, SM_SPLASH_TIMER_DONE); +} + +// ============================================================ +// Draw callback +// ============================================================ +static void sm_splash_draw_cb(Canvas* canvas, void* model) { + UNUSED(model); + canvas_clear(canvas); + + // ---------------------------------------------------------- + // Box around title block only. Measured at runtime for exact + // centering regardless of font metrics. + // ---------------------------------------------------------- + canvas_set_font(canvas, FontPrimary); + size_t w1 = canvas_string_width(canvas, "Smart Meter"); + size_t w2 = canvas_string_width(canvas, "Monitor"); + size_t box_w = (w1 > w2 ? w1 : w2) + 16U; + int32_t box_x = (int32_t)((128U - box_w) / 2U); + if(box_x < 1) box_x = 1; + canvas_draw_frame(canvas, box_x, 8, (uint8_t)box_w, 32); + canvas_draw_str(canvas, (int32_t)((128U - w1) / 2U), 22, "Smart Meter"); + canvas_draw_str(canvas, (int32_t)((128U - w2) / 2U), 34, "Monitor"); + + // ---------------------------------------------------------- + // Separator and byline (FontKeyboard ~6px/char). + // "v1.11 by UberGuidoZ" = 19 chars * 6 = 114px, x=(128-114)/2 = 7 + // "github.com/UberGuidoZ" = 21 chars * 6 = 126px, x=1 + // ---------------------------------------------------------- + canvas_draw_line(canvas, 10, 44, 118, 44); + canvas_set_font(canvas, FontKeyboard); + canvas_draw_str(canvas, 7, 54, "v1.11 by UberGuidoZ"); + canvas_draw_str(canvas, 1, 62, "github.com/UberGuidoZ"); +} + +// ============================================================ +// Input callback - any key skips the splash +// ============================================================ +static bool sm_splash_input_cb(InputEvent* event, void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + if(event->type == InputTypeShort || event->type == InputTypeLong) { + view_dispatcher_send_custom_event(app->view_dispatcher, SM_SPLASH_TIMER_DONE); + return true; + } + return false; +} + +// ============================================================ +// View setup +// ============================================================ +void scene_splash_setup_view(SmartMeterApp* app) { + furi_check(app != NULL); + app->view_splash = view_alloc(); + furi_check(app->view_splash != NULL); + view_set_draw_callback(app->view_splash, sm_splash_draw_cb); + view_set_input_callback(app->view_splash, sm_splash_input_cb); + view_set_context(app->view_splash, app); +} + +// ============================================================ +// Scene handlers +// ============================================================ +void scene_splash_on_enter(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + app->splash_timer = furi_timer_alloc(sm_splash_timer_cb, FuriTimerTypeOnce, app); + furi_check(app->splash_timer != NULL); + furi_timer_start(app->splash_timer, SM_SPLASH_DELAY_MS); + + view_dispatcher_switch_to_view(app->view_dispatcher, SmViewSplash); +} + +bool scene_splash_on_event(void* context, SceneManagerEvent event) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + + if(event.type == SceneManagerEventTypeCustom && + event.event == SM_SPLASH_TIMER_DONE) { + if(app->splash_timer != NULL) { + furi_timer_stop(app->splash_timer); + furi_timer_free(app->splash_timer); + app->splash_timer = NULL; + } + scene_manager_next_scene(app->scene_manager, SmSceneMenu); + return true; + } + return false; +} + +void scene_splash_on_exit(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + if(app->splash_timer != NULL) { + furi_timer_stop(app->splash_timer); + furi_timer_free(app->splash_timer); + app->splash_timer = NULL; + } +} \ No newline at end of file diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Source/smart_meter.h b/Applications/UberGuidoZ/smart_meter_monitor/Source/smart_meter.h new file mode 100644 index 000000000..a9765985e --- /dev/null +++ b/Applications/UberGuidoZ/smart_meter_monitor/Source/smart_meter.h @@ -0,0 +1,251 @@ +#pragma once + +// ######################################################### +// # smart_meter.h - Smart Meter Monitor for Flipper Zero # +// # By: UberGuidoZ | https://github.com/UberGuidoZ/ # +// # v1.11 # +// ######################################################### + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ------------------------------------------------------------ +// Screen geometry (actual OLED pixels) +// ------------------------------------------------------------ +#define SM_SCREEN_W 128U +#define SM_SCREEN_H 64U +#define SM_HEADER_H 9U // 8px text row + 1px separator + +// Waveform area +#define SM_WAVE_W 128U // full screen width = sample history +#define SM_WAVE_H 28U // 28px tall +#define SM_WAVE_Y_TOP 10U // top pixel of waveform +#define SM_WAVE_Y_BOT 37U // bottom pixel (SM_WAVE_Y_TOP + SM_WAVE_H - 1) + +// Stats rows (FontKeyboard, 8px tall, 2px gap above each) +#define SM_STATS1_Y 48U // baseline; glyph occupies y=40..48 +#define SM_STATS2_Y 59U // baseline; glyph occupies y=51..59 + +// RSSI scaling +#define SM_RSSI_MIN_DBM (-120) // mapped to 0px waveform height +#define SM_RSSI_MAX_DBM (-40) // mapped to SM_WAVE_H px height +#define SM_RSSI_RANGE 80 // SM_RSSI_MAX_DBM - SM_RSSI_MIN_DBM + +// History buffer = waveform width (1 sample per pixel column) +#define SM_HISTORY_SIZE SM_WAVE_W + +// ------------------------------------------------------------ +// Threshold +// ------------------------------------------------------------ +#define SM_THRESHOLD_DEFAULT (-90) +#define SM_THRESHOLD_MIN (-110) +#define SM_THRESHOLD_MAX (-60) +#define SM_THRESHOLD_STEP 5 + +// ------------------------------------------------------------ +// Burst detection hysteresis +// ------------------------------------------------------------ +#define SM_BURST_ENTER_SAMPLES 2U // consecutive above-threshold to enter burst +#define SM_BURST_EXIT_SAMPLES 3U // consecutive below-threshold to exit burst + +// ------------------------------------------------------------ +// Sample rates +// ------------------------------------------------------------ +#define SM_RATE_COUNT 5U +#define SM_DEFAULT_RATE 2U // 500 ms + +// ------------------------------------------------------------ +// Frequencies +// ------------------------------------------------------------ +#define SM_FREQ_COUNT 3U +#define SM_DEFAULT_FREQ 2U // 915 MHz (most common in US) + +// ------------------------------------------------------------ +// Splash +// ------------------------------------------------------------ +#define SM_SPLASH_DELAY_MS 3000U +#define SM_SPLASH_TIMER_DONE 0U + +// ------------------------------------------------------------ +// Instructions +// ------------------------------------------------------------ +#define SM_INSTR_PAGES 4U +#define SM_INSTR_BODY_LINES 5U + +// ------------------------------------------------------------ +// Scene IDs +// Order must match sm_on_enter/event/exit arrays in smart_meter_app.c. +// ------------------------------------------------------------ +typedef enum { + SmSceneSplash = 0, + SmSceneMenu, + SmSceneFreq, + SmSceneMonitor, + SmSceneInstructions, + SmSceneAbout, + SmSceneCount, +} SmSceneId; + +// ------------------------------------------------------------ +// View IDs +// ------------------------------------------------------------ +typedef enum { + SmViewSplash = 0, + SmViewMenu, + SmViewFreq, + SmViewMonitor, + SmViewInstructions, + SmViewAbout, + SmViewCount, +} SmViewId; + +// ------------------------------------------------------------ +// Custom events +// ------------------------------------------------------------ +typedef enum { + SmEventMenuStart = 0, + SmEventMenuFreq, + SmEventMenuInstr, + SmEventMenuAbout, +} SmMenuEvent; + +typedef enum { + SmEventMonitorToggle = 0, + SmEventMonitorReset, + SmEventMonitorRateUp, + SmEventMonitorRateDown, + SmEventMonitorFreqNext, + SmEventMonitorFreqPrev, + SmEventMonitorThreshUp, + SmEventMonitorThreshDown, +} SmMonitorEvent; + +typedef enum { + SmEventInstrPageLeft = 0, + SmEventInstrPageRight, +} SmInstrEvent; + +// ------------------------------------------------------------ +// Forward declaration - allows model structs below to hold +// a SmartMeterApp* before the full struct is defined. +// ------------------------------------------------------------ +typedef struct SmartMeterApp_s SmartMeterApp; + +// ------------------------------------------------------------ +// View models +// ------------------------------------------------------------ + +// Monitor view: draw callback reads app state through this pointer. +typedef struct { + SmartMeterApp* app; +} SmMonitorModel; + +// Instructions view: tracks which page is displayed. +typedef struct { + uint8_t page; +} SmInstrModel; + +// ------------------------------------------------------------ +// Full application state struct +// ------------------------------------------------------------ +struct SmartMeterApp_s { + // Scene / view management + SceneManager* scene_manager; + ViewDispatcher* view_dispatcher; + Submenu* submenu_menu; + Submenu* submenu_freq; + View* view_splash; + View* view_monitor; + View* view_instructions; + View* view_about; + + // Timers and sync + FuriTimer* sample_timer; + FuriTimer* splash_timer; + FuriMutex* mutex; + + // Radio state + bool radio_running; + bool monitoring; // capture on / off + bool ok_long_consumed; // prevents long-press release from also triggering short + uint8_t freq_index; // index into k_frequencies[] + uint8_t rate_index; // index into k_sample_rates[] + + // RSSI circular buffer + int8_t rssi_history[SM_HISTORY_SIZE]; + uint32_t rssi_head; // next write position + uint32_t rssi_count; // samples stored (capped at SM_HISTORY_SIZE) + int8_t rssi_current; // most recent RSSI value + + // Threshold (dBm, adjusted by UP/DOWN) + int8_t threshold_dbm; + + // Burst detection + bool in_burst; + uint8_t burst_above; // consecutive above-threshold samples + uint8_t burst_below; // consecutive below-threshold samples + + // Transmission stats + uint32_t burst_count; // total detected transmissions + uint32_t last_burst_tick; // furi_get_tick() of last burst exit + uint32_t last_interval_ms; // interval between last two bursts + uint32_t avg_interval_ms; // running average interval + + // Tx/minute - rolling 60s window of burst exit timestamps + uint32_t burst_timestamps[30U]; // circular buffer of burst ticks + uint8_t burst_ts_head; // next write index + uint8_t burst_ts_count; // entries stored (capped at 30) + uint8_t tx_per_min; // transmissions in last 60s + + // Burst duration / SCM framing hint + uint32_t burst_enter_tick; // tick when burst was entered + uint8_t burst_sample_count; // above-threshold samples in current burst + uint8_t last_burst_samples; // sample count of last completed burst + + // Notifications + NotificationApp* notifications; +}; + +// ------------------------------------------------------------ +// Scene handler declarations +// ------------------------------------------------------------ +void scene_splash_on_enter(void* context); +bool scene_splash_on_event(void* context, SceneManagerEvent event); +void scene_splash_on_exit(void* context); + +void scene_menu_on_enter(void* context); +bool scene_menu_on_event(void* context, SceneManagerEvent event); +void scene_menu_on_exit(void* context); + +void scene_freq_on_enter(void* context); +bool scene_freq_on_event(void* context, SceneManagerEvent event); +void scene_freq_on_exit(void* context); + +void scene_monitor_on_enter(void* context); +bool scene_monitor_on_event(void* context, SceneManagerEvent event); +void scene_monitor_on_exit(void* context); + +void scene_instructions_on_enter(void* context); +bool scene_instructions_on_event(void* context, SceneManagerEvent event); +void scene_instructions_on_exit(void* context); + +void scene_about_on_enter(void* context); +bool scene_about_on_event(void* context, SceneManagerEvent event); +void scene_about_on_exit(void* context); + +// ------------------------------------------------------------ +// View setup functions (called once from sm_alloc) +// ------------------------------------------------------------ +void scene_splash_setup_view(SmartMeterApp* app); +void scene_monitor_setup_view(SmartMeterApp* app); +void scene_instructions_setup_view(SmartMeterApp* app); +void scene_about_setup_view(SmartMeterApp* app); \ No newline at end of file diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Source/smart_meter_app.c b/Applications/UberGuidoZ/smart_meter_monitor/Source/smart_meter_app.c new file mode 100644 index 000000000..cd7b86662 --- /dev/null +++ b/Applications/UberGuidoZ/smart_meter_monitor/Source/smart_meter_app.c @@ -0,0 +1,189 @@ +// ###################################################### +// # smart_meter_app.c - entry point and app lifecycle # +// # By: UberGuidoZ | https://github.com/UberGuidoZ/ # +// # v1.11 # +// ###################################################### + +#include "smart_meter.h" +#include + +// ============================================================ +// Scene handler tables +// Order must match SmSceneId enum in smart_meter.h. +// ============================================================ +static void (*const sm_on_enter_handlers[])(void*) = { + scene_splash_on_enter, + scene_menu_on_enter, + scene_freq_on_enter, + scene_monitor_on_enter, + scene_instructions_on_enter, + scene_about_on_enter, +}; + +static bool (*const sm_on_event_handlers[])(void*, SceneManagerEvent) = { + scene_splash_on_event, + scene_menu_on_event, + scene_freq_on_event, + scene_monitor_on_event, + scene_instructions_on_event, + scene_about_on_event, +}; + +static void (*const sm_on_exit_handlers[])(void*) = { + scene_splash_on_exit, + scene_menu_on_exit, + scene_freq_on_exit, + scene_monitor_on_exit, + scene_instructions_on_exit, + scene_about_on_exit, +}; + +static const SceneManagerHandlers sm_scene_manager_handlers = { + .on_enter_handlers = sm_on_enter_handlers, + .on_event_handlers = sm_on_event_handlers, + .on_exit_handlers = sm_on_exit_handlers, + .scene_num = SmSceneCount, +}; + +// ============================================================ +// ViewDispatcher callbacks +// ============================================================ + +static bool sm_custom_event_cb(void* context, uint32_t event) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool sm_back_event_cb(void* context) { + SmartMeterApp* app = (SmartMeterApp*)context; + furi_check(app != NULL); + return scene_manager_handle_back_event(app->scene_manager); +} + +// ============================================================ +// Allocate all app resources +// ============================================================ +static SmartMeterApp* sm_alloc(void) { + SmartMeterApp* app = malloc(sizeof(SmartMeterApp)); + furi_check(app != NULL); + memset(app, 0, sizeof(SmartMeterApp)); + + app->freq_index = SM_DEFAULT_FREQ; + app->rate_index = SM_DEFAULT_RATE; + app->threshold_dbm = (int8_t)SM_THRESHOLD_DEFAULT; + app->monitoring = true; + + // ---------------------------------------------------------- + // Scene manager + // ---------------------------------------------------------- + app->scene_manager = scene_manager_alloc(&sm_scene_manager_handlers, app); + furi_check(app->scene_manager != NULL); + + // ---------------------------------------------------------- + // View dispatcher + // ---------------------------------------------------------- + app->view_dispatcher = view_dispatcher_alloc(); + furi_check(app->view_dispatcher != NULL); + + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_custom_event_callback(app->view_dispatcher, sm_custom_event_cb); + view_dispatcher_set_navigation_event_callback(app->view_dispatcher, sm_back_event_cb); + + Gui* gui = furi_record_open(RECORD_GUI); + view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen); + furi_record_close(RECORD_GUI); + + // ---------------------------------------------------------- + // Submenus + // ---------------------------------------------------------- + app->submenu_menu = submenu_alloc(); + furi_check(app->submenu_menu != NULL); + view_dispatcher_add_view( + app->view_dispatcher, SmViewMenu, submenu_get_view(app->submenu_menu)); + + app->submenu_freq = submenu_alloc(); + furi_check(app->submenu_freq != NULL); + view_dispatcher_add_view( + app->view_dispatcher, SmViewFreq, submenu_get_view(app->submenu_freq)); + + // ---------------------------------------------------------- + // Custom views + // ---------------------------------------------------------- + scene_splash_setup_view(app); + view_dispatcher_add_view(app->view_dispatcher, SmViewSplash, app->view_splash); + + scene_monitor_setup_view(app); + view_dispatcher_add_view(app->view_dispatcher, SmViewMonitor, app->view_monitor); + + scene_instructions_setup_view(app); + view_dispatcher_add_view(app->view_dispatcher, SmViewInstructions, app->view_instructions); + + scene_about_setup_view(app); + view_dispatcher_add_view(app->view_dispatcher, SmViewAbout, app->view_about); + + // ---------------------------------------------------------- + // Mutex and notifications + // ---------------------------------------------------------- + app->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + furi_check(app->mutex != NULL); + + app->notifications = furi_record_open(RECORD_NOTIFICATION); + furi_check(app->notifications != NULL); + + return app; +} + +// ============================================================ +// Free all resources in reverse-allocation order +// ============================================================ +static void sm_free(SmartMeterApp* app) { + furi_check(app != NULL); + + if(app->sample_timer != NULL) { + furi_timer_stop(app->sample_timer); + furi_timer_free(app->sample_timer); + app->sample_timer = NULL; + } + + if(app->splash_timer != NULL) { + furi_timer_stop(app->splash_timer); + furi_timer_free(app->splash_timer); + app->splash_timer = NULL; + } + + furi_record_close(RECORD_NOTIFICATION); + furi_mutex_free(app->mutex); + + view_dispatcher_remove_view(app->view_dispatcher, SmViewAbout); + view_dispatcher_remove_view(app->view_dispatcher, SmViewInstructions); + view_dispatcher_remove_view(app->view_dispatcher, SmViewMonitor); + view_dispatcher_remove_view(app->view_dispatcher, SmViewSplash); + view_dispatcher_remove_view(app->view_dispatcher, SmViewFreq); + view_dispatcher_remove_view(app->view_dispatcher, SmViewMenu); + + view_free(app->view_about); + view_free(app->view_instructions); + view_free(app->view_monitor); + view_free(app->view_splash); + submenu_free(app->submenu_freq); + submenu_free(app->submenu_menu); + + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + free(app); +} + +// ============================================================ +// Application entry point +// ============================================================ +int32_t smart_meter_app(void* p) { + UNUSED(p); + + SmartMeterApp* app = sm_alloc(); + scene_manager_next_scene(app->scene_manager, SmSceneSplash); + view_dispatcher_run(app->view_dispatcher); + sm_free(app); + return 0; +} \ No newline at end of file diff --git a/Applications/UberGuidoZ/smart_meter_monitor/Source/smart_meter_icon.png b/Applications/UberGuidoZ/smart_meter_monitor/Source/smart_meter_icon.png new file mode 100644 index 000000000..ebda0acf1 Binary files /dev/null and b/Applications/UberGuidoZ/smart_meter_monitor/Source/smart_meter_icon.png differ