Skip to content

Commit

Permalink
Merge pull request #22 from kbembedded/gblink-refactor
Browse files Browse the repository at this point in the history
Implement flexible gblink api, add support for, and autodetect MALVEKE board
  • Loading branch information
EstebanFuentealba authored Dec 17, 2023
2 parents bb9dac7 + a6d3c85 commit 2c7e87e
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 81 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "lib/flipper-gblink"]
path = lib/flipper-gblink
url = https://github.com/kbembedded/flipper-gblink
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ This is a Pokemon exchange application from Flipper Zero to Game Boy [(Generacti
It currently trades a Pokemon based on your choice of Pokemon, Level, Stats and 4 Moves.

## Hardware Interface
The Game Boy is connected to the Flipper Zero's GPIO pins via a GBC style Game Link Cable. A Flipper GPIO module with a proper Game Link Cable connector [is available here](https://www.tindie.com/products/kbembedded/game-link-gpio-module-for-flipper-zero-game-boy/)!
The Game Boy is connected to the Flipper Zero's GPIO pins via a GBC style Game Link Cable. The [Flipper GB Link module](https://www.tindie.com/products/kbembedded/game-link-gpio-module-for-flipper-zero-game-boy/) is an easy way to connect a Game Boy via a Game Link Cable to the Flipper Zero.

Additionally, the [MALVEKE - GAME BOY Tools for Flipper Zero](https://www.tindie.com/products/efuentealba/malveke-game-boy-tools-for-flipper-zero/) is supported by this tool.

Details on the hardware interface, as well as how to create your own adapter board, can be found in the [How Does It Work](#how-does-it-work) section below.

Expand Down Expand Up @@ -53,6 +55,7 @@ And use [**qFlipper**](https://flipperzero.one/update) to copy the generated **p

These instructions assume that you are starting at the Flipper Zero desktop. Otherwise, press the Back button until you are at the desktop.

- If using a MALVEKE board, plug it in to the GPIO header now. The app will auto-detect and select the correct pinout to support the MALVEKE EXT1 interface. If using the Flipper GB Link board, or any other pinout, they can be connected to the Flipper Zero now, or at any point in the future.
- Press the `OK` button on the Flipper to open the main menu.
- Choose `Applications` from the menu.
- Choose `GPIO` from the submenu.
Expand Down
11 changes: 7 additions & 4 deletions README_catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

## Introduction

Now supports MALVEKE board!

This is a Pokemon exchange application from Flipper Zero to Game Boy (Generación I). Flipper Zero emulates a "Slave" Game Boy connected to a Game Link Cable to be able to exchange any Pokemon from the First Generation (Red, Blue, Yellow) to a real Game Boy.

It is a Proof of Concept (POC) for using views, GPIO, and FURI (Flipper Universal Registry Implementation).
If a MALVEKE board is plugged in to GPIO before starting the app, the app will default to using the MALVEKE EXT1 interface.


## Connection: Flipper Zero GPIO - Game Boy

The pins should be connected as follows:
The original pinout is as follows:

| Cable Game Link (Socket) | Flipper Zero GPIO |
| ------------------------ | ----------------- |
Expand All @@ -18,6 +20,9 @@ The pins should be connected as follows:
| 3 (SI) | 7 (C3) |
| 2 (SO) | 5 (B3) |

Using the "Select Pinout" option, the Original, MALVEKE, or any custom pin configuration can be selected.


## How does it work?

The method used to communicate 2 Game Boys is based on the SPI protocol, which is a very simple serial communication protocol in which a master device communicates with one or more slave devices. The protocol is bidirectional and synchronous, and uses three basic signals:
Expand All @@ -34,5 +39,3 @@ The Game Boy link protocol is synchronous and requires the slave device to respo
## Tested In
- Game Boy Color (GBC)
- Game Boy Advance (GBA)


13 changes: 10 additions & 3 deletions application.fam
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ App(
entry_point="pokemon_app",
requires=["gui"],
stack_size=2 * 1024,
fap_version=[1,4],
fap_version=[1,5],
fap_category="GPIO",
fap_icon="pokemon_10px.png",
fap_icon_assets="assets",
fap_author="Esteban Fuentealba",
fap_weburl="https://github.com/EstebanFuentealba"
fap_author="Esteban Fuentealba, Kris Bahnsen, Darryn Cull",
fap_weburl="https://github.com/EstebanFuentealba",
fap_description="Pokemon exchange from Flipper Zero to Game Boy for Generation I (Pokemon Red, Blue, Yellow)",
fap_private_libs=[
Lib(
name="flipper-gblink",
sources=["gblink.c"],
),
],
)
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog - Patch Notes

## Version 1.5
- **Add Features:** Incorporate flipper-gblink library; Add support for MALVEKE board as well as custom pin selection; If MALVEKE board is detected, default to that pinout, otherwise use the original documented pinout.
- **BUG:** The current MALVEKE pinout and interrupt use breaks the OK button after entering the trade screen.

## Version 1.4
- **Bug Fixes:** More robust trade logic fixes issues with names, remove ability to use numbers in Pokemon/Trainer names as the game itself will not allow that, fix trade animation not always being animated, make FAP icon 1bpp.
- **Add Features:** Implement trade patch list that Game Boy expects and uses, add ability to return to main menu to modify a Pokemon traded to the Flipper and re-enter trade without the Game Boy needing to power cycle and re-connect through the Link Club, add back debug logging.
Expand Down
1 change: 1 addition & 0 deletions lib/flipper-gblink
Submodule flipper-gblink added at 91c642
20 changes: 20 additions & 0 deletions pokemon_app.c
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,20 @@ static void trade_block_free(TradeBlock* trade) {
free(trade);
}

/* The MALVEKE board has an esp32 which is set to TX on the flipper's default
* UART pins. If this pin shows signs of something connected, assume a MALVEKE
* board is being used.
*/
static bool detect_malveke(void) {
bool rc;

furi_hal_gpio_init(&gpio_usart_rx, GpioModeInput, GpioPullDown, GpioSpeedVeryHigh);
rc = furi_hal_gpio_read(&gpio_usart_rx);
furi_hal_gpio_init_simple(&gpio_usart_rx, GpioModeAnalog);

return rc;
}

PokemonFap* pokemon_alloc() {
PokemonFap* pokemon_fap = (PokemonFap*)malloc(sizeof(PokemonFap));

Expand All @@ -2157,6 +2171,11 @@ PokemonFap* pokemon_alloc() {
// Set up defaults
pokemon_fap->curr_pokemon = 0;
pokemon_fap->curr_stats = 0;
pokemon_fap->malveke_detected = detect_malveke();
memcpy(
&pokemon_fap->pins,
&common_pinouts[pokemon_fap->malveke_detected],
sizeof(struct gblink_pins));

// Set up trade party struct
pokemon_fap->trade_block = trade_block_alloc();
Expand Down Expand Up @@ -2189,6 +2208,7 @@ PokemonFap* pokemon_alloc() {
pokemon_fap->trade = trade_alloc(
pokemon_fap->trade_block,
pokemon_fap->pokemon_table,
&pokemon_fap->pins,
pokemon_fap->view_dispatcher,
AppViewTrade);

Expand Down
5 changes: 5 additions & 0 deletions pokemon_app.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <gui/modules/submenu.h>
#include <gui/modules/text_input.h>
#include <gui/modules/variable_item_list.h>
#include <gblink.h>

#include "pokemon_data.h"

Expand Down Expand Up @@ -75,6 +76,10 @@ struct pokemon_fap {
*/
TradeBlock* trade_block;

/* Pin definition to actual Game Link Cable interface */
struct gblink_pins pins;
int malveke_detected;

/* The currently selected pokemon */
int curr_pokemon;

Expand Down
10 changes: 10 additions & 0 deletions scenes/pokemon_menu.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "pokemon_ot_id.h"
#include "pokemon_ot_name.h"
#include "pokemon_trade.h"
#include "pokemon_pins.h"

static void scene_change_from_main_cb(void* context, uint32_t index) {
PokemonFap* pokemon_fap = (PokemonFap*)context;
Expand Down Expand Up @@ -95,6 +96,12 @@ void main_menu_scene_on_enter(void* context) {
pokemon_fap->submenu, buf, SelectOTNameScene, scene_change_from_main_cb, pokemon_fap);
submenu_add_item(
pokemon_fap->submenu, "Trade PKMN", TradeScene, scene_change_from_main_cb, pokemon_fap);
submenu_add_item(
pokemon_fap->submenu,
"Select Pinout",
SelectPinsScene,
scene_change_from_main_cb,
pokemon_fap);

submenu_set_selected_item(
pokemon_fap->submenu,
Expand Down Expand Up @@ -128,6 +135,7 @@ void (*const pokemon_scene_on_enter_handlers[])(void*) = {
select_ot_id_scene_on_enter,
select_ot_name_scene_on_enter,
trade_scene_on_enter,
select_pins_scene_on_enter,
};

void (*const pokemon_scene_on_exit_handlers[])(void*) = {
Expand All @@ -143,6 +151,7 @@ void (*const pokemon_scene_on_exit_handlers[])(void*) = {
select_ot_id_scene_on_exit,
select_ot_name_scene_on_exit,
null_scene_on_exit,
select_pins_scene_on_exit,
};

bool (*const pokemon_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
Expand All @@ -158,6 +167,7 @@ bool (*const pokemon_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
null_scene_on_event,
null_scene_on_event,
null_scene_on_event,
null_scene_on_event,
};

const SceneManagerHandlers pokemon_scene_manager_handlers = {
Expand Down
1 change: 1 addition & 0 deletions scenes/pokemon_menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ typedef enum {
SelectOTIDScene,
SelectOTNameScene,
TradeScene,
SelectPinsScene,
SceneCount,
} AppScene;

Expand Down
169 changes: 169 additions & 0 deletions scenes/pokemon_pins.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#include <gui/modules/variable_item_list.h>
#include <furi.h>

#include "../pokemon_app.h"
#include "pokemon_menu.h"

struct named_pins {
const char* text;
const GpioPin* pin;
};

static const struct named_pins named_pins[] = {
{"PA7", &gpio_ext_pa7},
{"PA6", &gpio_ext_pa6},
{"PA4", &gpio_ext_pa4},
{"PB3", &gpio_ext_pb3},
{"PB2", &gpio_ext_pb2},
{"PC3", &gpio_ext_pc3},
{"PC1", &gpio_ext_pc1},
{"PC0", &gpio_ext_pc0},
{},
};

#define NUM_PINS 8

/* This must match gblink's enum order */
static const char* named_groups[] = {
"Original",
"Malveke",
"Custom",
"",
};

struct itemlist_builder {
VariableItem* named;
VariableItem* serin;
VariableItem* serout;
VariableItem* clk;
uint8_t named_index;
uint8_t serin_index;
uint8_t serout_index;
uint8_t clk_index;
};

/* Just make it a global, whatever */
static struct itemlist_builder builder = {0};
static void select_pins_rebuild_list(PokemonFap* pokemon_fap);

static void select_pins_set(PokemonFap* pokemon_fap) {
pokemon_fap->pins.serin = named_pins[builder.serin_index].pin;
pokemon_fap->pins.serout = named_pins[builder.serout_index].pin;
pokemon_fap->pins.clk = named_pins[builder.clk_index].pin;
}

static void select_named_group_callback(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item);
PokemonFap* pokemon_fap = variable_item_get_context(item);

variable_item_set_current_value_text(item, named_groups[index]);
builder.named_index = index;
select_pins_rebuild_list(pokemon_fap);
variable_item_list_set_selected_item(pokemon_fap->variable_item_list, 0);
}

static void select_pins_serin_callback(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item);
PokemonFap* pokemon_fap = variable_item_get_context(item);

variable_item_set_current_value_text(item, named_pins[index].text);
builder.serin_index = index;
select_pins_rebuild_list(pokemon_fap);
variable_item_list_set_selected_item(pokemon_fap->variable_item_list, 1);
}

static void select_pins_serout_callback(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item);
PokemonFap* pokemon_fap = variable_item_get_context(item);

variable_item_set_current_value_text(item, named_pins[index].text);
builder.serout_index = index;
select_pins_rebuild_list(pokemon_fap);
variable_item_list_set_selected_item(pokemon_fap->variable_item_list, 2);
}

static void select_pins_clk_callback(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item);
PokemonFap* pokemon_fap = variable_item_get_context(item);

variable_item_set_current_value_text(item, named_pins[index].text);
builder.clk_index = index;
select_pins_rebuild_list(pokemon_fap);
variable_item_list_set_selected_item(pokemon_fap->variable_item_list, 3);
}

static void select_pins_rebuild_list(PokemonFap* pokemon_fap) {
int num;

/* HACK: TODO: It would be better to do this programmatically, but, I'm kind
* of done working on this feature so its going to be hardcoded for now.
*/
switch(builder.named_index) {
case 0: // Original
num = 1;
builder.serin_index = 5;
builder.serout_index = 3;
builder.clk_index = 4;
break;
case 1: // MALVEKE
num = 1;
builder.serin_index = 1;
builder.serout_index = 0;
builder.clk_index = 3;
break;
default:
num = NUM_PINS;
break;
}

/* HACK: */
pokemon_fap->malveke_detected = builder.named_index;

select_pins_set(pokemon_fap);

variable_item_list_reset(pokemon_fap->variable_item_list);

builder.named = variable_item_list_add(
pokemon_fap->variable_item_list, "Mode", 3, select_named_group_callback, pokemon_fap);
builder.serin = variable_item_list_add(
pokemon_fap->variable_item_list, "SI:", num, select_pins_serin_callback, pokemon_fap);
builder.serout = variable_item_list_add(
pokemon_fap->variable_item_list, "SO:", num, select_pins_serout_callback, pokemon_fap);
builder.clk = variable_item_list_add(
pokemon_fap->variable_item_list, "CLK:", num, select_pins_clk_callback, pokemon_fap);

variable_item_set_current_value_index(builder.named, builder.named_index);
variable_item_set_current_value_text(builder.named, named_groups[builder.named_index]);

variable_item_set_current_value_index(builder.serin, (num == 1 ? 0 : builder.serin_index));
variable_item_set_current_value_text(builder.serin, named_pins[builder.serin_index].text);

variable_item_set_current_value_index(builder.serout, (num == 1 ? 0 : builder.serout_index));
variable_item_set_current_value_text(builder.serout, named_pins[builder.serout_index].text);

variable_item_set_current_value_index(builder.clk, (num == 1 ? 0 : builder.clk_index));
variable_item_set_current_value_text(builder.clk, named_pins[builder.clk_index].text);
}

void select_pins_scene_on_exit(void* context) {
PokemonFap* pokemon_fap = (PokemonFap*)context;

view_dispatcher_switch_to_view(pokemon_fap->view_dispatcher, AppViewMainMenu);
view_dispatcher_remove_view(pokemon_fap->view_dispatcher, AppViewOpts);
}

void select_pins_scene_on_enter(void* context) {
PokemonFap* pokemon_fap = (PokemonFap*)context;

/* TODO: Figure out what defaults we should use for pins based on attached board! */
/* HACK: */
if(builder.named_index < 2) builder.named_index = pokemon_fap->malveke_detected;

select_pins_rebuild_list(pokemon_fap);

view_dispatcher_add_view(
pokemon_fap->view_dispatcher,
AppViewOpts,
variable_item_list_get_view(pokemon_fap->variable_item_list));
view_dispatcher_switch_to_view(pokemon_fap->view_dispatcher, AppViewOpts);
}
9 changes: 9 additions & 0 deletions scenes/pokemon_pins.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#ifndef POKEMON_PINS_H
#define POKEMON_PINS_H

#pragma once

void select_pins_scene_on_enter(void* context);
void select_pins_scene_on_exit(void* context);

#endif // POKEMON_PINS_H
Loading

0 comments on commit 2c7e87e

Please sign in to comment.