Просмотр исходного кода

Migrated existing state to a new repo.

- Dialer semi-functional - DTMF is untuned, code needs refactor
- There is definitely a memory leak.
- Bluebox and redbox functionality awaiting code refactor.
Aria Burrell 3 лет назад
Сommit
4754832d12

+ 5 - 0
README.md

@@ -0,0 +1,5 @@
+## DTMF Dolphin
+
+Dialer, and future Bluebox and Redbox for the Flipper Zero.
+
+Documentation and code completion pending. This is a work in progress.

+ 13 - 0
application.fam

@@ -0,0 +1,13 @@
+App(
+    appid="dtmf_dolphin",
+    name="DTMF Dolphin",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="dtmf_dolphin_app",
+    cdefines=["DTMF_DOLPHIN"],
+    requires=[
+        "gui",
+        "dialogs",
+    ],
+    stack_size=4 * 1024,
+    order=20,
+)

+ 136 - 0
dtmf_dolphin.c

@@ -0,0 +1,136 @@
+#include "dtmf_dolphin_i.h"
+
+#include <furi.h>
+#include <furi_hal.h>
+
+static bool dtmf_dolphin_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    DTMFDolphinApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+static bool dtmf_dolphin_app_back_event_callback(void* context) {
+    furi_assert(context);
+    DTMFDolphinApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+static void dtmf_dolphin_app_tick_event_callback(void* context) {
+    furi_assert(context);
+    DTMFDolphinApp* app = context;
+
+    // Needed to handle queueing to ISR and prioritization of audio
+    if (app->player.playing) {
+        dtmf_dolphin_player_handle_tick();
+    } else {
+        scene_manager_handle_tick_event(app->scene_manager);
+    }
+}
+
+static DTMFDolphinApp* app_alloc() {
+    DTMFDolphinApp* app = malloc(sizeof(DTMFDolphinApp));
+    app->player.half_samples = 4 * 1024;
+    app->player.sample_count = 8 * 1024;
+    app->player.sample_buffer = malloc(sizeof(uint16_t) * app->player.sample_count);
+    app->player.buffer_buffer = malloc(sizeof(uint8_t) * app->player.sample_count);
+    app->player.wf1_period = 0;
+    app->player.wf2_period = 0;
+    app->player.wf1_freq = 0;
+    app->player.wf2_freq = 0;
+    app->player.wf1_pos = 0;
+    app->player.wf2_pos = 0;
+    app->player.queue = furi_message_queue_alloc(10, sizeof(DTMFDolphinEvent));
+    app->player.volume = 2.0f;
+    app->player.playing = false;
+
+    app->gui = furi_record_open(RECORD_GUI);
+    app->view_dispatcher = view_dispatcher_alloc();
+    app->scene_manager = scene_manager_alloc(&dtmf_dolphin_scene_handlers, app);
+    view_dispatcher_enable_queue(app->view_dispatcher);
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, dtmf_dolphin_app_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, dtmf_dolphin_app_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, dtmf_dolphin_app_tick_event_callback, 100);
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    app->main_menu_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        DTMFDolphinViewMainMenu,
+        variable_item_list_get_view(app->main_menu_list));
+
+    app->dtmf_dolphin_dialer = dtmf_dolphin_dialer_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        DTMFDolphinViewDialer,
+        dtmf_dolphin_dialer_get_view(app->dtmf_dolphin_dialer));
+
+    app->dtmf_dolphin_bluebox = dtmf_dolphin_bluebox_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        DTMFDolphinViewBluebox,
+        dtmf_dolphin_bluebox_get_view(app->dtmf_dolphin_bluebox));
+
+    app->dtmf_dolphin_play = widget_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        DTMFDolphinViewPlay,
+        widget_get_view(app->dtmf_dolphin_play));
+
+    // app->dialer_button_panel = button_panel_alloc();
+    // app->bluebox_button_panel = button_panel_alloc();
+    // app->redbox_button_panel = button_panel_alloc();
+
+    app->notification = furi_record_open(RECORD_NOTIFICATION);
+    notification_message(app->notification, &sequence_display_backlight_enforce_on);
+
+    scene_manager_next_scene(app->scene_manager, DTMFDolphinSceneStart);
+
+    return app;
+}
+
+static void app_free(DTMFDolphinApp* app) {
+    furi_assert(app);
+    view_dispatcher_remove_view(app->view_dispatcher, DTMFDolphinViewMainMenu);
+    view_dispatcher_remove_view(app->view_dispatcher, DTMFDolphinViewBluebox);
+    view_dispatcher_remove_view(app->view_dispatcher, DTMFDolphinViewDialer);
+    view_dispatcher_remove_view(app->view_dispatcher, DTMFDolphinViewPlay);
+    variable_item_list_free(app->main_menu_list);
+
+    dtmf_dolphin_bluebox_free(app->dtmf_dolphin_bluebox);
+    dtmf_dolphin_dialer_free(app->dtmf_dolphin_dialer);
+    widget_free(app->dtmf_dolphin_play);
+
+    view_dispatcher_free(app->view_dispatcher);
+    scene_manager_free(app->scene_manager);
+
+    furi_message_queue_free(app->player.queue);
+    free(app->player.sample_buffer);
+
+    // button_panel_free(app->dialer_button_panel);
+    // button_panel_free(app->bluebox_button_panel);
+    // button_panel_free(app->redbox_button_panel);
+
+    notification_message(app->notification, &sequence_display_backlight_enforce_auto);
+
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_NOTIFICATION);
+    free(app);
+}
+
+int32_t dtmf_dolphin_app(void *p) {
+    UNUSED(p);
+    DTMFDolphinApp* app = app_alloc();
+
+    dtmf_dolphin_player_init(&(app->player));
+    
+    view_dispatcher_run(app->view_dispatcher);
+
+    app_free(app);
+    return 0;
+}

+ 14 - 0
dtmf_dolphin_event.h

@@ -0,0 +1,14 @@
+#pragma once
+
+typedef enum {
+    DTMFDolphinEventVolumeUp = 0,
+    DTMFDolphinEventVolumeDown,
+    DTMFDolphinBlueboxOkCB,
+    DTMFDolphinEventStartDialer,
+    DTMFDolphinEventStartBluebox,
+    DTMFDolphinEventStartRedbox,
+    DTMFDolphinEventPlayTones,
+    DTMFDolphinEventStopTones,
+    DTMFDolphinPlayerEventHalfTransfer,
+    DTMFDolphinPlayerEventFullTransfer,
+} DTMFDolphinEvent;

+ 52 - 0
dtmf_dolphin_hal.c

