Compare commits
No commits in common. "3782be78372275a342f5d9575bb1588a72ed131f" and "7cf1d6ee0d2e068467246802a0dc9dec9f13b74b" have entirely different histories.
3782be7837
...
7cf1d6ee0d
@ -1,11 +1,10 @@
|
|||||||
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, 1),
|
fap_version=(2, 0),
|
||||||
fap_icon="gpio_analyzer_icon.png",
|
|
||||||
)
|
)
|
||||||
@ -1,180 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
@ -1,197 +0,0 @@
|
|||||||
#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);
|
|
||||||
@ -1,226 +0,0 @@
|
|||||||
// #########################################################
|
|
||||||
// # 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;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 371 B |
|
Before Width: | Height: | Size: 371 B |
@ -1,91 +0,0 @@
|
|||||||
// #####################################################
|
|
||||||
// # 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);
|
|
||||||
}
|
|
||||||
@ -1,508 +0,0 @@
|
|||||||
// #########################################################
|
|
||||||
// # 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.
|
|
||||||
}
|
|
||||||
@ -1,223 +0,0 @@
|
|||||||
// #######################################################
|
|
||||||
// # 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.
|
|
||||||
}
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
// #####################################################
|
|
||||||
// # 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);
|
|
||||||
}
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
// #####################################################
|
|
||||||
// # 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||