Compare commits
8 Commits
7cf1d6ee0d
...
3782be7837
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3782be7837 | ||
|
|
a691e53f66 | ||
|
|
271786a17e | ||
|
|
2b3b4a9fa7 | ||
|
|
647729aa21 | ||
|
|
264cd9d6c5 | ||
|
|
421373f36e | ||
|
|
8fa4a625f4 |
180
Applications/UberGuidoZ/gpio_logic_analyzer/README.md
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
# GPIO Logic Analyzer v2.01 for Flipper Zero
|
||||||
|
|
||||||
|
By: UberGuidoZ | https://github.com/UberGuidoZ/Flipper
|
||||||
|
|
||||||
|
Code: [Applications/UberGuidoZ/gpio_logic_analyzer/Source](https://github.com/UberGuidoZ/Flipper/tree/main/Applications/UberGuidoZ/gpio_logic_analyzer/Source)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
1. Grab FAP from [Release folder](https://github.com/UberGuidoZ/Flipper/tree/main/Applications/UberGuidoZ/gpio_logic_analyzer/Release), build and side-load (see Building section below), or download from App Store.
|
||||||
|
2. On the Flipper, navigate to **Apps > GPIO > GPIO Logic Analyzer**.
|
||||||
|
3. Select **Instructions** to review the voltage limits and control map.
|
||||||
|
4. Select **Start Analyzer** to begin capture.
|
||||||
|
5. CSV logs are written automatically to `/ext/gpio_analyzer/` while capturing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
Screenshots of the various screens [can be found here](https://github.com/UberGuidoZ/Flipper/tree/main/Applications/UberGuidoZ/gpio_logic_analyzer/Screenshots).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main Menu
|
||||||
|
|
||||||
|
| Item | Action |
|
||||||
|
|-------------------|-----------------------------------------|
|
||||||
|
| Start Analyzer | Opens the live capture screen |
|
||||||
|
| Instructions | Four-page on-screen reference guide |
|
||||||
|
| About | Version, author, and project URL |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Analyzer Screen
|
||||||
|
|
||||||
|
```
|
||||||
|
GPIO Analyzer RUN 100Hz*
|
||||||
|
---------------------------------
|
||||||
|
A7 [~~~~|__________|~~~~] H
|
||||||
|
A6 [_______|~~~~|_______] L
|
||||||
|
A4 [____________________] L
|
||||||
|
B3 [~~~~~~~~~~~~~~~~~~~~] H
|
||||||
|
B2 [____________________] L
|
||||||
|
C3 [~~~~|_____|~~~~|____] L
|
||||||
|
C1 [_____|~~~~~~~|______] L
|
||||||
|
C0 [~~~~~~|______|~~~~~~] H
|
||||||
|
```
|
||||||
|
|
||||||
|
- Each row is one GPIO pin.
|
||||||
|
- The waveform shows the last 104 samples, scrolling left as new samples arrive.
|
||||||
|
- The newest sample is always the rightmost pixel column.
|
||||||
|
- `H` / `L` at the right edge shows the instantaneous pin state.
|
||||||
|
- Vertical lines in the waveform mark state transitions (rising/falling edges).
|
||||||
|
- `*` after the sample rate in the header means CSV logging is active.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
| Button | Action |
|
||||||
|
|-------------|---------------------------------|
|
||||||
|
| OK (short) | Toggle capture on / off |
|
||||||
|
| OK (long) | Clear waveform history |
|
||||||
|
| UP | Increase sample rate |
|
||||||
|
| DOWN | Decrease sample rate |
|
||||||
|
| BACK | Stop capture, close CSV, exit |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitored Pins
|
||||||
|
|
||||||
|
| Row | Label | Pin Name | Header Pin | Notes |
|
||||||
|
|-----|-------|----------|------------|---------------|
|
||||||
|
| 1 | A7 | PA7 | Pin 2 | 3.3V max |
|
||||||
|
| 2 | A6 | PA6 | Pin 3 | 3.3V max |
|
||||||
|
| 3 | A4 | PA4 | Pin 4 | 5V tolerant |
|
||||||
|
| 4 | B3 | PB3 | Pin 5 | 3.3V max |
|
||||||
|
| 5 | B2 | PB2 | Pin 6 | 3.3V max |
|
||||||
|
| 6 | C3 | PC3 | Pin 7 | 3.3V max |
|
||||||
|
| 7 | C1 | PC1 | Pin 15 | 3.3V max |
|
||||||
|
| 8 | C0 | PC0 | Pin 16 | 3.3V max |
|
||||||
|
|
||||||
|
All pins are configured as digital inputs with no internal pull resistor.<br>
|
||||||
|
They are returned to high-impedance analog mode when you exit the analyzer.
|
||||||
|
|
||||||
|
**Voltage warning**: Do not connect signals above 3.3V to any pin except PA4.<br>
|
||||||
|
No pull-up or pull-down resistors are enabled. Add external resistors to<br>
|
||||||
|
your circuit if the signals you are probing have undefined idle states.
|
||||||
|
|
||||||
|
***FAILURE TO FOLLOW THIS WARNING COULD CAUSE FLIPPER TO LEAK MAGIC SMOKE!***
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sample Rates
|
||||||
|
|
||||||
|
| Rate | Period | Notes |
|
||||||
|
|--------|--------|------------------------------------------------|
|
||||||
|
| 10 Hz | 100 ms | Safest for SD write overhead at any card speed |
|
||||||
|
| 50 Hz | 20 ms | Good general-purpose rate |
|
||||||
|
| 100 Hz | 10 ms | Default (balanced speed and reliability) |
|
||||||
|
| 200 Hz | 5 ms | Fast; occasional sample jitter near SD writes |
|
||||||
|
| 500 Hz | 2 ms | Best-effort; may drop samples during CSV flush |
|
||||||
|
|
||||||
|
Rates are FreeRTOS timer-based. The Flipper OS scheduler and SD write latency
|
||||||
|
can introduce jitter at high rates. For precision timing analysis, use a
|
||||||
|
dedicated logic analyzer. This tool is intended for qualitative signal
|
||||||
|
inspection and basic protocol debugging. YMMV, caveat emptor, all that.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CSV Logging
|
||||||
|
|
||||||
|
A new CSV file is created automatically each time you start capture.
|
||||||
|
|
||||||
|
**Location on SD card:**
|
||||||
|
```
|
||||||
|
/ext/gpio_analyzer/gla_YYYYMMDD_HHMMSS.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: `/ext/gpio_analyzer/gla_20240315_143022.csv`
|
||||||
|
|
||||||
|
**File format (comma-separated, CRLF line endings):**
|
||||||
|
```
|
||||||
|
sample,A7,A6,A4,B3,B2,C3,C1,C0
|
||||||
|
0,1,0,1,0,0,1,0,1
|
||||||
|
1,1,0,1,0,0,1,0,0
|
||||||
|
2,0,0,1,0,0,1,1,0
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- `sample` - monotonically incrementing sample number (starts at 0 each session).
|
||||||
|
- Pin columns - `1` = HIGH (3.3V), `0` = LOW (0V).
|
||||||
|
- Column order matches the on-screen row order (A7 top, C0 bottom).
|
||||||
|
- CRLF endings ensure correct display in Excel and LibreOffice Calc.
|
||||||
|
|
||||||
|
If the SD card is absent or the directory cannot be created, the app
|
||||||
|
continues without logging (no `*` in the header) and flashes the error LED.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Assumes repo is cloned to `gpio_logic_analyzer` and requires [ufbt](https://github.com/flipperdevices/flipperzero-ufbt):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install ufbt # install ufbt once
|
||||||
|
cd gpio_logic_analyzer
|
||||||
|
ufbt # build only (outputs build/.../gpio_logic_analyzer.fap)
|
||||||
|
ufbt launch # build and side-load over USB
|
||||||
|
```
|
||||||
|
|
||||||
|
To build inside the full firmware tree instead:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Place gpio_logic_analyzer/ under applications_user/
|
||||||
|
./fbt fap_gpio_logic_analyzer
|
||||||
|
```
|
||||||
|
|
||||||
|
To change the custom icon, drop a 10x10 pixel PNG named `gpio_analyzer_icon.png` overwriting the existing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
gpio_logic_analyzer/
|
||||||
|
application.fam -- ufbt app manifest
|
||||||
|
gpio_analyzer.h -- shared types, constants, app struct
|
||||||
|
gpio_analyzer_app.c -- entry point, alloc/free, scene handler table
|
||||||
|
scene_menu.c -- main menu (Submenu widget)
|
||||||
|
scene_analyzer.c -- GPIO capture, waveform renderer, CSV logging
|
||||||
|
scene_instructions.c -- 4-page on-screen reference (LEFT/RIGHT to page)
|
||||||
|
scene_about.c -- about screen
|
||||||
|
README.md -- this file
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
@ -1,10 +1,11 @@
|
|||||||
App(
|
App(
|
||||||
appid="gpio_logic_analyzer",
|
appid="gpio_logic_analyzer",
|
||||||
name="GPIO Logic Analyzer",
|
name="[GPIO] Logic Analyzer",
|
||||||
apptype=FlipperAppType.EXTERNAL,
|
apptype=FlipperAppType.EXTERNAL,
|
||||||
entry_point="gpio_analyzer_app",
|
entry_point="gpio_analyzer_app",
|
||||||
requires=["gui", "storage", "notification"],
|
requires=["gui", "storage", "notification"],
|
||||||
stack_size=3 * 1024,
|
stack_size=3 * 1024,
|
||||||
fap_category="GPIO",
|
fap_category="GPIO",
|
||||||
fap_version=(2, 0),
|
fap_version=(2, 1),
|
||||||
|
fap_icon="gpio_analyzer_icon.png",
|
||||||
)
|
)
|
||||||
@ -0,0 +1,197 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// ###########################################################
|
||||||
|
// # gpio_analyzer.h -GPIO Logic Analyzer for Flipper Zero #
|
||||||
|
// # By: UberGuidoZ | https://github.com/UberGuidoZ/ #
|
||||||
|
// # v2.01 #
|
||||||
|
// ###########################################################
|
||||||
|
|
||||||
|
#include <furi.h>
|
||||||
|
#include <furi_hal.h>
|
||||||
|
#include <furi_hal_gpio.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>
|
||||||
|
#include <storage/storage.h>
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Screen geometry (actual OLED pixels)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
#define GA_SCREEN_W 128U
|
||||||
|
#define GA_SCREEN_H 64U
|
||||||
|
#define GA_HEADER_H 9U // 8px text row + 1px separator
|
||||||
|
#define GA_ROW_H 7U // per-channel row height
|
||||||
|
#define GA_LABEL_X 1U // left edge of pin label column
|
||||||
|
#define GA_WAVE_X 15U // left edge of waveform column
|
||||||
|
#define GA_WAVE_W 104U // waveform pixel width (= sample history depth)
|
||||||
|
#define GA_STATE_X 120U // left edge of H/L state column
|
||||||
|
#define GA_WAVE_Y_HIGH 1U // row-relative y of HIGH sample dot
|
||||||
|
#define GA_WAVE_Y_LOW 5U // row-relative y of LOW sample dot
|
||||||
|
|
||||||
|
// Number of GPIO channels displayed
|
||||||
|
#define GA_PIN_COUNT 8U
|
||||||
|
|
||||||
|
// History buffer depth must equal waveform width for 1:1 pixel mapping.
|
||||||
|
// A _Static_assert in scene_analyzer.c enforces this at compile time.
|
||||||
|
#define GA_HISTORY_SIZE GA_WAVE_W
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Sample rate table sizing and default index
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
#define GA_RATE_COUNT 5U
|
||||||
|
#define GA_DEFAULT_RATE_IDX 2U // 100 Hz
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// CSV output
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
#define GA_CSV_DIR "/ext/gpio_analyzer"
|
||||||
|
#define GA_CSV_PATH_LEN 64U
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Instructions
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
#define GA_INSTR_PAGES 4U // total pages in the instructions view
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Scene IDs (order must match ga_scene_handlers[] in
|
||||||
|
// gpio_analyzer_app.c)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
typedef enum {
|
||||||
|
GaSceneSplash = 0,
|
||||||
|
GaSceneMenu,
|
||||||
|
GaSceneAnalyzer,
|
||||||
|
GaSceneInstructions,
|
||||||
|
GaSceneAbout,
|
||||||
|
GaSceneCount,
|
||||||
|
} GaSceneId;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// View IDs (registered with ViewDispatcher)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
typedef enum {
|
||||||
|
GaViewSplash = 0,
|
||||||
|
GaViewMenu,
|
||||||
|
GaViewAnalyzer,
|
||||||
|
GaViewInstructions,
|
||||||
|
GaViewAbout,
|
||||||
|
GaViewCount,
|
||||||
|
} GaViewId;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Custom events sent via view_dispatcher_send_custom_event()
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
typedef enum {
|
||||||
|
GaEventMenuStart = 0,
|
||||||
|
GaEventMenuInstructions = 1,
|
||||||
|
GaEventMenuAbout = 2,
|
||||||
|
} GaMenuEvent;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GaEventAnalyzerToggle = 0,
|
||||||
|
GaEventAnalyzerClearHistory = 1,
|
||||||
|
GaEventAnalyzerRateUp = 2,
|
||||||
|
GaEventAnalyzerRateDown = 3,
|
||||||
|
} GaAnalyzerEvent;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GaEventInstrPageLeft = 0,
|
||||||
|
GaEventInstrPageRight = 1,
|
||||||
|
} GaInstrEvent;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Per-channel capture state
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
typedef struct {
|
||||||
|
bool current_state;
|
||||||
|
uint8_t history[GA_HISTORY_SIZE];
|
||||||
|
uint32_t history_head;
|
||||||
|
uint32_t sample_count;
|
||||||
|
} GaChannel;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Forward declaration - allows view model structs below to
|
||||||
|
// hold a GpioAnalyzer* before the full struct is defined.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
typedef struct GpioAnalyzer_s GpioAnalyzer;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// View models
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
// Analyzer view: the draw callback reads channel data through app.
|
||||||
|
typedef struct {
|
||||||
|
GpioAnalyzer* app;
|
||||||
|
} GaAnalyzerModel;
|
||||||
|
|
||||||
|
// Instructions view: tracks which page is displayed.
|
||||||
|
typedef struct {
|
||||||
|
uint8_t page;
|
||||||
|
} GaInstrModel;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Full application state struct
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
struct GpioAnalyzer_s {
|
||||||
|
// Scene / view management
|
||||||
|
SceneManager* scene_manager;
|
||||||
|
ViewDispatcher* view_dispatcher;
|
||||||
|
Submenu* submenu;
|
||||||
|
View* view_splash;
|
||||||
|
View* view_analyzer;
|
||||||
|
View* view_instructions;
|
||||||
|
View* view_about;
|
||||||
|
|
||||||
|
// GPIO capture
|
||||||
|
FuriMutex* mutex;
|
||||||
|
FuriTimer* sample_timer;
|
||||||
|
FuriTimer* splash_timer; // one-shot auto-advance timer
|
||||||
|
GaChannel channels[GA_PIN_COUNT];
|
||||||
|
bool running;
|
||||||
|
uint8_t rate_index;
|
||||||
|
uint32_t sample_number;
|
||||||
|
|
||||||
|
// CSV logging
|
||||||
|
Storage* storage;
|
||||||
|
File* csv_file;
|
||||||
|
bool csv_open;
|
||||||
|
char csv_path[GA_CSV_PATH_LEN];
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
NotificationApp* notifications;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Scene handler declarations (implemented in each scene_*.c)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
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_analyzer_on_enter(void* context);
|
||||||
|
bool scene_analyzer_on_event(void* context, SceneManagerEvent event);
|
||||||
|
void scene_analyzer_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 ga_alloc)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
void scene_splash_setup_view(GpioAnalyzer* app);
|
||||||
|
void scene_analyzer_setup_view(GpioAnalyzer* app);
|
||||||
|
void scene_instructions_setup_view(GpioAnalyzer* app);
|
||||||
|
void scene_about_setup_view(GpioAnalyzer* app);
|
||||||
@ -0,0 +1,226 @@
|
|||||||
|
// #########################################################
|
||||||
|
// # gpio_analyzer_app.c - entry point and app lifecycle #
|
||||||
|
// # By: UberGuidoZ | https://github.com/UberGuidoZ/ #
|
||||||
|
// # v2.01 #
|
||||||
|
// #########################################################
|
||||||
|
|
||||||
|
#include "gpio_analyzer.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Scene handler tables
|
||||||
|
// The current Flipper SDK SceneManagerHandlers struct uses three
|
||||||
|
// separate arrays (on_enter, on_event, on_exit) rather than a
|
||||||
|
// single array of per-scene structs. Order must match GaSceneId.
|
||||||
|
// ============================================================
|
||||||
|
static void (*const ga_on_enter_handlers[])(void*) = {
|
||||||
|
scene_splash_on_enter,
|
||||||
|
scene_menu_on_enter,
|
||||||
|
scene_analyzer_on_enter,
|
||||||
|
scene_instructions_on_enter,
|
||||||
|
scene_about_on_enter,
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool (*const ga_on_event_handlers[])(void*, SceneManagerEvent) = {
|
||||||
|
scene_splash_on_event,
|
||||||
|
scene_menu_on_event,
|
||||||
|
scene_analyzer_on_event,
|
||||||
|
scene_instructions_on_event,
|
||||||
|
scene_about_on_event,
|
||||||
|
};
|
||||||
|
|
||||||
|
static void (*const ga_on_exit_handlers[])(void*) = {
|
||||||
|
scene_splash_on_exit,
|
||||||
|
scene_menu_on_exit,
|
||||||
|
scene_analyzer_on_exit,
|
||||||
|
scene_instructions_on_exit,
|
||||||
|
scene_about_on_exit,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const SceneManagerHandlers ga_scene_manager_handlers = {
|
||||||
|
.on_enter_handlers = ga_on_enter_handlers,
|
||||||
|
.on_event_handlers = ga_on_event_handlers,
|
||||||
|
.on_exit_handlers = ga_on_exit_handlers,
|
||||||
|
.scene_num = GaSceneCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ViewDispatcher callbacks
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Routes custom events (button/UI actions) to the active scene.
|
||||||
|
static bool ga_custom_event_cb(void* context, uint32_t event) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
return scene_manager_handle_custom_event(app->scene_manager, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes BACK presses to the active scene; scene manager pops
|
||||||
|
// the stack on unhandled backs. Returning false from the last
|
||||||
|
// scene causes ViewDispatcher to stop its run loop.
|
||||||
|
static bool ga_back_event_cb(void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
return scene_manager_handle_back_event(app->scene_manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Allocate and initialise all app resources
|
||||||
|
// ============================================================
|
||||||
|
static GpioAnalyzer* ga_alloc(void) {
|
||||||
|
GpioAnalyzer* app = malloc(sizeof(GpioAnalyzer));
|
||||||
|
furi_check(app != NULL);
|
||||||
|
memset(app, 0, sizeof(GpioAnalyzer));
|
||||||
|
|
||||||
|
app->rate_index = GA_DEFAULT_RATE_IDX;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Scene manager
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
app->scene_manager = scene_manager_alloc(&ga_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, ga_custom_event_cb);
|
||||||
|
view_dispatcher_set_navigation_event_callback(app->view_dispatcher, ga_back_event_cb);
|
||||||
|
|
||||||
|
// Attach to the GUI fullscreen layer.
|
||||||
|
Gui* gui = furi_record_open(RECORD_GUI);
|
||||||
|
view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
|
||||||
|
furi_record_close(RECORD_GUI);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Submenu (main menu view)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
app->submenu = submenu_alloc();
|
||||||
|
furi_check(app->submenu != NULL);
|
||||||
|
view_dispatcher_add_view(
|
||||||
|
app->view_dispatcher, GaViewMenu, submenu_get_view(app->submenu));
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Custom views (each scene_*.c sets up draw/input callbacks)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
scene_splash_setup_view(app);
|
||||||
|
view_dispatcher_add_view(app->view_dispatcher, GaViewSplash, app->view_splash);
|
||||||
|
|
||||||
|
scene_analyzer_setup_view(app);
|
||||||
|
view_dispatcher_add_view(app->view_dispatcher, GaViewAnalyzer, app->view_analyzer);
|
||||||
|
|
||||||
|
scene_instructions_setup_view(app);
|
||||||
|
view_dispatcher_add_view(app->view_dispatcher, GaViewInstructions, app->view_instructions);
|
||||||
|
|
||||||
|
scene_about_setup_view(app);
|
||||||
|
view_dispatcher_add_view(app->view_dispatcher, GaViewAbout, app->view_about);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Capture primitives (timer and mutex allocated once)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||||
|
furi_check(app->mutex != NULL);
|
||||||
|
|
||||||
|
// Timer is started/stopped by the analyzer scene.
|
||||||
|
// The timer callback is registered in scene_analyzer.c.
|
||||||
|
// We store only the handle here; setup is in scene_analyzer.c.
|
||||||
|
// (The timer is allocated there so the callback symbol stays local.)
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Storage and CSV file object
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
app->storage = furi_record_open(RECORD_STORAGE);
|
||||||
|
furi_check(app->storage != NULL);
|
||||||
|
|
||||||
|
app->csv_file = storage_file_alloc(app->storage);
|
||||||
|
furi_check(app->csv_file != NULL);
|
||||||
|
|
||||||
|
// Ensure the output directory exists on the SD card.
|
||||||
|
// FSE_EXIST is a success condition (directory already present).
|
||||||
|
FS_Error dir_err = storage_common_mkdir(app->storage, GA_CSV_DIR);
|
||||||
|
if(dir_err != FSE_OK && dir_err != FSE_EXIST) {
|
||||||
|
// Non-fatal: CSV open will simply fail later if the dir is absent.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Notifications
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
app->notifications = furi_record_open(RECORD_NOTIFICATION);
|
||||||
|
furi_check(app->notifications != NULL);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Free all resources in reverse-allocation order
|
||||||
|
// ============================================================
|
||||||
|
static void ga_free(GpioAnalyzer* app) {
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
// Timer was allocated in scene_analyzer.c and must be freed
|
||||||
|
// before the mutex it may reference.
|
||||||
|
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);
|
||||||
|
|
||||||
|
if(app->csv_file != NULL) {
|
||||||
|
if(app->csv_open) {
|
||||||
|
storage_file_close(app->csv_file);
|
||||||
|
app->csv_open = false;
|
||||||
|
}
|
||||||
|
storage_file_free(app->csv_file);
|
||||||
|
app->csv_file = NULL;
|
||||||
|
}
|
||||||
|
furi_record_close(RECORD_STORAGE);
|
||||||
|
|
||||||
|
furi_mutex_free(app->mutex);
|
||||||
|
|
||||||
|
// Remove views before freeing view dispatcher.
|
||||||
|
view_dispatcher_remove_view(app->view_dispatcher, GaViewAbout);
|
||||||
|
view_dispatcher_remove_view(app->view_dispatcher, GaViewInstructions);
|
||||||
|
view_dispatcher_remove_view(app->view_dispatcher, GaViewAnalyzer);
|
||||||
|
view_dispatcher_remove_view(app->view_dispatcher, GaViewMenu);
|
||||||
|
view_dispatcher_remove_view(app->view_dispatcher, GaViewSplash);
|
||||||
|
|
||||||
|
view_free(app->view_about);
|
||||||
|
view_free(app->view_instructions);
|
||||||
|
view_free(app->view_analyzer);
|
||||||
|
view_free(app->view_splash);
|
||||||
|
submenu_free(app->submenu);
|
||||||
|
|
||||||
|
view_dispatcher_free(app->view_dispatcher);
|
||||||
|
scene_manager_free(app->scene_manager);
|
||||||
|
|
||||||
|
free(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Application entry point
|
||||||
|
// ============================================================
|
||||||
|
int32_t gpio_analyzer_app(void* p) {
|
||||||
|
UNUSED(p);
|
||||||
|
|
||||||
|
GpioAnalyzer* app = ga_alloc();
|
||||||
|
|
||||||
|
// Push the main menu scene; ViewDispatcher run() blocks until
|
||||||
|
// view_dispatcher_stop() is called (by the back handler on the
|
||||||
|
// root menu scene returning false).
|
||||||
|
scene_manager_next_scene(app->scene_manager, GaSceneSplash);
|
||||||
|
view_dispatcher_run(app->view_dispatcher);
|
||||||
|
|
||||||
|
ga_free(app);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 371 B |
|
After Width: | Height: | Size: 371 B |
@ -0,0 +1,91 @@
|
|||||||
|
// #####################################################
|
||||||
|
// # scene_about.c - about screen #
|
||||||
|
// # By: UberGuidoZ | https://github.com/UberGuidoZ/ #
|
||||||
|
// # v2.01 #
|
||||||
|
// #####################################################
|
||||||
|
|
||||||
|
#include "gpio_analyzer.h"
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Draw callback - static content, no model needed
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void ga_about_draw_cb(Canvas* canvas, void* model) {
|
||||||
|
UNUSED(model);
|
||||||
|
canvas_clear(canvas);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Title - FontPrimary. Measure actual pixel widths at runtime
|
||||||
|
// so centering is exact regardless of font metrics.
|
||||||
|
// Box is sized from the wider string + 8px padding each side.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
canvas_set_font(canvas, FontPrimary);
|
||||||
|
size_t w1 = canvas_string_width(canvas, "GPIO Logic");
|
||||||
|
size_t w2 = canvas_string_width(canvas, "Analyzer");
|
||||||
|
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, "GPIO Logic");
|
||||||
|
canvas_draw_str(canvas, (int32_t)((128U - w2) / 2U), 34, "Analyzer");
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Separator between title box and byline.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
canvas_draw_line(canvas, 10, 44, 118, 44);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Byline - FontKeyboard (~6px/char, ~8px tall).
|
||||||
|
// "v2.01 by UberGuidoZ" = 19 chars * 6 = 114px, x=(128-114)/2 = 7
|
||||||
|
// "github.com/UberGuidoZ" = 21 chars * 6 = 126px, x=(128-126)/2 = 1
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
canvas_set_font(canvas, FontKeyboard);
|
||||||
|
canvas_draw_str(canvas, 7, 54, "v2.01 by UberGuidoZ");
|
||||||
|
canvas_draw_str(canvas, 1, 62, "github.com/UberGuidoZ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Input callback - no local inputs; BACK is handled by the
|
||||||
|
// ViewDispatcher navigation callback (pops the scene).
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static bool ga_about_input_cb(InputEvent* event, void* context) {
|
||||||
|
UNUSED(event);
|
||||||
|
UNUSED(context);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// View setup (called once from ga_alloc)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void scene_about_setup_view(GpioAnalyzer* app) {
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
app->view_about = view_alloc();
|
||||||
|
furi_check(app->view_about != NULL);
|
||||||
|
|
||||||
|
view_set_draw_callback(app->view_about, ga_about_draw_cb);
|
||||||
|
view_set_input_callback(app->view_about, ga_about_input_cb);
|
||||||
|
// No model needed for static content.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Scene handlers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void scene_about_on_enter(void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
view_dispatcher_switch_to_view(app->view_dispatcher, GaViewAbout);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,508 @@
|
|||||||
|
// #########################################################
|
||||||
|
// # scene_analyzer.c - capture, render, and CSV logging #
|
||||||
|
// # By: UberGuidoZ | https://github.com/UberGuidoZ/ #
|
||||||
|
// # v2.01 #
|
||||||
|
// #########################################################
|
||||||
|
//
|
||||||
|
// GPIO capture runs on a FuriTimer (FreeRTOS timer service task).
|
||||||
|
// The draw callback runs on the GUI thread. A FuriMutex guards
|
||||||
|
// all shared channel and CSV state.
|
||||||
|
//
|
||||||
|
// CONTROLS (while analyzer is on screen):
|
||||||
|
// OK (short) - toggle capture on / off
|
||||||
|
// OK (long) - clear waveform history
|
||||||
|
// UP - increase sample rate
|
||||||
|
// DOWN - decrease sample rate
|
||||||
|
// BACK - stop capture, close CSV, return to menu
|
||||||
|
//
|
||||||
|
// ##############################################################
|
||||||
|
|
||||||
|
#include "gpio_analyzer.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
// Enforce 1:1 pixel-to-sample mapping required by the renderer.
|
||||||
|
_Static_assert(
|
||||||
|
GA_HISTORY_SIZE == GA_WAVE_W,
|
||||||
|
"GA_HISTORY_SIZE must equal GA_WAVE_W for 1:1 pixel mapping");
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// GPIO pin table (rows displayed top-to-bottom)
|
||||||
|
// ============================================================
|
||||||
|
static const GpioPin* const k_pins[GA_PIN_COUNT] = {
|
||||||
|
&gpio_ext_pa7,
|
||||||
|
&gpio_ext_pa6,
|
||||||
|
&gpio_ext_pa4,
|
||||||
|
&gpio_ext_pb3,
|
||||||
|
&gpio_ext_pb2,
|
||||||
|
&gpio_ext_pc3,
|
||||||
|
&gpio_ext_pc1,
|
||||||
|
&gpio_ext_pc0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* const k_pin_labels[GA_PIN_COUNT] = {
|
||||||
|
"A7", "A6", "A4", "B3", "B2", "C3", "C1", "C0",
|
||||||
|
};
|
||||||
|
|
||||||
|
// CSV column header names (must match k_pin_labels order)
|
||||||
|
static const char* const k_csv_headers[GA_PIN_COUNT] = {
|
||||||
|
"A7", "A6", "A4", "B3", "B2", "C3", "C1", "C0",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pre-computed top-pixel y for each channel row.
|
||||||
|
static const uint8_t k_row_top[GA_PIN_COUNT] = {
|
||||||
|
GA_HEADER_H + 0U * GA_ROW_H, // 9
|
||||||
|
GA_HEADER_H + 1U * GA_ROW_H, // 16
|
||||||
|
GA_HEADER_H + 2U * GA_ROW_H, // 23
|
||||||
|
GA_HEADER_H + 3U * GA_ROW_H, // 30
|
||||||
|
GA_HEADER_H + 4U * GA_ROW_H, // 37
|
||||||
|
GA_HEADER_H + 5U * GA_ROW_H, // 44
|
||||||
|
GA_HEADER_H + 6U * GA_ROW_H, // 51
|
||||||
|
GA_HEADER_H + 7U * GA_ROW_H, // 58
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Sample rate table
|
||||||
|
// ============================================================
|
||||||
|
static const uint32_t k_sample_rates[GA_RATE_COUNT] = {
|
||||||
|
10U, // 10 Hz -- 100 ms
|
||||||
|
50U, // 50 Hz -- 20 ms
|
||||||
|
100U, // 100 Hz -- 10 ms
|
||||||
|
200U, // 200 Hz -- 5 ms
|
||||||
|
500U, // 500 Hz -- 2 ms
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* const k_rate_labels[GA_RATE_COUNT] = {
|
||||||
|
" 10Hz",
|
||||||
|
" 50Hz",
|
||||||
|
"100Hz",
|
||||||
|
"200Hz",
|
||||||
|
"500Hz",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Internal helpers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static uint32_t ga_period_ms(const GpioAnalyzer* app) {
|
||||||
|
uint32_t period = 1000U / k_sample_rates[app->rate_index];
|
||||||
|
return (period < 1U) ? 1U : period;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ga_gpio_init(void) {
|
||||||
|
for(uint8_t i = 0U; i < GA_PIN_COUNT; i++) {
|
||||||
|
furi_hal_gpio_init(k_pins[i], GpioModeInput, GpioPullNo, GpioSpeedLow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ga_gpio_deinit(void) {
|
||||||
|
// Return pins to high-impedance analog to avoid back-driving
|
||||||
|
// devices connected to the header after the app exits.
|
||||||
|
for(uint8_t i = 0U; i < GA_PIN_COUNT; i++) {
|
||||||
|
furi_hal_gpio_init(k_pins[i], GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clears the sample history for all channels. Caller must NOT hold
|
||||||
|
// the mutex. This function acquires and releases it internally.
|
||||||
|
static void ga_clear_history(GpioAnalyzer* app) {
|
||||||
|
furi_check(app != NULL);
|
||||||
|
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||||
|
for(uint8_t i = 0U; i < GA_PIN_COUNT; i++) {
|
||||||
|
memset(app->channels[i].history, 0, sizeof(app->channels[i].history));
|
||||||
|
app->channels[i].history_head = 0U;
|
||||||
|
app->channels[i].sample_count = 0U;
|
||||||
|
app->channels[i].current_state = false;
|
||||||
|
}
|
||||||
|
furi_mutex_release(app->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// CSV helpers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void ga_csv_open(GpioAnalyzer* app) {
|
||||||
|
furi_check(app != NULL);
|
||||||
|
if(app->csv_open) return;
|
||||||
|
|
||||||
|
// Build timestamped filename using the RTC.
|
||||||
|
DateTime dt;
|
||||||
|
furi_hal_rtc_get_datetime(&dt);
|
||||||
|
snprintf(
|
||||||
|
app->csv_path,
|
||||||
|
sizeof(app->csv_path),
|
||||||
|
GA_CSV_DIR "/gla_%04d%02d%02d_%02d%02d%02d.csv",
|
||||||
|
(int)dt.year,
|
||||||
|
(int)dt.month,
|
||||||
|
(int)dt.day,
|
||||||
|
(int)dt.hour,
|
||||||
|
(int)dt.minute,
|
||||||
|
(int)dt.second);
|
||||||
|
|
||||||
|
if(!storage_file_open(
|
||||||
|
app->csv_file, app->csv_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
||||||
|
// No SD card or permission error = continue without CSV.
|
||||||
|
notification_message(app->notifications, &sequence_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write header row: "sample,A7,A6,..." followed by CRLF for
|
||||||
|
// maximum compatibility with spreadsheet applications.
|
||||||
|
char header[48];
|
||||||
|
int hlen = snprintf(
|
||||||
|
header,
|
||||||
|
sizeof(header),
|
||||||
|
"sample,%s,%s,%s,%s,%s,%s,%s,%s\r\n",
|
||||||
|
k_csv_headers[0], k_csv_headers[1], k_csv_headers[2],
|
||||||
|
k_csv_headers[3], k_csv_headers[4], k_csv_headers[5],
|
||||||
|
k_csv_headers[6], k_csv_headers[7]);
|
||||||
|
if(hlen > 0 && hlen < (int)sizeof(header)) {
|
||||||
|
storage_file_write(app->csv_file, header, (uint16_t)hlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
app->csv_open = true;
|
||||||
|
app->sample_number = 0U;
|
||||||
|
notification_message(app->notifications, &sequence_success);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ga_csv_close(GpioAnalyzer* app) {
|
||||||
|
furi_check(app != NULL);
|
||||||
|
if(!app->csv_open) return;
|
||||||
|
storage_file_close(app->csv_file);
|
||||||
|
app->csv_open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes one CSV data row. Called from the timer callback while
|
||||||
|
// the mutex is already held by the caller.
|
||||||
|
static void ga_csv_write_row(GpioAnalyzer* app) {
|
||||||
|
if(!app->csv_open || app->csv_file == NULL) return;
|
||||||
|
|
||||||
|
char line[48];
|
||||||
|
int len = snprintf(
|
||||||
|
line,
|
||||||
|
sizeof(line),
|
||||||
|
"%lu,%d,%d,%d,%d,%d,%d,%d,%d\r\n",
|
||||||
|
(unsigned long)app->sample_number,
|
||||||
|
(int)app->channels[0].current_state,
|
||||||
|
(int)app->channels[1].current_state,
|
||||||
|
(int)app->channels[2].current_state,
|
||||||
|
(int)app->channels[3].current_state,
|
||||||
|
(int)app->channels[4].current_state,
|
||||||
|
(int)app->channels[5].current_state,
|
||||||
|
(int)app->channels[6].current_state,
|
||||||
|
(int)app->channels[7].current_state);
|
||||||
|
if(len > 0 && len < (int)sizeof(line)) {
|
||||||
|
storage_file_write(app->csv_file, line, (uint16_t)len);
|
||||||
|
}
|
||||||
|
app->sample_number++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Capture start / stop
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void ga_start_capture(GpioAnalyzer* app) {
|
||||||
|
furi_check(app != NULL);
|
||||||
|
if(app->running) return;
|
||||||
|
ga_csv_open(app);
|
||||||
|
app->running = true;
|
||||||
|
furi_timer_start(app->sample_timer, ga_period_ms(app));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ga_stop_capture(GpioAnalyzer* app) {
|
||||||
|
furi_check(app != NULL);
|
||||||
|
if(!app->running) return;
|
||||||
|
furi_timer_stop(app->sample_timer);
|
||||||
|
app->running = false;
|
||||||
|
ga_csv_close(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ga_apply_rate_change(GpioAnalyzer* app) {
|
||||||
|
if(app->running) {
|
||||||
|
furi_timer_stop(app->sample_timer);
|
||||||
|
furi_timer_start(app->sample_timer, ga_period_ms(app));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Timer callback (FreeRTOS timer service task)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void ga_sample_timer_cb(void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
// Skip this tick if the draw callback is holding the mutex,
|
||||||
|
// rather than stalling the timer service task.
|
||||||
|
if(furi_mutex_acquire(app->mutex, 0U) != FuriStatusOk) return;
|
||||||
|
|
||||||
|
for(uint8_t i = 0U; i < GA_PIN_COUNT; i++) {
|
||||||
|
bool state = furi_hal_gpio_read(k_pins[i]);
|
||||||
|
app->channels[i].current_state = state;
|
||||||
|
|
||||||
|
uint32_t pos = app->channels[i].history_head;
|
||||||
|
app->channels[i].history[pos] = state ? 1U : 0U;
|
||||||
|
app->channels[i].history_head = (pos + 1U) % GA_HISTORY_SIZE;
|
||||||
|
if(app->channels[i].sample_count < GA_HISTORY_SIZE) {
|
||||||
|
app->channels[i].sample_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ga_csv_write_row(app);
|
||||||
|
|
||||||
|
furi_mutex_release(app->mutex);
|
||||||
|
|
||||||
|
// Signal the ViewDispatcher to redraw the analyzer view.
|
||||||
|
// view_commit_model with update=true posts a paint message to
|
||||||
|
// the GUI thread without acquiring the view model lock.
|
||||||
|
view_commit_model(app->view_analyzer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Draw callback (GUI thread)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void ga_analyzer_draw_cb(Canvas* canvas, void* model) {
|
||||||
|
GaAnalyzerModel* m = (GaAnalyzerModel*)model;
|
||||||
|
if(m == NULL || m->app == NULL) return;
|
||||||
|
GpioAnalyzer* app = m->app;
|
||||||
|
|
||||||
|
// Use a short timeout so a slow timer never stalls the GUI.
|
||||||
|
if(furi_mutex_acquire(app->mutex, 25U) != FuriStatusOk) return;
|
||||||
|
|
||||||
|
canvas_clear(canvas);
|
||||||
|
canvas_set_font(canvas, FontSecondary);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Header bar
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
canvas_draw_str(canvas, (int32_t)GA_LABEL_X, 7, "GPIO Analyzer");
|
||||||
|
|
||||||
|
// Status: RUN/STP + rate + CSV indicator (* when logging).
|
||||||
|
char status[20];
|
||||||
|
snprintf(
|
||||||
|
status,
|
||||||
|
sizeof(status),
|
||||||
|
"%s %s%s",
|
||||||
|
app->running ? "RUN" : "STP",
|
||||||
|
k_rate_labels[app->rate_index],
|
||||||
|
app->csv_open ? "*" : " ");
|
||||||
|
|
||||||
|
// Rough right-align: FontSecondary glyphs are ~6px wide.
|
||||||
|
uint8_t sw = (uint8_t)(strlen(status) * 6U);
|
||||||
|
int32_t sx = (int32_t)GA_SCREEN_W - (int32_t)sw - 1;
|
||||||
|
if(sx < 0) sx = 0;
|
||||||
|
canvas_draw_str(canvas, sx, 7, status);
|
||||||
|
|
||||||
|
// Separator line below header.
|
||||||
|
canvas_draw_line(
|
||||||
|
canvas, 0, (int32_t)(GA_HEADER_H - 1U),
|
||||||
|
(int32_t)(GA_SCREEN_W - 1U), (int32_t)(GA_HEADER_H - 1U));
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Channel rows
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
canvas_set_font(canvas, FontKeyboard);
|
||||||
|
|
||||||
|
for(uint8_t i = 0U; i < GA_PIN_COUNT; i++) {
|
||||||
|
const GaChannel* ch = &app->channels[i];
|
||||||
|
uint8_t ry = k_row_top[i];
|
||||||
|
uint8_t tb = ry + GA_ROW_H - 1U;
|
||||||
|
uint8_t yhi = ry + GA_WAVE_Y_HIGH;
|
||||||
|
uint8_t ylo = ry + GA_WAVE_Y_LOW;
|
||||||
|
|
||||||
|
// Pin label (left column)
|
||||||
|
canvas_draw_str(canvas, (int32_t)GA_LABEL_X, (int32_t)tb, k_pin_labels[i]);
|
||||||
|
|
||||||
|
// State indicator (right column).
|
||||||
|
// FontKeyboard glyphs are ~8px tall but rows are only 7px,
|
||||||
|
// causing text to bleed into the row above. Instead, draw
|
||||||
|
// a pixel-precise 5x5 shape that fits cleanly within the row:
|
||||||
|
// filled box = HIGH, outline box = LOW.
|
||||||
|
// Box occupies y = ry+1 to ry+5, well within ry..ry+6.
|
||||||
|
if(ch->current_state) {
|
||||||
|
canvas_draw_box(canvas, (int32_t)GA_STATE_X + 1, (int32_t)ry + 1, 5, 5);
|
||||||
|
} else {
|
||||||
|
canvas_draw_frame(canvas, (int32_t)GA_STATE_X + 1, (int32_t)ry + 1, 5, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Waveform renderer
|
||||||
|
//
|
||||||
|
// The circular buffer holds up to GA_HISTORY_SIZE samples.
|
||||||
|
// When full: oldest = history[history_head]
|
||||||
|
// newest = history[(history_head - 1 + SIZE) % SIZE]
|
||||||
|
// When not full: oldest = history[0], newest = history[sample_count-1]
|
||||||
|
//
|
||||||
|
// We display min(sample_count, GA_WAVE_W) samples L-to-R,
|
||||||
|
// with the newest sample always at the rightmost pixel column.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
uint32_t count = ch->sample_count;
|
||||||
|
uint32_t head = ch->history_head;
|
||||||
|
uint32_t show = (count < (uint32_t)GA_WAVE_W) ? count : (uint32_t)GA_WAVE_W;
|
||||||
|
uint32_t skip = (uint32_t)GA_WAVE_W - show;
|
||||||
|
uint32_t oldest_idx = (count < GA_HISTORY_SIZE) ? 0U : head;
|
||||||
|
uint8_t prev_val = 0xFFU; // sentinel: no prior sample
|
||||||
|
|
||||||
|
for(uint32_t s = skip; s < (uint32_t)GA_WAVE_W; s++) {
|
||||||
|
uint32_t age = s - skip;
|
||||||
|
uint32_t sample_idx = (oldest_idx + age) % GA_HISTORY_SIZE;
|
||||||
|
uint8_t val = ch->history[sample_idx];
|
||||||
|
uint8_t px = (uint8_t)((uint32_t)GA_WAVE_X + s);
|
||||||
|
uint8_t py = val ? yhi : ylo;
|
||||||
|
|
||||||
|
// Draw vertical transition edge at rising and falling edges.
|
||||||
|
if(prev_val != 0xFFU && prev_val != val) {
|
||||||
|
canvas_draw_line(
|
||||||
|
canvas, (int32_t)px, (int32_t)yhi,
|
||||||
|
(int32_t)px, (int32_t)ylo);
|
||||||
|
}
|
||||||
|
canvas_draw_dot(canvas, (int32_t)px, (int32_t)py);
|
||||||
|
prev_val = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
furi_mutex_release(app->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Input callback (GUI thread)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static bool ga_analyzer_input_cb(InputEvent* event, void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
bool consumed = false;
|
||||||
|
|
||||||
|
if(event->type == InputTypeShort) {
|
||||||
|
switch(event->key) {
|
||||||
|
case InputKeyOk:
|
||||||
|
view_dispatcher_send_custom_event(
|
||||||
|
app->view_dispatcher, GaEventAnalyzerToggle);
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
case InputKeyUp:
|
||||||
|
view_dispatcher_send_custom_event(
|
||||||
|
app->view_dispatcher, GaEventAnalyzerRateUp);
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
case InputKeyDown:
|
||||||
|
view_dispatcher_send_custom_event(
|
||||||
|
app->view_dispatcher, GaEventAnalyzerRateDown);
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if(event->type == InputTypeLong && event->key == InputKeyOk) {
|
||||||
|
view_dispatcher_send_custom_event(
|
||||||
|
app->view_dispatcher, GaEventAnalyzerClearHistory);
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// View setup (called once from ga_alloc)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void scene_analyzer_setup_view(GpioAnalyzer* app) {
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
app->view_analyzer = view_alloc();
|
||||||
|
furi_check(app->view_analyzer != NULL);
|
||||||
|
|
||||||
|
view_set_draw_callback(app->view_analyzer, ga_analyzer_draw_cb);
|
||||||
|
view_set_input_callback(app->view_analyzer, ga_analyzer_input_cb);
|
||||||
|
view_set_context(app->view_analyzer, app);
|
||||||
|
|
||||||
|
// LockFree model: just a pointer to app. We protect the actual
|
||||||
|
// channel data with app->mutex rather than the view model lock.
|
||||||
|
view_allocate_model(
|
||||||
|
app->view_analyzer, ViewModelTypeLockFree, sizeof(GaAnalyzerModel));
|
||||||
|
|
||||||
|
GaAnalyzerModel* m = view_get_model(app->view_analyzer);
|
||||||
|
if(m != NULL) m->app = app;
|
||||||
|
view_commit_model(app->view_analyzer, false);
|
||||||
|
|
||||||
|
// Allocate the periodic sample timer here so the callback symbol
|
||||||
|
// stays file-local. The timer is started/stopped by the scene.
|
||||||
|
app->sample_timer = furi_timer_alloc(ga_sample_timer_cb, FuriTimerTypePeriodic, app);
|
||||||
|
furi_check(app->sample_timer != NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Scene handlers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void scene_analyzer_on_enter(void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
ga_gpio_init();
|
||||||
|
ga_clear_history(app);
|
||||||
|
ga_start_capture(app);
|
||||||
|
|
||||||
|
view_dispatcher_switch_to_view(app->view_dispatcher, GaViewAnalyzer);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool scene_analyzer_on_event(void* context, SceneManagerEvent event) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
bool consumed = false;
|
||||||
|
|
||||||
|
if(event.type == SceneManagerEventTypeCustom) {
|
||||||
|
switch(event.event) {
|
||||||
|
case GaEventAnalyzerToggle:
|
||||||
|
if(app->running) {
|
||||||
|
ga_stop_capture(app);
|
||||||
|
} else {
|
||||||
|
ga_start_capture(app);
|
||||||
|
}
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GaEventAnalyzerClearHistory:
|
||||||
|
// Stop the timer while clearing to prevent a partial-clear
|
||||||
|
// race. Resume immediately after if capture was running.
|
||||||
|
if(app->running) furi_timer_stop(app->sample_timer);
|
||||||
|
ga_clear_history(app);
|
||||||
|
if(app->running) furi_timer_start(app->sample_timer, ga_period_ms(app));
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GaEventAnalyzerRateUp:
|
||||||
|
if(app->rate_index < GA_RATE_COUNT - 1U) {
|
||||||
|
app->rate_index++;
|
||||||
|
ga_apply_rate_change(app);
|
||||||
|
}
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GaEventAnalyzerRateDown:
|
||||||
|
if(app->rate_index > 0U) {
|
||||||
|
app->rate_index--;
|
||||||
|
ga_apply_rate_change(app);
|
||||||
|
}
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scene_analyzer_on_exit(void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
ga_stop_capture(app);
|
||||||
|
ga_gpio_deinit();
|
||||||
|
// History is intentionally NOT cleared here. If the user
|
||||||
|
// navigates back and re-enters, they see a blank screen anyway
|
||||||
|
// because ga_clear_history() is called in on_enter.
|
||||||
|
}
|
||||||
@ -0,0 +1,223 @@
|
|||||||
|
// #######################################################
|
||||||
|
// # scene_instructions.c - 4-page on-screen reference #
|
||||||
|
// # By: UberGuidoZ | https://github.com/UberGuidoZ/ #
|
||||||
|
// # v2.01 #
|
||||||
|
// #######################################################
|
||||||
|
//
|
||||||
|
// Page 1: Voltage & Safety
|
||||||
|
// Page 2: Display Guide
|
||||||
|
// Page 3: Controls
|
||||||
|
// Page 4: CSV Logging
|
||||||
|
//
|
||||||
|
// LEFT - previous page
|
||||||
|
// RIGHT - next page
|
||||||
|
// BACK - return to menu
|
||||||
|
//
|
||||||
|
// ##############################################################
|
||||||
|
|
||||||
|
#include "gpio_analyzer.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Page content
|
||||||
|
// Each page: title + up to GA_INSTR_BODY_LINES body lines.
|
||||||
|
// Lines are rendered with FontKeyboard (~4px per char, 6px line pitch).
|
||||||
|
// Max safe line length: ~30 chars.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#define GA_INSTR_BODY_LINES 5U
|
||||||
|
|
||||||
|
static const char* const k_page_titles[GA_INSTR_PAGES] = {
|
||||||
|
"Voltage & Safety",
|
||||||
|
"Display Guide",
|
||||||
|
"Controls",
|
||||||
|
"CSV Logging",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* const k_page_body[GA_INSTR_PAGES][GA_INSTR_BODY_LINES] = {
|
||||||
|
// Page 0: Voltage & Safety (max 21 chars)
|
||||||
|
{
|
||||||
|
"3.3V MAX on all pins",
|
||||||
|
"PA4: 5V tolerant",
|
||||||
|
"Others: 3.3V ONLY",
|
||||||
|
"No pull resistors",
|
||||||
|
"Never short to VCC!",
|
||||||
|
},
|
||||||
|
// Page 1: Display Guide (max 21 chars)
|
||||||
|
{
|
||||||
|
"Each row = one pin",
|
||||||
|
"Labels: A7,A6...C0",
|
||||||
|
"Waveform scrolls left",
|
||||||
|
"Filled box = HIGH",
|
||||||
|
"Outline box = LOW",
|
||||||
|
},
|
||||||
|
// Page 2: Controls (max 21 chars)
|
||||||
|
{
|
||||||
|
"OK start/stop",
|
||||||
|
"OK hold clr history",
|
||||||
|
"UP/DOWN rate change",
|
||||||
|
"BACK exit to menu",
|
||||||
|
"* header = CSV active",
|
||||||
|
},
|
||||||
|
// Page 3: CSV Logging (max 21 chars)
|
||||||
|
{
|
||||||
|
"Auto-logs to SD card",
|
||||||
|
"gla_YYYYMMDD_HHMMSS",
|
||||||
|
"/ext/gpio_analyzer/",
|
||||||
|
"Cols: sample,A7..C0",
|
||||||
|
"0 = LOW 1 = HIGH",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Draw callback
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void ga_instructions_draw_cb(Canvas* canvas, void* model) {
|
||||||
|
GaInstrModel* m = (GaInstrModel*)model;
|
||||||
|
if(m == NULL) return;
|
||||||
|
|
||||||
|
uint8_t page = m->page;
|
||||||
|
if(page >= GA_INSTR_PAGES) page = 0U;
|
||||||
|
|
||||||
|
canvas_clear(canvas);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Title bar (FontSecondary, baseline y=7).
|
||||||
|
// Page indicator right-aligned on the same line: "1/4"
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
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)GA_INSTR_PAGES);
|
||||||
|
size_t iw = canvas_string_width(canvas, indicator);
|
||||||
|
canvas_draw_str(canvas, (int32_t)(127U - iw), 7, indicator);
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
canvas_draw_line(canvas, 0, 9, 127, 9);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Body text (FontKeyboard ~8px tall, 10px line spacing = 2px gap).
|
||||||
|
// 5 lines at y=19,29,39,49,59. Last line on page 3 is centered.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
canvas_set_font(canvas, FontKeyboard);
|
||||||
|
int32_t y = 19;
|
||||||
|
for(uint8_t i = 0U; i < GA_INSTR_BODY_LINES; i++) {
|
||||||
|
if(k_page_body[page][i] != NULL && k_page_body[page][i][0] != '\0') {
|
||||||
|
int32_t lx = 1;
|
||||||
|
if(page == 3U && i == GA_INSTR_BODY_LINES - 1U) {
|
||||||
|
size_t lw = canvas_string_width(canvas, k_page_body[page][i]);
|
||||||
|
lx = (int32_t)((128U - lw) / 2U);
|
||||||
|
if(lx < 1) lx = 1;
|
||||||
|
}
|
||||||
|
canvas_draw_str(canvas, lx, y, k_page_body[page][i]);
|
||||||
|
}
|
||||||
|
y += 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Input callback
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static bool ga_instructions_input_cb(InputEvent* event, void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
bool consumed = false;
|
||||||
|
|
||||||
|
if(event->type == InputTypeShort) {
|
||||||
|
switch(event->key) {
|
||||||
|
case InputKeyLeft:
|
||||||
|
view_dispatcher_send_custom_event(
|
||||||
|
app->view_dispatcher, GaEventInstrPageLeft);
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
case InputKeyRight:
|
||||||
|
view_dispatcher_send_custom_event(
|
||||||
|
app->view_dispatcher, GaEventInstrPageRight);
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// View setup (called once from ga_alloc)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void scene_instructions_setup_view(GpioAnalyzer* app) {
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
app->view_instructions = view_alloc();
|
||||||
|
furi_check(app->view_instructions != NULL);
|
||||||
|
|
||||||
|
view_set_draw_callback(app->view_instructions, ga_instructions_draw_cb);
|
||||||
|
view_set_input_callback(app->view_instructions, ga_instructions_input_cb);
|
||||||
|
view_set_context(app->view_instructions, app);
|
||||||
|
|
||||||
|
// LockFree model: just holds the current page number.
|
||||||
|
// Only ever written from the scene event handler (GUI thread).
|
||||||
|
view_allocate_model(
|
||||||
|
app->view_instructions, ViewModelTypeLockFree, sizeof(GaInstrModel));
|
||||||
|
|
||||||
|
GaInstrModel* 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) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
// Always start on page 0 when the user enters instructions.
|
||||||
|
GaInstrModel* 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, GaViewInstructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool scene_instructions_on_event(void* context, SceneManagerEvent event) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
bool consumed = false;
|
||||||
|
|
||||||
|
if(event.type == SceneManagerEventTypeCustom) {
|
||||||
|
GaInstrModel* m = view_get_model(app->view_instructions);
|
||||||
|
if(m == NULL) return false;
|
||||||
|
|
||||||
|
switch(event.event) {
|
||||||
|
case GaEventInstrPageLeft:
|
||||||
|
if(m->page > 0U) m->page--;
|
||||||
|
view_commit_model(app->view_instructions, true);
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GaEventInstrPageRight:
|
||||||
|
if(m->page < GA_INSTR_PAGES - 1U) m->page++;
|
||||||
|
view_commit_model(app->view_instructions, true);
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scene_instructions_on_exit(void* context) {
|
||||||
|
UNUSED(context);
|
||||||
|
// Nothing to tear down.
|
||||||
|
}
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
// #####################################################
|
||||||
|
// # scene_menu.c - main menu scene #
|
||||||
|
// # By: UberGuidoZ | https://github.com/UberGuidoZ/ #
|
||||||
|
// # v2.01 #
|
||||||
|
// #####################################################
|
||||||
|
|
||||||
|
#include "gpio_analyzer.h"
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Submenu callback - sends a custom event to the scene manager
|
||||||
|
// ============================================================
|
||||||
|
static void ga_menu_submenu_cb(void* context, uint32_t index) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Scene handlers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void scene_menu_on_enter(void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
submenu_reset(app->submenu);
|
||||||
|
submenu_set_header(app->submenu, "GPIO Logic Analyzer");
|
||||||
|
|
||||||
|
submenu_add_item(
|
||||||
|
app->submenu,
|
||||||
|
"Start Analyzer",
|
||||||
|
GaEventMenuStart,
|
||||||
|
ga_menu_submenu_cb,
|
||||||
|
app);
|
||||||
|
submenu_add_item(
|
||||||
|
app->submenu,
|
||||||
|
"Instructions",
|
||||||
|
GaEventMenuInstructions,
|
||||||
|
ga_menu_submenu_cb,
|
||||||
|
app);
|
||||||
|
submenu_add_item(
|
||||||
|
app->submenu,
|
||||||
|
"About",
|
||||||
|
GaEventMenuAbout,
|
||||||
|
ga_menu_submenu_cb,
|
||||||
|
app);
|
||||||
|
|
||||||
|
view_dispatcher_switch_to_view(app->view_dispatcher, GaViewMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool scene_menu_on_event(void* context, SceneManagerEvent event) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
bool consumed = false;
|
||||||
|
|
||||||
|
// BACK on the root menu exits the app entirely.
|
||||||
|
// Without this, scene manager would pop the menu and reveal
|
||||||
|
// the splash scene underneath, trapping the user.
|
||||||
|
if(event.type == SceneManagerEventTypeBack) {
|
||||||
|
view_dispatcher_stop(app->view_dispatcher);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(event.type == SceneManagerEventTypeCustom) {
|
||||||
|
switch(event.event) {
|
||||||
|
case GaEventMenuStart:
|
||||||
|
scene_manager_next_scene(app->scene_manager, GaSceneAnalyzer);
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
case GaEventMenuInstructions:
|
||||||
|
scene_manager_next_scene(app->scene_manager, GaSceneInstructions);
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
case GaEventMenuAbout:
|
||||||
|
scene_manager_next_scene(app->scene_manager, GaSceneAbout);
|
||||||
|
consumed = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scene_menu_on_exit(void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
submenu_reset(app->submenu);
|
||||||
|
}
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
// #####################################################
|
||||||
|
// # scene_splash.c - title / splash screen #
|
||||||
|
// # By: UberGuidoZ | https://github.com/UberGuidoZ/ #
|
||||||
|
// # v2.01 #
|
||||||
|
// #####################################################
|
||||||
|
//
|
||||||
|
// Displays a bordered title card for 1.5 seconds, then
|
||||||
|
// automatically advances to the main menu. Any key press
|
||||||
|
// also skips the splash immediately.
|
||||||
|
//
|
||||||
|
// Layout (128x64 OLED):
|
||||||
|
//
|
||||||
|
// +---------------------+ <- 2px border with corner marks
|
||||||
|
// | | <- Box around title text, centered
|
||||||
|
// | GPIO Logic | <- FontPrimary, line 1
|
||||||
|
// | Analyzer | <- FontPrimary, line 2
|
||||||
|
// |---------------------| <- separator
|
||||||
|
// | v2.01 by UberGuidoZ | <- FontKeyboard byline
|
||||||
|
// |github.com/UberGuidoZ| <- FontKeyboard URL
|
||||||
|
// +---------------------+
|
||||||
|
//
|
||||||
|
// ##############################################################
|
||||||
|
|
||||||
|
#include "gpio_analyzer.h"
|
||||||
|
|
||||||
|
// Auto-advance delay in milliseconds
|
||||||
|
#define GA_SPLASH_DELAY_MS 3000U
|
||||||
|
|
||||||
|
// Custom event fired when the timer expires
|
||||||
|
#define GA_SPLASH_TIMER_DONE 0U
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Splash timer callback (FreeRTOS timer service task)
|
||||||
|
// Fires once after GA_SPLASH_DELAY_MS.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void ga_splash_timer_cb(void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
view_dispatcher_send_custom_event(app->view_dispatcher, GA_SPLASH_TIMER_DONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Draw callback - pure static content, no model needed
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void ga_splash_draw_cb(Canvas* canvas, void* model) {
|
||||||
|
UNUSED(model);
|
||||||
|
canvas_clear(canvas);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Title - FontPrimary. Measure actual pixel widths at runtime
|
||||||
|
// so centering is exact regardless of font metrics.
|
||||||
|
// Box is sized from the wider string + 8px padding each side.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
canvas_set_font(canvas, FontPrimary);
|
||||||
|
size_t w1 = canvas_string_width(canvas, "GPIO Logic");
|
||||||
|
size_t w2 = canvas_string_width(canvas, "Analyzer");
|
||||||
|
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, "GPIO Logic");
|
||||||
|
canvas_draw_str(canvas, (int32_t)((128U - w2) / 2U), 34, "Analyzer");
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Separator between title box and byline.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
canvas_draw_line(canvas, 10, 44, 118, 44);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Byline - FontKeyboard (~6px/char, ~8px tall).
|
||||||
|
// "v2.01 by UberGuidoZ" = 19 chars * 6 = 114px, x=(128-114)/2 = 7
|
||||||
|
// "github.com/UberGuidoZ" = 21 chars * 6 = 126px, x=(128-126)/2 = 1
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
canvas_set_font(canvas, FontKeyboard);
|
||||||
|
canvas_draw_str(canvas, 7, 54, "v2.01 by UberGuidoZ");
|
||||||
|
canvas_draw_str(canvas, 1, 62, "github.com/UberGuidoZ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Input callback - any key skips the splash immediately
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static bool ga_splash_input_cb(InputEvent* event, void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
// Only act on key-down edges to avoid double-fires.
|
||||||
|
if(event->type == InputTypeShort || event->type == InputTypeLong) {
|
||||||
|
view_dispatcher_send_custom_event(app->view_dispatcher, GA_SPLASH_TIMER_DONE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// View setup (called once from ga_alloc)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void scene_splash_setup_view(GpioAnalyzer* app) {
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
app->view_splash = view_alloc();
|
||||||
|
furi_check(app->view_splash != NULL);
|
||||||
|
|
||||||
|
view_set_draw_callback(app->view_splash, ga_splash_draw_cb);
|
||||||
|
view_set_input_callback(app->view_splash, ga_splash_input_cb);
|
||||||
|
view_set_context(app->view_splash, app);
|
||||||
|
// No model - all content is static.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Scene handlers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void scene_splash_on_enter(void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
// Allocate a one-shot timer. It is freed in on_exit so the
|
||||||
|
// callback cannot fire after the scene has been torn down.
|
||||||
|
app->splash_timer = furi_timer_alloc(
|
||||||
|
ga_splash_timer_cb, FuriTimerTypeOnce, app);
|
||||||
|
furi_check(app->splash_timer != NULL);
|
||||||
|
furi_timer_start(app->splash_timer, GA_SPLASH_DELAY_MS);
|
||||||
|
|
||||||
|
view_dispatcher_switch_to_view(app->view_dispatcher, GaViewSplash);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool scene_splash_on_event(void* context, SceneManagerEvent event) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
if(event.type == SceneManagerEventTypeCustom &&
|
||||||
|
event.event == GA_SPLASH_TIMER_DONE) {
|
||||||
|
// Stop and free the timer before transitioning so it cannot
|
||||||
|
// double-fire if the user tapped a key and the timer also
|
||||||
|
// expired in the same tick.
|
||||||
|
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, GaSceneMenu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scene_splash_on_exit(void* context) {
|
||||||
|
GpioAnalyzer* app = (GpioAnalyzer*)context;
|
||||||
|
furi_check(app != NULL);
|
||||||
|
|
||||||
|
// Guard in case on_event already freed it (key-skip path).
|
||||||
|
if(app->splash_timer != NULL) {
|
||||||
|
furi_timer_stop(app->splash_timer);
|
||||||
|
furi_timer_free(app->splash_timer);
|
||||||
|
app->splash_timer = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||