@@ -0,0 +1,52 @@
+#include "dtmf_dolphin_hal.h"
+
+void dtmf_dolphin_speaker_init() {
+    LL_TIM_InitTypeDef TIM_InitStruct = {0};
+    TIM_InitStruct.Prescaler = 4;
+    TIM_InitStruct.Autoreload = 255;
+    LL_TIM_Init(FURI_HAL_SPEAKER_TIMER, &TIM_InitStruct);
+
+    LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
+    TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
+    TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
+    TIM_OC_InitStruct.CompareValue = 127;
+    LL_TIM_OC_Init(FURI_HAL_SPEAKER_TIMER, FURI_HAL_SPEAKER_CHANNEL, &TIM_OC_InitStruct);
+}
+
+void dtmf_dolphin_speaker_start() {
+    LL_TIM_EnableAllOutputs(FURI_HAL_SPEAKER_TIMER);
+    LL_TIM_EnableCounter(FURI_HAL_SPEAKER_TIMER);
+}
+
+void dtmf_dolphin_speaker_stop() {
+    LL_TIM_DisableAllOutputs(FURI_HAL_SPEAKER_TIMER);
+    LL_TIM_DisableCounter(FURI_HAL_SPEAKER_TIMER);
+}
+
+void dtmf_dolphin_dma_init(uint32_t address, size_t size) {
+    uint32_t dma_dst = (uint32_t) & (FURI_HAL_SPEAKER_TIMER->CCR1);
+
+    LL_DMA_ConfigAddresses(DMA_INSTANCE, address, dma_dst, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
+    LL_DMA_SetDataLength(DMA_INSTANCE, size);
+
+    LL_DMA_SetPeriphRequest(DMA_INSTANCE, LL_DMAMUX_REQ_TIM16_UP);
+    LL_DMA_SetDataTransferDirection(DMA_INSTANCE, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
+    LL_DMA_SetChannelPriorityLevel(DMA_INSTANCE, LL_DMA_PRIORITY_VERYHIGH);
+    LL_DMA_SetMode(DMA_INSTANCE, LL_DMA_MODE_CIRCULAR);
+    LL_DMA_SetPeriphIncMode(DMA_INSTANCE, LL_DMA_PERIPH_NOINCREMENT);
+    LL_DMA_SetMemoryIncMode(DMA_INSTANCE, LL_DMA_MEMORY_INCREMENT);
+    LL_DMA_SetPeriphSize(DMA_INSTANCE, LL_DMA_PDATAALIGN_HALFWORD);
+    LL_DMA_SetMemorySize(DMA_INSTANCE, LL_DMA_MDATAALIGN_HALFWORD);
+
+    LL_DMA_EnableIT_TC(DMA_INSTANCE);
+    LL_DMA_EnableIT_HT(DMA_INSTANCE);
+}
+
+void dtmf_dolphin_dma_start() {
+    LL_DMA_EnableChannel(DMA_INSTANCE);
+    LL_TIM_EnableDMAReq_UPDATE(FURI_HAL_SPEAKER_TIMER);
+}
+
+void dtmf_dolphin_dma_stop() {
+    LL_DMA_DisableChannel(DMA_INSTANCE);
+}

+ 32 - 0
dtmf_dolphin_hal.h

@@ -0,0 +1,32 @@
+#pragma once
+#include <furi.h>
+#include <furi_hal.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <stm32wb55xx.h>
+#include <stm32wbxx_ll_tim.h>
+#include <stm32wbxx_ll_dma.h>
+
+#define FURI_HAL_SPEAKER_TIMER TIM16
+#define FURI_HAL_SPEAKER_CHANNEL LL_TIM_CHANNEL_CH1
+#define DMA_INSTANCE DMA1, LL_DMA_CHANNEL_1
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void dtmf_dolphin_speaker_init();
+
+void dtmf_dolphin_speaker_start();
+
+void dtmf_dolphin_speaker_stop();
+
+void dtmf_dolphin_dma_init(uint32_t address, size_t size);
+
+void dtmf_dolphin_dma_start();
+
+void dtmf_dolphin_dma_stop();
+
+#ifdef __cplusplus
+}
+#endif

+ 52 - 0
dtmf_dolphin_i.h

@@ -0,0 +1,52 @@
+#pragma once
+
+#include "scenes/dtmf_dolphin_scene.h"
+
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/variable_item_list.h>
+#include <notification/notification_messages.h>
+#include <input/input.h>
+
+#include "dtmf_dolphin_event.h"
+#include "dtmf_dolphin_player.h"
+
+#include "views/dtmf_dolphin_dialer.h"
+#include "views/dtmf_dolphin_bluebox.h"
+
+#define TAG "DTMFDolphin"
+
+
+enum DTMFDolphinItem {
+    DTMFDolphinItemDialer,
+    DTMFDolphinItemBluebox,
+    DTMFDolphinItemRedbox,
+    DTMFDolphinItemPlay
+};
+
+typedef struct {
+    ViewDispatcher* view_dispatcher;
+    SceneManager* scene_manager;
+    VariableItemList* main_menu_list;
+    DTMFDolphinDialer* dtmf_dolphin_dialer;
+    DTMFDolphinBluebox* dtmf_dolphin_bluebox;
+    DTMFDolphinPlayer player;
+    Widget* dtmf_dolphin_play;
+
+    Gui* gui;
+    // ButtonPanel* dialer_button_panel;
+    // ButtonPanel* bluebox_button_panel;
+    // ButtonPanel* redbox_button_panel;
+    NotificationApp* notification;
+} DTMFDolphinApp;
+
+typedef enum {
+    DTMFDolphinViewMainMenu,
+    DTMFDolphinViewDialer,
+    DTMFDolphinViewBluebox,
+    DTMFDolphinViewRedbox,
+    DTMFDolphinViewPlay,
+} DTMFDolphinView;

+ 156 - 0
dtmf_dolphin_player.c

@@ -0,0 +1,156 @@
+#include "dtmf_dolphin_player.h"
+
+#define DTMF_DOLPHIN_SAMPLE_RATE (8000)
+
+typedef struct {
+    DTMFDolphinEvent type;
+} DTMFDolphinPlayerEvent;
+
+// Keep this here for accessibility in local scope event without context
+DTMFDolphinPlayer* player;
+
+void dtmf_dolphin_dma_isr(void* ctx) {
+    FuriMessageQueue *event_queue = ctx;
+
+    if (LL_DMA_IsActiveFlag_HT1(DMA1)) {
+        LL_DMA_ClearFlag_HT1(DMA1);
+
+        DTMFDolphinPlayerEvent event = {.type = DTMFDolphinPlayerEventHalfTransfer};
+        furi_message_queue_put(event_queue, &event, 0);
+    }
+
+    if(LL_DMA_IsActiveFlag_TC1(DMA1)) {
+        LL_DMA_ClearFlag_TC1(DMA1);
+
+        DTMFDolphinPlayerEvent event = {.type = DTMFDolphinPlayerEventFullTransfer};
+        furi_message_queue_put(event_queue, &event, 0);
+    }
+}
+
+bool dtmf_dolphin_player_init(void* context) {
+    player = context;
+
+    return false;
+}
+
+void dtmf_dolphin_player_clear_samples() {
+    for (size_t i = 0; i < player->sample_count; i++) {
+        player->sample_buffer[i] = 0;
+    }
+}
+
+bool dtmf_dolphin_player_generate_waveform(size_t index) {
+    uint16_t* sample_buffer_start = &player->sample_buffer[index];
+    if (!player->wf1_freq)
+        return false;
+
+    // Generate basic sine wave sample to fill sample_count
+    for (size_t i = 0; i < player->half_samples; i++) {
+        // float data = sin(i * PERIOD_2_PI / player->wf1_period) + 1;
+        float data = sin(player->wf1_pos * PERIOD_2_PI / player->wf1_period) + 1;
+        player->wf1_pos = (player->wf1_pos + 1) % player->wf1_period;
+
+        data *= player->volume;
+
+        // Downmix second tone with the first
+        if (player->wf2_freq) {
+            data /= 2;
+
+            float data_2 = sin(player->wf2_pos * PERIOD_2_PI / player->wf2_period) + 1;
+            player->wf2_pos = (player->wf2_pos + 1) % player->wf2_period;
+
+            data_2 *= player->volume / 2;
+
+            data += data_2;
+        }
+
+        data = tanhf(data);
+
+        data *= UINT8_MAX / 2; // scale -128..127
+        data += UINT8_MAX / 2; // to unsigned
+
+        if(data < 0) {
+            data = 0;
+        }
+
+        if(data > 255) {
+            data = 255;
+        }
+
+        player->buffer_buffer[i] = data;
+        sample_buffer_start[i] = data;
+    }
+
+    return true;
+}
+
+bool dtmf_dolphin_player_play_tones(float *freq) {
+    player->wf1_pos = 0;
+    player->wf2_pos = 0;
+    player->wf1_freq = 0;
+    player->wf2_freq = 0;
+    player->wf1_period = 0;
+    player->wf2_period = 0;
+    if (freq[0]) {
+        player->wf1_freq = freq[0];
+        player->wf1_period = player->sample_count / freq[0] * 4;
+    }
+    if (freq[1]) {
+        player->wf2_freq = freq[1];
+        player->wf2_period = player->sample_count / freq[1] * 4;
+    }
+    dtmf_dolphin_player_clear_samples();
+
+    dtmf_dolphin_player_generate_waveform(0);
+    dtmf_dolphin_player_generate_waveform(player->half_samples);
+
+    dtmf_dolphin_speaker_init();
+    dtmf_dolphin_dma_init((uint32_t)player->sample_buffer, player->sample_count);
+
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdDma1Ch1, dtmf_dolphin_dma_isr, player->queue);
+
+    dtmf_dolphin_dma_start();
+    dtmf_dolphin_speaker_start();
+
+    player->playing = true;
+
+    return true;
+}
+
+bool dtmf_dolphin_player_stop_tones() {
+    player->playing = false;
+
+    dtmf_dolphin_speaker_stop();
+    dtmf_dolphin_dma_stop();
+
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdDma1Ch1, NULL, NULL);
+   
+    return true;
+}
+
+bool dtmf_dolphin_player_handle_tick() {
+    DTMFDolphinPlayerEvent event;
+
+    if(furi_message_queue_get(player->queue, &event, FuriWaitForever) == FuriStatusOk) {
+        if (player->playing) {
+            if(event.type == DTMFDolphinPlayerEventHalfTransfer) {
+                dtmf_dolphin_player_generate_waveform(0);
+                // uint16_t* sample_buffer_start = &player->sample_buffer[0];
+                // for (size_t i = 0; i < player->half_samples; i++) {
+                //     sample_buffer_start[i] = player->buffer_buffer[i];
+                // }
+                return true;
+            } else if (event.type == DTMFDolphinPlayerEventFullTransfer) {
+                dtmf_dolphin_player_generate_waveform(player->half_samples);
+                // uint16_t* sample_buffer_start = &player->sample_buffer[player->half_samples];
+                // for (size_t i = 0; i < player->half_samples; i++) {
+                //     sample_buffer_start[i] = player->buffer_buffer[i];
+                // }
+                return true;
+            }
+        } else {
+            return true;
+        }
+    }
+    return false;
+}

