Compare commits
No commits in common. "c074cebb2eba2d2f9ea278d5759552a7809c54b6" and "0c1c85a60b0c846c6015d10b645fc4b921b9f604" have entirely different histories.
c074cebb2e
...
0c1c85a60b
@ -1,16 +1 @@
|
|||||||
# 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.
|
|
||||||
@ -1,148 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
Before Width: | Height: | Size: 664 B |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
@ -1,11 +0,0 @@
|
|||||||
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",
|
|
||||||
)
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
// ####################################################
|
|
||||||
// # 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);
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
// ####################################################
|
|
||||||
// # 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);
|
|
||||||
}
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
// ######################################################
|
|
||||||
// # 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);
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
// ####################################################
|
|
||||||
// # 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);
|
|
||||||
}
|
|
||||||
@ -1,488 +0,0 @@
|
|||||||
// ############################################################
|
|
||||||
// # 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);
|
|
||||||
}
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
// ####################################################
|
|
||||||
// # 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,251 +0,0 @@
|
|||||||
#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);
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
// ######################################################
|
|
||||||
// # 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;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 371 B |