Compare commits
5 Commits
0c1c85a60b
...
c074cebb2e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c074cebb2e | ||
|
|
7c17ddec8d | ||
|
|
e062cc24c4 | ||
|
|
e0fd8fede8 | ||
|
|
5a8af4e748 |
@ -1 +1,16 @@
|
||||
# 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.
|
||||
|
||||
148
Applications/UberGuidoZ/smart_meter_monitor/README.md
Normal 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.
|
||||
|
After Width: | Height: | Size: 664 B |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
@ -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",
|
||||
)
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
251
Applications/UberGuidoZ/smart_meter_monitor/Source/smart_meter.h
Normal 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);
|
||||
@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 371 B |