+ 47 - 0
dtmf_dolphin_player.h

@@ -0,0 +1,47 @@
+#pragma once
+#include "dtmf_dolphin_event.h"
+#include "dtmf_dolphin_hal.h"
+#include "dtmf_dolphin_tone.h"
+
+#define PERIOD_2_PI 6.2832
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+    size_t half_samples;
+    size_t sample_count;
+    float wf1_freq;
+    float wf2_freq;
+    uint16_t wf1_period;
+    uint16_t wf2_period;
+    uint16_t wf1_pos;
+    uint16_t wf2_pos;
+    uint8_t *buffer_buffer;
+    uint16_t *sample_buffer;
+    float volume;
+    bool playing;
+
+    FuriMessageQueue* queue;
+} DTMFDolphinPlayer;
+
+void dtmf_dolphin_dma_isr(void* ctx);
+
+bool dtmf_dolphin_player_init(void* context);
+
+void dtmf_dolphin_player_clear_samples();
+
+bool dtmf_dolphin_player_generate_waveform(size_t index);
+
+bool dtmf_dolphin_player_play_tones(float *freq);
+
+bool dtmf_dolphin_player_stop_tones();
+
+bool dtmf_dolphin_player_handle_tick();
+
+#ifdef __cplusplus
+}
+#endif
+
+

+ 84 - 0
dtmf_dolphin_tone.c

