Compare commits

..

5 Commits

Author SHA1 Message Date
UberGuidoZ
c074cebb2e
Release v1.11 (API 87.1) 2026-03-24 20:18:45 -07:00
UberGuidoZ
7c17ddec8d
Delete Applications/UberGuidoZ/smart_meter_monitor/Release/smart_meter_monitor.fap 2026-03-24 20:18:11 -07:00
UberGuidoZ
e062cc24c4
Release v1.11 (API 87.1) 2026-03-24 20:17:31 -07:00
UberGuidoZ
e0fd8fede8
Added Smart Meter Monitor v1.11 2026-03-24 20:14:57 -07:00
UberGuidoZ
5a8af4e748
Update README.md 2026-03-24 20:13:21 -07:00
22 changed files with 1583 additions and 1 deletions

View File

@ -1 +1,16 @@
# Flipper Apps by UberGuidoZ
# Flipper Apps by UberGuidoZ
[GPIO Logic Analyzer](https://github.com/UberGuidoZ/Flipper/tree/main/Applications/UberGuidoZ/gpio_logic_analyzer/)
A logic analyzer that monitors all 8 exposed GPIO pins on the Flipper Zero
18-pin header, draws live scrolling waveforms on the 128x64 OLED, and logs
every sample to a timestamped CSV file on the SD card, with instructions.
---
[Smart Meter Monitor](https://github.com/UberGuidoZ/Flipper/tree/main/Applications/UberGuidoZ/smart_meter_monitor/)
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.

View File

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -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",
)

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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 <stdio.h>
#include <string.h>
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);
}

View File

@ -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);
}

View File

@ -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 <string.h>
#include <stdio.h>
// ============================================================
// 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);
}

View File

@ -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;
}
}

View File

@ -0,0 +1,251 @@
#pragma once
// #########################################################
// # smart_meter.h - Smart Meter Monitor for Flipper Zero #
// # By: UberGuidoZ | https://github.com/UberGuidoZ/ #
// # v1.11 #
// #########################################################
#include <furi.h>
#include <furi_hal.h>
#include <furi_hal_subghz.h>
#include <datetime/datetime.h>
#include <gui/gui.h>
#include <gui/view.h>
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <gui/modules/submenu.h>
#include <input/input.h>
#include <notification/notification_messages.h>
// ------------------------------------------------------------
// 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);

View File

@ -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 <string.h>
// ============================================================
// 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B