Skip to content

Commit 2c7e87e

Browse files
Merge pull request #22 from kbembedded/gblink-refactor
Implement flexible gblink api, add support for, and autodetect MALVEKE board
2 parents bb9dac7 + a6d3c85 commit 2c7e87e

14 files changed

+277
-81
lines changed

.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "lib/flipper-gblink"]
2+
path = lib/flipper-gblink
3+
url = https://github.com/kbembedded/flipper-gblink

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ This is a Pokemon exchange application from Flipper Zero to Game Boy [(Generacti
2121
It currently trades a Pokemon based on your choice of Pokemon, Level, Stats and 4 Moves.
2222

2323
## Hardware Interface
24-
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/)!
24+
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.
25+
26+
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.
2527

2628
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.
2729

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

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

58+
- 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.
5659
- Press the `OK` button on the Flipper to open the main menu.
5760
- Choose `Applications` from the menu.
5861
- Choose `GPIO` from the submenu.

README_catalog.md

+7-4
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
## Introduction
44

5+
Now supports MALVEKE board!
6+
57
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.
68

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

911

1012
## Connection: Flipper Zero GPIO - Game Boy
1113

12-
The pins should be connected as follows:
14+
The original pinout is as follows:
1315

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

23+
Using the "Select Pinout" option, the Original, MALVEKE, or any custom pin configuration can be selected.
24+
25+
2126
## How does it work?
2227

2328
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:
@@ -34,5 +39,3 @@ The Game Boy link protocol is synchronous and requires the slave device to respo
3439
## Tested In
3540
- Game Boy Color (GBC)
3641
- Game Boy Advance (GBA)
37-
38-

application.fam

+10-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ App(
55
entry_point="pokemon_app",
66
requires=["gui"],
77
stack_size=2 * 1024,
8-
fap_version=[1,4],
8+
fap_version=[1,5],
99
fap_category="GPIO",
1010
fap_icon="pokemon_10px.png",
1111
fap_icon_assets="assets",
12-
fap_author="Esteban Fuentealba",
13-
fap_weburl="https://github.com/EstebanFuentealba"
12+
fap_author="Esteban Fuentealba, Kris Bahnsen, Darryn Cull",
13+
fap_weburl="https://github.com/EstebanFuentealba",
14+
fap_description="Pokemon exchange from Flipper Zero to Game Boy for Generation I (Pokemon Red, Blue, Yellow)",
15+
fap_private_libs=[
16+
Lib(
17+
name="flipper-gblink",
18+
sources=["gblink.c"],
19+
),
20+
],
1421
)

changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog - Patch Notes
22

3+
## Version 1.5
4+
- **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.
5+
- **BUG:** The current MALVEKE pinout and interrupt use breaks the OK button after entering the trade screen.
6+
37
## Version 1.4
48
- **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.
59
- **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.

lib/flipper-gblink

Submodule flipper-gblink added at 91c6422

pokemon_app.c

+20
Original file line numberDiff line numberDiff line change
@@ -2136,6 +2136,20 @@ static void trade_block_free(TradeBlock* trade) {
21362136
free(trade);
21372137
}
21382138

2139+
/* The MALVEKE board has an esp32 which is set to TX on the flipper's default
2140+
* UART pins. If this pin shows signs of something connected, assume a MALVEKE
2141+
* board is being used.
2142+
*/
2143+
static bool detect_malveke(void) {
2144+
bool rc;
2145+
2146+
furi_hal_gpio_init(&gpio_usart_rx, GpioModeInput, GpioPullDown, GpioSpeedVeryHigh);
2147+
rc = furi_hal_gpio_read(&gpio_usart_rx);
2148+
furi_hal_gpio_init_simple(&gpio_usart_rx, GpioModeAnalog);
2149+
2150+
return rc;
2151+
}
2152+
21392153
PokemonFap* pokemon_alloc() {
21402154
PokemonFap* pokemon_fap = (PokemonFap*)malloc(sizeof(PokemonFap));
21412155

@@ -2157,6 +2171,11 @@ PokemonFap* pokemon_alloc() {
21572171
// Set up defaults
21582172
pokemon_fap->curr_pokemon = 0;
21592173
pokemon_fap->curr_stats = 0;
2174+
pokemon_fap->malveke_detected = detect_malveke();
2175+
memcpy(
2176+
&pokemon_fap->pins,
2177+
&common_pinouts[pokemon_fap->malveke_detected],
2178+
sizeof(struct gblink_pins));
21602179

21612180
// Set up trade party struct
21622181
pokemon_fap->trade_block = trade_block_alloc();
@@ -2189,6 +2208,7 @@ PokemonFap* pokemon_alloc() {
21892208
pokemon_fap->trade = trade_alloc(
21902209
pokemon_fap->trade_block,
21912210
pokemon_fap->pokemon_table,
2211+
&pokemon_fap->pins,
21922212
pokemon_fap->view_dispatcher,
21932213
AppViewTrade);
21942214

pokemon_app.h

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <gui/modules/submenu.h>
1111
#include <gui/modules/text_input.h>
1212
#include <gui/modules/variable_item_list.h>
13+
#include <gblink.h>
1314

1415
#include "pokemon_data.h"
1516

@@ -75,6 +76,10 @@ struct pokemon_fap {
7576
*/
7677
TradeBlock* trade_block;
7778

79+
/* Pin definition to actual Game Link Cable interface */
80+
struct gblink_pins pins;
81+
int malveke_detected;
82+
7883
/* The currently selected pokemon */
7984
int curr_pokemon;
8085

scenes/pokemon_menu.c

+10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "pokemon_ot_id.h"
1212
#include "pokemon_ot_name.h"
1313
#include "pokemon_trade.h"
14+
#include "pokemon_pins.h"
1415

1516
static void scene_change_from_main_cb(void* context, uint32_t index) {
1617
PokemonFap* pokemon_fap = (PokemonFap*)context;
@@ -95,6 +96,12 @@ void main_menu_scene_on_enter(void* context) {
9596
pokemon_fap->submenu, buf, SelectOTNameScene, scene_change_from_main_cb, pokemon_fap);
9697
submenu_add_item(
9798
pokemon_fap->submenu, "Trade PKMN", TradeScene, scene_change_from_main_cb, pokemon_fap);
99+
submenu_add_item(
100+
pokemon_fap->submenu,
101+
"Select Pinout",
102+
SelectPinsScene,
103+
scene_change_from_main_cb,
104+
pokemon_fap);
98105

99106
submenu_set_selected_item(
100107
pokemon_fap->submenu,
@@ -128,6 +135,7 @@ void (*const pokemon_scene_on_enter_handlers[])(void*) = {
128135
select_ot_id_scene_on_enter,
129136
select_ot_name_scene_on_enter,
130137
trade_scene_on_enter,
138+
select_pins_scene_on_enter,
131139
};
132140

133141
void (*const pokemon_scene_on_exit_handlers[])(void*) = {
@@ -143,6 +151,7 @@ void (*const pokemon_scene_on_exit_handlers[])(void*) = {
143151
select_ot_id_scene_on_exit,
144152
select_ot_name_scene_on_exit,
145153
null_scene_on_exit,
154+
select_pins_scene_on_exit,
146155
};
147156

148157
bool (*const pokemon_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
@@ -158,6 +167,7 @@ bool (*const pokemon_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
158167
null_scene_on_event,
159168
null_scene_on_event,
160169
null_scene_on_event,
170+
null_scene_on_event,
161171
};
162172

163173
const SceneManagerHandlers pokemon_scene_manager_handlers = {

scenes/pokemon_menu.h

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ typedef enum {
1818
SelectOTIDScene,
1919
SelectOTNameScene,
2020
TradeScene,
21+
SelectPinsScene,
2122
SceneCount,
2223
} AppScene;
2324

scenes/pokemon_pins.c

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#include <gui/modules/variable_item_list.h>
2+
#include <furi.h>
3+
4+
#include "../pokemon_app.h"
5+
#include "pokemon_menu.h"
6+
7+
struct named_pins {
8+
const char* text;
9+
const GpioPin* pin;
10+
};
11+
12+
static const struct named_pins named_pins[] = {
13+
{"PA7", &gpio_ext_pa7},
14+
{"PA6", &gpio_ext_pa6},
15+
{"PA4", &gpio_ext_pa4},
16+
{"PB3", &gpio_ext_pb3},
17+
{"PB2", &gpio_ext_pb2},
18+
{"PC3", &gpio_ext_pc3},
19+
{"PC1", &gpio_ext_pc1},
20+
{"PC0", &gpio_ext_pc0},
21+
{},
22+
};
23+
24+
#define NUM_PINS 8
25+
26+
/* This must match gblink's enum order */
27+
static const char* named_groups[] = {
28+
"Original",
29+
"Malveke",
30+
"Custom",
31+
"",
32+
};
33+
34+
struct itemlist_builder {
35+
VariableItem* named;
36+
VariableItem* serin;
37+
VariableItem* serout;
38+
VariableItem* clk;
39+
uint8_t named_index;
40+
uint8_t serin_index;
41+
uint8_t serout_index;
42+
uint8_t clk_index;
43+
};
44+
45+
/* Just make it a global, whatever */
46+
static struct itemlist_builder builder = {0};
47+
static void select_pins_rebuild_list(PokemonFap* pokemon_fap);
48+
49+
static void select_pins_set(PokemonFap* pokemon_fap) {
50+
pokemon_fap->pins.serin = named_pins[builder.serin_index].pin;
51+
pokemon_fap->pins.serout = named_pins[builder.serout_index].pin;
52+
pokemon_fap->pins.clk = named_pins[builder.clk_index].pin;
53+
}
54+
55+
static void select_named_group_callback(VariableItem* item) {
56+
uint8_t index = variable_item_get_current_value_index(item);
57+
PokemonFap* pokemon_fap = variable_item_get_context(item);
58+
59+
variable_item_set_current_value_text(item, named_groups[index]);
60+
builder.named_index = index;
61+
select_pins_rebuild_list(pokemon_fap);
62+
variable_item_list_set_selected_item(pokemon_fap->variable_item_list, 0);
63+
}
64+
65+
static void select_pins_serin_callback(VariableItem* item) {
66+
uint8_t index = variable_item_get_current_value_index(item);
67+
PokemonFap* pokemon_fap = variable_item_get_context(item);
68+
69+
variable_item_set_current_value_text(item, named_pins[index].text);
70+
builder.serin_index = index;
71+
select_pins_rebuild_list(pokemon_fap);
72+
variable_item_list_set_selected_item(pokemon_fap->variable_item_list, 1);
73+
}
74+
75+
static void select_pins_serout_callback(VariableItem* item) {
76+
uint8_t index = variable_item_get_current_value_index(item);
77+
PokemonFap* pokemon_fap = variable_item_get_context(item);
78+
79+
variable_item_set_current_value_text(item, named_pins[index].text);
80+
builder.serout_index = index;
81+
select_pins_rebuild_list(pokemon_fap);
82+
variable_item_list_set_selected_item(pokemon_fap->variable_item_list, 2);
83+
}
84+
85+
static void select_pins_clk_callback(VariableItem* item) {
86+
uint8_t index = variable_item_get_current_value_index(item);
87+
PokemonFap* pokemon_fap = variable_item_get_context(item);
88+
89+
variable_item_set_current_value_text(item, named_pins[index].text);
90+
builder.clk_index = index;
91+
select_pins_rebuild_list(pokemon_fap);
92+
variable_item_list_set_selected_item(pokemon_fap->variable_item_list, 3);
93+
}
94+
95+
static void select_pins_rebuild_list(PokemonFap* pokemon_fap) {
96+
int num;
97+
98+
/* HACK: TODO: It would be better to do this programmatically, but, I'm kind
99+
* of done working on this feature so its going to be hardcoded for now.
100+
*/
101+
switch(builder.named_index) {
102+
case 0: // Original
103+
num = 1;
104+
builder.serin_index = 5;
105+
builder.serout_index = 3;
106+
builder.clk_index = 4;
107+
break;
108+
case 1: // MALVEKE
109+
num = 1;
110+
builder.serin_index = 1;
111+
builder.serout_index = 0;
112+
builder.clk_index = 3;
113+
break;
114+
default:
115+
num = NUM_PINS;
116+
break;
117+
}
118+
119+
/* HACK: */
120+
pokemon_fap->malveke_detected = builder.named_index;
121+
122+
select_pins_set(pokemon_fap);
123+
124+
variable_item_list_reset(pokemon_fap->variable_item_list);
125+
126+
builder.named = variable_item_list_add(
127+
pokemon_fap->variable_item_list, "Mode", 3, select_named_group_callback, pokemon_fap);
128+
builder.serin = variable_item_list_add(
129+
pokemon_fap->variable_item_list, "SI:", num, select_pins_serin_callback, pokemon_fap);
130+
builder.serout = variable_item_list_add(
131+
pokemon_fap->variable_item_list, "SO:", num, select_pins_serout_callback, pokemon_fap);
132+
builder.clk = variable_item_list_add(
133+
pokemon_fap->variable_item_list, "CLK:", num, select_pins_clk_callback, pokemon_fap);
134+
135+
variable_item_set_current_value_index(builder.named, builder.named_index);
136+
variable_item_set_current_value_text(builder.named, named_groups[builder.named_index]);
137+
138+
variable_item_set_current_value_index(builder.serin, (num == 1 ? 0 : builder.serin_index));
139+
variable_item_set_current_value_text(builder.serin, named_pins[builder.serin_index].text);
140+
141+
variable_item_set_current_value_index(builder.serout, (num == 1 ? 0 : builder.serout_index));
142+
variable_item_set_current_value_text(builder.serout, named_pins[builder.serout_index].text);
143+
144+
variable_item_set_current_value_index(builder.clk, (num == 1 ? 0 : builder.clk_index));
145+
variable_item_set_current_value_text(builder.clk, named_pins[builder.clk_index].text);
146+
}
147+
148+
void select_pins_scene_on_exit(void* context) {
149+
PokemonFap* pokemon_fap = (PokemonFap*)context;
150+
151+
view_dispatcher_switch_to_view(pokemon_fap->view_dispatcher, AppViewMainMenu);
152+
view_dispatcher_remove_view(pokemon_fap->view_dispatcher, AppViewOpts);
153+
}
154+
155+
void select_pins_scene_on_enter(void* context) {
156+
PokemonFap* pokemon_fap = (PokemonFap*)context;
157+
158+
/* TODO: Figure out what defaults we should use for pins based on attached board! */
159+
/* HACK: */
160+
if(builder.named_index < 2) builder.named_index = pokemon_fap->malveke_detected;
161+
162+
select_pins_rebuild_list(pokemon_fap);
163+
164+
view_dispatcher_add_view(
165+
pokemon_fap->view_dispatcher,
166+
AppViewOpts,
167+
variable_item_list_get_view(pokemon_fap->variable_item_list));
168+
view_dispatcher_switch_to_view(pokemon_fap->view_dispatcher, AppViewOpts);
169+
}

scenes/pokemon_pins.h

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#ifndef POKEMON_PINS_H
2+
#define POKEMON_PINS_H
3+
4+
#pragma once
5+
6+
void select_pins_scene_on_enter(void* context);
7+
void select_pins_scene_on_exit(void* context);
8+
9+
#endif // POKEMON_PINS_H

0 commit comments

Comments
 (0)