@@ -0,0 +1,84 @@
+#include "dtmf_dolphin_tone.h"
+
+const char* dtmf_dolphin_get_tone_name(uint8_t row, uint8_t col, DTMFDolphinToneSection block) {
+    if (block == DTMF_DOLPHIN_TONE_BLOCK_DIALER) {
+        for (int i = 0; i < DTMF_DOLPHIN_DIALER_TONE_COUNT; i++) {
+            if (dtmf_dolphin_dialer_tone[i].pos.row == row && dtmf_dolphin_dialer_tone[i].pos.col == col) {
+                return dtmf_dolphin_dialer_tone[i].name;
+            }
+        }
+    } else if (block == DTMF_DOLPHIN_TONE_BLOCK_BLUEBOX) {
+        // if (index < DTMF_DOLPHIN_BLUEBOX_TONE_COUNT) {
+        //     return dtmf_dolphin_bluebox_tone[index].name;
+        // }
+    }
+    return "N";
+}
+
+uint8_t dtmf_dolphin_get_tone_span(uint8_t row, uint8_t col, DTMFDolphinToneSection block) {
+    if (block == DTMF_DOLPHIN_TONE_BLOCK_DIALER) {
+        for (int i = 0; i < DTMF_DOLPHIN_DIALER_TONE_COUNT; i++) {
+            if (dtmf_dolphin_dialer_tone[i].pos.row == row && dtmf_dolphin_dialer_tone[i].pos.col == col) {
+                return dtmf_dolphin_dialer_tone[i].pos.span;
+            }
+        }
+    } else if (block == DTMF_DOLPHIN_TONE_BLOCK_BLUEBOX) {
+        // if (index < DTMF_DOLPHIN_BLUEBOX_TONE_COUNT) {
+        //     return dtmf_dolphin_bluebox_tone[index].name;
+        // }
+    }
+    return 1; // Default to 1
+}
+
+void dtmf_dolphin_get_tone_frequencies(float *freq, uint8_t row, uint8_t col, DTMFDolphinToneSection block) {
+    freq[0] = 0;
+    freq[1] = 0;
+
+    if (block == DTMF_DOLPHIN_TONE_BLOCK_DIALER) {
+        for (int i = 0; i < DTMF_DOLPHIN_DIALER_TONE_COUNT; i++) {
+            if (dtmf_dolphin_dialer_tone[i].pos.row == row && dtmf_dolphin_dialer_tone[i].pos.col == col) {
+                freq[0] = dtmf_dolphin_dialer_tone[i].frequency_1;
+                freq[1] = dtmf_dolphin_dialer_tone[i].frequency_2;
+            }
+        }
+    } else if (block == DTMF_DOLPHIN_TONE_BLOCK_BLUEBOX) {
+        // if (index < DTMF_DOLPHIN_BLUEBOX_TONE_COUNT) {
+        //     return dtmf_dolphin_bluebox_tone[index].name;
+        // }
+    }
+}
+
+void dtmf_dolphin_tone_get_max_pos(uint8_t *max_rows, uint8_t *max_cols, uint8_t *max_span, DTMFDolphinToneSection block) {
+    max_rows[0] = 0;
+    max_cols[0] = 0;
+    max_span[0] = 0;
+    uint8_t span[8] = { 0 };
+    if (block == DTMF_DOLPHIN_TONE_BLOCK_DIALER) {
+        for (int i = 0; i < DTMF_DOLPHIN_DIALER_TONE_COUNT; i++) {
+            if (dtmf_dolphin_dialer_tone[i].pos.row > max_rows[0])
+                max_rows[0] = dtmf_dolphin_dialer_tone[i].pos.row;
+            if (dtmf_dolphin_dialer_tone[i].pos.col > max_cols[0])
+                max_cols[0] = dtmf_dolphin_dialer_tone[i].pos.col;
+            span[dtmf_dolphin_dialer_tone[i].pos.row] += dtmf_dolphin_dialer_tone[i].pos.span;
+        }
+        max_rows[0]++;
+        max_cols[0]++;
+        for (int i = 0; i < max_rows[0]; i++) {
+            if (span[i] > max_span[0])
+                max_span[0] = span[i];
+        }
+    } else if (block == DTMF_DOLPHIN_TONE_BLOCK_BLUEBOX) {
+        // if (index < DTMF_DOLPHIN_BLUEBOX_TONE_COUNT) {
+        // for (int i; i < DTMF_DOLPHIN_BLUEBOX_TONE_COUNT; i++) {
+        //     if (dtmf_dolphin_bluebox_tone[i].pos.row > max_rows)
+        //         max_rows = dtmf_dolphin_bluebox_tone[i].pos.row;
+        //     if (dtmf_dolphin_bluebox_tone[i].pos.col > max_cols)
+        //         max_cols = dtmf_dolphin_bluebox_tone[i].pos.col;
+        // }
+    }
+}
+
+// void dtmf_dolphin_bluebox_generate(uint8_t index, uint8_t *buffer) {
+
+//     // TODO: Generate the waveform
+// }

+ 25 - 0
dtmf_dolphin_tone.h

@@ -0,0 +1,25 @@
+#pragma once
+#include "tones/dtmf_dolphin_dialer_tones.h"
+#include "tones/dtmf_dolphin_bluebox_tones.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+    DTMF_DOLPHIN_TONE_BLOCK_DIALER,
+    DTMF_DOLPHIN_TONE_BLOCK_BLUEBOX,
+    DTMF_DOLPHIN_TONE_BLOCK_REDBOX,
+} DTMFDolphinToneSection;
+
+const char* dtmf_dolphin_get_tone_name(uint8_t row, uint8_t col, DTMFDolphinToneSection block);
+
+uint8_t dtmf_dolphin_get_tone_span(uint8_t row, uint8_t col, DTMFDolphinToneSection block);
+
+void dtmf_dolphin_get_tone_frequencies(float *freq, uint8_t row, uint8_t col, DTMFDolphinToneSection block);
+
+void dtmf_dolphin_tone_get_max_pos(uint8_t *max_rows, uint8_t *max_cols, uint8_t *max_span, DTMFDolphinToneSection block);
+
+#ifdef __cplusplus
+}
+#endif

+ 30 - 0
scenes/dtmf_dolphin_scene.c

@@ -0,0 +1,30 @@
+#include "dtmf_dolphin_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const dtmf_dolphin_scene_on_enter_handlers[])(void*) = {
+#include "dtmf_dolphin_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const dtmf_dolphin_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "dtmf_dolphin_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const dtmf_dolphin_scene_on_exit_handlers[])(void* context) = {
+#include "dtmf_dolphin_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers dtmf_dolphin_scene_handlers = {
+    .on_enter_handlers = dtmf_dolphin_scene_on_enter_handlers,
+    .on_event_handlers = dtmf_dolphin_scene_on_event_handlers,
+    .on_exit_handlers = dtmf_dolphin_scene_on_exit_handlers,
+    .scene_num = DTMFDolphinSceneNum,
+};

+ 29 - 0
scenes/dtmf_dolphin_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) DTMFDolphinScene##id,
+typedef enum {
+#include "dtmf_dolphin_scene_config.h"
+    DTMFDolphinSceneNum,
+} DTMFDolphinScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers dtmf_dolphin_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "dtmf_dolphin_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "dtmf_dolphin_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "dtmf_dolphin_scene_config.h"
+#undef ADD_SCENE

+ 16 - 0
scenes/dtmf_dolphin_scene_bluebox.c

@@ -0,0 +1,16 @@
+#include "../dtmf_dolphin_i.h"
+
+void dtmf_dolphin_scene_bluebox_on_enter(void *context) {
+    DTMFDolphinApp* app = context;
+    view_dispatcher_switch_to_view(app->view_dispatcher, DTMFDolphinViewBluebox);
+}
+
+bool dtmf_dolphin_scene_bluebox_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void dtmf_dolphin_scene_bluebox_on_exit(void* context) {
+    UNUSED(context);
+}

+ 3 - 0
scenes/dtmf_dolphin_scene_config.h

@@ -0,0 +1,3 @@
+ADD_SCENE(dtmf_dolphin, start, Start)
+ADD_SCENE(dtmf_dolphin, dialer, Dialer)
+ADD_SCENE(dtmf_dolphin, bluebox, Bluebox)

+ 24 - 0
scenes/dtmf_dolphin_scene_dialer.c

