Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the application to be compatible with the latest Filpper firmware #13

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
.vscode
dist
dist/*
.vscode
.clang-format
.editorconfig
.env
.ufbt
122 changes: 70 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,58 @@
# Flipper-Plugin

Tutorial on how to build a basic "Hello world" plugin for Flipper Zero.

This tutorial includes:
This tutorial includes:

- Sourcecode template for a custom flipper plugin!
- A step by step story on how to create the base of a custom flipper plugin. (There are many other ways of creating apps for flipper, this example is based on the existing snake app)

The tutorial was written during development of [flappybird for flipper](https://github.com/DroomOne/flipperzero-firmware/tree/dev/applications/flappy_bird).
The tutorial was written during development of [flappybird for flipper](https://github.com/DroomOne/flipperzero-firmware/tree/dev/applications/flappy_bird).

![Screenshot](/img/qFlipper_zp57pxf0Dv.gif)

# Hello World - The story

__This is the step-by-step story version of the tutorial. You can skip this and directly continue to the sourcecode if you know what your doing. Make sure you don't forget to add the application to the makefile, and register its functions in `applications.c` (see chapter: Building the firmware + plugin)__

In this tutorial a simple hello world plugin is added to flipper. The goal is to render something in the screen, and make the buttons move that object. In this case it will be the classic "Hello World" text.

## Downloading the firmware

In this tutorial a simple hello world plugin is added to flipper. The goal is to render something in the screen, and make the buttons move that object. In this case it will be the classic "Hello World" text.
1. Clone or download [flipperzero-firmware](https://github.com/flipperdevices/flipperzero-firmware).

## Downloading the firmware
1. Clone or download [flipperzero-firmware](https://github.com/flipperdevices/flipperzero-firmware).
```sh
```sh
git clone https://github.com/flipperdevices/flipperzero-firmware
```
2. Create a folder for the custom plugin in `flipperzero-firmware/applications/`. For the hello-world app, this will be: `hello_world`.

2. Create a folder for the custom plugin in `flipperzero-firmware/applications/`. For the hello-world app, this will be: `hello_world`.

```sh
mkdir flipperzero-firmware/applications/hello_world
```
3. Create a new source file in the newly created folder. The file name has to match the name of the folder.

3. Create a new source file in the newly created folder. The file name has to match the name of the folder.

```sh
touch flipperzero-firmware/applications/hello_world/hello_world.c
```

## Plugin Main

For flipper to activate the plugin, a main function for the plugin has to be added. Following the naming convention of existing flipper plugins, this needs to be: `hello_world_app`.

## Plugin Main
For flipper to activate the plugin, a main function for the plugin has to be added. Following the naming convention of existing flipper plugins, this needs to be: `hello_world_app`.
- Create an `int32_t hello_world_app(void* p)` function that will function as the entry of the plguin.

- Create an `int32_t hello_world_app(void* p)` function that will function as the entry of the plguin.
For the plugin to keep track of what actions have been executed, we create a messagequeue.

For the plugin to keep track of what actions have been executed, we create a messagequeue.
- A by calling `osMessageQueueNew` we create a new `osMessageQueueId_t` that keeps track of events.
- A by calling `osMessageQueueNew` we create a new `osMessageQueueId_t` that keeps track of events.

The view_port is used to control the canvas (display) and userinput from the hardware. In order to use this, a `view_port` has to be allocated, and callbacks to their functions registered. (The callback functions will later be added to the code)
- `view_port_alloc()` will allocate a new `view_port`.

- `view_port_alloc()` will allocate a new `view_port`.
- `draw` and `input` callbacks originating from the `view_port` can be registerd with the functions
- `view_port_draw_callback_set`
- `view_port_input_callback_set`
- `view_port_draw_callback_set`
- `view_port_input_callback_set`
- Register the `view_port` to the `GUI`

```c
Expand All @@ -65,11 +72,12 @@ int32_t hello_world_app() {
}
```

## Callbacks
Flipper will let the plugin know once it is ready to deal with a new frame or once a button is pressed by the user.
## Callbacks

Flipper will let the plugin know once it is ready to deal with a new frame or once a button is pressed by the user.

**input_callback:**
Signals the plugin once a button is pressed. The event is queued in the event_queue. In the main thread the queue read and handled.
Signals the plugin once a button is pressed. The event is queued in the event_queue. In the main thread the queue read and handled.

A refrence to the queue is passed during the setup of the application.

Expand All @@ -90,10 +98,10 @@ static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queu
PluginEvent event = {.type = EventTypeKey, .input = *input_event};
furi_message_queue_put(event_queue, &event, FuriWaitForever);
}
```
```

**render_callback:**
Signals the plugin when flipper is ready to draw a new frame in the canvas. For the hello-world example this will be a simple frame around the outer edges.
Signals the plugin when flipper is ready to draw a new frame in the canvas. For the hello-world example this will be a simple frame around the outer edges.

```c
static void render_callback(Canvas* const canvas, void* ctx) {
Expand All @@ -102,9 +110,10 @@ static void render_callback(Canvas* const canvas, void* ctx) {
```

## Main Loop and plugin State
The main loop runs during the lifetime of the plugin. For each loop we try to pop an event from the queue, and handle the queue item such as button input / plugin events.

For this example we render a new frame, every time the loop is run. This can be done by calling `view_port_update(view_port);`.
The main loop runs during the lifetime of the plugin. For each loop we try to pop an event from the queue, and handle the queue item such as button input / plugin events.

For this example we render a new frame, every time the loop is run. This can be done by calling `view_port_update(view_port);`.

```c
PluginEvent event;
Expand Down Expand Up @@ -138,32 +147,36 @@ For this example we render a new frame, every time the loop is run. This can be
```

### Plugin State
Because of the callback system, the plugin is being manipulated by different threads. To overcome race conditions we have to create a shared object that is safe to use.

Because of the callback system, the plugin is being manipulated by different threads. To overcome race conditions we have to create a shared object that is safe to use.

1. Allocate a new PluginState struct, and initialise it before the main loop.

```c
typedef struct {
} PluginState;

// in main:
PluginState* plugin_state = malloc(sizeof(PluginState));
```
2. Using `ValueMutex` we create a mutex for the plugin state called `state_mutex`.
3. Initalise the mutex for `PluginState` using `init_mutex()`
4. Pass the mutex as argument to `view_port_draw_callback_set()` so we can safely access the shared state from flippers thread.

2. Using `FuriMutex` we create a mutex for the plugin state called `mutex` inside the `plugin_state` struct.
3. Initalise the mutex for `PluginState` using `furi_mutex_alloc()`
4. Pass the mutex as argument to `view_port_draw_callback_set()` so we can safely access the shared state from flippers thread.

```c
typedef struct {
FuriMutex* mutex*;
} PluginState;

int32_t hello_world_app() {
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));

PluginState* plugin_state = malloc(sizeof(PluginState));
ValueMutex state_mutex;
if (!init_mutex(&state_mutex, plugin_state, sizeof(PluginState))) {
plugin_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
if(!plugin_state->mutex) {
FURI_LOG_E("Hello_World", "cannot create mutex\r\n");
free(plugin_state);
free(plugin_state);
return 255;
}

Expand All @@ -174,40 +187,44 @@ int32_t hello_world_app() {
...
```

### Main Loop

### Main Loop
Let's deal with the mutex in our main loop. So we can update values from the main loop based on user input. As an example, we will move a hello-world text through the screen. Based on user input.

Let's deal with the mutex in our main loop. So we can update values from the main loop based on user input. As an example, we will move a hello-world text through the screen. Based on user input.
1. For this we add a `int x` and `int y` to your state.

1. For this we add a `int x` and `int y` to your state.
```c
typedef struct {
int x;
int y;
FuriMutex* mutex;
} PluginState;
```

2. Initialise the values of the struct using a new `hello_world_state_init()` function.
2. Initialise the values of the struct using a new `hello_world_state_init()` function.

```c
static void hello_world_state_init(PluginState* const plugin_state) {
plugin_state->x = 50;
plugin_state->y = 30;
}
```
Call it after allocating the object in the main function.

Call it after allocating the object in the main function.

```c
PluginState* plugin_state = malloc(sizeof(PluginState));
hello_world_state_init(plugin_state);
ValueMutex state_mutex;
...
```
4. Aquire a blocking mutex after a new event is handled in the queue. Write values to the locked plugin_state object when user presses buttons. And release when we finish working with the state.

4. Aquire a blocking mutex after a new event is handled in the queue. Write values to the locked plugin_state object when user presses buttons. And release when we finish working with the state.

```c
PluginEvent event;
for(bool processing = true; processing;) {
FuriStatus event_status = furi_message_queue_put(event_queue, &event, 100);
PluginState* plugin_state = (PluginState*)acquire_mutex_block(&state_mutex);
PluginState* plugin_state = (PluginState*)furi_mutex_acquire(plugin_state->mutex, FuriWaitForever);

if(event_status == FuriStatusOK) {
// press events
Expand Down Expand Up @@ -239,25 +256,26 @@ for(bool processing = true; processing;) {
}

view_port_update(view_port);
release_mutex(&state_mutex, plugin_state);
furi_mutex_release(plugin_state->mutex);
}
...
```

## Drawing Graphics
## Drawing Graphics

Creating graphics on flipper has been made easy by flippers developers. An canvas around the outer edges of the screen could be easly added with a single line: `canvas_draw_frame(canvas, 0, 0, 128, 64);`.
Creating graphics on flipper has been made easy by flippers developers. An canvas around the outer edges of the screen could be easly added with a single line: `canvas_draw_frame(canvas, 0, 0, 128, 64);`.

However, when it comes to dealing with user input, moving objects, changing processes we have to take into account that objects might be used by other threads. In the previous part we added a mutex in order to block any other thread writing to an object. For safe drawing graphics, we have to do the same.

1. Aquire a mutex by calling `acquire_mutex()`. The `render_callback()` has the context in a argument. Previously we told the set_callback function to use `plugin_state` for this.
1. Aquire a mutex by calling `furi_mutex_acquire()`. The `render_callback()` has the context in a argument. Previously we told the set_callback function to use `plugin_state` for this.
2. Check if the mutex is valid, otherwise skip this render
3. Do all the drawing that we like.. In this case a simple text Hello world, on the `x` and `y` positions we have set in the plugin_state.
4. Close the mutex again.
3. Do all the drawing that we like.. In this case a simple text Hello world, on the `x` and `y` positions we have set in the plugin_state.
4. Close the mutex again.

```c
static void render_callback(Canvas* const canvas, void* ctx) {
const PluginState* plugin_state = acquire_mutex((ValueMutex*)ctx, 25);
PluginState* plugin_state = (PluginState*) ctx;
furi_mutex_acquire(plugin_state->mutex, FuriWaitForever);
if(plugin_state == NULL) {
return;
}
Expand All @@ -267,22 +285,21 @@ static void render_callback(Canvas* const canvas, void* ctx) {
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, plugin_state.x, plugin_state.y, AlignRight, AlignBottom, "Hello World");

release_mutex((ValueMutex*)ctx, plugin_state);
furi_mutex_release(plugin_state->mutex);
}
```

```

## Building the firmware + plugin

Before the plugin is added to flipper. We have to let the compiler know, where to find the plugins files.
Before the plugin is added to flipper. We have to let the compiler know, where to find the plugins files.

1. The applications needs a manifest file to let the firmware know how to use it. Therefore create a file `applications\hello_world\application.fam`

```c
App(
appid="hello_world",
name="Hello World",
apptype=FlipperAppType.PLUGIN,
apptype=FlipperAppType.EXTERNAL,
entry_point="hello_world_app",
cdefines=["APP_HELLO_WORLD"],
requires=[
Expand All @@ -292,7 +309,8 @@ App(
order=20,
)
```
2. The application needs to be registered in the menu to be called. This is possible by adding an entry to `applications\meta\application.fam`

2. The application needs to be registered in the menu to be called. This is possible by adding an entry to `applications\meta\application.fam`

Add the application id to the list of provides for the specific menu. In this case under basic_plugins:

Expand All @@ -310,4 +328,4 @@ App(
)
```

Now you can build the application!
Now you can build the application!
2 changes: 1 addition & 1 deletion application.fam
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
App(
appid="hello_world",
name="Hello World",
apptype=FlipperAppType.PLUGIN,
apptype=FlipperAppType.EXTERNAL,
entry_point="hello_world_app",
cdefines=["APP_HELLO_WORLD"],
requires=[
Expand Down
20 changes: 11 additions & 9 deletions hello_world.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ typedef struct {
typedef struct {
int x;
int y;
FuriMutex* mutex;
} PluginState;

static void render_callback(Canvas* const canvas, void* ctx) {
const PluginState* plugin_state = acquire_mutex((ValueMutex*)ctx, 25);
PluginState* plugin_state = (PluginState*)ctx;
furi_mutex_acquire(plugin_state->mutex, FuriWaitForever);
if(plugin_state == NULL) {
return;
}
Expand All @@ -30,7 +32,7 @@ static void render_callback(Canvas* const canvas, void* ctx) {
canvas_draw_str_aligned(
canvas, plugin_state->x, plugin_state->y, AlignRight, AlignBottom, "Hello World!");

release_mutex((ValueMutex*)ctx, plugin_state);
furi_mutex_release(plugin_state->mutex);
}

static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
Expand All @@ -52,16 +54,16 @@ int32_t hello_world_app() {

hello_world_state_init(plugin_state);

ValueMutex state_mutex;
if(!init_mutex(&state_mutex, plugin_state, sizeof(PluginState))) {
FURI_LOG_E("Hello_world", "cannot create mutex\r\n");
plugin_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
if(!plugin_state->mutex) {
FURI_LOG_E("Hello_World", "cannot create mutex\r\n");
free(plugin_state);
return 255;
}

// Set system callbacks
ViewPort* view_port = view_port_alloc();
view_port_draw_callback_set(view_port, render_callback, &state_mutex);
view_port_draw_callback_set(view_port, render_callback, plugin_state);
view_port_input_callback_set(view_port, input_callback, event_queue);

// Open GUI and register view_port
Expand All @@ -72,7 +74,7 @@ int32_t hello_world_app() {
for(bool processing = true; processing;) {
FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);

PluginState* plugin_state = (PluginState*)acquire_mutex_block(&state_mutex);
furi_mutex_acquire(plugin_state->mutex, FuriWaitForever);

if(event_status == FuriStatusOk) {
// press events
Expand Down Expand Up @@ -106,15 +108,15 @@ int32_t hello_world_app() {
}

view_port_update(view_port);
release_mutex(&state_mutex, plugin_state);
furi_mutex_release(plugin_state->mutex);
}

view_port_enabled_set(view_port, false);
gui_remove_view_port(gui, view_port);
furi_record_close("gui");
view_port_free(view_port);
furi_message_queue_free(event_queue);
delete_mutex(&state_mutex);
furi_mutex_free(plugin_state->mutex);

return 0;
}