@@ -0,0 +1,24 @@
+#include "../dtmf_dolphin_i.h"
+
+void dtmf_dolphin_scene_dialer_on_enter(void *context) {
+    DTMFDolphinApp* app = context;
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, DTMFDolphinViewDialer);
+}
+
+bool dtmf_dolphin_scene_dialer_on_event(void* context, SceneManagerEvent event) {
+    DTMFDolphinApp* app = context;
+    UNUSED(app);
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        consumed = true;
+    }
+
+    return consumed;
+    return false;
+}
+
+void dtmf_dolphin_scene_dialer_on_exit(void* context) {
+    UNUSED(context);
+}

+ 61 - 0
scenes/dtmf_dolphin_scene_start.c

@@ -0,0 +1,61 @@
+#include "../dtmf_dolphin_i.h"
+
+static void dtmf_dolphin_scene_start_main_menu_enter_callback(void* context, uint32_t index) {
+    DTMFDolphinApp* app = context;
+    if (index == DTMFDolphinItemDialer) {
+        view_dispatcher_send_custom_event(
+            app->view_dispatcher,
+            DTMFDolphinEventStartDialer
+        );
+    } else if (index == DTMFDolphinItemBluebox) {
+        view_dispatcher_send_custom_event(
+            app->view_dispatcher,
+            DTMFDolphinEventStartBluebox
+        );
+    }
+}
+
+void dtmf_dolphin_scene_start_on_enter(void* context) {
+    DTMFDolphinApp* app = context;
+    VariableItemList* var_item_list = app->main_menu_list;
+
+    // VariableItem* item;
+    variable_item_list_set_enter_callback(
+        var_item_list,
+        dtmf_dolphin_scene_start_main_menu_enter_callback,
+        app);
+
+    variable_item_list_add(var_item_list, "Dialer", 0, NULL, NULL);
+    // variable_item_list_add(var_item_list, "Bluebox", 0, NULL, NULL);
+
+    variable_item_list_set_selected_item(
+        var_item_list,
+        scene_manager_get_scene_state(app->scene_manager, DTMFDolphinSceneStart));
+
+    view_dispatcher_switch_to_view(
+        app->view_dispatcher,
+        DTMFDolphinViewMainMenu);
+}
+
+bool dtmf_dolphin_scene_start_on_event(void* context, SceneManagerEvent event) {
+    DTMFDolphinApp* app = context;
+    UNUSED(app);
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if (event.event == DTMFDolphinEventStartDialer) {
+            scene_manager_set_scene_state(app->scene_manager, DTMFDolphinSceneDialer, DTMFDolphinItemDialer);
+            scene_manager_next_scene(app->scene_manager, DTMFDolphinSceneDialer);
+        } else if (event.event == DTMFDolphinEventStartBluebox) {
+            scene_manager_set_scene_state(app->scene_manager, DTMFDolphinSceneBluebox, DTMFDolphinItemBluebox);
+            scene_manager_next_scene(app->scene_manager, DTMFDolphinSceneBluebox);
+        }
+        consumed = true;
+    }
+    return consumed;
+}
+
+void dtmf_dolphin_scene_start_on_exit(void* context) {
+    DTMFDolphinApp* app = context;
+    variable_item_list_reset(app->main_menu_list);
+}

+ 29 - 0
tones/dtmf_dolphin_bluebox_tones.h

@@ -0,0 +1,29 @@
+#pragma once
+#include <stdint.h>
+
+#define DTMF_DOLPHIN_BLUEBOX_TONE_COUNT 16
+
+typedef struct {
+    const char *name;
+    const float frequency_1;
+    const float frequency_2;
+} DTMFDolphinBlueboxTones;
+
+static const DTMFDolphinBlueboxTones dtmf_dolphin_bluebox_tone[DTMF_DOLPHIN_BLUEBOX_TONE_COUNT] = {
+    {"2600 Hz", 2600.0, 0.0},
+    {"1", 700.0, 900.0},
+    {"2", 700.0, 1100.0},
+    {"3", 900.0, 1100.0},
+    {"4", 700.0, 1300.0},
+    {"5", 900.0, 1300.0},
+    {"6", 1100.0, 1300.0},
+    {"7", 700.0, 1500.0},
+    {"8", 900.0, 1500.0},
+    {"9", 1100.0, 1500.0},
+    {"0", 1300.0, 1500.0},
+    {"Key Pulse (KP)", 1100.0, 1700.0},
+    {"Start (ST)", 1500.0, 1700.0},
+    {"CCITT 11", 700.0, 1700.0},
+    {"CCITT 12", 900.0, 1700.0},
+    {"CCITT KP2", 1300.0, 1700.0},
+};

+ 43 - 0
tones/dtmf_dolphin_dialer_tones.h

@@ -0,0 +1,43 @@
+#pragma once
+#include <stdint.h>
+
+#define DTMF_DOLPHIN_DIALER_TONE_COUNT 16
+
+typedef struct DTMFDolphinDialerTonePos {
+    const uint8_t row;
+    const uint8_t col;
+    const uint8_t span;
+} DTMFDolphinDialerTonePos;
+
+typedef struct {
+    const char *name;
+    const float frequency_1;
+    const float frequency_2;
+    const struct DTMFDolphinDialerTonePos pos;
+} DTMFDolphinDialerTones;
+
+/*  Via https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling
+        1209 Hz 	1336 Hz 	1477 Hz 	1633 Hz
+697 Hz 	1 	        2 	        3 	        A
+770 Hz 	4 	        5 	        6 	        B
+852 Hz 	7 	        8 	        9 	        C
+941 Hz 	* 	        0 	        # 	        D */
+
+static const DTMFDolphinDialerTones dtmf_dolphin_dialer_tone[DTMF_DOLPHIN_DIALER_TONE_COUNT] = {
+    {"1", 697.0, 1209.0, {0, 0, 1}},
+    {"2", 697.0, 1336.0, {0, 1, 1}},
+    {"3", 697.0, 1477.0, {0, 2, 1}},
+    {"A", 697.0, 1633.0, {0, 3, 1}},
+    {"4", 770.0, 1209.0, {1, 0, 1}},
+    {"5", 770.0, 1336.0, {1, 1, 1}},
+    {"6", 770.0, 1477.0, {1, 2, 1}},
+    {"B", 770.0, 1633.0, {1, 3, 1}},
+    {"7", 852.0, 1209.0, {2, 0, 1}},
+    {"8", 852.0, 1336.0, {2, 1, 1}},
+    {"9", 852.0, 1477.0, {2, 2, 1}},
+    {"C", 852.0, 1633.0, {2, 3, 1}},
+    {"*", 941.0, 1209.0, {3, 0, 1}},
+    {"0", 941.0, 1336.0, {3, 1, 1}},
+    {"#", 941.0, 1477.0, {3, 2, 1}},
+    {"D", 941.0, 1633.0, {3, 3, 1}},
+};

+ 131 - 0
views/dtmf_dolphin_bluebox.c

@@ -0,0 +1,131 @@
+#include "dtmf_dolphin_bluebox.h"
+
+#include <gui/elements.h>
+
+typedef struct DTMFDolphinBluebox {
+    View* view;
+    DTMFDolphinBlueboxOkCallback callback;
+    void* context;
+} DTMFDolphinBluebox;
+
+typedef struct {
+    uint8_t index;
+} DTMFDolphinBlueboxModel;
+
+static bool dtmf_dolphin_bluebox_process_left(DTMFDolphinBluebox* dtmf_dolphin_bluebox);
+static bool dtmf_dolphin_bluebox_process_right(DTMFDolphinBluebox* dtmf_dolphin_bluebox);
+static bool dtmf_dolphin_bluebox_process_ok(DTMFDolphinBluebox* dtmf_dolphin_bluebox, InputEvent* event);
+
+static void dtmf_dolphin_bluebox_draw_callback(Canvas* canvas, void* _model) {
+    DTMFDolphinBlueboxModel* model = _model;
+    UNUSED(model);
+    canvas_set_font(canvas, FontPrimary);
+    elements_multiline_text_aligned(canvas, 64, 2, AlignCenter, AlignTop, "Bluebox Mode");
+    canvas_set_font(canvas, FontSecondary);
+    elements_multiline_text_aligned(
+        canvas, 64, 16, AlignCenter, AlignTop, "Press < or > to select");
+    // elements_multiline_text_aligned(
+    //     canvas, 64, 32, AlignCenter, AlignTop, dtmf_dolphin_get_tone_name(model->index, DTMF_DOLPHIN_TONE_BLOCK_BLUEBOX));
+}
+
+static bool dtmf_dolphin_bluebox_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    DTMFDolphinBluebox* dtmf_dolphin_bluebox = context;
+    bool consumed = false;
+
+    if(event->type == InputTypeShort) {
+        if(event->key == InputKeyRight) {
+            consumed = dtmf_dolphin_bluebox_process_right(dtmf_dolphin_bluebox);
+        } else if(event->key == InputKeyLeft) {
+            consumed = dtmf_dolphin_bluebox_process_left(dtmf_dolphin_bluebox);
+        }
+    } else if(event->key == InputKeyOk) {
+        consumed = dtmf_dolphin_bluebox_process_ok(dtmf_dolphin_bluebox, event);
+    }
+
+    return consumed;
+}
+
+static bool dtmf_dolphin_bluebox_process_left(DTMFDolphinBluebox* dtmf_dolphin_bluebox) {
+    with_view_model(
+        dtmf_dolphin_bluebox->view, (DTMFDolphinBlueboxModel * model) {
+            if(model->index) {
+                model->index--;
+            }
+            return true;
+        });
+    return true;
+}
+
+static bool dtmf_dolphin_bluebox_process_right(DTMFDolphinBluebox* dtmf_dolphin_bluebox) {
+    with_view_model(
+        dtmf_dolphin_bluebox->view, (DTMFDolphinBlueboxModel * model) {
+            if(model->index < DTMF_DOLPHIN_BLUEBOX_TONE_COUNT) {
+                model->index++;
+            }
+            return true;
+        });
+    return true;
+}
+
+static bool dtmf_dolphin_bluebox_process_ok(DTMFDolphinBluebox* dtmf_dolphin_bluebox, InputEvent* event) {
+    bool consumed = false;
+
+    with_view_model(
+        dtmf_dolphin_bluebox->view, (DTMFDolphinBlueboxModel* model) {
+            if(event->type == InputTypePress) {
+                if(model->index < DTMF_DOLPHIN_BLUEBOX_TONE_COUNT) {
+                    // TODO: Do the thing
+                } else {
+                    // TODO: Do the thing
+                }
+                consumed = true;
+            } else if(event->type == InputTypeRelease) {
+                if(model->index < DTMF_DOLPHIN_BLUEBOX_TONE_COUNT) {
+                    // gpio_item_set_pin(Model->pin_idx, false);
+                } else {
+                    // gpio_item_set_all_pins(false);
+                }
+                consumed = true;
+            }
+            dtmf_dolphin_bluebox->callback(event->type, dtmf_dolphin_bluebox->context);
+            return true;
+        });
+
+    return consumed;
+}
+
+DTMFDolphinBluebox* dtmf_dolphin_bluebox_alloc() {
+    DTMFDolphinBluebox* dtmf_dolphin_bluebox = malloc(sizeof(DTMFDolphinBluebox));
+
+    dtmf_dolphin_bluebox->view = view_alloc();
+    view_allocate_model(dtmf_dolphin_bluebox->view, ViewModelTypeLocking, sizeof(DTMFDolphinBlueboxModel));
+    view_set_context(dtmf_dolphin_bluebox->view, dtmf_dolphin_bluebox);
+    view_set_draw_callback(dtmf_dolphin_bluebox->view, dtmf_dolphin_bluebox_draw_callback);
+    view_set_input_callback(dtmf_dolphin_bluebox->view, dtmf_dolphin_bluebox_input_callback);
+
+    return dtmf_dolphin_bluebox;
+}
+
+void dtmf_dolphin_bluebox_free(DTMFDolphinBluebox* dtmf_dolphin_bluebox) {
+    furi_assert(dtmf_dolphin_bluebox);
+    view_free(dtmf_dolphin_bluebox->view);
+    free(dtmf_dolphin_bluebox);
+}
+
+View* dtmf_dolphin_bluebox_get_view(DTMFDolphinBluebox* dtmf_dolphin_bluebox) {
+    furi_assert(dtmf_dolphin_bluebox);
+    return dtmf_dolphin_bluebox->view;
+}
+
+void dtmf_dolphin_bluebox_set_ok_callback(DTMFDolphinBluebox* dtmf_dolphin_bluebox, DTMFDolphinBlueboxOkCallback callback, void* context) {
+    furi_assert(dtmf_dolphin_bluebox);
+    furi_assert(callback);
+    with_view_model(
+        dtmf_dolphin_bluebox->view, (DTMFDolphinBlueboxModel * model) {
+            UNUSED(model);
+            dtmf_dolphin_bluebox->callback = callback;
+            dtmf_dolphin_bluebox->context = context;
+            return false;
+        });
+}

+ 15 - 0
views/dtmf_dolphin_bluebox.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../dtmf_dolphin_tone.h"
+
+typedef struct DTMFDolphinBluebox DTMFDolphinBluebox;
+typedef void (*DTMFDolphinBlueboxOkCallback)(InputType type, void* context);
+
+DTMFDolphinBluebox* dtmf_dolphin_bluebox_alloc();
+
+void dtmf_dolphin_bluebox_free(DTMFDolphinBluebox* dtmf_dolphin_bluebox);
+
+View* dtmf_dolphin_bluebox_get_view(DTMFDolphinBluebox* dtmf_dolphin_bluebox);
+
+void dtmf_dolphin_bluebox_set_ok_callback(DTMFDolphinBluebox* dtmf_dolphin_bluebox, DTMFDolphinBlueboxOkCallback callback, void* context);

+ 8 - 0
views/dtmf_dolphin_common.h

@@ -0,0 +1,8 @@
+#pragma once
+#include "../dtmf_dolphin_player.h"
+
+#define DTMF_DOLPHIN_NUMPAD_X 1
+#define DTMF_DOLPHIN_NUMPAD_Y 14
+#define DTMF_DOLPHIN_BUTTON_WIDTH 13
+#define DTMF_DOLPHIN_BUTTON_HEIGHT 13
+#define DTMF_DOLPHIN_BUTTON_PADDING 1  // all sides

+ 271 - 0
views/dtmf_dolphin_dialer.c

@@ -0,0 +1,271 @@
+#include "dtmf_dolphin_dialer.h"
+
+#include <gui/elements.h>
+
+typedef struct DTMFDolphinDialer {
+    View* view;
+    DTMFDolphinDialerOkCallback callback;
+    void* context;
+} DTMFDolphinDialer;
+
+typedef struct {
+    uint8_t row;
+    uint8_t col;
+    float *freq;
+} DTMFDolphinDialerModel;
+
+static bool dtmf_dolphin_dialer_process_up(DTMFDolphinDialer* dtmf_dolphin_dialer);
+static bool dtmf_dolphin_dialer_process_down(DTMFDolphinDialer* dtmf_dolphin_dialer);
+static bool dtmf_dolphin_dialer_process_left(DTMFDolphinDialer* dtmf_dolphin_dialer);
+static bool dtmf_dolphin_dialer_process_right(DTMFDolphinDialer* dtmf_dolphin_dialer);
+static bool dtmf_dolphin_dialer_process_ok(DTMFDolphinDialer* dtmf_dolphin_dialer, InputEvent* event);
+
+void draw_button(Canvas* canvas, uint8_t row, uint8_t col, bool invert) {
+
+    uint8_t left = DTMF_DOLPHIN_NUMPAD_X + \
+        // ((col + 1) * DTMF_DOLPHIN_BUTTON_PADDING) + 
+        (col * DTMF_DOLPHIN_BUTTON_WIDTH);
+        // (col * DTMF_DOLPHIN_BUTTON_PADDING);
+    uint8_t top = DTMF_DOLPHIN_NUMPAD_Y + \
+        // ((row + 1) * DTMF_DOLPHIN_BUTTON_PADDING) + 
+        (row * DTMF_DOLPHIN_BUTTON_HEIGHT);
+        // (row * DTMF_DOLPHIN_BUTTON_PADDING);
+
+    uint8_t span = dtmf_dolphin_get_tone_span(row, col, DTMF_DOLPHIN_TONE_BLOCK_DIALER);
+
+    canvas_set_color(canvas, ColorBlack);
+    
+    if (invert)
+        canvas_draw_rbox(canvas, left, top,
+            (DTMF_DOLPHIN_BUTTON_WIDTH * span) - (DTMF_DOLPHIN_BUTTON_PADDING * 2),
+            DTMF_DOLPHIN_BUTTON_HEIGHT - (DTMF_DOLPHIN_BUTTON_PADDING * 2),
+            2);
+    else
+        canvas_draw_rframe(canvas, left, top,
+            (DTMF_DOLPHIN_BUTTON_WIDTH * span) - (DTMF_DOLPHIN_BUTTON_PADDING * 2),
+            DTMF_DOLPHIN_BUTTON_HEIGHT- (DTMF_DOLPHIN_BUTTON_PADDING * 2),
+            2);
+
+    if (invert)
+        canvas_invert_color(canvas);
+
+
+    canvas_set_font(canvas, FontSecondary);
+    // canvas_set_color(canvas, invert ? ColorWhite : ColorBlack);
+    canvas_draw_str_aligned(canvas,
+        left - 1 + (int) ((DTMF_DOLPHIN_BUTTON_WIDTH * span) / 2),
+        top + (int) (DTMF_DOLPHIN_BUTTON_HEIGHT / 2),
+        AlignCenter,
+        AlignCenter,
+        dtmf_dolphin_get_tone_name(row, col, DTMF_DOLPHIN_TONE_BLOCK_DIALER));
+
+    if (invert)
+        canvas_invert_color(canvas);
+}
+
+void draw_dialer(Canvas* canvas, void* _model) {
+    DTMFDolphinDialerModel* model = _model;
+    uint8_t max_rows;
+    uint8_t max_cols;
+    uint8_t max_span;
+    dtmf_dolphin_tone_get_max_pos(&max_rows, &max_cols, &max_span, DTMF_DOLPHIN_TONE_BLOCK_DIALER);
+
+    canvas_set_font(canvas, FontSecondary);
+
+    for (int r = 0; r < max_rows; r++) {
+        for (int c = 0; c < max_cols; c++) {
+            if (model->row == r && model->col == c)
+                draw_button(canvas, r, c, true);
+            else
+                draw_button(canvas, r, c, false);
+        }
+    }
+}
+
+void update_frequencies(DTMFDolphinDialerModel *model) {
+    dtmf_dolphin_get_tone_frequencies(model->freq, model->row, model->col, DTMF_DOLPHIN_TONE_BLOCK_DIALER);
+}
+
+static void dtmf_dolphin_dialer_draw_callback(Canvas* canvas, void* _model) {
+    DTMFDolphinDialerModel* model = _model;
+    uint8_t max_rows;
+    uint8_t max_cols;
+    uint8_t max_span;
+    dtmf_dolphin_tone_get_max_pos(&max_rows, &max_cols, &max_span, DTMF_DOLPHIN_TONE_BLOCK_DIALER);
+
+    canvas_set_font(canvas, FontPrimary);
+    elements_multiline_text(canvas, 2, 10, "Dialer");
+    canvas_draw_line(canvas,
+        (max_span * DTMF_DOLPHIN_BUTTON_WIDTH) + 1, 0,
+        (max_span * DTMF_DOLPHIN_BUTTON_WIDTH) + 1, canvas_height(canvas));
+    elements_multiline_text(canvas, (max_span * DTMF_DOLPHIN_BUTTON_WIDTH) + 4, 10, "Detail");
+    canvas_draw_line(canvas, 0, DTMF_DOLPHIN_NUMPAD_Y - 3, canvas_width(canvas), DTMF_DOLPHIN_NUMPAD_Y - 3);
+    // elements_multiline_text_aligned(canvas, 64, 2, AlignCenter, AlignTop, "Dialer Mode");
+
+    draw_dialer(canvas, model);
+
+    string_t output;
+    string_init(output);
+
+    string_cat_printf(
+        output,
+        "F1: %u Hz\nF2: %u Hz",
+        model->freq[0] ? (unsigned int) model->freq[0] : 0,
+        model->freq[1] ? (unsigned int) model->freq[1] : 0);
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_set_color(canvas, ColorBlack);
+    elements_multiline_text(canvas, (max_span * DTMF_DOLPHIN_BUTTON_WIDTH) + 4, 21, string_get_cstr(output));
+
+    string_clear(output);
+}
+
+static bool dtmf_dolphin_dialer_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    DTMFDolphinDialer* dtmf_dolphin_dialer = context;
+    bool consumed = false;
+
+    if(event->type == InputTypeShort) {
+        if(event->key == InputKeyRight) {
+            consumed = dtmf_dolphin_dialer_process_right(dtmf_dolphin_dialer);
+        } else if(event->key == InputKeyLeft) {
+            consumed = dtmf_dolphin_dialer_process_left(dtmf_dolphin_dialer);
+        } else if(event->key == InputKeyUp) {
+            consumed = dtmf_dolphin_dialer_process_up(dtmf_dolphin_dialer);
+        } else if(event->key == InputKeyDown) {
+            consumed = dtmf_dolphin_dialer_process_down(dtmf_dolphin_dialer);
+        }
+
+    } else if(event->key == InputKeyOk) {
+        consumed = dtmf_dolphin_dialer_process_ok(dtmf_dolphin_dialer, event);
+    }
+
+    return consumed;
+}
+
+static bool dtmf_dolphin_dialer_process_up(DTMFDolphinDialer* dtmf_dolphin_dialer) {
+    with_view_model(
+        dtmf_dolphin_dialer->view, (DTMFDolphinDialerModel * model) {
+            if(model->row > 0) {
+                model->row--;
+                update_frequencies(model);
+            }
+            return true;
+        });
+    return true;
+}
+
+static bool dtmf_dolphin_dialer_process_down(DTMFDolphinDialer* dtmf_dolphin_dialer) {
+    uint8_t max_rows = 0;
+    uint8_t max_cols = 0;
+    uint8_t max_span = 0;
+    dtmf_dolphin_tone_get_max_pos(&max_rows, &max_cols, &max_span, DTMF_DOLPHIN_TONE_BLOCK_DIALER);
+
+    with_view_model(
+        dtmf_dolphin_dialer->view, (DTMFDolphinDialerModel * model) {
+            if(model->row < max_rows - 1) {
+                model->row++;
+                update_frequencies(model);
+            }
+            return true;
+        });
+    return true;
+}
+
+static bool dtmf_dolphin_dialer_process_left(DTMFDolphinDialer* dtmf_dolphin_dialer) {
+    with_view_model(
+        dtmf_dolphin_dialer->view, (DTMFDolphinDialerModel * model) {
+            if(model->col > 0) {
+                model->col--;
+                update_frequencies(model);
+            }
+            return true;
+        });
+    return true;
+}
+
+static bool dtmf_dolphin_dialer_process_right(DTMFDolphinDialer* dtmf_dolphin_dialer) {
+    uint8_t max_rows = 0;
+    uint8_t max_cols = 0;
+    uint8_t max_span = 0;
+    dtmf_dolphin_tone_get_max_pos(&max_rows, &max_cols, &max_span, DTMF_DOLPHIN_TONE_BLOCK_DIALER);
+
+    with_view_model(
+        dtmf_dolphin_dialer->view, (DTMFDolphinDialerModel * model) {
+            if(model->col < max_cols - 1) {
+                model->col++;
+                update_frequencies(model);
+            }
+            return true;
+        });
+    return true;
+}
+
+static bool dtmf_dolphin_dialer_process_ok(DTMFDolphinDialer* dtmf_dolphin_dialer, InputEvent* event) {
+    bool consumed = false;
+
+    with_view_model(
+        dtmf_dolphin_dialer->view, (DTMFDolphinDialerModel * model) {
+            if (event->type == InputTypePress) {
+                dtmf_dolphin_player_play_tones(model->freq);
+            } else if (event->type == InputTypeRelease) {
+                dtmf_dolphin_player_stop_tones();
+            }
+
+            return true;
+        });
+
+    return consumed;
+}
+
+DTMFDolphinDialer* dtmf_dolphin_dialer_alloc() {
+    DTMFDolphinDialer* dtmf_dolphin_dialer = malloc(sizeof(DTMFDolphinDialer));
+
+    dtmf_dolphin_dialer->view = view_alloc();
+    view_allocate_model(dtmf_dolphin_dialer->view, ViewModelTypeLocking, sizeof(DTMFDolphinDialerModel));
+
+    with_view_model(
+        dtmf_dolphin_dialer->view, (DTMFDolphinDialerModel * model) {
+            model->col = 0;
+            model->row = 0;
+            model->freq = malloc(sizeof(float) * 2);
+            update_frequencies(model);
+            return true;
+        }
+    );
+
+    view_set_context(dtmf_dolphin_dialer->view, dtmf_dolphin_dialer);
+    view_set_draw_callback(dtmf_dolphin_dialer->view, dtmf_dolphin_dialer_draw_callback);
+    view_set_input_callback(dtmf_dolphin_dialer->view, dtmf_dolphin_dialer_input_callback);
+
+    return dtmf_dolphin_dialer;
+}
+
+void dtmf_dolphin_dialer_free(DTMFDolphinDialer* dtmf_dolphin_dialer) {
+    furi_assert(dtmf_dolphin_dialer);
+    with_view_model(
+        dtmf_dolphin_dialer->view, (DTMFDolphinDialerModel * model) {
+            free(model->freq);
+            return true;
+        }
+    );
+    view_free(dtmf_dolphin_dialer->view);
+    free(dtmf_dolphin_dialer);
+}
+
+View* dtmf_dolphin_dialer_get_view(DTMFDolphinDialer* dtmf_dolphin_dialer) {
+    furi_assert(dtmf_dolphin_dialer);
+    return dtmf_dolphin_dialer->view;
+}
+
+// void dtmf_dolphin_dialer_set_ok_callback(DTMFDolphinDialer* dtmf_dolphin_dialer, DTMFDolphinDialerOkCallback callback, void* context) {
+//     furi_assert(dtmf_dolphin_dialer);
+//     furi_assert(callback);
+//     with_view_model(
+//         dtmf_dolphin_dialer->view, (DTMFDolphinDialerModel * model) {
+//             UNUSED(model);
+//             dtmf_dolphin_dialer->callback = callback;
+//             dtmf_dolphin_dialer->context = context;
+//             return false;
+//         });
+// }

+ 17 - 0
views/dtmf_dolphin_dialer.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../dtmf_dolphin_event.h"
+#include "../dtmf_dolphin_tone.h"
+#include "dtmf_dolphin_common.h"
+
+typedef struct DTMFDolphinDialer DTMFDolphinDialer;
+typedef void (*DTMFDolphinDialerOkCallback)(InputType type, void* context);
+
+DTMFDolphinDialer* dtmf_dolphin_dialer_alloc();
+
+void dtmf_dolphin_dialer_free(DTMFDolphinDialer* dtmf_dolphin_dialer);
+
+View* dtmf_dolphin_dialer_get_view(DTMFDolphinDialer* dtmf_dolphin_dialer);
+
+void dtmf_dolphin_dialer_set_ok_callback(DTMFDolphinDialer* dtmf_dolphin_dialer, DTMFDolphinDialerOkCallback callback, void* context);