소스 검색

Add malveke_gb_cartridge from https://github.com/EstebanFuentealba/MALVEKE-Flipper-Zero

git-subtree-dir: malveke_gb_cartridge
git-subtree-mainline: 18d81bdf6e6d5c7ba442a46ea17820b95be20356
git-subtree-split: a4ca1758c03ff2f1b165ef45ccb7aecc8dbf39c3
Willy-JL 1 년 전
부모
커밋
d716970d83
70개의 변경된 파일7060개의 추가작업 그리고 0개의 파일을 삭제
  1. 191 0
      malveke_gb_cartridge/.clang-format
  2. BIN
      malveke_gb_cartridge/.flipcorg/banner.png
  3. BIN
      malveke_gb_cartridge/.flipcorg/gallery/1.png
  4. BIN
      malveke_gb_cartridge/.flipcorg/gallery/2.png
  5. BIN
      malveke_gb_cartridge/.flipcorg/gallery/3.png
  6. BIN
      malveke_gb_cartridge/.flipcorg/gallery/4.png
  7. 1 0
      malveke_gb_cartridge/.gitsubtree
  8. 2 0
      malveke_gb_cartridge/.vscode/compile_commands.json
  9. 6 0
      malveke_gb_cartridge/.vscode/settings.json
  10. 100 0
      malveke_gb_cartridge/README.md
  11. 22 0
      malveke_gb_cartridge/README_catalog.md
  12. 20 0
      malveke_gb_cartridge/application.fam
  13. 16 0
      malveke_gb_cartridge/docs/changelog.md
  14. BIN
      malveke_gb_cartridge/docs/images/flipper-zero-flat-1.png
  15. BIN
      malveke_gb_cartridge/docs/images/flipper-zero-flat-2.png
  16. BIN
      malveke_gb_cartridge/docs/images/flipper-zero-flat-3.png
  17. BIN
      malveke_gb_cartridge/docs/images/flipper-zero-flat-4.png
  18. BIN
      malveke_gb_cartridge/docs/images/flipper-zero-flat-6.gif
  19. BIN
      malveke_gb_cartridge/docs/images/proto.jpg
  20. BIN
      malveke_gb_cartridge/docs/psd/gb_cartridge.psd
  21. BIN
      malveke_gb_cartridge/docs/psd/gb_cartridge_ram.psd
  22. 217 0
      malveke_gb_cartridge/gb_cartridge_app.c
  23. 139 0
      malveke_gb_cartridge/gb_cartridge_app.h
  24. 2757 0
      malveke_gb_cartridge/helpers/cJSON.c
  25. 328 0
      malveke_gb_cartridge/helpers/cJSON.h
  26. 83 0
      malveke_gb_cartridge/helpers/gb_cartridge_custom_event.h
  27. 26 0
      malveke_gb_cartridge/helpers/gb_cartridge_speaker.c
  28. 4 0
      malveke_gb_cartridge/helpers/gb_cartridge_speaker.h
  29. 46 0
      malveke_gb_cartridge/helpers/sequential_file.c
  30. 15 0
      malveke_gb_cartridge/helpers/sequential_file.h
  31. BIN
      malveke_gb_cartridge/icons/ArrowUpEmpty_14x15.png
  32. BIN
      malveke_gb_cartridge/icons/ArrowUpFilled_14x15.png
  33. BIN
      malveke_gb_cartridge/icons/Space_100x18.png
  34. BIN
      malveke_gb_cartridge/icons/Space_80x18.png
  35. BIN
      malveke_gb_cartridge/icons/Space_95x18.png
  36. BIN
      malveke_gb_cartridge/icons/_icon.png
  37. BIN
      malveke_gb_cartridge/icons/board.png
  38. BIN
      malveke_gb_cartridge/icons/boilerplate_10px.png
  39. BIN
      malveke_gb_cartridge/icons/cartridge.png
  40. BIN
      malveke_gb_cartridge/icons/cartridge_42x64.png
  41. BIN
      malveke_gb_cartridge/icons/game_boy.png
  42. BIN
      malveke_gb_cartridge/icons/icon.png
  43. BIN
      malveke_gb_cartridge/icons/red_16x15.png
  44. 89 0
      malveke_gb_cartridge/malveke_notifications.c
  45. 20 0
      malveke_gb_cartridge/malveke_notifications.h
  46. 30 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene.c
  47. 29 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene.h
  48. 8 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene_config.h
  49. 130 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene_menu.c
  50. 55 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene_scene_1.c
  51. 51 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene_scene_2.c
  52. 58 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene_scene_3.c
  53. 51 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene_scene_4.c
  54. 51 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene_scene_5.c
  55. 133 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene_settings.c
  56. 55 0
      malveke_gb_cartridge/scenes/gb_cartridge_scene_startscreen.c
  57. 195 0
      malveke_gb_cartridge/uart.c
  58. 21 0
      malveke_gb_cartridge/uart.h
  59. 410 0
      malveke_gb_cartridge/views/gb_cartridge_scene_1.c
  60. 19 0
      malveke_gb_cartridge/views/gb_cartridge_scene_1.h
  61. 368 0
      malveke_gb_cartridge/views/gb_cartridge_scene_2.c
  62. 19 0
      malveke_gb_cartridge/views/gb_cartridge_scene_2.h
  63. 369 0
      malveke_gb_cartridge/views/gb_cartridge_scene_3.c
  64. 19 0
      malveke_gb_cartridge/views/gb_cartridge_scene_3.h
  65. 295 0
      malveke_gb_cartridge/views/gb_cartridge_scene_4.c
  66. 19 0
      malveke_gb_cartridge/views/gb_cartridge_scene_4.h
  67. 397 0
      malveke_gb_cartridge/views/gb_cartridge_scene_5.c
  68. 19 0
      malveke_gb_cartridge/views/gb_cartridge_scene_5.h
  69. 155 0
      malveke_gb_cartridge/views/gb_cartridge_startscreen.c
  70. 22 0
      malveke_gb_cartridge/views/gb_cartridge_startscreen.h

+ 191 - 0
malveke_gb_cartridge/.clang-format

@@ -0,0 +1,191 @@
+---
+Language:        Cpp
+AccessModifierOffset: -4
+AlignAfterOpenBracket: AlwaysBreak
+AlignArrayOfStructures: None
+AlignConsecutiveMacros: None
+AlignConsecutiveAssignments: None
+AlignConsecutiveBitFields: None
+AlignConsecutiveDeclarations: None
+AlignEscapedNewlines: Left
+AlignOperands:   Align
+AlignTrailingComments: false
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortEnumsOnASingleLine: true
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortLambdasOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLoopsOnASingleLine: true
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+AttributeMacros:
+  - __capability
+BinPackArguments: false
+BinPackParameters: false
+BraceWrapping:
+  AfterCaseLabel:  false
+  AfterClass:      false
+  AfterControlStatement: Never
+  AfterEnum:       false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  AfterExternBlock: false
+  BeforeCatch:     false
+  BeforeElse:      false
+  BeforeLambdaBody: false
+  BeforeWhile:     false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: true
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeComma
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: false
+ColumnLimit:     99
+CommentPragmas:  '^ IWYU pragma:'
+QualifierAlignment: Leave
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DeriveLineEnding: true
+DerivePointerAlignment: false
+DisableFormat:   false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+PackConstructorInitializers: BinPack
+BasedOnStyle:    ''
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+AllowAllConstructorInitializersOnNextLine: true
+FixNamespaceComments: false
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+IfMacros:
+  - KJ_IF_MAYBE
+IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
+    Priority:        3
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseLabels: false
+IndentCaseBlocks: false
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentExternBlock: AfterExternBlock
+IndentRequires:  false
+IndentWidth:     4
+IndentWrappedFunctionNames: true
+InsertTrailingCommas: None
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+LambdaBodyIndentation: Signature
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 4
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 10
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakString: 10
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 100
+PenaltyReturnTypeOnItsOwnLine: 60
+PenaltyIndentedWhitespace: 0
+PointerAlignment: Left
+PPIndentWidth:   -1
+ReferenceAlignment: Pointer
+ReflowComments:  false
+RemoveBracesLLVM: false
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SortIncludes:    Never
+SortJavaStaticImport: Before
+SortUsingDeclarations: false
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: Never
+SpaceBeforeParensOptions:
+  AfterControlStatements: false
+  AfterForeachMacros: false
+  AfterFunctionDefinitionName: false
+  AfterFunctionDeclarationName: false
+  AfterIfMacros:   false
+  AfterOverloadedOperator: false
+  BeforeNonEmptyParentheses: false
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  Never
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: false
+SpacesInCStyleCastParentheses: false
+SpacesInLineCommentPrefix:
+  Minimum:         1
+  Maximum:         -1
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+BitFieldColonSpacing: Both
+Standard:        c++03
+StatementAttributeLikeMacros:
+  - Q_EMIT
+StatementMacros:
+  - Q_UNUSED
+  - QT_REQUIRE_VERSION
+TabWidth:        4
+UseCRLF:         false
+UseTab:          Never
+WhitespaceSensitiveMacros:
+  - STRINGIZE
+  - PP_STRINGIZE
+  - BOOST_PP_STRINGIZE
+  - NS_SWIFT_NAME
+  - CF_SWIFT_NAME
+...
+

BIN
malveke_gb_cartridge/.flipcorg/banner.png


BIN
malveke_gb_cartridge/.flipcorg/gallery/1.png


BIN
malveke_gb_cartridge/.flipcorg/gallery/2.png


BIN
malveke_gb_cartridge/.flipcorg/gallery/3.png


BIN
malveke_gb_cartridge/.flipcorg/gallery/4.png


+ 1 - 0
malveke_gb_cartridge/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/EstebanFuentealba/MALVEKE-Flipper-Zero main flipper_companion_apps/applications/external/malveke_gb_cartridge

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 2 - 0
malveke_gb_cartridge/.vscode/compile_commands.json


+ 6 - 0
malveke_gb_cartridge/.vscode/settings.json

@@ -0,0 +1,6 @@
+{
+    "files.associations": {
+        "ios": "c",
+        "string.h": "c"
+    }
+}

+ 100 - 0
malveke_gb_cartridge/README.md

@@ -0,0 +1,100 @@
+# ***GAME BOY*** Cartridge (GB/GBC) MALVEKE
+
+<figure>
+    <img src="./docs/images/proto.jpg" />
+    <figcaption>MALVEKE Prototype V2.3</figcaption>
+</figure>
+
+<p align='center'>
+<a href="https://www.tindie.com/stores/efuentealba/?ref=offsite_badges&utm_source=sellers_efuentealba&utm_medium=badges&utm_campaign=badge_large"><img src="https://d2ss6ovg47m0r5.cloudfront.net/badges/tindie-larges.png" alt="I sell on Tindie" width="200" height="104"></a>
+</p>
+
+## Introduction
+
+<div align="center">
+
+**Official** | **Unleashed** | **RogueMaster** | **Xtreme**
+:- | :- | :- | :- 
+[![FlipC.org](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero/badge?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_cartridge&firmware=official)](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_cartridge&firmware=official)|[![FlipC.org](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero/badge?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_cartridge&firmware=unleashed)](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_cartridge&firmware=unleashed)|[![FlipC.org](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero/badge?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_cartridge&firmware=roguemaster)](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_cartridge&firmware=roguemaster)|[![FlipC.org](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero/badge?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_cartridge&firmware=xtreme)](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_cartridge&firmware=xtreme)
+</div>
+<br>
+
+<div align="center">
+
+https://github.com/EstebanFuentealba/MALVEKE-Flipper-Zero/assets/442927/a1d06e1f-5626-4a67-b058-24ff8f24eaf6
+
+</div>
+
+## Features
+- Read info from GAME BOY/GAME BOY Color Cartridges
+- Backing up and restoring your saves (RAM)
+- Backing up and restoring your games (ROM) (WIP)
+
+
+
+## Instructions for use.
+
+These instructions assume that you are starting at the Flipper Zero desktop. Otherwise, press the Back button until you are at the desktop.
+
+- Disconnect the Flipper Zero from the USB port if you are currently using it.
+- Press the `OK` button on the Flipper to open the main menu.
+- Choose `Applications` from the menu.
+- Choose `GPIO` from the submenu.
+- Choose `GAME BOY Cartridge (GB/GBC) MALVEKE`
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-1.png" width="400" />
+        <br />
+    </p>
+
+- If the **MALVEKE** board and the cartridge are connected, The Flipper Zero will show the loading screen of the application.
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-2.png" width="400" />
+        <br />
+    </p>
+
+- Press the `OK` to enter to menu.
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-3.png" width="400" />
+        <br />
+    </p>
+    
+    - Select `Cartridge Information` to view details about the inserted cartridge.
+
+    
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-4.png" width="400" />
+        <br />
+    </p>
+
+
+    - Select `Dump ROM Cartridge` to create a complete backup of the game into a file. This file will be saved in the directory `SD Card/apps_data/malveke/roms` and can be loaded into GAME BOY emulators.
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-6.gif" width="400" />
+        <br />
+    </p>
+
+    - Choose `Dump RAM Cartridge` to create a backup of the game state, which stores the progress of a specific video game. This file will be saved in the directory `SD Card/apps_data/malveke/rams`.
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-6.gif" width="400" />
+        <br />
+    </p>
+- Press the `Back` to back to principal menu.
+
+
+
+<p align='center'>
+<br />
+<br />
+From Talcahuano 🇨🇱 with ❤ 
+</p>

+ 22 - 0
malveke_gb_cartridge/README_catalog.md

@@ -0,0 +1,22 @@
+# MALVEKE **GAME BOY** Cartridge (GB/GBC)
+
+## Introduction
+MALVEKE app, its Interacts with GAME BOY and GAME BOY Color cartridges. You can Dump & Restore RAM and ROM.
+
+## Features
+
+- Read info from GAME BOY/GAME BOY Color Cartridges
+- Backing up and restoring your saves (RAM)
+- Backing up and restoring your games (ROM) (WIP)
+
+## Instructions for use.
+
+These instructions assume that you are starting at the Flipper Zero desktop. Otherwise, press the Back button until you are at the desktop.
+
+- Press the **OK** button on the Flipper to open the main menu.
+- Choose **Applications** from the menu.
+- Choose **GPIO** from the submenu.
+- Choose **MALVEKE GAME BOY Cartridge (GB/GBC)**
+- If the **MALVEKE** board and the cartridge are connected, The Flipper Zero will show the loading screen of the application.
+- Press the **OK** to enter to menu.
+- Press the **Back** to back to principal menu.

+ 20 - 0
malveke_gb_cartridge/application.fam

@@ -0,0 +1,20 @@
+App(
+    appid="malveke_gb_cartridge",
+    name="GAME BOY Cartridge (GB/GBC) MALVEKE",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="gb_cartridge_app",
+    cdefines=["APP_BOILERPLATE"],
+    requires=[
+        "gui",
+        "storage",
+    ],
+    stack_size=2 * 1024,
+    order=10,
+    fap_libs=["assets"],
+    fap_icon="icons/icon.png",
+    fap_icon_assets="icons",
+    fap_category="GPIO",
+    fap_version=[2,2],
+    fap_author="Esteban Fuentealba",
+    fap_weburl="https://github.com/EstebanFuentealba/MALVEKE-Flipper-Zero"
+)

+ 16 - 0
malveke_gb_cartridge/docs/changelog.md

@@ -0,0 +1,16 @@
+# Changelog - Patch Notes
+
+## Version 2.2
+Fix "Parse Error" (gcc optimization)
+
+## Version 2.1
+Fix FuriHalRtc: new datetime lib.
+
+## Version 2.0
+Implement new serial API with new module expansion protocol.
+
+## Version 1.0
+The initial version of the application encompasses functionalities to view cartridge information, back up ROM and RAM of both GAME BOY & GAME BOY Color cartridges, and enables RAM restoration as well.
+
+## Version 1.1
+To improve the visibility of the application names, the names were changed by adding a 'MALVEKE' postfix.

BIN
malveke_gb_cartridge/docs/images/flipper-zero-flat-1.png


BIN
malveke_gb_cartridge/docs/images/flipper-zero-flat-2.png


BIN
malveke_gb_cartridge/docs/images/flipper-zero-flat-3.png


BIN
malveke_gb_cartridge/docs/images/flipper-zero-flat-4.png


BIN
malveke_gb_cartridge/docs/images/flipper-zero-flat-6.gif


BIN
malveke_gb_cartridge/docs/images/proto.jpg


BIN
malveke_gb_cartridge/docs/psd/gb_cartridge.psd


BIN
malveke_gb_cartridge/docs/psd/gb_cartridge_ram.psd


+ 217 - 0
malveke_gb_cartridge/gb_cartridge_app.c

@@ -0,0 +1,217 @@
+#include "gb_cartridge_app.h"
+
+bool gb_cartridge_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+void gb_cartridge_app_tick_event_callback(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+//leave app if back button pressed
+bool gb_cartridge_app_navigation_event_callback(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+GBCartridge* gb_cartridge_app_app_alloc() {
+    GBCartridge* app = malloc(sizeof(GBCartridge));
+    app->gui = furi_record_open(RECORD_GUI);
+    app->storage = furi_record_open(RECORD_STORAGE);
+    app->notification = furi_record_open(RECORD_NOTIFICATION);
+
+    //  Create MALVEKE dir
+    if(storage_common_stat(app->storage, MALVEKE_APP_FOLDER, NULL) == FSE_NOT_EXIST) {
+        storage_simply_mkdir(app->storage, MALVEKE_APP_FOLDER);
+    }
+    //  Create MALVEKE RAM dir
+    if(storage_common_stat(app->storage, MALVEKE_APP_FOLDER_RAMS, NULL) == FSE_NOT_EXIST) {
+        storage_simply_mkdir(app->storage, MALVEKE_APP_FOLDER_RAMS);
+    }
+    //  Create MALVEKE ROM dir
+    if(storage_common_stat(app->storage, MALVEKE_APP_FOLDER_ROMS, NULL) == FSE_NOT_EXIST) {
+        storage_simply_mkdir(app->storage, MALVEKE_APP_FOLDER_ROMS);
+    }
+    //  Create MALVEKE Photos dir
+    if(storage_common_stat(app->storage, MALVEKE_APP_FOLDER_PHOTOS, NULL) == FSE_NOT_EXIST) {
+        storage_simply_mkdir(app->storage, MALVEKE_APP_FOLDER_PHOTOS);
+    }
+
+    //Turn backlight on, believe me this makes testing your app easier
+    notification_message(app->notification, &sequence_display_backlight_on);
+
+    //Scene additions
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    app->scene_manager = scene_manager_alloc(&gb_cartridge_scene_handlers, app);
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, gb_cartridge_app_navigation_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, gb_cartridge_app_tick_event_callback, 100);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, gb_cartridge_app_custom_event_callback);
+    app->submenu = variable_item_list_alloc();
+
+    // Set defaults, in case no config loaded
+    app->gameboy_rom_option_selected_index = 0;
+    app->gameboy_rom_option_selected_text = "gb";
+    app->haptic = 1;
+    app->speaker = 1;
+    app->led = 1;
+    app->save_settings = 1;
+
+    app->cart_title = " - ";
+    app->cart_dump_rom_filename = "malveke_rom";
+    app->cart_dump_rom_extension = "gb";
+
+    app->cart_dump_ram_filename = "malveke_ram";
+    app->cart_dump_ram_extension = "sav";
+
+    // Used for File Browser
+    app->dialogs = furi_record_open(RECORD_DIALOGS);
+    app->file_path = furi_string_alloc();
+
+    app->uart = usart_init(app);
+    app->lp_uart = lp_uart_init(app);
+
+    view_dispatcher_add_view(
+        app->view_dispatcher, GBCartridgeViewIdMenu, variable_item_list_get_view(app->submenu));
+    app->gb_cartridge_startscreen = gb_cartridge_startscreen_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        GBCartridgeViewIdStartscreen,
+        gb_cartridge_startscreen_get_view(app->gb_cartridge_startscreen));
+    app->gb_cartridge_scene_1 = gb_cartridge_scene_1_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        GBCartridgeViewIdScene1,
+        gb_cartridge_scene_1_get_view(app->gb_cartridge_scene_1));
+    app->gb_cartridge_scene_2 = gb_cartridge_scene_2_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        GBCartridgeViewIdScene2,
+        gb_cartridge_scene_2_get_view(app->gb_cartridge_scene_2));
+    app->gb_cartridge_scene_3 = gb_cartridge_scene_3_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        GBCartridgeViewIdScene3,
+        gb_cartridge_scene_3_get_view(app->gb_cartridge_scene_3));
+    app->gb_cartridge_scene_4 = gb_cartridge_scene_4_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        GBCartridgeViewIdScene4,
+        gb_cartridge_scene_4_get_view(app->gb_cartridge_scene_4));
+    app->gb_cartridge_scene_5 = gb_cartridge_scene_5_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        GBCartridgeViewIdScene5,
+        gb_cartridge_scene_5_get_view(app->gb_cartridge_scene_5));
+
+    // app->button_menu = button_menu_alloc();
+    // view_dispatcher_add_view(app->view_dispatcher, GBCartridgeViewIdScene3, button_menu_get_view(app->button_menu));
+
+    app->variable_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        GBCartridgeViewIdSettings,
+        variable_item_list_get_view(app->variable_item_list));
+
+    //End Scene Additions
+
+    //  Enable 5v
+    furi_hal_power_enable_otg();
+    furi_delay_ms(1);
+    furi_hal_power_insomnia_enter();
+    return app;
+}
+
+void gb_cartridge_app_app_free(GBCartridge* app) {
+    furi_assert(app);
+
+    // Views
+    view_dispatcher_remove_view(app->view_dispatcher, GBCartridgeViewIdMenu);
+    view_dispatcher_remove_view(app->view_dispatcher, GBCartridgeViewIdScene1);
+    view_dispatcher_remove_view(app->view_dispatcher, GBCartridgeViewIdScene2);
+    view_dispatcher_remove_view(app->view_dispatcher, GBCartridgeViewIdScene3);
+    view_dispatcher_remove_view(app->view_dispatcher, GBCartridgeViewIdScene4);
+    view_dispatcher_remove_view(app->view_dispatcher, GBCartridgeViewIdScene5);
+    view_dispatcher_remove_view(app->view_dispatcher, GBCartridgeViewIdSettings);
+    view_dispatcher_remove_view(app->view_dispatcher, GBCartridgeViewIdStartscreen);
+
+    variable_item_list_free(app->submenu);
+
+    // View Dispatcher
+    view_dispatcher_free(app->view_dispatcher);
+    // Scene manager
+    scene_manager_free(app->scene_manager);
+
+    //
+    uart_free(app->uart);
+    uart_free(app->lp_uart);
+
+    furi_record_close(RECORD_DIALOGS);
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_STORAGE);
+
+    app->gui = NULL;
+    app->notification = NULL;
+    app->storage = NULL;
+
+    //Remove whatever is left
+    free(app);
+}
+
+int32_t gb_cartridge_app(void* p) {
+    UNUSED(p);
+    // Disable expansion protocol to avoid interference with UART Handle
+    Expansion* expansion = furi_record_open(RECORD_EXPANSION);
+    expansion_disable(expansion);
+    // uint8_t attempts = 0;
+    // while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
+    //     furi_hal_power_enable_otg();
+    //     furi_delay_ms(10);
+    // }
+    // furi_delay_ms(200);
+    // furi_hal_power_disable_external_3_3v();
+    // furi_delay_ms(100);
+    // furi_hal_power_enable_external_3_3v();
+    // furi_delay_ms(200);
+
+    GBCartridge* app = gb_cartridge_app_app_alloc();
+
+    // if user hasn't confirmed whether to save pcaps and logs to sdcard, then prompt when scene starts
+    // app->need_to_prompt_settings_init =
+    //     (!storage_file_exists(app->storage, SAVE_PCAP_SETTING_FILEPATH) ||
+    //      !storage_file_exists(app->storage, SAVE_LOGS_SETTING_FILEPATH));
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    scene_manager_next_scene(
+        app->scene_manager, GBCartridgeSceneStartscreen); //Start with start screen
+    //scene_manager_next_scene(app->scene_manager, GBCartridgeSceneMenu); //if you want to directly start with Menu
+    // scene_manager_next_scene(app->scene_manager, GBCartridgeSceneScene_4);
+
+    furi_hal_power_suppress_charge_enter();
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    furi_hal_power_suppress_charge_exit();
+    gb_cartridge_app_app_free(app);
+
+    if(furi_hal_power_is_otg_enabled()) {
+        furi_hal_power_disable_otg();
+    }
+
+    // Return previous state of expansion
+    expansion_enable(expansion);
+    furi_record_close(RECORD_EXPANSION);
+
+    return 0;
+}

+ 139 - 0
malveke_gb_cartridge/gb_cartridge_app.h

@@ -0,0 +1,139 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <stdlib.h>
+#include <dialogs/dialogs.h>
+#include <expansion/expansion.h>
+#include <notification/notification_messages.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/button_menu.h>
+#include <gui/modules/dialog_ex.h>
+#include <stdlib.h>
+#include <string.h>
+#include <storage/storage.h>
+#include <flipper_format/flipper_format_i.h>
+
+#include "scenes/gb_cartridge_scene.h"
+#include "views/gb_cartridge_startscreen.h"
+#include "views/gb_cartridge_scene_1.h"
+#include "views/gb_cartridge_scene_2.h"
+#include "views/gb_cartridge_scene_3.h"
+#include "views/gb_cartridge_scene_4.h"
+#include "views/gb_cartridge_scene_5.h"
+#include "uart.h"
+#include "helpers/cJSON.h"
+#include "malveke_notifications.h"
+
+#define TAG "GBCartridge"
+
+#define MALVEKE_APP_FOLDER_USER "apps_data/malveke"
+#define MALVEKE_APP_FOLDER EXT_PATH(MALVEKE_APP_FOLDER_USER)
+#define MALVEKE_APP_FOLDER_ROMS MALVEKE_APP_FOLDER "/roms"
+#define MALVEKE_APP_FOLDER_RAMS MALVEKE_APP_FOLDER "/rams"
+#define MALVEKE_APP_FOLDER_PHOTOS MALVEKE_APP_FOLDER "/photos"
+
+#define UI_PADDING 3
+#define UI_PROGRESS_COLS 8
+#define UI_PROGRESS_ROWS 4
+
+#define BUFFER_SIZE 64
+
+#ifndef MAX
+#define MAX(a, b) ((a) > (b)) ? (a) : (b)
+#endif
+
+#ifndef MIN
+#define MIN(a, b) ((a) < (b)) ? (a) : (b)
+#endif
+
+typedef struct {
+    Gui* gui;
+    Storage* storage;
+    NotificationApp* notification;
+    ViewDispatcher* view_dispatcher;
+    VariableItemList* submenu;
+    bool need_to_prompt_settings_init;
+    SceneManager* scene_manager;
+    VariableItemList* variable_item_list;
+    GBCartridgeStartscreen* gb_cartridge_startscreen;
+    GBCartridgeScene1* gb_cartridge_scene_1;
+    GBCartridgeScene2* gb_cartridge_scene_2;
+    GBCartridgeScene3* gb_cartridge_scene_3;
+    GBCartridgeScene4* gb_cartridge_scene_4;
+    GBCartridgeScene5* gb_cartridge_scene_5;
+
+    DialogsApp* dialogs; // File Browser
+    FuriString* file_path; // File Browser
+    uint32_t haptic;
+    uint32_t speaker;
+    uint32_t led;
+    uint32_t save_settings;
+    ButtonMenu* button_menu; // Button Menu
+
+    Uart* uart;
+    Uart* lp_uart;
+
+    char* cart_title;
+    char* cart_dump_rom_filename;
+    char* cart_dump_rom_extension;
+
+    char* cart_dump_ram_filename;
+    char* cart_dump_ram_extension;
+
+    File* cart_rom;
+    File* cart_ram;
+    File* cart_log;
+
+    uint32_t gameboy_rom_option_selected_index;
+    char* gameboy_rom_option_selected_text;
+
+    FuriThread* thread;
+    bool is_writing_rom;
+    bool is_writing_ram;
+
+    int rom_banks;
+    int ram_banks;
+
+} GBCartridge;
+
+typedef enum {
+    GBCartridgeViewIdStartscreen,
+    GBCartridgeViewIdMenu,
+    GBCartridgeViewIdScene1,
+    GBCartridgeViewIdScene2,
+    GBCartridgeViewIdScene3,
+    GBCartridgeViewIdScene4,
+    GBCartridgeViewIdScene5,
+    GBCartridgeViewIdSettings,
+} GBCartridgeViewId;
+
+typedef enum {
+    GBCartridgeHapticOff,
+    GBCartridgeHapticOn,
+} GBCartridgeHapticState;
+
+typedef enum {
+    GBCartridgeSpeakerOff,
+    GBCartridgeSpeakerOn,
+} GBCartridgeSpeakerState;
+
+typedef enum {
+    GBCartridgeLedOff,
+    GBCartridgeLedOn,
+} GBCartridgeLedState;
+
+typedef enum {
+    GBCartridgeSettingsOff,
+    GBCartridgeSettingsOn,
+} GBCartridgeSettingsStoreState;
+
+typedef enum {
+    GBCartridgeRomOptionGB,
+    GBCartridgeRomOptionGBC,
+} GBCartridgeRomOptionState;

+ 2757 - 0
malveke_gb_cartridge/helpers/cJSON.c

@@ -0,0 +1,2757 @@
+/*
+  Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+*/
+
+/* cJSON */
+/* JSON parser in C. */
+
+/* disable warnings about old C89 functions in MSVC */
+#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER)
+#define _CRT_SECURE_NO_DEPRECATE
+#endif
+
+#ifdef __GNUC__
+#pragma GCC visibility push(default)
+#endif
+#if defined(_MSC_VER)
+#pragma warning(push)
+/* disable warning about single line comments in system headers */
+#pragma warning(disable : 4001)
+#endif
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <stdlib.h>
+#include <limits.h>
+#include <ctype.h>
+#include <float.h>
+
+#ifdef ENABLE_LOCALES
+#include <locale.h>
+#endif
+
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+#ifdef __GNUC__
+#pragma GCC visibility pop
+#endif
+
+#include "cJSON.h"
+
+/* define our own boolean type */
+#ifdef true
+#undef true
+#endif
+#define true ((cJSON_bool)1)
+
+#ifdef false
+#undef false
+#endif
+#define false ((cJSON_bool)0)
+
+/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */
+#ifndef isinf
+#define isinf(d) (isnan((d - d)) && !isnan(d))
+#endif
+#ifndef isnan
+#define isnan(d) (d != d)
+#endif
+
+#ifndef NAN
+#ifdef _WIN32
+#define NAN sqrt(-1.0)
+#else
+#define NAN 0.0 / 0.0
+#endif
+#endif
+
+#pragma GCC optimize("O1")
+
+typedef struct {
+    const unsigned char* json;
+    size_t position;
+} error;
+static error global_error = {NULL, 0};
+
+CJSON_PUBLIC(const char*) cJSON_GetErrorPtr(void) {
+    return (const char*)(global_error.json + global_error.position);
+}
+
+CJSON_PUBLIC(char*) cJSON_GetStringValue(const cJSON* const item) {
+    if(!cJSON_IsString(item)) {
+        return NULL;
+    }
+
+    return item->valuestring;
+}
+
+CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON* const item) {
+    if(!cJSON_IsNumber(item)) {
+        return (double)NAN;
+    }
+
+    return item->valuedouble;
+}
+
+/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */
+#if(CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 16)
+#error cJSON.h and cJSON.c have different versions. Make sure that both have the same.
+#endif
+
+CJSON_PUBLIC(const char*) cJSON_Version(void) {
+    static char version[15];
+    sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH);
+
+    return version;
+}
+
+/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */
+static int case_insensitive_strcmp(const unsigned char* string1, const unsigned char* string2) {
+    if((string1 == NULL) || (string2 == NULL)) {
+        return 1;
+    }
+
+    if(string1 == string2) {
+        return 0;
+    }
+
+    for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) {
+        if(*string1 == '\0') {
+            return 0;
+        }
+    }
+
+    return tolower(*string1) - tolower(*string2);
+}
+
+typedef struct internal_hooks {
+    void*(CJSON_CDECL* allocate)(size_t size);
+    void(CJSON_CDECL* deallocate)(void* pointer);
+    void*(CJSON_CDECL* reallocate)(void* pointer, size_t size);
+} internal_hooks;
+
+#if defined(_MSC_VER)
+/* work around MSVC error C2322: '...' address of dllimport '...' is not static */
+static void* CJSON_CDECL internal_malloc(size_t size) {
+    return malloc(size);
+}
+static void CJSON_CDECL internal_free(void* pointer) {
+    free(pointer);
+}
+static void* CJSON_CDECL internal_realloc(void* pointer, size_t size) {
+    return realloc(pointer, size);
+}
+#else
+#define internal_malloc malloc
+#define internal_free free
+#define internal_realloc realloc
+#endif
+
+/* strlen of character literals resolved at compile time */
+#define static_strlen(string_literal) (sizeof(string_literal) - sizeof(""))
+
+static internal_hooks global_hooks = {internal_malloc, internal_free, internal_realloc};
+
+static unsigned char*
+    cJSON_strdup(const unsigned char* string, const internal_hooks* const hooks) {
+    size_t length = 0;
+    unsigned char* copy = NULL;
+
+    if(string == NULL) {
+        return NULL;
+    }
+
+    length = strlen((const char*)string) + sizeof("");
+    copy = (unsigned char*)hooks->allocate(length);
+    if(copy == NULL) {
+        return NULL;
+    }
+    memcpy(copy, string, length);
+
+    return copy;
+}
+
+CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks) {
+    if(hooks == NULL) {
+        /* Reset hooks */
+        global_hooks.allocate = malloc;
+        global_hooks.deallocate = free;
+        global_hooks.reallocate = realloc;
+        return;
+    }
+
+    global_hooks.allocate = malloc;
+    if(hooks->malloc_fn != NULL) {
+        global_hooks.allocate = hooks->malloc_fn;
+    }
+
+    global_hooks.deallocate = free;
+    if(hooks->free_fn != NULL) {
+        global_hooks.deallocate = hooks->free_fn;
+    }
+
+    /* use realloc only if both free and malloc are used */
+    global_hooks.reallocate = NULL;
+    if((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) {
+        global_hooks.reallocate = realloc;
+    }
+}
+
+/* Internal constructor. */
+static cJSON* cJSON_New_Item(const internal_hooks* const hooks) {
+    cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON));
+    if(node) {
+        memset(node, '\0', sizeof(cJSON));
+    }
+
+    return node;
+}
+
+/* Delete a cJSON structure. */
+CJSON_PUBLIC(void) cJSON_Delete(cJSON* item) {
+    cJSON* next = NULL;
+    while(item != NULL) {
+        next = item->next;
+        if(!(item->type & cJSON_IsReference) && (item->child != NULL)) {
+            cJSON_Delete(item->child);
+        }
+        if(!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) {
+            global_hooks.deallocate(item->valuestring);
+        }
+        if(!(item->type & cJSON_StringIsConst) && (item->string != NULL)) {
+            global_hooks.deallocate(item->string);
+        }
+        global_hooks.deallocate(item);
+        item = next;
+    }
+}
+
+/* get the decimal point character of the current locale */
+static unsigned char get_decimal_point(void) {
+#ifdef ENABLE_LOCALES
+    struct lconv* lconv = localeconv();
+    return (unsigned char)lconv->decimal_point[0];
+#else
+    return '.';
+#endif
+}
+
+typedef struct {
+    const unsigned char* content;
+    size_t length;
+    size_t offset;
+    size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */
+    internal_hooks hooks;
+} parse_buffer;
+
+/* check if the given size is left to read in a given parse buffer (starting with 1) */
+#define can_read(buffer, size) \
+    ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length))
+/* check if the buffer can be accessed at the given index (starting with 0) */
+#define can_access_at_index(buffer, index) \
+    ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length))
+#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index))
+/* get a pointer to the buffer at the position */
+#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset)
+
+/* Converts an array of characters to double. Alternative implementation of strtod() */
+double string_to_double(const char* str, char** endptr) {
+    double result = 0.0;
+    int sign = 1;
+    const char* p = str;
+
+    while(isspace((unsigned char)*p)) p++;
+
+    if(*p == '-') {
+        sign = -1;
+        p++;
+    } else if(*p == '+') {
+        p++;
+    }
+
+    while(isdigit((unsigned char)*p)) {
+        result = result * (double)(10) + ((double)(*p - '0'));
+        p++;
+    }
+
+    if(*p == '.') {
+        double fraction = 0.1;
+        p++;
+
+        while(isdigit((unsigned char)p[0])) {
+            fraction *= 0.1L;
+            result += (p++[0] - '0') * fraction;
+        }
+    }
+
+    if(*p == 'e' || *p == 'E') {
+        int exponent = 0;
+        int exp_sign = 1;
+        p++;
+
+        if(*p == '-') {
+            exp_sign = -1;
+            p++;
+        } else if(*p == '+') {
+            p++;
+        }
+
+        while(isdigit((unsigned char)*p)) {
+            exponent = exponent * 10 + (*p - '0');
+            p++;
+        }
+
+        exponent *= exp_sign;
+        result *= pow(10, exponent);
+    }
+
+    *endptr = (char*)p;
+
+    return sign * result;
+}
+
+/* Parse the input text to generate a number, and populate the result into item. */
+static cJSON_bool parse_number(cJSON* const item, parse_buffer* const input_buffer) {
+    double number = 0;
+    unsigned char* after_end = NULL;
+    unsigned char number_c_string[64];
+    unsigned char decimal_point = get_decimal_point();
+    size_t i = 0;
+
+    if((input_buffer == NULL) || (input_buffer->content == NULL)) {
+        return false;
+    }
+
+    /* copy the number into a temporary buffer and replace '.' with the decimal point
+     * of the current locale (for string_to_double)
+     * This also takes care of '\0' not necessarily being available for marking the end of the input */
+    for(i = 0; (i < (sizeof(number_c_string) - 1)) && can_access_at_index(input_buffer, i); i++) {
+        switch(buffer_at_offset(input_buffer)[i]) {
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+        case '6':
+        case '7':
+        case '8':
+        case '9':
+        case '+':
+        case '-':
+        case 'e':
+        case 'E':
+            number_c_string[i] = buffer_at_offset(input_buffer)[i];
+            break;
+
+        case '.':
+            number_c_string[i] = decimal_point;
+            break;
+
+        default:
+            goto loop_end;
+        }
+    }
+loop_end:
+    number_c_string[i] = '\0';
+
+    number = string_to_double((const char*)number_c_string, (char**)&after_end);
+    if(number_c_string == after_end) {
+        return false; /* parse_error */
+    }
+
+    item->valuedouble = number;
+
+    /* use saturation in case of overflow */
+    if(number >= INT_MAX) {
+        item->valueint = INT_MAX;
+    } else if(number <= (double)INT_MIN) {
+        item->valueint = INT_MIN;
+    } else {
+        item->valueint = (int)number;
+    }
+
+    item->type = cJSON_Number;
+
+    input_buffer->offset += (size_t)(after_end - number_c_string);
+    return true;
+}
+
+/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */
+CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON* object, double number) {
+    if(number >= INT_MAX) {
+        object->valueint = INT_MAX;
+    } else if(number <= (double)INT_MIN) {
+        object->valueint = INT_MIN;
+    } else {
+        object->valueint = (int)number;
+    }
+
+    return object->valuedouble = number;
+}
+
+CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON* object, const char* valuestring) {
+    char* copy = NULL;
+    /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */
+    if(!(object->type & cJSON_String) || (object->type & cJSON_IsReference)) {
+        return NULL;
+    }
+    if(strlen(valuestring) <= strlen(object->valuestring)) {
+        strcpy(object->valuestring, valuestring);
+        return object->valuestring;
+    }
+    copy = (char*)cJSON_strdup((const unsigned char*)valuestring, &global_hooks);
+    if(copy == NULL) {
+        return NULL;
+    }
+    if(object->valuestring != NULL) {
+        cJSON_free(object->valuestring);
+    }
+    object->valuestring = copy;
+
+    return copy;
+}
+
+typedef struct {
+    unsigned char* buffer;
+    size_t length;
+    size_t offset;
+    size_t depth; /* current nesting depth (for formatted printing) */
+    cJSON_bool noalloc;
+    cJSON_bool format; /* is this print a formatted print */
+    internal_hooks hooks;
+} printbuffer;
+
+/* realloc printbuffer if necessary to have at least "needed" bytes more */
+static unsigned char* ensure(printbuffer* const p, size_t needed) {
+    unsigned char* newbuffer = NULL;
+    size_t newsize = 0;
+
+    if((p == NULL) || (p->buffer == NULL)) {
+        return NULL;
+    }
+
+    if((p->length > 0) && (p->offset >= p->length)) {
+        /* make sure that offset is valid */
+        return NULL;
+    }
+
+    if(needed > INT_MAX) {
+        /* sizes bigger than INT_MAX are currently not supported */
+        return NULL;
+    }
+
+    needed += p->offset + 1;
+    if(needed <= p->length) {
+        return p->buffer + p->offset;
+    }
+
+    if(p->noalloc) {
+        return NULL;
+    }
+
+    /* calculate new buffer size */
+    if(needed > (INT_MAX / 2)) {
+        /* overflow of int, use INT_MAX if possible */
+        if(needed <= INT_MAX) {
+            newsize = INT_MAX;
+        } else {
+            return NULL;
+        }
+    } else {
+        newsize = needed * 2;
+    }
+
+    if(p->hooks.reallocate != NULL) {
+        /* reallocate with realloc if available */
+        newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize);
+        if(newbuffer == NULL) {
+            p->hooks.deallocate(p->buffer);
+            p->length = 0;
+            p->buffer = NULL;
+
+            return NULL;
+        }
+    } else {
+        /* otherwise reallocate manually */
+        newbuffer = (unsigned char*)p->hooks.allocate(newsize);
+        if(!newbuffer) {
+            p->hooks.deallocate(p->buffer);
+            p->length = 0;
+            p->buffer = NULL;
+
+            return NULL;
+        }
+
+        memcpy(newbuffer, p->buffer, p->offset + 1);
+        p->hooks.deallocate(p->buffer);
+    }
+    p->length = newsize;
+    p->buffer = newbuffer;
+
+    return newbuffer + p->offset;
+}
+
+/* calculate the new length of the string in a printbuffer and update the offset */
+static void update_offset(printbuffer* const buffer) {
+    const unsigned char* buffer_pointer = NULL;
+    if((buffer == NULL) || (buffer->buffer == NULL)) {
+        return;
+    }
+    buffer_pointer = buffer->buffer + buffer->offset;
+
+    buffer->offset += strlen((const char*)buffer_pointer);
+}
+
+/* securely comparison of floating-point variables */
+static cJSON_bool compare_double(double a, double b) {
+    double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b);
+    return (fabs(a - b) <= maxVal * DBL_EPSILON);
+}
+
+/* Render the number nicely from the given item into a string. */
+static cJSON_bool print_number(const cJSON* const item, printbuffer* const output_buffer) {
+    unsigned char* output_pointer = NULL;
+    double d = item->valuedouble;
+    int length = 0;
+    size_t i = 0;
+    unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */
+    unsigned char decimal_point = get_decimal_point();
+    double test = 0.0;
+
+    if(output_buffer == NULL) {
+        return false;
+    }
+
+    /* This checks for NaN and Infinity */
+    if(isnan(d) || isinf(d)) {
+        length = snprintf((char*)number_buffer, sizeof(number_buffer), "null");
+    } else if(d == (double)item->valueint) {
+        // length = sprintf((char*)number_buffer, "%d", item->valueint);
+        length = snprintf((char*)number_buffer, sizeof(number_buffer), "%d", item->valueint);
+
+    } else {
+        /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */
+        // length = sprintf((char*)number_buffer, "%1.15g", d);
+        length = snprintf((char*)number_buffer, sizeof(number_buffer), "%1.15g", d);
+
+        /* Check whether the original double can be recovered */
+        if((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) {
+            /* If not, print with 17 decimal places of precision */
+            // length = sprintf((char*)number_buffer, "%1.17g", d);
+            length = snprintf((char*)number_buffer, sizeof(number_buffer), "%1.17g", d);
+        }
+    }
+
+    /* sprintf failed or buffer overrun occurred */
+    if((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) {
+        return false;
+    }
+
+    /* reserve appropriate space in the output */
+    output_pointer = ensure(output_buffer, (size_t)length + sizeof(""));
+    if(output_pointer == NULL) {
+        return false;
+    }
+
+    /* copy the printed number to the output and replace locale
+     * dependent decimal point with '.' */
+    for(i = 0; i < ((size_t)length); i++) {
+        if(number_buffer[i] == decimal_point) {
+            output_pointer[i] = '.';
+            continue;
+        }
+
+        output_pointer[i] = number_buffer[i];
+    }
+    output_pointer[i] = '\0';
+
+    output_buffer->offset += (size_t)length;
+
+    return true;
+}
+
+/* parse 4 digit hexadecimal number */
+static unsigned parse_hex4(const unsigned char* const input) {
+    unsigned int h = 0;
+    size_t i = 0;
+
+    for(i = 0; i < 4; i++) {
+        /* parse digit */
+        if((input[i] >= '0') && (input[i] <= '9')) {
+            h += (unsigned int)input[i] - '0';
+        } else if((input[i] >= 'A') && (input[i] <= 'F')) {
+            h += (unsigned int)10 + input[i] - 'A';
+        } else if((input[i] >= 'a') && (input[i] <= 'f')) {
+            h += (unsigned int)10 + input[i] - 'a';
+        } else /* invalid */
+        {
+            return 0;
+        }
+
+        if(i < 3) {
+            /* shift left to make place for the next nibble */
+            h = h << 4;
+        }
+    }
+
+    return h;
+}
+
+/* converts a UTF-16 literal to UTF-8
+ * A literal can be one or two sequences of the form \uXXXX */
+static unsigned char utf16_literal_to_utf8(
+    const unsigned char* const input_pointer,
+    const unsigned char* const input_end,
+    unsigned char** output_pointer) {
+    long unsigned int codepoint = 0;
+    unsigned int first_code = 0;
+    const unsigned char* first_sequence = input_pointer;
+    unsigned char utf8_length = 0;
+    unsigned char utf8_position = 0;
+    unsigned char sequence_length = 0;
+    unsigned char first_byte_mark = 0;
+
+    if((input_end - first_sequence) < 6) {
+        /* input ends unexpectedly */
+        goto fail;
+    }
+
+    /* get the first utf16 sequence */
+    first_code = parse_hex4(first_sequence + 2);
+
+    /* check that the code is valid */
+    if(((first_code >= 0xDC00) && (first_code <= 0xDFFF))) {
+        goto fail;
+    }
+
+    /* UTF16 surrogate pair */
+    if((first_code >= 0xD800) && (first_code <= 0xDBFF)) {
+        const unsigned char* second_sequence = first_sequence + 6;
+        unsigned int second_code = 0;
+        sequence_length = 12; /* \uXXXX\uXXXX */
+
+        if((input_end - second_sequence) < 6) {
+            /* input ends unexpectedly */
+            goto fail;
+        }
+
+        if((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) {
+            /* missing second half of the surrogate pair */
+            goto fail;
+        }
+
+        /* get the second utf16 sequence */
+        second_code = parse_hex4(second_sequence + 2);
+        /* check that the code is valid */
+        if((second_code < 0xDC00) || (second_code > 0xDFFF)) {
+            /* invalid second half of the surrogate pair */
+            goto fail;
+        }
+
+        /* calculate the unicode codepoint from the surrogate pair */
+        codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF));
+    } else {
+        sequence_length = 6; /* \uXXXX */
+        codepoint = first_code;
+    }
+
+    /* encode as UTF-8
+     * takes at maximum 4 bytes to encode:
+     * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */
+    if(codepoint < 0x80) {
+        /* normal ascii, encoding 0xxxxxxx */
+        utf8_length = 1;
+    } else if(codepoint < 0x800) {
+        /* two bytes, encoding 110xxxxx 10xxxxxx */
+        utf8_length = 2;
+        first_byte_mark = 0xC0; /* 11000000 */
+    } else if(codepoint < 0x10000) {
+        /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */
+        utf8_length = 3;
+        first_byte_mark = 0xE0; /* 11100000 */
+    } else if(codepoint <= 0x10FFFF) {
+        /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */
+        utf8_length = 4;
+        first_byte_mark = 0xF0; /* 11110000 */
+    } else {
+        /* invalid unicode codepoint */
+        goto fail;
+    }
+
+    /* encode as utf8 */
+    for(utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) {
+        /* 10xxxxxx */
+        (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF);
+        codepoint >>= 6;
+    }
+    /* encode first byte */
+    if(utf8_length > 1) {
+        (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF);
+    } else {
+        (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F);
+    }
+
+    *output_pointer += utf8_length;
+
+    return sequence_length;
+
+fail:
+    return 0;
+}
+
+/* Parse the input text into an unescaped cinput, and populate item. */
+static cJSON_bool parse_string(cJSON* const item, parse_buffer* const input_buffer) {
+    const unsigned char* input_pointer = buffer_at_offset(input_buffer) + 1;
+    const unsigned char* input_end = buffer_at_offset(input_buffer) + 1;
+    unsigned char* output_pointer = NULL;
+    unsigned char* output = NULL;
+
+    /* not a string */
+    if(buffer_at_offset(input_buffer)[0] != '\"') {
+        goto fail;
+    }
+
+    {
+        /* calculate approximate size of the output (overestimate) */
+        size_t allocation_length = 0;
+        size_t skipped_bytes = 0;
+        while(((size_t)(input_end - input_buffer->content) < input_buffer->length) &&
+              (*input_end != '\"')) {
+            /* is escape sequence */
+            if(input_end[0] == '\\') {
+                if((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) {
+                    /* prevent buffer overflow when last input character is a backslash */
+                    goto fail;
+                }
+                skipped_bytes++;
+                input_end++;
+            }
+            input_end++;
+        }
+        if(((size_t)(input_end - input_buffer->content) >= input_buffer->length) ||
+           (*input_end != '\"')) {
+            goto fail; /* string ended unexpectedly */
+        }
+
+        /* This is at most how much we need for the output */
+        allocation_length = (size_t)(input_end - buffer_at_offset(input_buffer)) - skipped_bytes;
+        output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof(""));
+        if(output == NULL) {
+            goto fail; /* allocation failure */
+        }
+    }
+
+    output_pointer = output;
+    /* loop through the string literal */
+    while(input_pointer < input_end) {
+        if(*input_pointer != '\\') {
+            *output_pointer++ = *input_pointer++;
+        }
+        /* escape sequence */
+        else {
+            unsigned char sequence_length = 2;
+            if((input_end - input_pointer) < 1) {
+                goto fail;
+            }
+
+            switch(input_pointer[1]) {
+            case 'b':
+                *output_pointer++ = '\b';
+                break;
+            case 'f':
+                *output_pointer++ = '\f';
+                break;
+            case 'n':
+                *output_pointer++ = '\n';
+                break;
+            case 'r':
+                *output_pointer++ = '\r';
+                break;
+            case 't':
+                *output_pointer++ = '\t';
+                break;
+            case '\"':
+            case '\\':
+            case '/':
+                *output_pointer++ = input_pointer[1];
+                break;
+
+            /* UTF-16 literal */
+            case 'u':
+                sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer);
+                if(sequence_length == 0) {
+                    /* failed to convert UTF16-literal to UTF-8 */
+                    goto fail;
+                }
+                break;
+
+            default:
+                goto fail;
+            }
+            input_pointer += sequence_length;
+        }
+    }
+
+    /* zero terminate the output */
+    *output_pointer = '\0';
+
+    item->type = cJSON_String;
+    item->valuestring = (char*)output;
+
+    input_buffer->offset = (size_t)(input_end - input_buffer->content);
+    input_buffer->offset++;
+
+    return true;
+
+fail:
+    if(output != NULL) {
+        input_buffer->hooks.deallocate(output);
+    }
+
+    if(input_pointer != NULL) {
+        input_buffer->offset = (size_t)(input_pointer - input_buffer->content);
+    }
+
+    return false;
+}
+
+/* Render the cstring provided to an escaped version that can be printed. */
+static cJSON_bool
+    print_string_ptr(const unsigned char* const input, printbuffer* const output_buffer) {
+    const unsigned char* input_pointer = NULL;
+    unsigned char* output = NULL;
+    unsigned char* output_pointer = NULL;
+    size_t output_length = 0;
+    /* numbers of additional characters needed for escaping */
+    size_t escape_characters = 0;
+
+    if(output_buffer == NULL) {
+        return false;
+    }
+
+    /* empty string */
+    if(input == NULL) {
+        output = ensure(output_buffer, sizeof("\"\""));
+        if(output == NULL) {
+            return false;
+        }
+        strcpy((char*)output, "\"\"");
+
+        return true;
+    }
+
+    /* set "flag" to 1 if something needs to be escaped */
+    for(input_pointer = input; *input_pointer; input_pointer++) {
+        switch(*input_pointer) {
+        case '\"':
+        case '\\':
+        case '\b':
+        case '\f':
+        case '\n':
+        case '\r':
+        case '\t':
+            /* one character escape sequence */
+            escape_characters++;
+            break;
+        default:
+            if(*input_pointer < 32) {
+                /* UTF-16 escape sequence uXXXX */
+                escape_characters += 5;
+            }
+            break;
+        }
+    }
+    output_length = (size_t)(input_pointer - input) + escape_characters;
+
+    output = ensure(output_buffer, output_length + sizeof("\"\""));
+    if(output == NULL) {
+        return false;
+    }
+
+    /* no characters have to be escaped */
+    if(escape_characters == 0) {
+        output[0] = '\"';
+        memcpy(output + 1, input, output_length);
+        output[output_length + 1] = '\"';
+        output[output_length + 2] = '\0';
+
+        return true;
+    }
+
+    output[0] = '\"';
+    output_pointer = output + 1;
+    /* copy the string */
+    for(input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) {
+        if((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) {
+            /* normal character, copy */
+            *output_pointer = *input_pointer;
+        } else {
+            /* character needs to be escaped */
+            *output_pointer++ = '\\';
+            switch(*input_pointer) {
+            case '\\':
+                *output_pointer = '\\';
+                break;
+            case '\"':
+                *output_pointer = '\"';
+                break;
+            case '\b':
+                *output_pointer = 'b';
+                break;
+            case '\f':
+                *output_pointer = 'f';
+                break;
+            case '\n':
+                *output_pointer = 'n';
+                break;
+            case '\r':
+                *output_pointer = 'r';
+                break;
+            case '\t':
+                *output_pointer = 't';
+                break;
+            default:
+                /* escape and print as unicode codepoint */
+                // sprintf((char*)output_pointer, "u%04x", *input_pointer);
+                snprintf((char*)output_pointer, 6, "u%04x", *input_pointer);
+
+                output_pointer += 4;
+                break;
+            }
+        }
+    }
+    output[output_length + 1] = '\"';
+    output[output_length + 2] = '\0';
+
+    return true;
+}
+
+/* Invoke print_string_ptr (which is useful) on an item. */
+static cJSON_bool print_string(const cJSON* const item, printbuffer* const p) {
+    return print_string_ptr((unsigned char*)item->valuestring, p);
+}
+
+/* Predeclare these prototypes. */
+static cJSON_bool parse_value(cJSON* const item, parse_buffer* const input_buffer);
+static cJSON_bool print_value(const cJSON* const item, printbuffer* const output_buffer);
+static cJSON_bool parse_array(cJSON* const item, parse_buffer* const input_buffer);
+static cJSON_bool print_array(const cJSON* const item, printbuffer* const output_buffer);
+static cJSON_bool parse_object(cJSON* const item, parse_buffer* const input_buffer);
+static cJSON_bool print_object(const cJSON* const item, printbuffer* const output_buffer);
+
+/* Utility to jump whitespace and cr/lf */
+static parse_buffer* buffer_skip_whitespace(parse_buffer* const buffer) {
+    if((buffer == NULL) || (buffer->content == NULL)) {
+        return NULL;
+    }
+
+    if(cannot_access_at_index(buffer, 0)) {
+        return buffer;
+    }
+
+    while(can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) {
+        buffer->offset++;
+    }
+
+    if(buffer->offset == buffer->length) {
+        buffer->offset--;
+    }
+
+    return buffer;
+}
+
+/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */
+static parse_buffer* skip_utf8_bom(parse_buffer* const buffer) {
+    if((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) {
+        return NULL;
+    }
+
+    if(can_access_at_index(buffer, 4) &&
+       (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) {
+        buffer->offset += 3;
+    }
+
+    return buffer;
+}
+
+CJSON_PUBLIC(cJSON*)
+cJSON_ParseWithOpts(
+    const char* value,
+    const char** return_parse_end,
+    cJSON_bool require_null_terminated) {
+    size_t buffer_length;
+
+    if(NULL == value) {
+        return NULL;
+    }
+
+    /* Adding null character size due to require_null_terminated. */
+    buffer_length = strlen(value) + sizeof("");
+
+    return cJSON_ParseWithLengthOpts(
+        value, buffer_length, return_parse_end, require_null_terminated);
+}
+
+/* Parse an object - create a new root, and populate. */
+CJSON_PUBLIC(cJSON*)
+cJSON_ParseWithLengthOpts(
+    const char* value,
+    size_t buffer_length,
+    const char** return_parse_end,
+    cJSON_bool require_null_terminated) {
+    parse_buffer buffer = {0, 0, 0, 0, {0, 0, 0}};
+    cJSON* item = NULL;
+
+    /* reset error position */
+    global_error.json = NULL;
+    global_error.position = 0;
+
+    if(value == NULL || 0 == buffer_length) {
+        goto fail;
+    }
+
+    buffer.content = (const unsigned char*)value;
+    buffer.length = buffer_length;
+    buffer.offset = 0;
+    buffer.hooks = global_hooks;
+
+    item = cJSON_New_Item(&global_hooks);
+    if(item == NULL) /* memory fail */
+    {
+        goto fail;
+    }
+
+    if(!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) {
+        /* parse failure. ep is set. */
+        goto fail;
+    }
+
+    /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */
+    if(require_null_terminated) {
+        buffer_skip_whitespace(&buffer);
+        if((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') {
+            goto fail;
+        }
+    }
+    if(return_parse_end) {
+        *return_parse_end = (const char*)buffer_at_offset(&buffer);
+    }
+
+    return item;
+
+fail:
+    if(item != NULL) {
+        cJSON_Delete(item);
+    }
+
+    if(value != NULL) {
+        error local_error;
+        local_error.json = (const unsigned char*)value;
+        local_error.position = 0;
+
+        if(buffer.offset < buffer.length) {
+            local_error.position = buffer.offset;
+        } else if(buffer.length > 0) {
+            local_error.position = buffer.length - 1;
+        }
+
+        if(return_parse_end != NULL) {
+            *return_parse_end = (const char*)local_error.json + local_error.position;
+        }
+
+        global_error = local_error;
+    }
+
+    return NULL;
+}
+
+/* Default options for cJSON_Parse */
+CJSON_PUBLIC(cJSON*) cJSON_Parse(const char* value) {
+    return cJSON_ParseWithOpts(value, 0, 0);
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_ParseWithLength(const char* value, size_t buffer_length) {
+    return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0);
+}
+
+#define cjson_min(a, b) (((a) < (b)) ? (a) : (b))
+
+static unsigned char*
+    print(const cJSON* const item, cJSON_bool format, const internal_hooks* const hooks) {
+    static const size_t default_buffer_size = 256;
+    printbuffer buffer[1];
+    unsigned char* printed = NULL;
+
+    memset(buffer, 0, sizeof(buffer));
+
+    /* create buffer */
+    buffer->buffer = (unsigned char*)hooks->allocate(default_buffer_size);
+    buffer->length = default_buffer_size;
+    buffer->format = format;
+    buffer->hooks = *hooks;
+    if(buffer->buffer == NULL) {
+        goto fail;
+    }
+
+    /* print the value */
+    if(!print_value(item, buffer)) {
+        goto fail;
+    }
+    update_offset(buffer);
+
+    /* check if reallocate is available */
+    if(hooks->reallocate != NULL) {
+        printed = (unsigned char*)hooks->reallocate(buffer->buffer, buffer->offset + 1);
+        if(printed == NULL) {
+            goto fail;
+        }
+        buffer->buffer = NULL;
+    } else /* otherwise copy the JSON over to a new buffer */
+    {
+        printed = (unsigned char*)hooks->allocate(buffer->offset + 1);
+        if(printed == NULL) {
+            goto fail;
+        }
+        memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1));
+        printed[buffer->offset] = '\0'; /* just to be sure */
+
+        /* free the buffer */
+        hooks->deallocate(buffer->buffer);
+    }
+
+    return printed;
+
+fail:
+    if(buffer->buffer != NULL) {
+        hooks->deallocate(buffer->buffer);
+    }
+
+    if(printed != NULL) {
+        hooks->deallocate(printed);
+    }
+
+    return NULL;
+}
+
+/* Render a cJSON item/entity/structure to text. */
+CJSON_PUBLIC(char*) cJSON_Print(const cJSON* item) {
+    return (char*)print(item, true, &global_hooks);
+}
+
+CJSON_PUBLIC(char*) cJSON_PrintUnformatted(const cJSON* item) {
+    return (char*)print(item, false, &global_hooks);
+}
+
+CJSON_PUBLIC(char*) cJSON_PrintBuffered(const cJSON* item, int prebuffer, cJSON_bool fmt) {
+    printbuffer p = {0, 0, 0, 0, 0, 0, {0, 0, 0}};
+
+    if(prebuffer < 0) {
+        return NULL;
+    }
+
+    p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer);
+    if(!p.buffer) {
+        return NULL;
+    }
+
+    p.length = (size_t)prebuffer;
+    p.offset = 0;
+    p.noalloc = false;
+    p.format = fmt;
+    p.hooks = global_hooks;
+
+    if(!print_value(item, &p)) {
+        global_hooks.deallocate(p.buffer);
+        return NULL;
+    }
+
+    return (char*)p.buffer;
+}
+
+CJSON_PUBLIC(cJSON_bool)
+cJSON_PrintPreallocated(cJSON* item, char* buffer, const int length, const cJSON_bool format) {
+    printbuffer p = {0, 0, 0, 0, 0, 0, {0, 0, 0}};
+
+    if((length < 0) || (buffer == NULL)) {
+        return false;
+    }
+
+    p.buffer = (unsigned char*)buffer;
+    p.length = (size_t)length;
+    p.offset = 0;
+    p.noalloc = true;
+    p.format = format;
+    p.hooks = global_hooks;
+
+    return print_value(item, &p);
+}
+
+/* Parser core - when encountering text, process appropriately. */
+static cJSON_bool parse_value(cJSON* const item, parse_buffer* const input_buffer) {
+    if((input_buffer == NULL) || (input_buffer->content == NULL)) {
+        return false; /* no input */
+    }
+
+    /* parse the different types of values */
+    /* null */
+    if(can_read(input_buffer, 4) &&
+       (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0)) {
+        item->type = cJSON_NULL;
+        input_buffer->offset += 4;
+        return true;
+    }
+    /* false */
+    if(can_read(input_buffer, 5) &&
+       (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0)) {
+        item->type = cJSON_False;
+        input_buffer->offset += 5;
+        return true;
+    }
+    /* true */
+    if(can_read(input_buffer, 4) &&
+       (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0)) {
+        item->type = cJSON_True;
+        item->valueint = 1;
+        input_buffer->offset += 4;
+        return true;
+    }
+    /* string */
+    if(can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) {
+        return parse_string(item, input_buffer);
+    }
+    /* number */
+    if(can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') ||
+                                                ((buffer_at_offset(input_buffer)[0] >= '0') &&
+                                                 (buffer_at_offset(input_buffer)[0] <= '9')))) {
+        return parse_number(item, input_buffer);
+    }
+    /* array */
+    if(can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) {
+        return parse_array(item, input_buffer);
+    }
+    /* object */
+    if(can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) {
+        return parse_object(item, input_buffer);
+    }
+
+    return false;
+}
+
+/* Render a value to text. */
+static cJSON_bool print_value(const cJSON* const item, printbuffer* const output_buffer) {
+    unsigned char* output = NULL;
+
+    if((item == NULL) || (output_buffer == NULL)) {
+        return false;
+    }
+
+    switch((item->type) & 0xFF) {
+    case cJSON_NULL:
+        output = ensure(output_buffer, 5);
+        if(output == NULL) {
+            return false;
+        }
+        strcpy((char*)output, "null");
+        return true;
+
+    case cJSON_False:
+        output = ensure(output_buffer, 6);
+        if(output == NULL) {
+            return false;
+        }
+        strcpy((char*)output, "false");
+        return true;
+
+    case cJSON_True:
+        output = ensure(output_buffer, 5);
+        if(output == NULL) {
+            return false;
+        }
+        strcpy((char*)output, "true");
+        return true;
+
+    case cJSON_Number:
+        return print_number(item, output_buffer);
+
+    case cJSON_Raw: {
+        size_t raw_length = 0;
+        if(item->valuestring == NULL) {
+            return false;
+        }
+
+        raw_length = strlen(item->valuestring) + sizeof("");
+        output = ensure(output_buffer, raw_length);
+        if(output == NULL) {
+            return false;
+        }
+        memcpy(output, item->valuestring, raw_length);
+        return true;
+    }
+
+    case cJSON_String:
+        return print_string(item, output_buffer);
+
+    case cJSON_Array:
+        return print_array(item, output_buffer);
+
+    case cJSON_Object:
+        return print_object(item, output_buffer);
+
+    default:
+        return false;
+    }
+}
+
+/* Build an array from input text. */
+static cJSON_bool parse_array(cJSON* const item, parse_buffer* const input_buffer) {
+    cJSON* head = NULL; /* head of the linked list */
+    cJSON* current_item = NULL;
+
+    if(input_buffer->depth >= CJSON_NESTING_LIMIT) {
+        return false; /* to deeply nested */
+    }
+    input_buffer->depth++;
+
+    if(buffer_at_offset(input_buffer)[0] != '[') {
+        /* not an array */
+        goto fail;
+    }
+
+    input_buffer->offset++;
+    buffer_skip_whitespace(input_buffer);
+    if(can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) {
+        /* empty array */
+        goto success;
+    }
+
+    /* check if we skipped to the end of the buffer */
+    if(cannot_access_at_index(input_buffer, 0)) {
+        input_buffer->offset--;
+        goto fail;
+    }
+
+    /* step back to character in front of the first element */
+    input_buffer->offset--;
+    /* loop through the comma separated array elements */
+    do {
+        /* allocate next item */
+        cJSON* new_item = cJSON_New_Item(&(input_buffer->hooks));
+        if(new_item == NULL) {
+            goto fail; /* allocation failure */
+        }
+
+        /* attach next item to list */
+        if(head == NULL) {
+            /* start the linked list */
+            current_item = head = new_item;
+        } else {
+            /* add to the end and advance */
+            current_item->next = new_item;
+            new_item->prev = current_item;
+            current_item = new_item;
+        }
+
+        /* parse next value */
+        input_buffer->offset++;
+        buffer_skip_whitespace(input_buffer);
+        if(!parse_value(current_item, input_buffer)) {
+            goto fail; /* failed to parse value */
+        }
+        buffer_skip_whitespace(input_buffer);
+    } while(can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ','));
+
+    if(cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') {
+        goto fail; /* expected end of array */
+    }
+
+success:
+    input_buffer->depth--;
+
+    if(head != NULL) {
+        head->prev = current_item;
+    }
+
+    item->type = cJSON_Array;
+    item->child = head;
+
+    input_buffer->offset++;
+
+    return true;
+
+fail:
+    if(head != NULL) {
+        cJSON_Delete(head);
+    }
+
+    return false;
+}
+
+/* Render an array to text */
+static cJSON_bool print_array(const cJSON* const item, printbuffer* const output_buffer) {
+    unsigned char* output_pointer = NULL;
+    size_t length = 0;
+    cJSON* current_element = item->child;
+
+    if(output_buffer == NULL) {
+        return false;
+    }
+
+    /* Compose the output array. */
+    /* opening square bracket */
+    output_pointer = ensure(output_buffer, 1);
+    if(output_pointer == NULL) {
+        return false;
+    }
+
+    *output_pointer = '[';
+    output_buffer->offset++;
+    output_buffer->depth++;
+
+    while(current_element != NULL) {
+        if(!print_value(current_element, output_buffer)) {
+            return false;
+        }
+        update_offset(output_buffer);
+        if(current_element->next) {
+            length = (size_t)(output_buffer->format ? 2 : 1);
+            output_pointer = ensure(output_buffer, length + 1);
+            if(output_pointer == NULL) {
+                return false;
+            }
+            *output_pointer++ = ',';
+            if(output_buffer->format) {
+                *output_pointer++ = ' ';
+            }
+            *output_pointer = '\0';
+            output_buffer->offset += length;
+        }
+        current_element = current_element->next;
+    }
+
+    output_pointer = ensure(output_buffer, 2);
+    if(output_pointer == NULL) {
+        return false;
+    }
+    *output_pointer++ = ']';
+    *output_pointer = '\0';
+    output_buffer->depth--;
+
+    return true;
+}
+
+/* Build an object from the text. */
+static cJSON_bool parse_object(cJSON* const item, parse_buffer* const input_buffer) {
+    cJSON* head = NULL; /* linked list head */
+    cJSON* current_item = NULL;
+
+    if(input_buffer->depth >= CJSON_NESTING_LIMIT) {
+        return false; /* to deeply nested */
+    }
+    input_buffer->depth++;
+
+    if(cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) {
+        goto fail; /* not an object */
+    }
+
+    input_buffer->offset++;
+    buffer_skip_whitespace(input_buffer);
+    if(can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) {
+        goto success; /* empty object */
+    }
+
+    /* check if we skipped to the end of the buffer */
+    if(cannot_access_at_index(input_buffer, 0)) {
+        input_buffer->offset--;
+        goto fail;
+    }
+
+    /* step back to character in front of the first element */
+    input_buffer->offset--;
+    /* loop through the comma separated array elements */
+    do {
+        /* allocate next item */
+        cJSON* new_item = cJSON_New_Item(&(input_buffer->hooks));
+        if(new_item == NULL) {
+            goto fail; /* allocation failure */
+        }
+
+        /* attach next item to list */
+        if(head == NULL) {
+            /* start the linked list */
+            current_item = head = new_item;
+        } else {
+            /* add to the end and advance */
+            current_item->next = new_item;
+            new_item->prev = current_item;
+            current_item = new_item;
+        }
+
+        /* parse the name of the child */
+        input_buffer->offset++;
+        buffer_skip_whitespace(input_buffer);
+        if(!parse_string(current_item, input_buffer)) {
+            goto fail; /* failed to parse name */
+        }
+        buffer_skip_whitespace(input_buffer);
+
+        /* swap valuestring and string, because we parsed the name */
+        current_item->string = current_item->valuestring;
+        current_item->valuestring = NULL;
+
+        if(cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) {
+            goto fail; /* invalid object */
+        }
+
+        /* parse the value */
+        input_buffer->offset++;
+        buffer_skip_whitespace(input_buffer);
+        if(!parse_value(current_item, input_buffer)) {
+            goto fail; /* failed to parse value */
+        }
+        buffer_skip_whitespace(input_buffer);
+    } while(can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ','));
+
+    if(cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) {
+        goto fail; /* expected end of object */
+    }
+
+success:
+    input_buffer->depth--;
+
+    if(head != NULL) {
+        head->prev = current_item;
+    }
+
+    item->type = cJSON_Object;
+    item->child = head;
+
+    input_buffer->offset++;
+    return true;
+
+fail:
+    if(head != NULL) {
+        cJSON_Delete(head);
+    }
+
+    return false;
+}
+
+/* Render an object to text. */
+static cJSON_bool print_object(const cJSON* const item, printbuffer* const output_buffer) {
+    unsigned char* output_pointer = NULL;
+    size_t length = 0;
+    cJSON* current_item = item->child;
+
+    if(output_buffer == NULL) {
+        return false;
+    }
+
+    /* Compose the output: */
+    length = (size_t)(output_buffer->format ? 2 : 1); /* fmt: {\n */
+    output_pointer = ensure(output_buffer, length + 1);
+    if(output_pointer == NULL) {
+        return false;
+    }
+
+    *output_pointer++ = '{';
+    output_buffer->depth++;
+    if(output_buffer->format) {
+        *output_pointer++ = '\n';
+    }
+    output_buffer->offset += length;
+
+    while(current_item) {
+        if(output_buffer->format) {
+            size_t i;
+            output_pointer = ensure(output_buffer, output_buffer->depth);
+            if(output_pointer == NULL) {
+                return false;
+            }
+            for(i = 0; i < output_buffer->depth; i++) {
+                *output_pointer++ = '\t';
+            }
+            output_buffer->offset += output_buffer->depth;
+        }
+
+        /* print key */
+        if(!print_string_ptr((unsigned char*)current_item->string, output_buffer)) {
+            return false;
+        }
+        update_offset(output_buffer);
+
+        length = (size_t)(output_buffer->format ? 2 : 1);
+        output_pointer = ensure(output_buffer, length);
+        if(output_pointer == NULL) {
+            return false;
+        }
+        *output_pointer++ = ':';
+        if(output_buffer->format) {
+            *output_pointer++ = '\t';
+        }
+        output_buffer->offset += length;
+
+        /* print value */
+        if(!print_value(current_item, output_buffer)) {
+            return false;
+        }
+        update_offset(output_buffer);
+
+        /* print comma if not last */
+        length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0));
+        output_pointer = ensure(output_buffer, length + 1);
+        if(output_pointer == NULL) {
+            return false;
+        }
+        if(current_item->next) {
+            *output_pointer++ = ',';
+        }
+
+        if(output_buffer->format) {
+            *output_pointer++ = '\n';
+        }
+        *output_pointer = '\0';
+        output_buffer->offset += length;
+
+        current_item = current_item->next;
+    }
+
+    output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2);
+    if(output_pointer == NULL) {
+        return false;
+    }
+    if(output_buffer->format) {
+        size_t i;
+        for(i = 0; i < (output_buffer->depth - 1); i++) {
+            *output_pointer++ = '\t';
+        }
+    }
+    *output_pointer++ = '}';
+    *output_pointer = '\0';
+    output_buffer->depth--;
+
+    return true;
+}
+
+/* Get Array size/item / object item. */
+CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON* array) {
+    cJSON* child = NULL;
+    size_t size = 0;
+
+    if(array == NULL) {
+        return 0;
+    }
+
+    child = array->child;
+
+    while(child != NULL) {
+        size++;
+        child = child->next;
+    }
+
+    /* FIXME: Can overflow here. Cannot be fixed without breaking the API */
+
+    return (int)size;
+}
+
+static cJSON* get_array_item(const cJSON* array, size_t index) {
+    cJSON* current_child = NULL;
+
+    if(array == NULL) {
+        return NULL;
+    }
+
+    current_child = array->child;
+    while((current_child != NULL) && (index > 0)) {
+        index--;
+        current_child = current_child->next;
+    }
+
+    return current_child;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_GetArrayItem(const cJSON* array, int index) {
+    if(index < 0) {
+        return NULL;
+    }
+
+    return get_array_item(array, (size_t)index);
+}
+
+static cJSON* get_object_item(
+    const cJSON* const object,
+    const char* const name,
+    const cJSON_bool case_sensitive) {
+    cJSON* current_element = NULL;
+
+    if((object == NULL) || (name == NULL)) {
+        return NULL;
+    }
+
+    current_element = object->child;
+    if(case_sensitive) {
+        while((current_element != NULL) && (current_element->string != NULL) &&
+              (strcmp(name, current_element->string) != 0)) {
+            current_element = current_element->next;
+        }
+    } else {
+        while((current_element != NULL) &&
+              (case_insensitive_strcmp(
+                   (const unsigned char*)name, (const unsigned char*)(current_element->string)) !=
+               0)) {
+            current_element = current_element->next;
+        }
+    }
+
+    if((current_element == NULL) || (current_element->string == NULL)) {
+        return NULL;
+    }
+
+    return current_element;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_GetObjectItem(const cJSON* const object, const char* const string) {
+    return get_object_item(object, string, false);
+}
+
+CJSON_PUBLIC(cJSON*)
+cJSON_GetObjectItemCaseSensitive(const cJSON* const object, const char* const string) {
+    return get_object_item(object, string, true);
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON* object, const char* string) {
+    return cJSON_GetObjectItem(object, string) ? 1 : 0;
+}
+
+/* Utility for array list handling. */
+static void suffix_object(cJSON* prev, cJSON* item) {
+    prev->next = item;
+    item->prev = prev;
+}
+
+/* Utility for handling references. */
+static cJSON* create_reference(const cJSON* item, const internal_hooks* const hooks) {
+    cJSON* reference = NULL;
+    if(item == NULL) {
+        return NULL;
+    }
+
+    reference = cJSON_New_Item(hooks);
+    if(reference == NULL) {
+        return NULL;
+    }
+
+    memcpy(reference, item, sizeof(cJSON));
+    reference->string = NULL;
+    reference->type |= cJSON_IsReference;
+    reference->next = reference->prev = NULL;
+    return reference;
+}
+
+static cJSON_bool add_item_to_array(cJSON* array, cJSON* item) {
+    cJSON* child = NULL;
+
+    if((item == NULL) || (array == NULL) || (array == item)) {
+        return false;
+    }
+
+    child = array->child;
+    /*
+     * To find the last item in array quickly, we use prev in array
+     */
+    if(child == NULL) {
+        /* list is empty, start new one */
+        array->child = item;
+        item->prev = item;
+        item->next = NULL;
+    } else {
+        /* append to the end */
+        if(child->prev) {
+            suffix_object(child->prev, item);
+            array->child->prev = item;
+        }
+    }
+
+    return true;
+}
+
+/* Add item to array/object. */
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON* array, cJSON* item) {
+    return add_item_to_array(array, item);
+}
+
+#if defined(__clang__) || \
+    (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5))))
+#pragma GCC diagnostic push
+#endif
+#ifdef __GNUC__
+#pragma GCC diagnostic ignored "-Wcast-qual"
+#endif
+/* helper function to cast away const */
+static void* cast_away_const(const void* string) {
+    return (void*)string;
+}
+#if defined(__clang__) || \
+    (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5))))
+#pragma GCC diagnostic pop
+#endif
+
+static cJSON_bool add_item_to_object(
+    cJSON* const object,
+    const char* const string,
+    cJSON* const item,
+    const internal_hooks* const hooks,
+    const cJSON_bool constant_key) {
+    char* new_key = NULL;
+    int new_type = cJSON_Invalid;
+
+    if((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) {
+        return false;
+    }
+
+    if(constant_key) {
+        new_key = (char*)cast_away_const(string);
+        new_type = item->type | cJSON_StringIsConst;
+    } else {
+        new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks);
+        if(new_key == NULL) {
+            return false;
+        }
+
+        new_type = item->type & ~cJSON_StringIsConst;
+    }
+
+    if(!(item->type & cJSON_StringIsConst) && (item->string != NULL)) {
+        hooks->deallocate(item->string);
+    }
+
+    item->string = new_key;
+    item->type = new_type;
+
+    return add_item_to_array(object, item);
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON* object, const char* string, cJSON* item) {
+    return add_item_to_object(object, string, item, &global_hooks, false);
+}
+
+/* Add an item to an object with constant string as key */
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON* object, const char* string, cJSON* item) {
+    return add_item_to_object(object, string, item, &global_hooks, true);
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON* array, cJSON* item) {
+    if(array == NULL) {
+        return false;
+    }
+
+    return add_item_to_array(array, create_reference(item, &global_hooks));
+}
+
+CJSON_PUBLIC(cJSON_bool)
+cJSON_AddItemReferenceToObject(cJSON* object, const char* string, cJSON* item) {
+    if((object == NULL) || (string == NULL)) {
+        return false;
+    }
+
+    return add_item_to_object(
+        object, string, create_reference(item, &global_hooks), &global_hooks, false);
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON* const object, const char* const name) {
+    cJSON* null = cJSON_CreateNull();
+    if(add_item_to_object(object, name, null, &global_hooks, false)) {
+        return null;
+    }
+
+    cJSON_Delete(null);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON* const object, const char* const name) {
+    cJSON* true_item = cJSON_CreateTrue();
+    if(add_item_to_object(object, name, true_item, &global_hooks, false)) {
+        return true_item;
+    }
+
+    cJSON_Delete(true_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON* const object, const char* const name) {
+    cJSON* false_item = cJSON_CreateFalse();
+    if(add_item_to_object(object, name, false_item, &global_hooks, false)) {
+        return false_item;
+    }
+
+    cJSON_Delete(false_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*)
+cJSON_AddBoolToObject(cJSON* const object, const char* const name, const cJSON_bool boolean) {
+    cJSON* bool_item = cJSON_CreateBool(boolean);
+    if(add_item_to_object(object, name, bool_item, &global_hooks, false)) {
+        return bool_item;
+    }
+
+    cJSON_Delete(bool_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*)
+cJSON_AddNumberToObject(cJSON* const object, const char* const name, const double number) {
+    cJSON* number_item = cJSON_CreateNumber(number);
+    if(add_item_to_object(object, name, number_item, &global_hooks, false)) {
+        return number_item;
+    }
+
+    cJSON_Delete(number_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*)
+cJSON_AddStringToObject(cJSON* const object, const char* const name, const char* const string) {
+    cJSON* string_item = cJSON_CreateString(string);
+    if(add_item_to_object(object, name, string_item, &global_hooks, false)) {
+        return string_item;
+    }
+
+    cJSON_Delete(string_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*)
+cJSON_AddRawToObject(cJSON* const object, const char* const name, const char* const raw) {
+    cJSON* raw_item = cJSON_CreateRaw(raw);
+    if(add_item_to_object(object, name, raw_item, &global_hooks, false)) {
+        return raw_item;
+    }
+
+    cJSON_Delete(raw_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON* const object, const char* const name) {
+    cJSON* object_item = cJSON_CreateObject();
+    if(add_item_to_object(object, name, object_item, &global_hooks, false)) {
+        return object_item;
+    }
+
+    cJSON_Delete(object_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON* const object, const char* const name) {
+    cJSON* array = cJSON_CreateArray();
+    if(add_item_to_object(object, name, array, &global_hooks, false)) {
+        return array;
+    }
+
+    cJSON_Delete(array);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_DetachItemViaPointer(cJSON* parent, cJSON* const item) {
+    if((parent == NULL) || (item == NULL)) {
+        return NULL;
+    }
+
+    if(item != parent->child) {
+        /* not the first element */
+        item->prev->next = item->next;
+    }
+    if(item->next != NULL) {
+        /* not the last element */
+        item->next->prev = item->prev;
+    }
+
+    if(item == parent->child) {
+        /* first element */
+        parent->child = item->next;
+    } else if(item->next == NULL) {
+        /* last element */
+        parent->child->prev = item->prev;
+    }
+
+    /* make sure the detached item doesn't point anywhere anymore */
+    item->prev = NULL;
+    item->next = NULL;
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_DetachItemFromArray(cJSON* array, int which) {
+    if(which < 0) {
+        return NULL;
+    }
+
+    return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which));
+}
+
+CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON* array, int which) {
+    cJSON_Delete(cJSON_DetachItemFromArray(array, which));
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_DetachItemFromObject(cJSON* object, const char* string) {
+    cJSON* to_detach = cJSON_GetObjectItem(object, string);
+
+    return cJSON_DetachItemViaPointer(object, to_detach);
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_DetachItemFromObjectCaseSensitive(cJSON* object, const char* string) {
+    cJSON* to_detach = cJSON_GetObjectItemCaseSensitive(object, string);
+
+    return cJSON_DetachItemViaPointer(object, to_detach);
+}
+
+CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON* object, const char* string) {
+    cJSON_Delete(cJSON_DetachItemFromObject(object, string));
+}
+
+CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON* object, const char* string) {
+    cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string));
+}
+
+/* Replace array/object items with new ones. */
+CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON* array, int which, cJSON* newitem) {
+    cJSON* after_inserted = NULL;
+
+    if(which < 0) {
+        return false;
+    }
+
+    after_inserted = get_array_item(array, (size_t)which);
+    if(after_inserted == NULL) {
+        return add_item_to_array(array, newitem);
+    }
+
+    newitem->next = after_inserted;
+    newitem->prev = after_inserted->prev;
+    after_inserted->prev = newitem;
+    if(after_inserted == array->child) {
+        array->child = newitem;
+    } else {
+        newitem->prev->next = newitem;
+    }
+    return true;
+}
+
+CJSON_PUBLIC(cJSON_bool)
+cJSON_ReplaceItemViaPointer(cJSON* const parent, cJSON* const item, cJSON* replacement) {
+    if((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL)) {
+        return false;
+    }
+
+    if(replacement == item) {
+        return true;
+    }
+
+    replacement->next = item->next;
+    replacement->prev = item->prev;
+
+    if(replacement->next != NULL) {
+        replacement->next->prev = replacement;
+    }
+    if(parent->child == item) {
+        if(parent->child->prev == parent->child) {
+            replacement->prev = replacement;
+        }
+        parent->child = replacement;
+    } else { /*
+         * To find the last item in array quickly, we use prev in array.
+         * We can't modify the last item's next pointer where this item was the parent's child
+         */
+        if(replacement->prev != NULL) {
+            replacement->prev->next = replacement;
+        }
+        if(replacement->next == NULL) {
+            parent->child->prev = replacement;
+        }
+    }
+
+    item->next = NULL;
+    item->prev = NULL;
+    cJSON_Delete(item);
+
+    return true;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON* array, int which, cJSON* newitem) {
+    if(which < 0) {
+        return false;
+    }
+
+    return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem);
+}
+
+static cJSON_bool replace_item_in_object(
+    cJSON* object,
+    const char* string,
+    cJSON* replacement,
+    cJSON_bool case_sensitive) {
+    if((replacement == NULL) || (string == NULL)) {
+        return false;
+    }
+
+    /* replace the name in the replacement */
+    if(!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) {
+        cJSON_free(replacement->string);
+    }
+    replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks);
+    if(replacement->string == NULL) {
+        return false;
+    }
+
+    replacement->type &= ~cJSON_StringIsConst;
+
+    return cJSON_ReplaceItemViaPointer(
+        object, get_object_item(object, string, case_sensitive), replacement);
+}
+
+CJSON_PUBLIC(cJSON_bool)
+cJSON_ReplaceItemInObject(cJSON* object, const char* string, cJSON* newitem) {
+    return replace_item_in_object(object, string, newitem, false);
+}
+
+CJSON_PUBLIC(cJSON_bool)
+cJSON_ReplaceItemInObjectCaseSensitive(cJSON* object, const char* string, cJSON* newitem) {
+    return replace_item_in_object(object, string, newitem, true);
+}
+
+/* Create basic types: */
+CJSON_PUBLIC(cJSON*) cJSON_CreateNull(void) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item) {
+        item->type = cJSON_NULL;
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateTrue(void) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item) {
+        item->type = cJSON_True;
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateFalse(void) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item) {
+        item->type = cJSON_False;
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateBool(cJSON_bool boolean) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item) {
+        item->type = boolean ? cJSON_True : cJSON_False;
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateNumber(double num) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item) {
+        item->type = cJSON_Number;
+        item->valuedouble = num;
+
+        /* use saturation in case of overflow */
+        if(num >= INT_MAX) {
+            item->valueint = INT_MAX;
+        } else if(num <= (double)INT_MIN) {
+            item->valueint = INT_MIN;
+        } else {
+            item->valueint = (int)num;
+        }
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateString(const char* string) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item) {
+        item->type = cJSON_String;
+        item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks);
+        if(!item->valuestring) {
+            cJSON_Delete(item);
+            return NULL;
+        }
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateStringReference(const char* string) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item != NULL) {
+        item->type = cJSON_String | cJSON_IsReference;
+        item->valuestring = (char*)cast_away_const(string);
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateObjectReference(const cJSON* child) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item != NULL) {
+        item->type = cJSON_Object | cJSON_IsReference;
+        item->child = (cJSON*)cast_away_const(child);
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateArrayReference(const cJSON* child) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item != NULL) {
+        item->type = cJSON_Array | cJSON_IsReference;
+        item->child = (cJSON*)cast_away_const(child);
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateRaw(const char* raw) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item) {
+        item->type = cJSON_Raw;
+        item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks);
+        if(!item->valuestring) {
+            cJSON_Delete(item);
+            return NULL;
+        }
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateArray(void) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item) {
+        item->type = cJSON_Array;
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateObject(void) {
+    cJSON* item = cJSON_New_Item(&global_hooks);
+    if(item) {
+        item->type = cJSON_Object;
+    }
+
+    return item;
+}
+
+/* Create Arrays: */
+CJSON_PUBLIC(cJSON*) cJSON_CreateIntArray(const int* numbers, int count) {
+    size_t i = 0;
+    cJSON* n = NULL;
+    cJSON* p = NULL;
+    cJSON* a = NULL;
+
+    if((count < 0) || (numbers == NULL)) {
+        return NULL;
+    }
+
+    a = cJSON_CreateArray();
+
+    for(i = 0; a && (i < (size_t)count); i++) {
+        n = cJSON_CreateNumber(numbers[i]);
+        if(!n) {
+            cJSON_Delete(a);
+            return NULL;
+        }
+        if(!i) {
+            a->child = n;
+        } else {
+            suffix_object(p, n);
+        }
+        p = n;
+    }
+
+    if(a && a->child) {
+        a->child->prev = n;
+    }
+
+    return a;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateFloatArray(const float* numbers, int count) {
+    size_t i = 0;
+    cJSON* n = NULL;
+    cJSON* p = NULL;
+    cJSON* a = NULL;
+
+    if((count < 0) || (numbers == NULL)) {
+        return NULL;
+    }
+
+    a = cJSON_CreateArray();
+
+    for(i = 0; a && (i < (size_t)count); i++) {
+        n = cJSON_CreateNumber((double)numbers[i]);
+        if(!n) {
+            cJSON_Delete(a);
+            return NULL;
+        }
+        if(!i) {
+            a->child = n;
+        } else {
+            suffix_object(p, n);
+        }
+        p = n;
+    }
+
+    if(a && a->child) {
+        a->child->prev = n;
+    }
+
+    return a;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateDoubleArray(const double* numbers, int count) {
+    size_t i = 0;
+    cJSON* n = NULL;
+    cJSON* p = NULL;
+    cJSON* a = NULL;
+
+    if((count < 0) || (numbers == NULL)) {
+        return NULL;
+    }
+
+    a = cJSON_CreateArray();
+
+    for(i = 0; a && (i < (size_t)count); i++) {
+        n = cJSON_CreateNumber(numbers[i]);
+        if(!n) {
+            cJSON_Delete(a);
+            return NULL;
+        }
+        if(!i) {
+            a->child = n;
+        } else {
+            suffix_object(p, n);
+        }
+        p = n;
+    }
+
+    if(a && a->child) {
+        a->child->prev = n;
+    }
+
+    return a;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_CreateStringArray(const char* const* strings, int count) {
+    size_t i = 0;
+    cJSON* n = NULL;
+    cJSON* p = NULL;
+    cJSON* a = NULL;
+
+    if((count < 0) || (strings == NULL)) {
+        return NULL;
+    }
+
+    a = cJSON_CreateArray();
+
+    for(i = 0; a && (i < (size_t)count); i++) {
+        n = cJSON_CreateString(strings[i]);
+        if(!n) {
+            cJSON_Delete(a);
+            return NULL;
+        }
+        if(!i) {
+            a->child = n;
+        } else {
+            suffix_object(p, n);
+        }
+        p = n;
+    }
+
+    if(a && a->child) {
+        a->child->prev = n;
+    }
+
+    return a;
+}
+
+/* Duplication */
+CJSON_PUBLIC(cJSON*) cJSON_Duplicate(const cJSON* item, cJSON_bool recurse) {
+    cJSON* newitem = NULL;
+    cJSON* child = NULL;
+    cJSON* next = NULL;
+    cJSON* newchild = NULL;
+
+    /* Bail on bad ptr */
+    if(!item) {
+        goto fail;
+    }
+    /* Create new item */
+    newitem = cJSON_New_Item(&global_hooks);
+    if(!newitem) {
+        goto fail;
+    }
+    /* Copy over all vars */
+    newitem->type = item->type & (~cJSON_IsReference);
+    newitem->valueint = item->valueint;
+    newitem->valuedouble = item->valuedouble;
+    if(item->valuestring) {
+        newitem->valuestring =
+            (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks);
+        if(!newitem->valuestring) {
+            goto fail;
+        }
+    }
+    if(item->string) {
+        newitem->string = (item->type & cJSON_StringIsConst) ?
+                              item->string :
+                              (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks);
+        if(!newitem->string) {
+            goto fail;
+        }
+    }
+    /* If non-recursive, then we're done! */
+    if(!recurse) {
+        return newitem;
+    }
+    /* Walk the ->next chain for the child. */
+    child = item->child;
+    while(child != NULL) {
+        newchild = cJSON_Duplicate(
+            child, true); /* Duplicate (with recurse) each item in the ->next chain */
+        if(!newchild) {
+            goto fail;
+        }
+        if(next != NULL) {
+            /* If newitem->child already set, then crosswire ->prev and ->next and move on */
+            next->next = newchild;
+            newchild->prev = next;
+            next = newchild;
+        } else {
+            /* Set newitem->child and move to it */
+            newitem->child = newchild;
+            next = newchild;
+        }
+        child = child->next;
+    }
+    if(newitem && newitem->child) {
+        newitem->child->prev = newchild;
+    }
+
+    return newitem;
+
+fail:
+    if(newitem != NULL) {
+        cJSON_Delete(newitem);
+    }
+
+    return NULL;
+}
+
+static void skip_oneline_comment(char** input) {
+    *input += static_strlen("//");
+
+    for(; (*input)[0] != '\0'; ++(*input)) {
+        if((*input)[0] == '\n') {
+            *input += static_strlen("\n");
+            return;
+        }
+    }
+}
+
+static void skip_multiline_comment(char** input) {
+    *input += static_strlen("/*");
+
+    for(; (*input)[0] != '\0'; ++(*input)) {
+        if(((*input)[0] == '*') && ((*input)[1] == '/')) {
+            *input += static_strlen("*/");
+            return;
+        }
+    }
+}
+
+static void minify_string(char** input, char** output) {
+    (*output)[0] = (*input)[0];
+    *input += static_strlen("\"");
+    *output += static_strlen("\"");
+
+    for(; (*input)[0] != '\0'; (void)++(*input), ++(*output)) {
+        (*output)[0] = (*input)[0];
+
+        if((*input)[0] == '\"') {
+            (*output)[0] = '\"';
+            *input += static_strlen("\"");
+            *output += static_strlen("\"");
+            return;
+        } else if(((*input)[0] == '\\') && ((*input)[1] == '\"')) {
+            (*output)[1] = (*input)[1];
+            *input += static_strlen("\"");
+            *output += static_strlen("\"");
+        }
+    }
+}
+
+CJSON_PUBLIC(void) cJSON_Minify(char* json) {
+    char* into = json;
+
+    if(json == NULL) {
+        return;
+    }
+
+    while(json[0] != '\0') {
+        switch(json[0]) {
+        case ' ':
+        case '\t':
+        case '\r':
+        case '\n':
+            json++;
+            break;
+
+        case '/':
+            if(json[1] == '/') {
+                skip_oneline_comment(&json);
+            } else if(json[1] == '*') {
+                skip_multiline_comment(&json);
+            } else {
+                json++;
+            }
+            break;
+
+        case '\"':
+            minify_string(&json, (char**)&into);
+            break;
+
+        default:
+            into[0] = json[0];
+            json++;
+            into++;
+        }
+    }
+
+    /* and null-terminate. */
+    *into = '\0';
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON* const item) {
+    if(item == NULL) {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_Invalid;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON* const item) {
+    if(item == NULL) {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_False;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON* const item) {
+    if(item == NULL) {
+        return false;
+    }
+
+    return (item->type & 0xff) == cJSON_True;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON* const item) {
+    if(item == NULL) {
+        return false;
+    }
+
+    return (item->type & (cJSON_True | cJSON_False)) != 0;
+}
+CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON* const item) {
+    if(item == NULL) {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_NULL;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON* const item) {
+    if(item == NULL) {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_Number;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON* const item) {
+    if(item == NULL) {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_String;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON* const item) {
+    if(item == NULL) {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_Array;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON* const item) {
+    if(item == NULL) {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_Object;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON* const item) {
+    if(item == NULL) {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_Raw;
+}
+
+CJSON_PUBLIC(cJSON_bool)
+cJSON_Compare(const cJSON* const a, const cJSON* const b, const cJSON_bool case_sensitive) {
+    if((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) {
+        return false;
+    }
+
+    /* check if type is valid */
+    switch(a->type & 0xFF) {
+    case cJSON_False:
+    case cJSON_True:
+    case cJSON_NULL:
+    case cJSON_Number:
+    case cJSON_String:
+    case cJSON_Raw:
+    case cJSON_Array:
+    case cJSON_Object:
+        break;
+
+    default:
+        return false;
+    }
+
+    /* identical objects are equal */
+    if(a == b) {
+        return true;
+    }
+
+    switch(a->type & 0xFF) {
+    /* in these cases and equal type is enough */
+    case cJSON_False:
+    case cJSON_True:
+    case cJSON_NULL:
+        return true;
+
+    case cJSON_Number:
+        if(compare_double(a->valuedouble, b->valuedouble)) {
+            return true;
+        }
+        return false;
+
+    case cJSON_String:
+    case cJSON_Raw:
+        if((a->valuestring == NULL) || (b->valuestring == NULL)) {
+            return false;
+        }
+        if(strcmp(a->valuestring, b->valuestring) == 0) {
+            return true;
+        }
+
+        return false;
+
+    case cJSON_Array: {
+        cJSON* a_element = a->child;
+        cJSON* b_element = b->child;
+
+        for(; (a_element != NULL) && (b_element != NULL);) {
+            if(!cJSON_Compare(a_element, b_element, case_sensitive)) {
+                return false;
+            }
+
+            a_element = a_element->next;
+            b_element = b_element->next;
+        }
+
+        /* one of the arrays is longer than the other */
+        if(a_element != b_element) {
+            return false;
+        }
+
+        return true;
+    }
+
+    case cJSON_Object: {
+        cJSON* a_element = NULL;
+        cJSON* b_element = NULL;
+        cJSON_ArrayForEach(a_element, a) {
+            /* TODO This has O(n^2) runtime, which is horrible! */
+            b_element = get_object_item(b, a_element->string, case_sensitive);
+            if(b_element == NULL) {
+                return false;
+            }
+
+            if(!cJSON_Compare(a_element, b_element, case_sensitive)) {
+                return false;
+            }
+        }
+
+        /* doing this twice, once on a and b to prevent true comparison if a subset of b
+             * TODO: Do this the proper way, this is just a fix for now */
+        cJSON_ArrayForEach(b_element, b) {
+            a_element = get_object_item(a, b_element->string, case_sensitive);
+            if(a_element == NULL) {
+                return false;
+            }
+
+            if(!cJSON_Compare(b_element, a_element, case_sensitive)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    default:
+        return false;
+    }
+}
+
+CJSON_PUBLIC(void*) cJSON_malloc(size_t size) {
+    return global_hooks.allocate(size);
+}
+
+CJSON_PUBLIC(void) cJSON_free(void* object) {
+    global_hooks.deallocate(object);
+}

+ 328 - 0
malveke_gb_cartridge/helpers/cJSON.h

@@ -0,0 +1,328 @@
+/*
+  Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+*/
+
+#ifndef cJSON__h
+#define cJSON__h
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if !defined(__WINDOWS__) && \
+    (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
+#define __WINDOWS__
+#endif
+
+#ifdef __WINDOWS__
+
+/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention.  For windows you have 3 define options:
+
+CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
+CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
+CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
+
+For *nix builds that support visibility attribute, you can define similar behavior by
+
+setting default visibility to hidden by adding
+-fvisibility=hidden (for gcc)
+or
+-xldscope=hidden (for sun cc)
+to CFLAGS
+
+then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
+
+*/
+
+#define CJSON_CDECL __cdecl
+#define CJSON_STDCALL __stdcall
+
+/* export symbols by default, this is necessary for copy pasting the C and header file */
+#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && \
+    !defined(CJSON_EXPORT_SYMBOLS)
+#define CJSON_EXPORT_SYMBOLS
+#endif
+
+#if defined(CJSON_HIDE_SYMBOLS)
+#define CJSON_PUBLIC(type) type CJSON_STDCALL
+#elif defined(CJSON_EXPORT_SYMBOLS)
+#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
+#elif defined(CJSON_IMPORT_SYMBOLS)
+#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
+#endif
+#else /* !__WINDOWS__ */
+#define CJSON_CDECL
+#define CJSON_STDCALL
+
+#if(defined(__GNUC__) || defined(__SUNPRO_CC) || defined(__SUNPRO_C)) && \
+    defined(CJSON_API_VISIBILITY)
+#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
+#else
+#define CJSON_PUBLIC(type) type
+#endif
+#endif
+
+/* project version */
+#define CJSON_VERSION_MAJOR 1
+#define CJSON_VERSION_MINOR 7
+#define CJSON_VERSION_PATCH 16
+
+#include <stddef.h>
+
+/* cJSON Types: */
+#define cJSON_Invalid (0)
+#define cJSON_False (1 << 0)
+#define cJSON_True (1 << 1)
+#define cJSON_NULL (1 << 2)
+#define cJSON_Number (1 << 3)
+#define cJSON_String (1 << 4)
+#define cJSON_Array (1 << 5)
+#define cJSON_Object (1 << 6)
+#define cJSON_Raw (1 << 7) /* raw json */
+
+#define cJSON_IsReference 256
+#define cJSON_StringIsConst 512
+
+/* The cJSON structure: */
+typedef struct cJSON {
+    /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
+    struct cJSON* next;
+    struct cJSON* prev;
+    /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
+    struct cJSON* child;
+
+    /* The type of the item, as above. */
+    int type;
+
+    /* The item's string, if type==cJSON_String  and type == cJSON_Raw */
+    char* valuestring;
+    /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
+    int valueint;
+    /* The item's number, if type==cJSON_Number */
+    double valuedouble;
+
+    /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
+    char* string;
+} cJSON;
+
+typedef struct cJSON_Hooks {
+    /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
+    void*(CJSON_CDECL* malloc_fn)(size_t sz);
+    void(CJSON_CDECL* free_fn)(void* ptr);
+} cJSON_Hooks;
+
+typedef int cJSON_bool;
+
+/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
+ * This is to prevent stack overflows. */
+#ifndef CJSON_NESTING_LIMIT
+#define CJSON_NESTING_LIMIT 1000
+#endif
+
+/* returns the version of cJSON as a string */
+CJSON_PUBLIC(const char*) cJSON_Version(void);
+
+/* Supply malloc, realloc and free functions to cJSON */
+CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
+
+/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
+/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
+CJSON_PUBLIC(cJSON*) cJSON_Parse(const char* value);
+CJSON_PUBLIC(cJSON*) cJSON_ParseWithLength(const char* value, size_t buffer_length);
+/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
+/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
+CJSON_PUBLIC(cJSON*)
+cJSON_ParseWithOpts(
+    const char* value,
+    const char** return_parse_end,
+    cJSON_bool require_null_terminated);
+CJSON_PUBLIC(cJSON*)
+cJSON_ParseWithLengthOpts(
+    const char* value,
+    size_t buffer_length,
+    const char** return_parse_end,
+    cJSON_bool require_null_terminated);
+
+/* Render a cJSON entity to text for transfer/storage. */
+CJSON_PUBLIC(char*) cJSON_Print(const cJSON* item);
+/* Render a cJSON entity to text for transfer/storage without any formatting. */
+CJSON_PUBLIC(char*) cJSON_PrintUnformatted(const cJSON* item);
+/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
+CJSON_PUBLIC(char*) cJSON_PrintBuffered(const cJSON* item, int prebuffer, cJSON_bool fmt);
+/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
+/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
+CJSON_PUBLIC(cJSON_bool)
+cJSON_PrintPreallocated(cJSON* item, char* buffer, const int length, const cJSON_bool format);
+/* Delete a cJSON entity and all subentities. */
+CJSON_PUBLIC(void) cJSON_Delete(cJSON* item);
+
+/* Returns the number of items in an array (or object). */
+CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON* array);
+/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
+CJSON_PUBLIC(cJSON*) cJSON_GetArrayItem(const cJSON* array, int index);
+/* Get item "string" from object. Case insensitive. */
+CJSON_PUBLIC(cJSON*) cJSON_GetObjectItem(const cJSON* const object, const char* const string);
+CJSON_PUBLIC(cJSON*)
+cJSON_GetObjectItemCaseSensitive(const cJSON* const object, const char* const string);
+CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON* object, const char* string);
+/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
+CJSON_PUBLIC(const char*) cJSON_GetErrorPtr(void);
+
+/* Check item type and return its value */
+CJSON_PUBLIC(char*) cJSON_GetStringValue(const cJSON* const item);
+CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON* const item);
+
+/* These functions check the type of an item */
+CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON* const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON* const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON* const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON* const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON* const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON* const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON* const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON* const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON* const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON* const item);
+
+/* These calls create a cJSON item of the appropriate type. */
+CJSON_PUBLIC(cJSON*) cJSON_CreateNull(void);
+CJSON_PUBLIC(cJSON*) cJSON_CreateTrue(void);
+CJSON_PUBLIC(cJSON*) cJSON_CreateFalse(void);
+CJSON_PUBLIC(cJSON*) cJSON_CreateBool(cJSON_bool boolean);
+CJSON_PUBLIC(cJSON*) cJSON_CreateNumber(double num);
+CJSON_PUBLIC(cJSON*) cJSON_CreateString(const char* string);
+/* raw json */
+CJSON_PUBLIC(cJSON*) cJSON_CreateRaw(const char* raw);
+CJSON_PUBLIC(cJSON*) cJSON_CreateArray(void);
+CJSON_PUBLIC(cJSON*) cJSON_CreateObject(void);
+
+/* Create a string where valuestring references a string so
+ * it will not be freed by cJSON_Delete */
+CJSON_PUBLIC(cJSON*) cJSON_CreateStringReference(const char* string);
+/* Create an object/array that only references it's elements so
+ * they will not be freed by cJSON_Delete */
+CJSON_PUBLIC(cJSON*) cJSON_CreateObjectReference(const cJSON* child);
+CJSON_PUBLIC(cJSON*) cJSON_CreateArrayReference(const cJSON* child);
+
+/* These utilities create an Array of count items.
+ * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
+CJSON_PUBLIC(cJSON*) cJSON_CreateIntArray(const int* numbers, int count);
+CJSON_PUBLIC(cJSON*) cJSON_CreateFloatArray(const float* numbers, int count);
+CJSON_PUBLIC(cJSON*) cJSON_CreateDoubleArray(const double* numbers, int count);
+CJSON_PUBLIC(cJSON*) cJSON_CreateStringArray(const char* const* strings, int count);
+
+/* Append item to the specified array/object. */
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON* array, cJSON* item);
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON* object, const char* string, cJSON* item);
+/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
+ * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
+ * writing to `item->string` */
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON* object, const char* string, cJSON* item);
+/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON* array, cJSON* item);
+CJSON_PUBLIC(cJSON_bool)
+cJSON_AddItemReferenceToObject(cJSON* object, const char* string, cJSON* item);
+
+/* Remove/Detach items from Arrays/Objects. */
+CJSON_PUBLIC(cJSON*) cJSON_DetachItemViaPointer(cJSON* parent, cJSON* const item);
+CJSON_PUBLIC(cJSON*) cJSON_DetachItemFromArray(cJSON* array, int which);
+CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON* array, int which);
+CJSON_PUBLIC(cJSON*) cJSON_DetachItemFromObject(cJSON* object, const char* string);
+CJSON_PUBLIC(cJSON*) cJSON_DetachItemFromObjectCaseSensitive(cJSON* object, const char* string);
+CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON* object, const char* string);
+CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON* object, const char* string);
+
+/* Update array items. */
+CJSON_PUBLIC(cJSON_bool)
+cJSON_InsertItemInArray(
+    cJSON* array,
+    int which,
+    cJSON* newitem); /* Shifts pre-existing items to the right. */
+CJSON_PUBLIC(cJSON_bool)
+cJSON_ReplaceItemViaPointer(cJSON* const parent, cJSON* const item, cJSON* replacement);
+CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON* array, int which, cJSON* newitem);
+CJSON_PUBLIC(cJSON_bool)
+cJSON_ReplaceItemInObject(cJSON* object, const char* string, cJSON* newitem);
+CJSON_PUBLIC(cJSON_bool)
+cJSON_ReplaceItemInObjectCaseSensitive(cJSON* object, const char* string, cJSON* newitem);
+
+/* Duplicate a cJSON item */
+CJSON_PUBLIC(cJSON*) cJSON_Duplicate(const cJSON* item, cJSON_bool recurse);
+/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
+ * need to be released. With recurse!=0, it will duplicate any children connected to the item.
+ * The item->next and ->prev pointers are always zero on return from Duplicate. */
+/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
+ * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
+CJSON_PUBLIC(cJSON_bool)
+cJSON_Compare(const cJSON* const a, const cJSON* const b, const cJSON_bool case_sensitive);
+
+/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
+ * The input pointer json cannot point to a read-only address area, such as a string constant, 
+ * but should point to a readable and writable address area. */
+CJSON_PUBLIC(void) cJSON_Minify(char* json);
+
+/* Helper functions for creating and adding items to an object at the same time.
+ * They return the added item or NULL on failure. */
+CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON* const object, const char* const name);
+CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON* const object, const char* const name);
+CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON* const object, const char* const name);
+CJSON_PUBLIC(cJSON*)
+cJSON_AddBoolToObject(cJSON* const object, const char* const name, const cJSON_bool boolean);
+CJSON_PUBLIC(cJSON*)
+cJSON_AddNumberToObject(cJSON* const object, const char* const name, const double number);
+CJSON_PUBLIC(cJSON*)
+cJSON_AddStringToObject(cJSON* const object, const char* const name, const char* const string);
+CJSON_PUBLIC(cJSON*)
+cJSON_AddRawToObject(cJSON* const object, const char* const name, const char* const raw);
+CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON* const object, const char* const name);
+CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON* const object, const char* const name);
+
+/* When assigning an integer value, it needs to be propagated to valuedouble too. */
+#define cJSON_SetIntValue(object, number) \
+    ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
+/* helper for the cJSON_SetNumberValue macro */
+CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON* object, double number);
+#define cJSON_SetNumberValue(object, number) \
+    ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
+/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
+CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON* object, const char* valuestring);
+
+/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
+#define cJSON_SetBoolValue(object, boolValue)                                \
+    ((object != NULL && ((object)->type & (cJSON_False | cJSON_True))) ?     \
+         (object)->type = ((object)->type & (~(cJSON_False | cJSON_True))) | \
+                          ((boolValue) ? cJSON_True : cJSON_False) :         \
+         cJSON_Invalid)
+
+/* Macro for iterating over an array or object */
+#define cJSON_ArrayForEach(element, array)                                  \
+    for(element = (array != NULL) ? (array)->child : NULL; element != NULL; \
+        element = element->next)
+
+/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
+CJSON_PUBLIC(void*) cJSON_malloc(size_t size);
+CJSON_PUBLIC(void) cJSON_free(void* object);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif

+ 83 - 0
malveke_gb_cartridge/helpers/gb_cartridge_custom_event.h

@@ -0,0 +1,83 @@
+#pragma once
+
+typedef enum {
+    GBCartridgeCustomEventStartscreenUp,
+    GBCartridgeCustomEventStartscreenDown,
+    GBCartridgeCustomEventStartscreenLeft,
+    GBCartridgeCustomEventStartscreenRight,
+    GBCartridgeCustomEventStartscreenOk,
+    GBCartridgeCustomEventStartscreenBack,
+    GBCartridgeCustomEventScene1Up,
+    GBCartridgeCustomEventScene1Down,
+    GBCartridgeCustomEventScene1Left,
+    GBCartridgeCustomEventScene1Right,
+    GBCartridgeCustomEventScene1Ok,
+    GBCartridgeCustomEventScene1Back,
+
+    GBCartridgeCustomEventScene2Up,
+    GBCartridgeCustomEventScene2Down,
+    GBCartridgeCustomEventScene2Left,
+    GBCartridgeCustomEventScene2Right,
+    GBCartridgeCustomEventScene2Ok,
+    GBCartridgeCustomEventScene2Back,
+
+    GBCartridgeCustomEventScene3Up,
+    GBCartridgeCustomEventScene3Down,
+    GBCartridgeCustomEventScene3Left,
+    GBCartridgeCustomEventScene3Right,
+    GBCartridgeCustomEventScene3Ok,
+    GBCartridgeCustomEventScene3Back,
+
+    GBCartridgeCustomEventScene4Up,
+    GBCartridgeCustomEventScene4Down,
+    GBCartridgeCustomEventScene4Left,
+    GBCartridgeCustomEventScene4Right,
+    GBCartridgeCustomEventScene4Ok,
+    GBCartridgeCustomEventScene4Back,
+
+    GBCartridgeCustomEventScene5Up,
+    GBCartridgeCustomEventScene5Down,
+    GBCartridgeCustomEventScene5Left,
+    GBCartridgeCustomEventScene5Right,
+    GBCartridgeCustomEventScene5Ok,
+    GBCartridgeCustomEventScene5Back,
+} GBCartridgeCustomEvent;
+
+enum GBCartridgeCustomEventType {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    GBCartridgeCustomEventMenuVoid,
+    GBCartridgeCustomEventMenuSelected,
+};
+
+#pragma pack(push, 1)
+typedef union {
+    uint32_t packed_value;
+    struct {
+        uint16_t type;
+        int16_t value;
+    } content;
+} GBCartridgeCustomEventMenu;
+#pragma pack(pop)
+
+static inline uint32_t gb_cartridge_custom_menu_event_pack(uint16_t type, int16_t value) {
+    GBCartridgeCustomEventMenu event = {.content = {.type = type, .value = value}};
+    return event.packed_value;
+}
+static inline void
+    gb_cartridge_custom_menu_event_unpack(uint32_t packed_value, uint16_t* type, int16_t* value) {
+    GBCartridgeCustomEventMenu event = {.packed_value = packed_value};
+    if(type) *type = event.content.type;
+    if(value) *value = event.content.value;
+}
+
+static inline uint16_t gb_cartridge_custom_menu_event_get_type(uint32_t packed_value) {
+    uint16_t type;
+    gb_cartridge_custom_menu_event_unpack(packed_value, &type, NULL);
+    return type;
+}
+
+static inline int16_t gb_cartridge_custom_menu_event_get_value(uint32_t packed_value) {
+    int16_t value;
+    gb_cartridge_custom_menu_event_unpack(packed_value, NULL, &value);
+    return value;
+}

+ 26 - 0
malveke_gb_cartridge/helpers/gb_cartridge_speaker.c

@@ -0,0 +1,26 @@
+#include "gb_cartridge_speaker.h"
+#include "../gb_cartridge_app.h"
+
+#define NOTE_INPUT 587.33f
+
+void gb_cartridge_play_input_sound(void* context) {
+    GBCartridge* app = context;
+    if(app->speaker != 1) {
+        return;
+    }
+    float volume = 1.0f;
+    if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
+        furi_hal_speaker_start(NOTE_INPUT, volume);
+    }
+}
+
+void gb_cartridge_stop_all_sound(void* context) {
+    GBCartridge* app = context;
+    if(app->speaker != 1) {
+        return;
+    }
+    if(furi_hal_speaker_is_mine()) {
+        furi_hal_speaker_stop();
+        furi_hal_speaker_release();
+    }
+}

+ 4 - 0
malveke_gb_cartridge/helpers/gb_cartridge_speaker.h

@@ -0,0 +1,4 @@
+#define NOTE_INPUT 587.33f
+
+void gb_cartridge_play_input_sound(void* context);
+void gb_cartridge_stop_all_sound(void* context);

+ 46 - 0
malveke_gb_cartridge/helpers/sequential_file.c

@@ -0,0 +1,46 @@
+#include "sequential_file.h"
+
+char* sequential_file_resolve_path(
+    Storage* storage,
+    const char* dir,
+    const char* prefix,
+    const char* extension) {
+    if(storage == NULL || dir == NULL || prefix == NULL || extension == NULL) {
+        return NULL;
+    }
+
+    char file_path[256];
+    int file_index = 0;
+
+    do {
+        if(snprintf(
+               file_path, sizeof(file_path), "%s/%s_%d.%s", dir, prefix, file_index, extension) <
+           0) {
+            return NULL;
+        }
+        file_index++;
+    } while(storage_file_exists(storage, file_path));
+
+    return strdup(file_path);
+}
+
+bool sequential_file_open(
+    Storage* storage,
+    File* file,
+    const char* dir,
+    const char* prefix,
+    const char* extension) {
+    if(storage == NULL || file == NULL || dir == NULL || prefix == NULL || extension == NULL) {
+        return false;
+    }
+
+    char* file_path = sequential_file_resolve_path(storage, dir, prefix, extension);
+    if(file_path == NULL) {
+        return false;
+    }
+
+    bool success = storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS);
+    free(file_path);
+
+    return success;
+}

+ 15 - 0
malveke_gb_cartridge/helpers/sequential_file.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <storage/storage.h>
+
+char* sequential_file_resolve_path(
+    Storage* storage,
+    const char* dir,
+    const char* prefix,
+    const char* extension);
+bool sequential_file_open(
+    Storage* storage,
+    File* file,
+    const char* dir,
+    const char* prefix,
+    const char* extension);

BIN
malveke_gb_cartridge/icons/ArrowUpEmpty_14x15.png


BIN
malveke_gb_cartridge/icons/ArrowUpFilled_14x15.png


BIN
malveke_gb_cartridge/icons/Space_100x18.png


BIN
malveke_gb_cartridge/icons/Space_80x18.png


BIN
malveke_gb_cartridge/icons/Space_95x18.png


BIN
malveke_gb_cartridge/icons/_icon.png


BIN
malveke_gb_cartridge/icons/board.png


BIN
malveke_gb_cartridge/icons/boilerplate_10px.png


BIN
malveke_gb_cartridge/icons/cartridge.png


BIN
malveke_gb_cartridge/icons/cartridge_42x64.png


BIN
malveke_gb_cartridge/icons/game_boy.png


BIN
malveke_gb_cartridge/icons/icon.png


BIN
malveke_gb_cartridge/icons/red_16x15.png


+ 89 - 0
malveke_gb_cartridge/malveke_notifications.c

@@ -0,0 +1,89 @@
+#include "malveke_notifications.h"
+
+const NotificationMessage message_delay_note_325 = {
+    .type = NotificationMessageTypeDelay,
+    .data.delay.length = 325,
+};
+const NotificationMessage message_delay_note_108 = {
+    .type = NotificationMessageTypeDelay,
+    .data.delay.length = 108,
+};
+const NotificationMessage message_delay_note_216 = {
+    .type = NotificationMessageTypeDelay,
+    .data.delay.length = 216,
+};
+
+const NotificationMessage message_delay_note_1302 = {
+    .type = NotificationMessageTypeDelay,
+    .data.delay.length = 1302,
+};
+const NotificationMessage message_note_523 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 523.00f,
+    .data.sound.volume = 1.0f,
+};
+const NotificationMessage message_note_622 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 622.00f,
+    .data.sound.volume = 1.0f,
+};
+const NotificationMessage message_note_831 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 831.00f,
+    .data.sound.volume = 1.0f,
+};
+const NotificationMessage message_note_1047 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 1047.00f,
+    .data.sound.volume = 1.0f,
+};
+const NotificationMessage message_note_1109 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 1109.00f,
+    .data.sound.volume = 1.0f,
+};
+
+static const NotificationSequence sequence_get_item = {
+
+    &message_vibro_on,
+
+    &message_note_1047,
+    &message_delay_note_325,
+    &message_sound_off,
+
+    &message_vibro_off,
+
+    &message_note_1047,
+    &message_delay_note_108,
+    &message_sound_off,
+
+    &message_note_1047,
+    &message_delay_note_108,
+    &message_sound_off,
+
+    &message_note_1047,
+    &message_delay_note_108,
+    &message_sound_off,
+
+    &message_note_1109,
+    &message_delay_note_216,
+    &message_sound_off,
+
+    &message_note_1109,
+    &message_delay_note_216,
+    &message_sound_off,
+
+    &message_note_1109,
+    &message_delay_note_216,
+    &message_sound_off,
+
+    &message_note_1047,
+    &message_delay_note_1302,
+    &message_sound_off,
+
+    NULL,
+};
+
+void notification_success(NotificationApp* notification) {
+    notification_message(notification, &sequence_get_item);
+}

+ 20 - 0
malveke_gb_cartridge/malveke_notifications.h

@@ -0,0 +1,20 @@
+// malveke_notifications.h
+
+#ifndef MALVEKE_NOTIFICATIONS_H
+#define MALVEKE_NOTIFICATIONS_H
+
+#include <furi.h>
+#include <notification/notification_messages.h>
+
+// extern const NotificationMessage message_delay_note_325;
+// extern const NotificationMessage message_delay_note_108;
+// extern const NotificationMessage message_delay_note_216;
+// extern const NotificationMessage message_delay_note_1302;
+// extern const NotificationMessage message_note_523;
+// extern const NotificationMessage message_note_622;
+// extern const NotificationMessage message_note_831;
+// extern const NotificationMessage message_note_1047;
+// extern const NotificationMessage message_note_1109;
+
+void notification_success(NotificationApp* notification);
+#endif // MALVEKE_NOTIFICATIONS_H

+ 30 - 0
malveke_gb_cartridge/scenes/gb_cartridge_scene.c

@@ -0,0 +1,30 @@
+#include "gb_cartridge_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const gb_cartridge_on_enter_handlers[])(void*) = {
+#include "gb_cartridge_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 gb_cartridge_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "gb_cartridge_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 gb_cartridge_on_exit_handlers[])(void* context) = {
+#include "gb_cartridge_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers gb_cartridge_scene_handlers = {
+    .on_enter_handlers = gb_cartridge_on_enter_handlers,
+    .on_event_handlers = gb_cartridge_on_event_handlers,
+    .on_exit_handlers = gb_cartridge_on_exit_handlers,
+    .scene_num = GBCartridgeSceneNum,
+};

+ 29 - 0
malveke_gb_cartridge/scenes/gb_cartridge_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) GBCartridgeScene##id,
+typedef enum {
+#include "gb_cartridge_scene_config.h"
+    GBCartridgeSceneNum,
+} GBCartridgeScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers gb_cartridge_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "gb_cartridge_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 "gb_cartridge_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 "gb_cartridge_scene_config.h"
+#undef ADD_SCENE

+ 8 - 0
malveke_gb_cartridge/scenes/gb_cartridge_scene_config.h

@@ -0,0 +1,8 @@
+ADD_SCENE(gb_cartridge, startscreen, Startscreen)
+ADD_SCENE(gb_cartridge, menu, Menu)
+ADD_SCENE(gb_cartridge, scene_1, Scene_1)
+ADD_SCENE(gb_cartridge, scene_2, Scene_2)
+ADD_SCENE(gb_cartridge, scene_3, Scene_3)
+// ADD_SCENE(gb_cartridge, scene_4, Scene_4)
+ADD_SCENE(gb_cartridge, scene_5, Scene_5)
+ADD_SCENE(gb_cartridge, settings, Settings)

+ 130 - 0
malveke_gb_cartridge/scenes/gb_cartridge_scene_menu.c

@@ -0,0 +1,130 @@
+#include "../gb_cartridge_app.h"
+#include <lib/toolbox/value_index.h>
+#include <gui/elements.h>
+#include <ctype.h>
+
+enum SubmenuIndex {
+    SubmenuIndexScene1,
+    SubmenuIndexScene2,
+    SubmenuIndexScene3,
+    // SubmenuIndexScene4,
+    SubmenuIndexScene5,
+    SubmenuIndexSettings,
+};
+
+const char* const gameboy_rom_option_text[2] = {
+    "gb",
+    "gbc",
+};
+const uint32_t gameboy_rom_option_value[2] = {
+    GBCartridgeRomOptionGB,
+    GBCartridgeRomOptionGBC,
+};
+
+void toUpperCase(char* str) {
+    while(*str) {
+        *str = toupper((unsigned char)*str);
+        str++;
+    }
+}
+
+void gb_cartridge_scene_menu_submenu_callback(void* context, uint32_t index) {
+    GBCartridge* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+// static char* rom_option_uppercase(uint32_t index) {
+//     const char *gameboy_rom_option = gameboy_rom_option_text[index];
+//     char *gameboy_rom_option_uppercase = (char*)malloc(strlen(gameboy_rom_option) + 1); // +1 para el carácter nulo
+
+//     if (gameboy_rom_option_uppercase) {
+//         strcpy(gameboy_rom_option_uppercase, gameboy_rom_option);
+//         toUpperCase(gameboy_rom_option_uppercase);
+//     } else {
+//         // Manejo de error en caso de falta de memoria
+//         return NULL;
+//     }
+
+//     return gameboy_rom_option_uppercase;
+// }
+// static void gameboy_cartridge_set_rom_option(VariableItem* item) {
+//     GBCartridge* app = variable_item_get_context(item);
+//     uint8_t index = variable_item_get_current_value_index(item);
+//     variable_item_set_current_value_text(item, rom_option_uppercase(index));
+//     app->gameboy_rom_option_selected_index = gameboy_rom_option_value[index];
+//     app->gameboy_rom_option_selected_text = (char*)gameboy_rom_option_text[index];
+// }
+
+void gb_cartridge_scene_menu_on_enter(void* context) {
+    GBCartridge* app = context;
+
+    variable_item_list_add(app->submenu, "Cartridge Information", 1, NULL, NULL);
+    variable_item_list_add(app->submenu, "Dump ROM Cartridge", 1, NULL, NULL);
+    variable_item_list_add(app->submenu, "Dump RAM Cartridge", 1, NULL, NULL);
+
+    //  TODO: Implements Write ROM
+    // VariableItem* item = variable_item_list_add(
+    //     app->submenu,
+    //     "Write ROM",
+    //     2,
+    //     gameboy_cartridge_set_rom_option,
+    //     app);
+    // app->gameboy_rom_option_selected_index = value_index_uint32(app->gameboy_rom_option_selected_index, gameboy_rom_option_value, 2);
+    // variable_item_set_current_value_index(item, app->gameboy_rom_option_selected_index);
+    // variable_item_set_current_value_text(item, rom_option_uppercase(app->gameboy_rom_option_selected_index));
+
+    variable_item_list_add(app->submenu, "Write RAM", 1, NULL, NULL);
+    // variable_item_list_add(app->submenu, "Settings", 1, NULL, NULL);
+
+    variable_item_list_set_enter_callback(
+        app->submenu, gb_cartridge_scene_menu_submenu_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, GBCartridgeViewIdMenu);
+}
+
+bool gb_cartridge_scene_menu_on_event(void* context, SceneManagerEvent event) {
+    GBCartridge* app = context;
+    UNUSED(app);
+    if(event.type == SceneManagerEventTypeBack) {
+        //exit app
+        scene_manager_stop(app->scene_manager);
+        view_dispatcher_stop(app->view_dispatcher);
+        return true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexScene1) {
+            scene_manager_set_scene_state(
+                app->scene_manager, GBCartridgeSceneMenu, SubmenuIndexScene1);
+            scene_manager_next_scene(app->scene_manager, GBCartridgeSceneScene_1);
+            return true;
+        } else if(event.event == SubmenuIndexScene2) {
+            scene_manager_set_scene_state(
+                app->scene_manager, GBCartridgeSceneMenu, SubmenuIndexScene2);
+            scene_manager_next_scene(app->scene_manager, GBCartridgeSceneScene_2);
+            return true;
+        } else if(event.event == SubmenuIndexScene3) {
+            scene_manager_set_scene_state(
+                app->scene_manager, GBCartridgeSceneMenu, SubmenuIndexScene3);
+            scene_manager_next_scene(app->scene_manager, GBCartridgeSceneScene_3);
+            // } else if(event.event == SubmenuIndexScene4) {
+            //     scene_manager_set_scene_state(
+            //         app->scene_manager, GBCartridgeSceneMenu, SubmenuIndexScene4);
+            //     scene_manager_next_scene(app->scene_manager, GBCartridgeSceneScene_4);
+        } else if(event.event == SubmenuIndexScene5) {
+            scene_manager_set_scene_state(
+                app->scene_manager, GBCartridgeSceneMenu, SubmenuIndexScene5);
+            scene_manager_next_scene(app->scene_manager, GBCartridgeSceneScene_5);
+        } else if(event.event == SubmenuIndexSettings) {
+            scene_manager_set_scene_state(
+                app->scene_manager, GBCartridgeSceneMenu, SubmenuIndexSettings);
+            scene_manager_next_scene(app->scene_manager, GBCartridgeSceneSettings);
+            return true;
+        }
+    }
+    return false;
+}
+
+void gb_cartridge_scene_menu_on_exit(void* context) {
+    GBCartridge* app = context;
+    // submenu_reset(app->submenu);
+    variable_item_list_set_selected_item(app->submenu, 0);
+    variable_item_list_reset(app->submenu);
+}

+ 55 - 0
malveke_gb_cartridge/scenes/gb_cartridge_scene_scene_1.c

@@ -0,0 +1,55 @@
+#include "../gb_cartridge_app.h"
+#include "../helpers/gb_cartridge_custom_event.h"
+#include "../views/gb_cartridge_scene_1.h"
+
+void gb_cartridge_scene_1_callback(GBCartridgeCustomEvent event, void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void gb_cartridge_scene_scene_1_on_enter(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    gb_cartridge_scene_1_set_callback(
+        app->gb_cartridge_scene_1, gb_cartridge_scene_1_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, GBCartridgeViewIdScene1);
+}
+
+bool gb_cartridge_scene_scene_1_on_event(void* context, SceneManagerEvent event) {
+    GBCartridge* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case GBCartridgeCustomEventScene1Left:
+        case GBCartridgeCustomEventScene1Right:
+            break;
+        case GBCartridgeCustomEventScene1Up:
+        case GBCartridgeCustomEventScene1Down:
+            break;
+        case GBCartridgeCustomEventScene1Back:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, GBCartridgeSceneMenu)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void gb_cartridge_scene_scene_1_on_exit(void* context) {
+    GBCartridge* app = context;
+    // UNUSED(app);
+    // Unregister rx callback
+    uart_set_handle_rx_data_cb(app->uart, NULL);
+    uart_set_handle_rx_data_cb(app->lp_uart, NULL);
+}

+ 51 - 0
malveke_gb_cartridge/scenes/gb_cartridge_scene_scene_2.c

@@ -0,0 +1,51 @@
+#include "../gb_cartridge_app.h"
+#include "../helpers/gb_cartridge_custom_event.h"
+#include "../views/gb_cartridge_scene_2.h"
+
+void gb_cartridge_scene_2_callback(GBCartridgeCustomEvent event, void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void gb_cartridge_scene_scene_2_on_enter(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    gb_cartridge_scene_2_set_callback(
+        app->gb_cartridge_scene_2, gb_cartridge_scene_2_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, GBCartridgeViewIdScene2);
+}
+
+bool gb_cartridge_scene_scene_2_on_event(void* context, SceneManagerEvent event) {
+    GBCartridge* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case GBCartridgeCustomEventScene2Left:
+        case GBCartridgeCustomEventScene2Right:
+            break;
+        case GBCartridgeCustomEventScene2Up:
+        case GBCartridgeCustomEventScene2Down:
+            break;
+        case GBCartridgeCustomEventScene2Back:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, GBCartridgeSceneMenu)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void gb_cartridge_scene_scene_2_on_exit(void* context) {
+    GBCartridge* app = context;
+    UNUSED(app);
+}

+ 58 - 0
malveke_gb_cartridge/scenes/gb_cartridge_scene_scene_3.c

@@ -0,0 +1,58 @@
+#include "../gb_cartridge_app.h"
+#include "../helpers/gb_cartridge_custom_event.h"
+#include "../views/gb_cartridge_scene_3.h"
+
+void gb_cartridge_scene_3_callback(GBCartridgeCustomEvent event, void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void gb_cartridge_scene_scene_3_on_enter(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    gb_cartridge_scene_3_set_callback(
+        app->gb_cartridge_scene_3, gb_cartridge_scene_3_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, GBCartridgeViewIdScene3);
+}
+
+bool gb_cartridge_scene_scene_3_on_event(void* context, SceneManagerEvent event) {
+    GBCartridge* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case GBCartridgeCustomEventScene3Left:
+        case GBCartridgeCustomEventScene3Right:
+            break;
+        case GBCartridgeCustomEventScene3Up:
+        case GBCartridgeCustomEventScene3Down:
+            break;
+        case GBCartridgeCustomEventScene3Back:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, GBCartridgeSceneMenu)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void gb_cartridge_scene_scene_3_on_exit(void* context) {
+    GBCartridge* app = context;
+
+    //  with_view_model(
+    //     app->gb_cartridge_scene_3->view,
+    //     GameBoyCartridgeRAMBackupModel * model,
+    //     {
+
+    //     },false);
+    UNUSED(app);
+}

+ 51 - 0
malveke_gb_cartridge/scenes/gb_cartridge_scene_scene_4.c

@@ -0,0 +1,51 @@
+#include "../gb_cartridge_app.h"
+#include "../helpers/gb_cartridge_custom_event.h"
+#include "../views/gb_cartridge_scene_4.h"
+
+void gb_cartridge_scene_4_callback(GBCartridgeCustomEvent event, void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void gb_cartridge_scene_scene_4_on_enter(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    gb_cartridge_scene_4_set_callback(
+        app->gb_cartridge_scene_4, gb_cartridge_scene_4_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, GBCartridgeViewIdScene4);
+}
+
+bool gb_cartridge_scene_scene_4_on_event(void* context, SceneManagerEvent event) {
+    GBCartridge* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case GBCartridgeCustomEventScene4Left:
+        case GBCartridgeCustomEventScene4Right:
+            break;
+        case GBCartridgeCustomEventScene4Up:
+        case GBCartridgeCustomEventScene4Down:
+            break;
+        case GBCartridgeCustomEventScene4Back:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, GBCartridgeSceneMenu)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void gb_cartridge_scene_scene_4_on_exit(void* context) {
+    GBCartridge* app = context;
+    UNUSED(app);
+}

+ 51 - 0
malveke_gb_cartridge/scenes/gb_cartridge_scene_scene_5.c

@@ -0,0 +1,51 @@
+#include "../gb_cartridge_app.h"
+#include "../helpers/gb_cartridge_custom_event.h"
+#include "../views/gb_cartridge_scene_5.h"
+
+void gb_cartridge_scene_5_callback(GBCartridgeCustomEvent event, void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void gb_cartridge_scene_scene_5_on_enter(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    gb_cartridge_scene_5_set_callback(
+        app->gb_cartridge_scene_5, gb_cartridge_scene_5_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, GBCartridgeViewIdScene5);
+}
+
+bool gb_cartridge_scene_scene_5_on_event(void* context, SceneManagerEvent event) {
+    GBCartridge* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case GBCartridgeCustomEventScene5Left:
+        case GBCartridgeCustomEventScene5Right:
+            break;
+        case GBCartridgeCustomEventScene5Up:
+        case GBCartridgeCustomEventScene5Down:
+            break;
+        case GBCartridgeCustomEventScene5Back:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, GBCartridgeSceneMenu)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void gb_cartridge_scene_scene_5_on_exit(void* context) {
+    GBCartridge* app = context;
+    UNUSED(app);
+}

+ 133 - 0
malveke_gb_cartridge/scenes/gb_cartridge_scene_settings.c

@@ -0,0 +1,133 @@
+#include "../gb_cartridge_app.h"
+#include <lib/toolbox/value_index.h>
+
+enum SettingsIndex {
+    SettingsIndexHaptic = 10,
+    SettingsIndexValue1,
+    SettingsIndexValue2,
+};
+
+const char* const haptic_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t haptic_value[2] = {
+    GBCartridgeHapticOff,
+    GBCartridgeHapticOn,
+};
+
+const char* const speaker_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t speaker_value[2] = {
+    GBCartridgeSpeakerOff,
+    GBCartridgeSpeakerOn,
+};
+
+const char* const led_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t led_value[2] = {
+    GBCartridgeLedOff,
+    GBCartridgeLedOn,
+};
+
+const char* const settings_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t settings_value[2] = {
+    GBCartridgeSettingsOff,
+    GBCartridgeSettingsOn,
+};
+
+static void gb_cartridge_scene_settings_set_haptic(VariableItem* item) {
+    GBCartridge* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, haptic_text[index]);
+    app->haptic = haptic_value[index];
+}
+
+static void gb_cartridge_scene_settings_set_speaker(VariableItem* item) {
+    GBCartridge* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, speaker_text[index]);
+    app->speaker = speaker_value[index];
+}
+
+static void gb_cartridge_scene_settings_set_led(VariableItem* item) {
+    GBCartridge* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, led_text[index]);
+    app->led = led_value[index];
+}
+
+static void gb_cartridge_scene_settings_set_save_settings(VariableItem* item) {
+    GBCartridge* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, settings_text[index]);
+    app->save_settings = settings_value[index];
+}
+
+void gb_cartridge_scene_settings_submenu_callback(void* context, uint32_t index) {
+    GBCartridge* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void gb_cartridge_scene_settings_on_enter(void* context) {
+    GBCartridge* app = context;
+    VariableItem* item;
+    uint8_t value_index;
+
+    // Vibro on/off
+    item = variable_item_list_add(
+        app->variable_item_list, "Vibro/Haptic:", 2, gb_cartridge_scene_settings_set_haptic, app);
+    value_index = value_index_uint32(app->haptic, haptic_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, haptic_text[value_index]);
+
+    // Sound on/off
+    item = variable_item_list_add(
+        app->variable_item_list, "Sound:", 2, gb_cartridge_scene_settings_set_speaker, app);
+    value_index = value_index_uint32(app->speaker, speaker_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, speaker_text[value_index]);
+
+    // LED Effects on/off
+    item = variable_item_list_add(
+        app->variable_item_list, "LED FX:", 2, gb_cartridge_scene_settings_set_led, app);
+    value_index = value_index_uint32(app->led, led_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, led_text[value_index]);
+
+    // Save Settings to File
+    item = variable_item_list_add(
+        app->variable_item_list,
+        "Save Settings",
+        2,
+        gb_cartridge_scene_settings_set_save_settings,
+        app);
+    value_index = value_index_uint32(app->save_settings, settings_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, settings_text[value_index]);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, GBCartridgeViewIdSettings);
+}
+
+bool gb_cartridge_scene_settings_on_event(void* context, SceneManagerEvent event) {
+    GBCartridge* app = context;
+    UNUSED(app);
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+    }
+    return consumed;
+}
+
+void gb_cartridge_scene_settings_on_exit(void* context) {
+    GBCartridge* app = context;
+    variable_item_list_set_selected_item(app->variable_item_list, 0);
+    variable_item_list_reset(app->variable_item_list);
+}

+ 55 - 0
malveke_gb_cartridge/scenes/gb_cartridge_scene_startscreen.c

@@ -0,0 +1,55 @@
+#include "../gb_cartridge_app.h"
+#include "../helpers/gb_cartridge_custom_event.h"
+#include "../views/gb_cartridge_startscreen.h"
+
+void gb_cartridge_scene_startscreen_callback(GBCartridgeCustomEvent event, void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void gb_cartridge_scene_startscreen_on_enter(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    gb_cartridge_startscreen_set_callback(
+        app->gb_cartridge_startscreen, gb_cartridge_scene_startscreen_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, GBCartridgeViewIdStartscreen);
+}
+
+bool gb_cartridge_scene_startscreen_on_event(void* context, SceneManagerEvent event) {
+    GBCartridge* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case GBCartridgeCustomEventStartscreenLeft:
+        case GBCartridgeCustomEventStartscreenRight:
+            break;
+        case GBCartridgeCustomEventStartscreenUp:
+        case GBCartridgeCustomEventStartscreenDown:
+            break;
+        case GBCartridgeCustomEventStartscreenOk:
+            scene_manager_next_scene(app->scene_manager, GBCartridgeSceneMenu);
+            consumed = true;
+            break;
+        case GBCartridgeCustomEventStartscreenBack:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, GBCartridgeSceneStartscreen)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void gb_cartridge_scene_startscreen_on_exit(void* context) {
+    GBCartridge* app = context;
+    UNUSED(app);
+}

+ 195 - 0
malveke_gb_cartridge/uart.c

@@ -0,0 +1,195 @@
+#include "uart.h"
+
+#define UART_CH (FuriHalSerialIdUsart)
+#define LP_UART_CH (FuriHalSerialIdLpuart)
+#define BAUDRATE (115200UL)
+
+struct Uart {
+    void* app;
+    FuriThread* rx_thread;
+    FuriHalSerialHandle* serial_handle;
+    FuriHalSerialId channel;
+    FuriThread* worker_thread;
+    FuriStreamBuffer* rx_stream;
+    uint8_t rx_buf[RX_BUF_SIZE + 1];
+    void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context);
+};
+
+typedef enum {
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+} WorkerEventFlags;
+
+#define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone)
+
+void uart_set_handle_rx_data_cb(
+    Uart* uart,
+    void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context)) {
+    furi_assert(uart);
+    uart->handle_rx_data_cb = handle_rx_data_cb;
+}
+
+static void wifi_marauder_uart_on_irq_cb(
+    FuriHalSerialHandle* handle,
+    FuriHalSerialRxEvent event,
+    void* context) {
+    Uart* uart = (Uart*)context;
+
+    if(event == FuriHalSerialRxEventData) {
+        uint8_t data = furi_hal_serial_async_rx(handle);
+        furi_stream_buffer_send(uart->rx_stream, &data, 1, 0);
+        furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtRxDone);
+    }
+}
+
+static void
+    uart_on_irq_cb(FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* context) {
+    Uart* uart = (Uart*)context;
+    UNUSED(handle);
+
+    if(event == FuriHalSerialRxEventData) {
+        uint8_t data = furi_hal_serial_async_rx(handle);
+        furi_stream_buffer_send(uart->rx_stream, &data, 1, 0);
+        furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtRxDone);
+    }
+}
+
+// Define una constante para el prefijo que estamos buscando
+#define JSON_PREFIX "JSON:"
+
+// Variables globales
+static char json_buffer[2048]; // Ajusta el tamaño según tus necesidades
+static size_t json_buffer_index = 0;
+static bool json_capture_active = false;
+// static bool json_finded = false;
+
+// Prototipo de la función
+// static void process_json_buffer();
+
+static void process_json_buffer(void* context) {
+    Uart* uart = (Uart*)context;
+    // Agregamos el terminador nulo al final del buffer
+    json_buffer[json_buffer_index] = '\0';
+    if(uart->handle_rx_data_cb) {
+        uart->handle_rx_data_cb((uint8_t*)json_buffer, json_buffer_index, uart->app);
+        memset(json_buffer, 0, sizeof(json_buffer));
+    }
+
+    // Reiniciamos el buffer
+    json_buffer_index = 0;
+}
+
+static void uart_echo_push_to_list(void* context, uint8_t data) {
+    Uart* uart = (Uart*)context;
+    if(!json_capture_active) {
+        if(data == JSON_PREFIX[json_buffer_index]) {
+            json_buffer[json_buffer_index++] = data; // Agregar el carácter al buffer
+            if(json_buffer_index == strlen(JSON_PREFIX)) {
+                // Encontramos el prefijo, comenzamos a capturar
+                json_buffer_index = 0;
+                json_capture_active = true;
+            }
+        } else {
+            // Reiniciamos el índice si no coincide con el prefijo
+            json_buffer_index = 0;
+        }
+    } else {
+        // Capturamos caracteres hasta encontrar '\n'
+        json_buffer[json_buffer_index++] = data;
+        if(data == '\n') {
+            // Terminamos de capturar la línea, procesamos el buffer
+            json_capture_active = false;
+            process_json_buffer(uart);
+        }
+    }
+}
+static int32_t uart_worker(void* context) {
+    furi_assert(context);
+    Uart* uart = (Uart*)context;
+
+    while(1) {
+        uint32_t events =
+            furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever);
+        furi_check((events & FuriFlagError) == 0);
+
+        if(events & WorkerEvtStop) break;
+        if(events & WorkerEvtRxDone) {
+            if(uart->channel == UART_CH) {
+                size_t length = 0;
+                do {
+                    uint8_t data[64];
+                    length = furi_stream_buffer_receive(uart->rx_stream, data, 64, 0);
+
+                    if(length > 0) {
+                        for(size_t i = 0; i < length; i++) {
+                            uart_echo_push_to_list(uart, data[i]);
+                            // FURI_LOG_I("UART", "[in]: %c - %d", (const char)data[i], data[i]);
+                        }
+                    }
+                } while(length > 0);
+            } else if(uart->channel == LP_UART_CH) {
+                size_t len =
+                    furi_stream_buffer_receive(uart->rx_stream, uart->rx_buf, RX_BUF_SIZE, 0);
+                if(len > 0) {
+                    if(uart->handle_rx_data_cb)
+                        uart->handle_rx_data_cb(uart->rx_buf, len, uart->app);
+                }
+            }
+        }
+    }
+    furi_stream_buffer_free(uart->rx_stream);
+
+    return 0;
+}
+void uart_tx(void* app, uint8_t* data, size_t len) {
+    Uart* uart = (Uart*)app;
+    furi_hal_serial_tx(uart->serial_handle, data, len);
+}
+
+Uart* _uart_init(void* app, FuriHalSerialId channel, const char* thread_name) {
+    Uart* uart = (Uart*)malloc(sizeof(Uart));
+    uart->app = app;
+    uart->channel = channel;
+    uart->rx_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1);
+    uart->rx_thread = furi_thread_alloc();
+    furi_thread_set_name(uart->rx_thread, thread_name);
+    furi_thread_set_stack_size(uart->rx_thread, 1024);
+    furi_thread_set_context(uart->rx_thread, uart);
+    furi_thread_set_callback(uart->rx_thread, uart_worker);
+    furi_thread_start(uart->rx_thread);
+    uart->serial_handle = furi_hal_serial_control_acquire(channel);
+    if(!uart->serial_handle) {
+        furi_delay_ms(5000);
+    }
+    furi_check(uart->serial_handle);
+    furi_hal_serial_init(uart->serial_handle, BAUDRATE);
+    furi_hal_serial_async_rx_start(
+        uart->serial_handle,
+        channel == FuriHalSerialIdUsart ? uart_on_irq_cb : wifi_marauder_uart_on_irq_cb,
+        uart,
+        false);
+
+    return uart;
+}
+
+Uart* usart_init(void* app) {
+    return _uart_init(app, UART_CH, "UartRxThread");
+}
+
+Uart* lp_uart_init(void* app) {
+    return _uart_init(app, LP_UART_CH, "LPUartRxThread");
+}
+
+void uart_free(Uart* uart) {
+    furi_assert(uart);
+
+    furi_hal_serial_async_rx_stop(uart->serial_handle);
+    furi_hal_serial_deinit(uart->serial_handle);
+    furi_hal_serial_control_release(uart->serial_handle);
+
+    furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtStop);
+    furi_thread_join(uart->rx_thread);
+    furi_thread_free(uart->rx_thread);
+
+    free(uart);
+}

+ 21 - 0
malveke_gb_cartridge/uart.h

@@ -0,0 +1,21 @@
+#ifndef UART_H
+#define UART_H
+
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+
+#define RX_BUF_SIZE (1024)
+
+typedef struct Uart Uart;
+
+void uart_set_handle_rx_data_cb(
+    Uart* uart,
+    void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context));
+void uart_tx(void* app, uint8_t* data, size_t len);
+Uart* usart_init(void* app);
+Uart* lp_uart_init(void* app);
+void uart_free(Uart* uart);
+
+#endif /* UART_H */

+ 410 - 0
malveke_gb_cartridge/views/gb_cartridge_scene_1.c

@@ -0,0 +1,410 @@
+#include "../gb_cartridge_app.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+#include <stdio.h> // Para sprintf
+#include <string.h> // Para strlen
+
+struct GBCartridgeScene1 {
+    View* view;
+    GBCartridgeScene1Callback callback;
+    void* context;
+    GBCartridge* app;
+};
+
+typedef struct {
+    char* cart_title;
+    char* cart_serial;
+    char* cart_checksum;
+    char* cart_ROMSize;
+    char* cart_RAMSize;
+    char* cart_gb_type;
+    bool cart_gb_sgb;
+    int ramBanks;
+    int romBanks;
+    uint8_t cart_logo[48 * 8];
+
+} GameBoyCartridgeModel;
+
+void gameboy_information_handle_rx_data_cb(uint8_t* buf, size_t len, void* context) {
+    furi_assert(context);
+    UNUSED(len);
+    UNUSED(buf);
+    GBCartridge* instance = context;
+
+    with_view_model(
+        instance->gb_cartridge_scene_1->view,
+        GameBoyCartridgeModel * model,
+        {
+            cJSON* json = cJSON_Parse((char*)buf);
+            if(json == NULL) {
+                model->cart_title = "Parse error";
+            } else {
+                ;
+                //  Title
+                cJSON* title = cJSON_GetObjectItemCaseSensitive(json, "title");
+                if(cJSON_IsString(title) && (title->valuestring != NULL)) {
+                    model->cart_title = strdup(title->valuestring);
+
+                } else {
+                    model->cart_title = "None";
+                }
+                //  Serial
+                cJSON* serial = cJSON_GetObjectItemCaseSensitive(json, "serial");
+                if(cJSON_IsString(serial) && (serial->valuestring != NULL)) {
+                    model->cart_serial = strdup(serial->valuestring);
+                } else {
+                    model->cart_serial = "";
+                }
+                //  Checksum
+                cJSON* checksum = cJSON_GetObjectItemCaseSensitive(json, "checksum");
+                if(cJSON_IsString(checksum) && (checksum->valuestring != NULL)) {
+                    model->cart_checksum = strdup(checksum->valuestring);
+                } else {
+                    model->cart_checksum = "None";
+                }
+                //  ROMSize
+                cJSON* ROMSize = cJSON_GetObjectItemCaseSensitive(json, "ROMSize");
+                if(cJSON_IsString(ROMSize) && (ROMSize->valuestring != NULL)) {
+                    model->cart_ROMSize = strdup(ROMSize->valuestring);
+                } else {
+                    model->cart_ROMSize = "None";
+                }
+                //  RAMSize
+                cJSON* RAMSize = cJSON_GetObjectItemCaseSensitive(json, "RAMSize");
+                if(cJSON_IsString(RAMSize) && (RAMSize->valuestring != NULL)) {
+                    model->cart_RAMSize = strdup(RAMSize->valuestring);
+                } else {
+                    model->cart_RAMSize = "None";
+                }
+                //  GB Type
+                cJSON* gb_type = cJSON_GetObjectItemCaseSensitive(json, "gb_type");
+                if(cJSON_IsString(gb_type) && (gb_type->valuestring != NULL)) {
+                    model->cart_gb_type = strdup(gb_type->valuestring);
+                } else {
+                    model->cart_gb_type = "dump";
+                }
+
+                //  SGB ?
+                cJSON* gb_sgb = cJSON_GetObjectItemCaseSensitive(json, "gb_sgb");
+                if(cJSON_IsBool(gb_sgb)) {
+                    model->cart_gb_sgb = cJSON_IsTrue(gb_sgb);
+                } else {
+                    model->cart_gb_sgb = false;
+                }
+
+                //  Rom Banks
+                cJSON* romBanks = cJSON_GetObjectItemCaseSensitive(json, "romBanks");
+                if(cJSON_IsNumber(romBanks)) {
+                    model->romBanks = romBanks->valueint;
+                } else {
+                    model->romBanks = 0;
+                }
+
+                //  Ram Banks
+                cJSON* ramBanks = cJSON_GetObjectItemCaseSensitive(json, "ramBanks");
+                if(cJSON_IsNumber(ramBanks)) {
+                    model->ramBanks = ramBanks->valueint;
+                } else {
+                    model->ramBanks = 0;
+                }
+
+                cJSON* gb_logo = cJSON_GetObjectItemCaseSensitive(json, "logo");
+                if(cJSON_IsArray(gb_logo)) {
+                    // Leer los elementos del arreglo "logo"
+                    for(int i = 0; i < cJSON_GetArraySize(gb_logo); i++) {
+                        cJSON* logoElement = cJSON_GetArrayItem(gb_logo, i);
+                        if(cJSON_IsNumber(logoElement)) {
+                            model->cart_logo[i] = logoElement->valueint;
+                        }
+                    }
+                }
+
+                FuriString* path = furi_string_alloc();
+                //  int buffer_size = strlen(model->cart_title) + strlen(model->cart_serial) + strlen(model->cart_gb_type) + 3; // 3 para los guiones bajos y el punto
+                // char filename[255];
+                if(strcmp(model->cart_serial, "") == 0) {
+                    furi_string_cat_printf(path, "%s", model->cart_title);
+                } else {
+                    furi_string_cat_printf(path, "%s_%s", model->cart_title, model->cart_serial);
+                }
+                // snprintf(filename,255,  "%s_%s.%s", model->cart_title, model->cart_serial, model->cart_gb_type);
+                instance->cart_dump_rom_filename = (char*)furi_string_get_cstr(path);
+                instance->cart_dump_rom_extension = model->cart_gb_type;
+
+                instance->cart_dump_ram_filename = (char*)furi_string_get_cstr(path);
+                instance->cart_dump_ram_extension = "sav";
+
+                instance->rom_banks = model->romBanks;
+                instance->ram_banks = model->ramBanks;
+            }
+        },
+        true);
+}
+void gb_cartridge_scene_1_set_callback(
+    GBCartridgeScene1* instance,
+    GBCartridgeScene1Callback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+    instance->app = (GBCartridge*)context;
+}
+
+//  https://www.youtube.com/watch?v=ix5yZm4fwFQ
+void draw_logo(Canvas* canvas, GameBoyCartridgeModel* model, int start_x, int start_y) {
+    uint16_t x, y;
+    uint8_t row[4];
+    uint8_t list[48 * 8] = {0};
+    for(x = 0; x < 48 / 2; x += 2) {
+        row[0] = (model->cart_logo[x] >> 4) & 0xF;
+        row[1] = model->cart_logo[x] & 0xF;
+        row[2] = (model->cart_logo[x + 1] >> 4) & 0xF;
+        row[3] = model->cart_logo[x + 1] & 0xF;
+        for(y = 0; y < 4; y++) {
+            // set first bit
+            if((row[y] / 8) == 1) {
+                list[(x * 2) + (y * 48)] = 1;
+                row[y] -= 8;
+            }
+            // then second bit
+            if((row[y] / 4) == 1) {
+                list[((x * 2) + 1) + (y * 48)] = 1;
+                row[y] -= 4;
+            }
+            // then third bit
+            if((row[y] / 2) == 1) {
+                list[((x * 2) + 2) + (y * 48)] = 1;
+                row[y] -= 2;
+            }
+            // then fourth bit
+            if((row[y] / 1) == 1) {
+                list[((x * 2) + 3) + (y * 48)] = 1;
+            }
+        }
+    }
+
+    // then do bottom half
+    for(x = 48 / 2; x < 96 / 2; x += 2) {
+        // convert 2 bytes of data
+        row[0] = (model->cart_logo[x] >> 4) & 0xF;
+        row[1] = model->cart_logo[x] & 0xF;
+        row[2] = (model->cart_logo[x + 1] >> 4) & 0xF;
+        row[3] = model->cart_logo[x + 1] & 0xF;
+
+        for(y = 0; y < 4; y++) {
+            // set first bit
+            if((row[y] / 8) == 1) {
+                list[144 + (x * 2) + (y * 48)] = 1;
+                row[y] -= 8;
+            }
+            // then second bit
+            if((row[y] / 4) == 1) {
+                list[145 + (x * 2) + (y * 48)] = 1;
+                row[y] -= 4;
+            }
+            // then third bit
+            if((row[y] / 2) == 1) {
+                list[146 + (x * 2) + (y * 48)] = 1;
+                row[y] -= 2;
+            }
+            // then fourth bit
+            if((row[y] / 1) == 1) {
+                list[147 + (x * 2) + (y * 48)] = 1;
+            }
+        }
+    }
+    // UNUSED(row);
+
+    //  ESCALA 1
+    for(y = 0; y < 8; y++) {
+        for(x = 0; x < 48; x++) {
+            int indice = y * 48 + x;
+            if(list[indice] == 1) {
+                canvas_draw_dot(canvas, x + start_x, y + start_y);
+            }
+        }
+    }
+}
+void gb_cartridge_scene_1_draw(Canvas* canvas, GameBoyCartridgeModel* model) {
+    UNUSED(model);
+    canvas_clear(canvas);
+    // canvas_set_color(canvas, ColorBlack);
+    // canvas_set_font(canvas, FontPrimary);
+    // canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, "This is Scene 1");
+    // canvas_set_font(canvas, FontSecondary);
+    // canvas_draw_str_aligned(canvas, 0, 22, AlignLeft, AlignTop, "An empty scene to be");
+    // canvas_draw_str_aligned(canvas, 0, 32, AlignLeft, AlignTop, "used as boilerplate");
+    // Clear the screen.
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_set_bitmap_mode(canvas, 1);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 128 / 2, 1, AlignCenter, AlignTop, model->cart_title);
+    // canvas_draw_str_aligned(canvas, 128 / 2, 1, AlignCenter, AlignTop, "Prueba"); //  title
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 2, 20, "Game Code / REV");
+    canvas_set_font(canvas, FontPrimary);
+    // canvas_draw_str(canvas, 87, 20, "APSS-0");  //  serial
+    canvas_draw_str_aligned(canvas, 126, 20, AlignRight, AlignBottom, model->cart_serial);
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 2, 30, "Boot Logo");
+    // canvas_draw_box(canvas, 78, 22, 48, 8); //  TODO: Implementar
+    draw_logo(canvas, model, 78, 22);
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 2, 40, "ROM Checksum");
+    canvas_set_font(canvas, FontPrimary);
+    // canvas_draw_str(canvas, 87, 40, "0X04C7"); //  checksum
+    canvas_draw_str_aligned(canvas, 126, 39, AlignRight, AlignBottom, model->cart_checksum);
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 2, 50, "ROM Size");
+    canvas_set_font(canvas, FontPrimary);
+    // canvas_draw_str(canvas, 98, 49, "1 MiB"); //  ROMSize
+    canvas_draw_str_aligned(canvas, 126, 49, AlignRight, AlignBottom, model->cart_ROMSize);
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 2, 60, "Save Type");
+    canvas_set_font(canvas, FontPrimary);
+    // canvas_draw_str(canvas, 63, 60, "SRAM 32KiB"); //  RAMSize
+    canvas_draw_str_aligned(canvas, 126, 59, AlignRight, AlignBottom, model->cart_RAMSize);
+}
+
+static void gb_cartridge_scene_1_model_init(GameBoyCartridgeModel* const model) {
+    UNUSED(model);
+    // FuriString* cart_title;
+    // FuriString* cart_serial;
+    // FuriString* cart_checksum;
+    // FuriString* cart_ROMSize;
+    // FuriString* cart_RAMSize;
+    model->cart_title = "Loading...";
+    model->cart_serial = "";
+    model->cart_checksum = "";
+    model->cart_ROMSize = "";
+    model->cart_RAMSize = "";
+    model->cart_gb_type = "";
+    model->cart_gb_sgb = false;
+    for(int i = 0; i < 48 * 8; i++) {
+        model->cart_logo[i] = 0;
+    }
+}
+
+bool gb_cartridge_scene_1_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    GBCartridgeScene1* instance = context;
+    bool consumed = false;
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            // with_view_model(
+            //     instance->view,
+            //     GameBoyCartridgeModel * model,
+            //     {
+            //         UNUSED(model);
+            //         instance->callback(GBCartridgeCustomEventScene1Back, instance->context);
+            //     },
+            //     true);
+            consumed = true;
+            break;
+        case InputKeyOk:
+
+            with_view_model(
+                ((GBCartridge*)instance->app)->gb_cartridge_scene_1->view,
+                GameBoyCartridgeModel * model,
+                {
+                    model->cart_title = "Refresh...";
+                    model->cart_serial = "";
+                    model->cart_checksum = "";
+                    model->cart_ROMSize = "";
+                    model->cart_RAMSize = "";
+                    // Reiniciar el array a 0 utilizando un bucle
+                    for(size_t i = 0; i < sizeof(model->cart_logo) / sizeof(model->cart_logo[0]);
+                        i++) {
+                        model->cart_logo[i] = 0;
+                    }
+                    // Register callbacks to receive data
+                    uart_set_handle_rx_data_cb(
+                        ((GBCartridge*)instance->app)->uart,
+                        gameboy_information_handle_rx_data_cb); // setup callback for general log rx thread
+                    const char gbcartridge_command[] = "gbcartridge -i\n";
+                    uart_tx(
+                        ((GBCartridge*)instance->app)->uart,
+                        (uint8_t*)gbcartridge_command,
+                        strlen(gbcartridge_command));
+                },
+                true);
+            consumed = true;
+            break;
+        case InputKeyLeft:
+        case InputKeyRight:
+        case InputKeyUp:
+        case InputKeyDown:
+
+            with_view_model(
+                instance->view, GameBoyCartridgeModel * model, { UNUSED(model); }, true);
+            consumed = true;
+            break;
+        case InputKeyMAX:
+            break;
+        }
+    }
+    return consumed;
+}
+
+void gb_cartridge_scene_1_exit(void* context) {
+    furi_assert(context);
+}
+
+void gb_cartridge_scene_1_enter(void* context) {
+    furi_assert(context);
+    GBCartridgeScene1* instance = (GBCartridgeScene1*)context;
+    with_view_model(
+        instance->view,
+        GameBoyCartridgeModel * model,
+        { gb_cartridge_scene_1_model_init(model); },
+        true);
+
+    // Register callbacks to receive data
+    uart_set_handle_rx_data_cb(
+        ((GBCartridge*)instance->app)->uart,
+        gameboy_information_handle_rx_data_cb); // setup callback for general log rx thread
+    const char gbcartridge_command[] = "gbcartridge -i\n";
+    uart_tx(
+        ((GBCartridge*)instance->app)->uart,
+        (uint8_t*)gbcartridge_command,
+        strlen(gbcartridge_command));
+}
+
+GBCartridgeScene1* gb_cartridge_scene_1_alloc() {
+    GBCartridgeScene1* instance = malloc(sizeof(GBCartridgeScene1));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(GameBoyCartridgeModel));
+    view_set_context(instance->view, instance); // furi_assert crashes in events without this
+    view_set_draw_callback(instance->view, (ViewDrawCallback)gb_cartridge_scene_1_draw);
+    view_set_input_callback(instance->view, gb_cartridge_scene_1_input);
+    view_set_enter_callback(instance->view, gb_cartridge_scene_1_enter);
+    view_set_exit_callback(instance->view, gb_cartridge_scene_1_exit);
+
+    with_view_model(
+        instance->view,
+        GameBoyCartridgeModel * model,
+        { gb_cartridge_scene_1_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void gb_cartridge_scene_1_free(GBCartridgeScene1* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, GameBoyCartridgeModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* gb_cartridge_scene_1_get_view(GBCartridgeScene1* instance) {
+    furi_assert(instance);
+    return instance->view;
+}

+ 19 - 0
malveke_gb_cartridge/views/gb_cartridge_scene_1.h

@@ -0,0 +1,19 @@
+#pragma once
+#include <furi.h>
+#include <gui/view.h>
+#include "../helpers/gb_cartridge_custom_event.h"
+
+typedef struct GBCartridgeScene1 GBCartridgeScene1;
+
+typedef void (*GBCartridgeScene1Callback)(GBCartridgeCustomEvent event, void* context);
+
+void gb_cartridge_scene_1_set_callback(
+    GBCartridgeScene1* gb_cartridge_scene_1,
+    GBCartridgeScene1Callback callback,
+    void* context);
+
+View* gb_cartridge_scene_1_get_view(GBCartridgeScene1* gb_cartridge_static);
+
+GBCartridgeScene1* gb_cartridge_scene_1_alloc();
+
+void gb_cartridge_scene_1_free(GBCartridgeScene1* gb_cartridge_static);

+ 368 - 0
malveke_gb_cartridge/views/gb_cartridge_scene_2.c

@@ -0,0 +1,368 @@
+#include "../gb_cartridge_app.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+#include <gui/elements.h>
+#include <notification/notification_messages.h>
+#include "../helpers/gb_cartridge_speaker.h"
+#include "../helpers/sequential_file.h"
+#include <stdio.h> // Para sprintf
+#include <string.h> // Para strlen
+
+struct GBCartridgeScene2 {
+    View* view;
+    GBCartridgeScene2Callback callback;
+    void* context;
+    GBCartridge* app;
+};
+static uint64_t last_toggle_time = 0;
+
+typedef struct {
+    char* event_type;
+    int progress;
+    int total_rom;
+    int transfered;
+    int romBanks;
+    int elapsed_time;
+    int start_time;
+    char* cart_dump_rom_filename;
+    char* cart_dump_rom_extension;
+    char* cart_dump_rom_filename_sequential;
+    bool rx_active;
+} GameBoyCartridgeROMBackupModel;
+
+void dump_handle_rx_data_cb(uint8_t* buf, size_t len, void* context) {
+    UNUSED(len);
+    UNUSED(buf);
+    GBCartridge* instance = context;
+    if(instance->is_writing_rom) {
+        storage_file_write(instance->cart_rom, buf, len);
+    }
+    with_view_model(
+        instance->gb_cartridge_scene_2->view,
+        GameBoyCartridgeROMBackupModel * model,
+        {
+            model->transfered += len;
+            uint64_t current_time = furi_hal_rtc_get_timestamp();
+            model->elapsed_time = current_time - model->start_time;
+            if(current_time - last_toggle_time >= 0.2) {
+                model->rx_active = !model->rx_active;
+                last_toggle_time = current_time;
+            }
+        },
+        true);
+}
+
+void gb_cartridge_scene_2_set_callback(
+    GBCartridgeScene2* instance,
+    GBCartridgeScene2Callback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void gameboy_rom_backup_handle_rx_data_cb(uint8_t* buf, size_t len, void* context) {
+    // furi_assert(context);
+    UNUSED(len);
+    // UNUSED(buf);
+    GBCartridge* instance = context;
+
+    with_view_model(
+        instance->gb_cartridge_scene_2->view,
+        GameBoyCartridgeROMBackupModel * model,
+        {
+            UNUSED(model);
+            cJSON* json = cJSON_Parse((char*)buf);
+            if(json == NULL) {
+                // furi_string_cat_printf(model->cart_type, "%s\n", "Parse error");
+                // model->cart_title_strlen = furi_string_size(model->cart_title);
+            } else {
+                cJSON* type = cJSON_GetObjectItemCaseSensitive(json, "type");
+                if(cJSON_IsString(type) && (type->valuestring != NULL)) {
+                    model->event_type = strdup(type->valuestring);
+                } else {
+                    model->event_type = "None";
+                }
+                //  Total
+                cJSON* total = cJSON_GetObjectItemCaseSensitive(json, "total");
+                if(cJSON_IsNumber(total)) {
+                    model->total_rom = total->valueint;
+                } else {
+                    model->total_rom = 0;
+                }
+                //  Progress
+                cJSON* progress = cJSON_GetObjectItemCaseSensitive(json, "progress");
+                if(cJSON_IsNumber(progress)) {
+                    model->progress = progress->valueint;
+                } else {
+                    model->progress = 0;
+                }
+                //  RomBanks
+                cJSON* romBanks = cJSON_GetObjectItemCaseSensitive(json, "romBanks");
+                if(cJSON_IsNumber(romBanks)) {
+                    model->romBanks = romBanks->valueint;
+                } else {
+                    model->romBanks = 0;
+                }
+            }
+            if(strcmp(model->event_type, "success") == 0) {
+                model->progress = 100;
+                // cJSON* total = cJSON_GetObjectItemCaseSensitive(json, "total");
+                // if(cJSON_IsNumber(total)) {
+                //     model->transfered = total->valueint;
+                // }
+                // if(instance->cart_rom && storage_file_is_open(instance->cart_rom)) {
+                //     storage_file_close(instance->cart_rom);
+                // }
+                notification_success(instance->notification);
+            }
+        },
+        true);
+}
+static void drawProgressBar(Canvas* canvas, int progress) {
+    for(int x = 0; x < 64 - 14 - UI_PADDING - UI_PADDING - UI_PADDING - UI_PADDING; x += 5) {
+        for(int row = 0; row < 20; row += 5) {
+            if(progress > 0) {
+                canvas_draw_box(
+                    canvas, 14 /*ARROW*/ + UI_PADDING + 2 + x + 4, /*45*/ 26 + row, 4, 4);
+                progress--;
+            } else {
+                canvas_draw_frame(
+                    canvas, 14 /*ARROW*/ + UI_PADDING + 2 + x + 4, /*45*/ 26 + row, 4, 4);
+            }
+        }
+    }
+}
+
+void gb_cartridge_scene_2_draw(Canvas* canvas, GameBoyCartridgeROMBackupModel* model) {
+    // Clear the screen.
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontKeyboard);
+    canvas_draw_frame(canvas, 0, 23, (128 / 2), 25);
+
+    canvas_set_bitmap_mode(canvas, 1);
+    canvas_set_font(canvas, FontPrimary);
+    char progressText[32];
+    int progress = 0;
+    if(model->total_rom > 0 && model->transfered > 0) {
+        progress = model->transfered * 100 / model->total_rom;
+    }
+    snprintf(progressText, sizeof(progressText), "%d%% Dump ROM...", progress);
+    canvas_draw_str_aligned(canvas, 128 / 2, 0, AlignCenter, AlignTop, progressText);
+    canvas_set_font(canvas, FontSecondary);
+
+    char* filename = strrchr(model->cart_dump_rom_filename_sequential, '/');
+    filename++;
+
+    canvas_draw_str_aligned(canvas, 128 / 2, 12, AlignCenter, AlignTop, filename);
+
+    char total_rom_str[20]; // Declarar un buffer para almacenar la versión en cadena del entero
+    snprintf(
+        total_rom_str,
+        sizeof(total_rom_str),
+        "of %.2lf MiB",
+        (double)(model->total_rom / 1024.0 / 1024.0));
+
+    char transfered_rom_str[20];
+    snprintf(
+        transfered_rom_str,
+        sizeof(transfered_rom_str),
+        "%.2lf MiB",
+        (double)(model->transfered / 1024.0 / 1024.0));
+
+    // Calcula la Tasa de Transferencia en KiB/s
+    char transfer_rate_str[20];
+    if(model->transfered > 0 && model->elapsed_time > 0) {
+        double transfer_rate_kibps =
+            (double)model->transfered / ((double)model->elapsed_time) / (double)1024.0;
+        snprintf(transfer_rate_str, sizeof(transfer_rate_str), "%.2lf KiB/s", transfer_rate_kibps);
+    } else {
+        snprintf(transfer_rate_str, sizeof(transfer_rate_str), "0 KiB/s");
+    }
+
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 22 + 2, AlignLeft, AlignTop, transfered_rom_str);
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 30 + 2, AlignLeft, AlignTop, total_rom_str);
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 38 + 2, AlignLeft, AlignTop, transfer_rate_str);
+
+    if(model->rx_active) {
+        canvas_draw_icon_ex(canvas, UI_PADDING, 28, &I_ArrowUpFilled_14x15, IconRotation180);
+    } else {
+        canvas_draw_icon_ex(canvas, UI_PADDING, 28, &I_ArrowUpEmpty_14x15, IconRotation180);
+    }
+
+    char totalText[32];
+    snprintf(totalText, sizeof(totalText), "%d", model->total_rom);
+
+    drawProgressBar(
+        canvas,
+        (progress * UI_PROGRESS_ROWS * UI_PROGRESS_COLS) /
+            100); // Pinta las primeras 10 cajas de negro
+
+    elements_button_center(canvas, "Start");
+}
+
+static void gb_cartridge_scene_2_model_init(GameBoyCartridgeROMBackupModel* const model) {
+    // model->screen_text = 0;
+    model->progress = 0;
+    model->total_rom = 0;
+    model->transfered = 0;
+    model->romBanks = 0;
+    model->elapsed_time = 0;
+    model->start_time = 0;
+}
+
+bool gb_cartridge_scene_2_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    GBCartridgeScene2* instance = context;
+
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                instance->view,
+                GameBoyCartridgeROMBackupModel * model,
+                {
+                    UNUSED(model);
+                    GBCartridge* app = (GBCartridge*)instance->context;
+                    UNUSED(app);
+
+                    model->total_rom = 0;
+                    model->transfered = 0;
+                    model->elapsed_time = 0;
+
+                    // // Unregister rx callback
+                    uart_set_handle_rx_data_cb(app->uart, NULL);
+                    uart_set_handle_rx_data_cb(app->lp_uart, NULL);
+                    //  Close file
+                    app->is_writing_rom = false;
+                    if(app->cart_rom && storage_file_is_open(app->cart_rom)) {
+                        storage_file_close(app->cart_rom);
+                    }
+                    notification_message(
+                        app->notification, &sequence_display_backlight_enforce_auto);
+                    instance->callback(GBCartridgeCustomEventScene2Back, instance->context);
+                },
+                true);
+            break;
+        case InputKeyUp:
+        case InputKeyDown:
+        case InputKeyLeft:
+        case InputKeyRight:
+            break;
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                GameBoyCartridgeROMBackupModel * model,
+                {
+                    GBCartridge* app = ((GBCartridge*)instance->context);
+                    UNUSED(app);
+                    model->start_time =
+                        furi_hal_rtc_get_timestamp(); // Registra el tiempo de inicio
+                    app->cart_rom = storage_file_alloc(app->storage);
+
+                    if(storage_file_open(
+                           app->cart_rom,
+                           model->cart_dump_rom_filename_sequential,
+                           FSAM_WRITE,
+                           FSOM_CREATE_ALWAYS)) {
+                        const char gbcartridge_command[] = "gbcartridge -d -o\n";
+                        uart_tx(
+                            app->uart, (uint8_t*)gbcartridge_command, strlen(gbcartridge_command));
+                    } else {
+                        dialog_message_show_storage_error(app->dialogs, "Cannot open dump file");
+                    }
+                },
+                true);
+
+            break;
+        case InputKeyMAX:
+            break;
+        }
+    }
+
+    return true;
+}
+
+void gb_cartridge_scene_2_exit(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    gb_cartridge_stop_all_sound(app);
+}
+
+void gb_cartridge_scene_2_enter(void* context) {
+    furi_assert(context);
+    GBCartridgeScene2* instance = context;
+    GBCartridge* app = (GBCartridge*)instance->context;
+    //  backlight on
+    notification_message(app->notification, &sequence_display_backlight_enforce_on);
+    // furi_hal_power_disable_otg();
+    UNUSED(app);
+    // dolphin_deed(DolphinDeedPluginStart);
+    with_view_model(
+        app->gb_cartridge_scene_2->view,
+        GameBoyCartridgeROMBackupModel * model,
+        {
+            model->cart_dump_rom_filename = app->cart_dump_rom_filename;
+            model->cart_dump_rom_extension = app->cart_dump_rom_extension;
+            model->total_rom = app->rom_banks * 16 * 1024;
+            // char *filename = strrchr(model->cart_dump_rom_filename_sequential, '/');
+            // filename++;
+            char* filename = sequential_file_resolve_path(
+                app->storage,
+                MALVEKE_APP_FOLDER_ROMS,
+                app->cart_dump_rom_filename,
+                model->cart_dump_rom_extension);
+            model->cart_dump_rom_filename_sequential = filename;
+            // Register callbacks to receive data
+            uart_set_handle_rx_data_cb(
+                app->uart,
+                gameboy_rom_backup_handle_rx_data_cb); // setup callback for general log rx thread
+            uart_set_handle_rx_data_cb(
+                app->lp_uart, dump_handle_rx_data_cb); // setup callback for general log rx thread
+            app->is_writing_rom = true;
+        },
+        false);
+}
+
+GBCartridgeScene2* gb_cartridge_scene_2_alloc() {
+    GBCartridgeScene2* instance = malloc(sizeof(GBCartridgeScene2));
+    instance->view = view_alloc();
+    view_allocate_model(
+        instance->view, ViewModelTypeLocking, sizeof(GameBoyCartridgeROMBackupModel));
+    view_set_context(instance->view, instance);
+    view_set_draw_callback(instance->view, (ViewDrawCallback)gb_cartridge_scene_2_draw);
+    view_set_input_callback(instance->view, gb_cartridge_scene_2_input);
+    view_set_enter_callback(instance->view, gb_cartridge_scene_2_enter);
+    view_set_exit_callback(instance->view, gb_cartridge_scene_2_exit);
+
+    with_view_model(
+        instance->view,
+        GameBoyCartridgeROMBackupModel * model,
+        { gb_cartridge_scene_2_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void gb_cartridge_scene_2_free(GBCartridgeScene2* instance) {
+    furi_assert(instance);
+
+    view_free(instance->view);
+    free(instance);
+}
+
+View* gb_cartridge_scene_2_get_view(GBCartridgeScene2* instance) {
+    furi_assert(instance);
+
+    return instance->view;
+}

+ 19 - 0
malveke_gb_cartridge/views/gb_cartridge_scene_2.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/gb_cartridge_custom_event.h"
+
+typedef struct GBCartridgeScene2 GBCartridgeScene2;
+
+typedef void (*GBCartridgeScene2Callback)(GBCartridgeCustomEvent event, void* context);
+
+void gb_cartridge_scene_2_set_callback(
+    GBCartridgeScene2* instance,
+    GBCartridgeScene2Callback callback,
+    void* context);
+
+GBCartridgeScene2* gb_cartridge_scene_2_alloc();
+
+void gb_cartridge_scene_2_free(GBCartridgeScene2* gb_cartridge_static);
+
+View* gb_cartridge_scene_2_get_view(GBCartridgeScene2* boilerpate_static);

+ 369 - 0
malveke_gb_cartridge/views/gb_cartridge_scene_3.c

@@ -0,0 +1,369 @@
+#include "../gb_cartridge_app.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+#include <gui/elements.h>
+#include <notification/notification_messages.h>
+#include "../helpers/gb_cartridge_speaker.h"
+#include "../helpers/sequential_file.h"
+#include <stdio.h> // Para sprintf
+#include <string.h> // Para strlen
+
+struct GBCartridgeScene3 {
+    View* view;
+    GBCartridgeScene3Callback callback;
+    void* context;
+    GBCartridge* app;
+};
+static uint64_t last_toggle_time = 0;
+// static const NotificationSequence sequence_alarm = {
+//     &message_display_backlight_on,
+//     &message_red_255,
+//     &message_vibro_on,
+//     &message_note_c5,
+//     &message_delay_100,
+//     &message_vibro_off,
+//     &message_sound_off,
+//     &message_note_c7,
+//     &message_delay_500,
+//     &message_sound_off,
+//     &message_display_backlight_off,
+//     &message_red_0,
+//     &message_delay_50,
+//     NULL,
+// };
+
+typedef struct {
+    char* event_type;
+    int progress;
+    int total_ram;
+    int transfered;
+    int ramBanks;
+    int elapsed_time;
+    int start_time;
+    // char* cart_dump_ram_filename;
+    // char* cart_dump_ram_extension;
+    char* cart_dump_ram_filename_sequential;
+    bool rx_active;
+} GameBoyCartridgeRAMBackupModel;
+
+void dump_ram_handle_rx_data_cb(uint8_t* buf, size_t len, void* context) {
+    UNUSED(len);
+    UNUSED(buf);
+    GBCartridge* instance = context;
+    if(instance->is_writing_ram) {
+        storage_file_write(instance->cart_ram, buf, len);
+    }
+    with_view_model(
+        instance->gb_cartridge_scene_3->view,
+        GameBoyCartridgeRAMBackupModel * model,
+        {
+            model->transfered += len;
+            uint64_t current_time = furi_hal_rtc_get_timestamp();
+            model->elapsed_time = current_time - model->start_time;
+            if(current_time - last_toggle_time >= 0.2) {
+                model->rx_active = !model->rx_active;
+                last_toggle_time = current_time;
+            }
+        },
+        true);
+}
+
+void gb_cartridge_scene_3_set_callback(
+    GBCartridgeScene3* instance,
+    GBCartridgeScene3Callback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+void gameboy_ram_backup_handle_rx_data_cb(uint8_t* buf, size_t len, void* context) {
+    furi_assert(context);
+    UNUSED(len);
+    UNUSED(buf);
+    GBCartridge* instance = context;
+    with_view_model(
+        instance->gb_cartridge_scene_3->view,
+        GameBoyCartridgeRAMBackupModel * model,
+        {
+            UNUSED(model);
+            cJSON* json = cJSON_Parse((char*)buf);
+            if(json == NULL) {
+            } else {
+                cJSON* type = cJSON_GetObjectItemCaseSensitive(json, "type");
+                if(cJSON_IsString(type) && (type->valuestring != NULL)) {
+                    model->event_type = strdup(type->valuestring);
+                } else {
+                    model->event_type = "None";
+                }
+                //  Total
+                cJSON* total = cJSON_GetObjectItemCaseSensitive(json, "total");
+                if(cJSON_IsNumber(total)) {
+                    model->total_ram = total->valueint;
+                } else {
+                    model->total_ram = 0;
+                }
+                //  Progress
+                cJSON* progress = cJSON_GetObjectItemCaseSensitive(json, "progress");
+                if(cJSON_IsNumber(progress)) {
+                    model->progress = progress->valueint;
+                } else {
+                    model->progress = 0;
+                }
+                //  RamBanks
+                cJSON* ramBanks = cJSON_GetObjectItemCaseSensitive(json, "ramBanks");
+                if(cJSON_IsNumber(ramBanks)) {
+                    model->ramBanks = ramBanks->valueint;
+                } else {
+                    model->ramBanks = 0;
+                }
+            }
+            if(strcmp(model->event_type, "success") == 0) {
+                model->progress = 100;
+                // if(instance->cart_ram && storage_file_is_open(instance->cart_ram)) {
+                //     storage_file_close(instance->cart_ram);
+                // }
+                notification_success(instance->notification);
+            }
+        },
+        true);
+}
+static void drawProgressBar(Canvas* canvas, int progress) {
+    for(int x = 0; x < 64 - 14 - UI_PADDING - UI_PADDING - UI_PADDING - UI_PADDING; x += 5) {
+        for(int row = 0; row < 20; row += 5) {
+            if(progress > 0) {
+                canvas_draw_box(
+                    canvas, 14 /*ARROW*/ + UI_PADDING + 2 + x + 4, /*45*/ 26 + row, 4, 4);
+                progress--;
+            } else {
+                canvas_draw_frame(
+                    canvas, 14 /*ARROW*/ + UI_PADDING + 2 + x + 4, /*45*/ 26 + row, 4, 4);
+            }
+        }
+    }
+}
+
+void gb_cartridge_scene_3_draw(Canvas* canvas, GameBoyCartridgeRAMBackupModel* model) {
+    // Clear the screen.
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontKeyboard);
+    canvas_draw_frame(canvas, 0, 23, (128 / 2), 25);
+
+    canvas_set_bitmap_mode(canvas, 1);
+    canvas_set_font(canvas, FontPrimary);
+    char progressText[32];
+    int progress = 0;
+    if(model->total_ram > 0 && model->transfered > 0) {
+        progress = model->transfered * 100 / model->total_ram;
+    }
+    snprintf(progressText, sizeof(progressText), "%d%% Dump RAM...", progress);
+    canvas_draw_str_aligned(canvas, 128 / 2, 0, AlignCenter, AlignTop, progressText);
+    canvas_set_font(canvas, FontSecondary);
+
+    char* filename = strrchr(model->cart_dump_ram_filename_sequential, '/');
+    filename++;
+    canvas_draw_str_aligned(canvas, 128 / 2, 12, AlignCenter, AlignTop, filename);
+
+    char total_ram_str[20];
+    snprintf(
+        total_ram_str,
+        sizeof(total_ram_str),
+        "of %.2lf MiB",
+        (double)(model->total_ram / 1024.0 / 1024.0));
+
+    char transfered_ram_str[20];
+    snprintf(
+        transfered_ram_str,
+        sizeof(transfered_ram_str),
+        "%.2lf MiB",
+        (double)(model->transfered / 1024.0 / 1024.0));
+
+    // Calcula la Tasa de Transferencia en KiB/s
+    char transfer_rate_str[20];
+    if(model->transfered > 0 && model->elapsed_time > 0) {
+        double transfer_rate_kibps =
+            (double)model->transfered / ((double)model->elapsed_time) / (double)1024.0;
+        snprintf(transfer_rate_str, sizeof(transfer_rate_str), "%.2lf KiB/s", transfer_rate_kibps);
+    } else {
+        snprintf(transfer_rate_str, sizeof(transfer_rate_str), "0 KiB/s");
+    }
+
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 22 + 2, AlignLeft, AlignTop, transfered_ram_str);
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 30 + 2, AlignLeft, AlignTop, total_ram_str);
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 38 + 2, AlignLeft, AlignTop, transfer_rate_str);
+
+    if(model->rx_active) {
+        canvas_draw_icon_ex(canvas, UI_PADDING, 28, &I_ArrowUpFilled_14x15, IconRotation180);
+    } else {
+        canvas_draw_icon_ex(canvas, UI_PADDING, 28, &I_ArrowUpEmpty_14x15, IconRotation180);
+    }
+
+    char totalText[32];
+    snprintf(totalText, sizeof(totalText), "%d", model->total_ram);
+    // canvas_draw_str(canvas, 69, 38, totalText);
+
+    drawProgressBar(
+        canvas,
+        (progress * UI_PROGRESS_ROWS * UI_PROGRESS_COLS) /
+            100); // Pinta las primeras 10 cajas de negro
+    // free(totalText);
+
+    elements_button_center(canvas, "Start");
+}
+
+static void gb_cartridge_scene_3_model_init(GameBoyCartridgeRAMBackupModel* const model) {
+    model->progress = 0;
+    model->total_ram = 0;
+    model->transfered = 0;
+    model->ramBanks = 0;
+    model->elapsed_time = 0;
+    model->start_time = 0;
+}
+
+bool gb_cartridge_scene_3_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    GBCartridgeScene3* instance = context;
+
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                instance->view,
+                GameBoyCartridgeRAMBackupModel * model,
+                {
+                    UNUSED(model);
+                    GBCartridge* app = (GBCartridge*)instance->context;
+                    UNUSED(app);
+                    model->total_ram = 0;
+                    model->transfered = 0;
+                    model->elapsed_time = 0;
+                    // Unregister rx callback
+                    uart_set_handle_rx_data_cb(app->uart, NULL);
+                    uart_set_handle_rx_data_cb(app->lp_uart, NULL);
+
+                    app->is_writing_ram = false;
+                    if(app->cart_ram && storage_file_is_open(app->cart_ram)) {
+                        storage_file_close(app->cart_ram);
+                    }
+                    instance->callback(GBCartridgeCustomEventScene3Back, instance->context);
+                },
+                true);
+            break;
+        case InputKeyUp:
+        case InputKeyDown:
+        case InputKeyLeft:
+        case InputKeyRight:
+            break;
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                GameBoyCartridgeRAMBackupModel * model,
+                {
+                    GBCartridge* app = ((GBCartridge*)instance->context);
+                    UNUSED(app);
+                    model->start_time =
+                        furi_hal_rtc_get_timestamp(); // Registra el tiempo de inicio
+                    app->cart_ram = storage_file_alloc(app->storage);
+
+                    if(storage_file_open(
+                           app->cart_ram,
+                           model->cart_dump_ram_filename_sequential,
+                           FSAM_WRITE,
+                           FSOM_CREATE_ALWAYS)) {
+                        const char gbcartridge_command[] = "gbcartridge -d -a\n";
+                        uart_tx(
+                            app->uart, (uint8_t*)gbcartridge_command, strlen(gbcartridge_command));
+                    } else {
+                        dialog_message_show_storage_error(app->dialogs, "Cannot open dump file");
+                    }
+                },
+                true);
+            break;
+        case InputKeyMAX:
+            break;
+        }
+    }
+
+    return true;
+}
+
+void gb_cartridge_scene_3_exit(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    gb_cartridge_stop_all_sound(app);
+}
+
+void gb_cartridge_scene_3_enter(void* context) {
+    furi_assert(context);
+    GBCartridgeScene3* instance = context;
+    GBCartridge* app = (GBCartridge*)instance->context;
+
+    UNUSED(app);
+    with_view_model(
+        app->gb_cartridge_scene_3->view,
+        GameBoyCartridgeRAMBackupModel * model,
+        {
+            // gb_cartridge_scene_3_model_init(model);
+            // model->cart_dump_ram_filename  = app->cart_dump_ram_filename;
+            // model->cart_dump_ram_extension = app->cart_dump_ram_extension;
+            // char *filename = strrchr(sequential_file_resolve_path(app->storage, MALVEKE_APP_FOLDER_RAMS, app->cart_dump_ram_filename, app->cart_dump_ram_extension), '/');
+            // filename++;
+            char* filename = sequential_file_resolve_path(
+                app->storage,
+                MALVEKE_APP_FOLDER_RAMS,
+                app->cart_dump_ram_filename,
+                app->cart_dump_ram_extension);
+            model->cart_dump_ram_filename_sequential = filename;
+            app->is_writing_ram = true;
+            // Register callbacks to receive data
+            uart_set_handle_rx_data_cb(
+                app->uart,
+                gameboy_ram_backup_handle_rx_data_cb); // setup callback for general log rx thread
+            uart_set_handle_rx_data_cb(
+                app->lp_uart,
+                dump_ram_handle_rx_data_cb); // setup callback for general log rx thread
+        },
+        false);
+}
+
+GBCartridgeScene3* gb_cartridge_scene_3_alloc() {
+    GBCartridgeScene3* instance = malloc(sizeof(GBCartridgeScene3));
+    instance->view = view_alloc();
+    view_allocate_model(
+        instance->view, ViewModelTypeLocking, sizeof(GameBoyCartridgeRAMBackupModel));
+    view_set_context(instance->view, instance);
+    view_set_draw_callback(instance->view, (ViewDrawCallback)gb_cartridge_scene_3_draw);
+    view_set_input_callback(instance->view, gb_cartridge_scene_3_input);
+    view_set_enter_callback(instance->view, gb_cartridge_scene_3_enter);
+    view_set_exit_callback(instance->view, gb_cartridge_scene_3_exit);
+
+    with_view_model(
+        instance->view,
+        GameBoyCartridgeRAMBackupModel * model,
+        { gb_cartridge_scene_3_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void gb_cartridge_scene_3_free(GBCartridgeScene3* instance) {
+    furi_assert(instance);
+
+    view_free(instance->view);
+    free(instance);
+}
+
+View* gb_cartridge_scene_3_get_view(GBCartridgeScene3* instance) {
+    furi_assert(instance);
+
+    return instance->view;
+}

+ 19 - 0
malveke_gb_cartridge/views/gb_cartridge_scene_3.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/gb_cartridge_custom_event.h"
+
+typedef struct GBCartridgeScene3 GBCartridgeScene3;
+
+typedef void (*GBCartridgeScene3Callback)(GBCartridgeCustomEvent event, void* context);
+
+void gb_cartridge_scene_3_set_callback(
+    GBCartridgeScene3* instance,
+    GBCartridgeScene3Callback callback,
+    void* context);
+
+GBCartridgeScene3* gb_cartridge_scene_3_alloc();
+
+void gb_cartridge_scene_3_free(GBCartridgeScene3* gb_cartridge_static);
+
+View* gb_cartridge_scene_3_get_view(GBCartridgeScene3* boilerpate_static);

+ 295 - 0
malveke_gb_cartridge/views/gb_cartridge_scene_4.c

@@ -0,0 +1,295 @@
+#include "../gb_cartridge_app.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+#include <gui/elements.h>
+#include <notification/notification_messages.h>
+#include <dialogs/dialogs.h>
+#include <gui/modules/dialog_ex.h>
+#include <toolbox/stream/file_stream.h>
+#include "../helpers/gb_cartridge_speaker.h"
+#include "../helpers/sequential_file.h"
+#include <stdio.h> // Para sprintf
+#include <string.h> // Para strlen
+#include <lib/toolbox/stream/file_stream.h>
+
+struct GBCartridgeScene4 {
+    View* view;
+    GBCartridgeScene4Callback callback;
+    void* context;
+    GBCartridge* app;
+};
+
+typedef struct {
+    char* event_type;
+    int progress;
+    int total_rom;
+    int transfered;
+    int romBanks;
+    int elapsed_time;
+    int start_time;
+
+    char* cart_dump_rom_filename_sequential;
+    bool rx_active;
+
+    char* gameboy_rom_option_selected_text;
+} GameBoyCartridgeROMWriteModel;
+
+void gb_cartridge_scene_4_set_callback(
+    GBCartridgeScene4* instance,
+    GBCartridgeScene4Callback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+static void drawProgressBar(Canvas* canvas, int progress) {
+    for(int x = 0; x < 64 - 14 - UI_PADDING - UI_PADDING - UI_PADDING - UI_PADDING; x += 5) {
+        for(int row = 0; row < 20; row += 5) {
+            if(progress > 0) {
+                canvas_draw_box(
+                    canvas, 14 /*ARROW*/ + UI_PADDING + 2 + x + 4, /*45*/ 26 + row, 4, 4);
+                progress--;
+            } else {
+                canvas_draw_frame(
+                    canvas, 14 /*ARROW*/ + UI_PADDING + 2 + x + 4, /*45*/ 26 + row, 4, 4);
+            }
+        }
+    }
+}
+
+void gb_cartridge_scene_4_draw(Canvas* canvas, GameBoyCartridgeROMWriteModel* model) {
+    // Clear the screen.
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontKeyboard);
+    canvas_draw_frame(canvas, 0, 24, (128 / 2), 25);
+
+    canvas_set_bitmap_mode(canvas, 1);
+    canvas_set_font(canvas, FontPrimary);
+    char progressText[42];
+    int progress = 0;
+    if(model->total_rom > 0 && model->transfered > 0) {
+        progress = model->transfered * 100 / model->total_rom;
+    }
+    snprintf(
+        progressText,
+        sizeof(progressText),
+        "%d%% Write ROM... GB: %s",
+        progress,
+        model->gameboy_rom_option_selected_text);
+    canvas_draw_str_aligned(canvas, 128 / 2, 0, AlignCenter, AlignTop, progressText);
+    canvas_set_font(canvas, FontSecondary);
+
+    // char *filename = strrchr(model->cart_dump_rom_filename_sequential, '/');
+    // filename++;
+    // canvas_draw_str_aligned(canvas, 128/2, 12, AlignCenter, AlignTop, filename);
+
+    char total_rom_str[20];
+    snprintf(
+        total_rom_str,
+        sizeof(total_rom_str),
+        "of %.2lf MiB",
+        (double)(model->total_rom / 1024.0 / 1024.0));
+
+    char transfered_rom_str[20];
+    snprintf(
+        transfered_rom_str,
+        sizeof(transfered_rom_str),
+        "%.2lf MiB",
+        (double)(model->transfered / 1024.0 / 1024.0));
+
+    // Calcula la Tasa de Transferencia en KiB/s
+    char transfer_rate_str[20];
+    if(model->transfered > 0 && model->elapsed_time > 0) {
+        double transfer_rate_kibps =
+            (double)model->transfered / ((double)model->elapsed_time) / (double)1024.0;
+        snprintf(transfer_rate_str, sizeof(transfer_rate_str), "%.2lf KiB/s", transfer_rate_kibps);
+    } else {
+        snprintf(transfer_rate_str, sizeof(transfer_rate_str), "0 KiB/s");
+    }
+
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 22 + 2, AlignLeft, AlignTop, transfered_rom_str);
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 40 + 2, AlignLeft, AlignTop, total_rom_str);
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 48 + 2, AlignLeft, AlignTop, transfer_rate_str);
+
+    if(model->rx_active) {
+        canvas_draw_icon_ex(canvas, UI_PADDING, 28, &I_ArrowUpFilled_14x15, IconRotation180);
+    } else {
+        canvas_draw_icon_ex(canvas, UI_PADDING, 28, &I_ArrowUpEmpty_14x15, IconRotation180);
+    }
+
+    char totalText[42];
+    snprintf(totalText, sizeof(totalText), "%d", model->total_rom);
+    // canvas_draw_str(canvas, 69, 48, totalText);
+
+    drawProgressBar(
+        canvas,
+        (progress * UI_PROGRESS_ROWS * UI_PROGRESS_COLS) /
+            100); // Pinta las primeras 10 cajas de negro
+    // free(totalText);
+
+    elements_button_center(canvas, "Write");
+}
+
+static void gb_cartridge_scene_4_model_init(GameBoyCartridgeROMWriteModel* const model) {
+    model->progress = 0;
+    model->total_rom = 0;
+    model->transfered = 0;
+    model->romBanks = 0;
+    model->elapsed_time = 0;
+    model->start_time = 0;
+}
+// static bool select_rom_file(GBCartridge *app, Stream* stream) {
+//     bool result = false;
+//     FuriString* file_path = furi_string_alloc();
+//     furi_string_set(file_path, MALVEKE_APP_FOLDER);
+//     DialogsFileBrowserOptions browser_options;
+//     dialog_file_browser_set_basic_options(
+//         &browser_options, app->gameboy_rom_option_selected_text, NULL);
+//     browser_options.base_path = MALVEKE_APP_FOLDER;
+//     browser_options.skip_assets = true;
+
+//     // Input events and views are managed by file_browser
+//     bool res = dialog_file_browser_show( app->dialogs, file_path, file_path, &browser_options);
+//     UNUSED(res);
+//     FURI_LOG_I(TAG, "File selected: %s", furi_string_get_cstr(file_path));
+//     if(res) {
+//         if(!file_stream_open(stream, furi_string_get_cstr(file_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+//             FURI_LOG_D(TAG, "Cannot open file \"%s\"", furi_string_get_cstr(file_path));
+//             file_stream_close(stream);
+//         } else {
+//             FURI_LOG_D(TAG, "Open file \"%s\"", furi_string_get_cstr(file_path));
+//             result = true;
+//         }
+//     }
+//     furi_string_free(file_path);
+//     return result;
+// }
+bool gb_cartridge_scene_4_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    GBCartridgeScene4* instance = context;
+
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                instance->view,
+                GameBoyCartridgeROMWriteModel * model,
+                {
+                    UNUSED(model);
+                    GBCartridge* app = (GBCartridge*)instance->context;
+                    // Unregister rx callback
+                    uart_set_handle_rx_data_cb(app->uart, NULL);
+                    instance->callback(GBCartridgeCustomEventScene4Back, instance->context);
+                },
+                true);
+            break;
+        case InputKeyUp:
+        case InputKeyDown:
+        case InputKeyLeft:
+        case InputKeyRight:
+            break;
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                GameBoyCartridgeROMWriteModel * model,
+                {
+                    GBCartridge* app = ((GBCartridge*)instance->context);
+                    UNUSED(model);
+                    UNUSED(app);
+                    // uint8_t buffer[BUFFER_SIZE];
+                    // size_t bytesRead;
+                    // Stream* file_stream = file_stream_alloc(app->storage);
+
+                    // if(select_rom_file(app, file_stream)) {
+                    //     const char gbcartridge_start_command[] = "gbcartridge -w -o -s\n";
+                    //     uart_tx(app->uart, (uint8_t*)gbcartridge_start_command, strlen(gbcartridge_start_command));
+                    //     furi_delay_ms(500);
+
+                    //     while (file_stream_read(file_stream, buffer, sizeof(buffer), &bytesRead) && bytesRead > 0) {
+                    //         // Send 64 bytes at a time
+                    //         uart_tx(app->lp_uart, (uint8_t*)buffer, bytesRead);
+                    //     }
+
+                    //     const char gbcartridge_end_command[] = "gbcartridge -w -o -e\n";
+                    //     uart_tx(app->uart, (uint8_t*)gbcartridge_end_command, strlen(gbcartridge_end_command));
+
+                    //     file_stream_close(file_stream);
+                    // }
+                    // stream_free(file_stream);
+                },
+                true);
+            break;
+        case InputKeyMAX:
+            break;
+        }
+    }
+
+    return true;
+}
+
+void gb_cartridge_scene_4_exit(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    gb_cartridge_stop_all_sound(app);
+}
+
+void gb_cartridge_scene_4_enter(void* context) {
+    furi_assert(context);
+    GBCartridgeScene4* instance = context;
+    GBCartridge* app = (GBCartridge*)instance->context;
+
+    UNUSED(app);
+    with_view_model(
+        app->gb_cartridge_scene_4->view,
+        GameBoyCartridgeROMWriteModel * model,
+        {
+            UNUSED(model);
+            gb_cartridge_scene_4_model_init(model);
+            model->gameboy_rom_option_selected_text = app->gameboy_rom_option_selected_text;
+        },
+        false);
+}
+
+GBCartridgeScene4* gb_cartridge_scene_4_alloc() {
+    GBCartridgeScene4* instance = malloc(sizeof(GBCartridgeScene4));
+    instance->view = view_alloc();
+    view_allocate_model(
+        instance->view, ViewModelTypeLocking, sizeof(GameBoyCartridgeROMWriteModel));
+    view_set_context(instance->view, instance);
+    view_set_draw_callback(instance->view, (ViewDrawCallback)gb_cartridge_scene_4_draw);
+    view_set_input_callback(instance->view, gb_cartridge_scene_4_input);
+    view_set_enter_callback(instance->view, gb_cartridge_scene_4_enter);
+    view_set_exit_callback(instance->view, gb_cartridge_scene_4_exit);
+
+    with_view_model(
+        instance->view,
+        GameBoyCartridgeROMWriteModel * model,
+        { gb_cartridge_scene_4_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void gb_cartridge_scene_4_free(GBCartridgeScene4* instance) {
+    furi_assert(instance);
+
+    view_free(instance->view);
+    free(instance);
+}
+
+View* gb_cartridge_scene_4_get_view(GBCartridgeScene4* instance) {
+    furi_assert(instance);
+
+    return instance->view;
+}

+ 19 - 0
malveke_gb_cartridge/views/gb_cartridge_scene_4.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/gb_cartridge_custom_event.h"
+
+typedef struct GBCartridgeScene4 GBCartridgeScene4;
+
+typedef void (*GBCartridgeScene4Callback)(GBCartridgeCustomEvent event, void* context);
+
+void gb_cartridge_scene_4_set_callback(
+    GBCartridgeScene4* instance,
+    GBCartridgeScene4Callback callback,
+    void* context);
+
+GBCartridgeScene4* gb_cartridge_scene_4_alloc();
+
+void gb_cartridge_scene_4_free(GBCartridgeScene4* gb_cartridge_static);
+
+View* gb_cartridge_scene_4_get_view(GBCartridgeScene4* boilerpate_static);

+ 397 - 0
malveke_gb_cartridge/views/gb_cartridge_scene_5.c

@@ -0,0 +1,397 @@
+#include "../gb_cartridge_app.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+#include <gui/elements.h>
+#include <notification/notification_messages.h>
+#include <dialogs/dialogs.h>
+#include <gui/modules/dialog_ex.h>
+#include <toolbox/stream/file_stream.h>
+#include "../helpers/gb_cartridge_speaker.h"
+#include "../helpers/sequential_file.h"
+#include <stdio.h> // Para sprintf
+#include <string.h> // Para strlen
+
+static uint64_t last_toggle_time = 0;
+struct GBCartridgeScene5 {
+    View* view;
+    GBCartridgeScene5Callback callback;
+    void* context;
+    GBCartridge* app;
+};
+
+typedef struct {
+    char* event_type;
+    int progress;
+    int total_ram;
+    int transfered;
+    int ramBanks;
+    int elapsed_time;
+    int start_time;
+
+    char* cart_dump_ram_filename_sequential;
+    bool rx_active;
+
+    char* event_title;
+
+    uint32_t offset;
+    uint32_t value;
+
+    File* selectedfile;
+
+} GameBoyCartridgeRAMWriteModel;
+
+static bool select_ram_file(GBCartridge* app, File* file) {
+    bool result = false;
+    FuriString* file_path = furi_string_alloc();
+    furi_string_set(file_path, MALVEKE_APP_FOLDER_RAMS);
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, "sav", NULL);
+    browser_options.base_path = MALVEKE_APP_FOLDER_RAMS;
+    browser_options.skip_assets = true;
+
+    // Input events and views are managed by file_browser
+    bool res = dialog_file_browser_show(app->dialogs, file_path, file_path, &browser_options);
+    // UNUSED(res);
+    // FURI_LOG_I(TAG, "File selected: %s", furi_string_get_cstr(file_path));
+    if(res) {
+        if(!storage_file_open(file, furi_string_get_cstr(file_path), FSAM_READ, FSOM_OPEN_EXISTING))
+        // if (!file_stream_open(stream, furi_string_get_cstr(file_path), FSAM_READ, FSOM_OPEN_EXISTING))
+        {
+            // FURI_LOG_D(TAG, "Cannot open file \"%s\"", furi_string_get_cstr(file_path));
+            // file_stream_close(stream);
+        } else {
+            // FURI_LOG_D(TAG, "Open file \"%s\"", furi_string_get_cstr(file_path));
+            result = true;
+        }
+    }
+    furi_string_free(file_path);
+    return result;
+}
+
+static int32_t cartridge_writting_worker_thread(void* thread_context) {
+    GBCartridge* app = thread_context;
+    UNUSED(app);
+    File* file = storage_file_alloc(app->storage);
+
+    if(select_ram_file(app, file)) {
+        uint16_t fileSize = storage_file_size(file);
+
+        FURI_LOG_I(TAG, "fileSize: %d ", fileSize);
+        with_view_model(
+            app->gb_cartridge_scene_5->view,
+            GameBoyCartridgeRAMWriteModel * model,
+            {
+                model->total_ram = fileSize;
+                model->selectedfile = file;
+            },
+            true);
+
+        char gbcartridge_start_command[80]; // A reasonably sized buffer.
+        snprintf(
+            gbcartridge_start_command,
+            sizeof(gbcartridge_start_command),
+            "gbcartridge -w -a %d\n",
+            fileSize);
+
+        uart_tx(app->uart, (uint8_t*)gbcartridge_start_command, strlen(gbcartridge_start_command));
+
+        furi_delay_ms(500); // wait
+        uint8_t* the_savefile = NULL;
+        size_t savefile_size = 0;
+        with_view_model(
+            app->gb_cartridge_scene_5->view,
+            GameBoyCartridgeRAMWriteModel * model,
+            {
+                model->event_title = "Transferring...";
+                model->transfered = 0;
+                model->start_time = furi_hal_rtc_get_timestamp(); // Registra el tiempo de inicio
+            },
+            true);
+        the_savefile = malloc(fileSize); // to be freed by caller
+        uint8_t* buf_ptr = the_savefile;
+        size_t read = 0;
+        while(read < fileSize) {
+            size_t to_read = fileSize - read;
+            if(to_read > UINT16_MAX) to_read = UINT16_MAX;
+            uint16_t now_read = storage_file_read(file, buf_ptr, (uint16_t)to_read);
+            read += now_read;
+            buf_ptr += now_read;
+        }
+        savefile_size = read;
+        uart_tx(app->uart, (uint8_t*)the_savefile, savefile_size);
+        uart_tx(app->uart, (uint8_t*)("\n"), 1);
+        with_view_model(
+            app->gb_cartridge_scene_5->view,
+            GameBoyCartridgeRAMWriteModel * model,
+            { model->event_title = "Writing Cartridge..."; },
+            true);
+        free(the_savefile);
+
+        if(file && storage_file_is_open(file)) {
+            storage_file_close(file);
+        }
+    }
+
+    return 0;
+}
+void gb_cartridge_scene_5_set_callback(
+    GBCartridgeScene5* instance,
+    GBCartridgeScene5Callback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+static void drawProgressBar(Canvas* canvas, int progress) {
+    for(int x = 0; x < 64 - 14 - UI_PADDING - UI_PADDING - UI_PADDING - UI_PADDING; x += 5) {
+        for(int row = 0; row < 20; row += 5) {
+            if(progress > 0) {
+                canvas_draw_box(
+                    canvas, 14 /*ARROW*/ + UI_PADDING + 2 + x + 4, /*45*/ 26 + row, 4, 4);
+                progress--;
+            } else {
+                canvas_draw_frame(
+                    canvas, 14 /*ARROW*/ + UI_PADDING + 2 + x + 4, /*45*/ 26 + row, 4, 4);
+            }
+        }
+    }
+}
+
+void gb_cartridge_scene_5_draw(Canvas* canvas, GameBoyCartridgeRAMWriteModel* model) {
+    // Clear the screen.
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontKeyboard);
+    canvas_draw_frame(canvas, 0, 24, (128 / 2), 25);
+
+    canvas_set_bitmap_mode(canvas, 1);
+    canvas_set_font(canvas, FontPrimary);
+    char progressText[42];
+    int progress = 0;
+    if(model->total_ram > 0 && model->transfered > 0) {
+        progress = model->transfered * 100 / model->total_ram;
+    }
+    snprintf(progressText, sizeof(progressText), "%d%% Write RAM...", progress);
+    canvas_draw_str_aligned(canvas, 128 / 2, 0, AlignCenter, AlignTop, progressText);
+    canvas_set_font(canvas, FontSecondary);
+
+    canvas_draw_str_aligned(canvas, 128 / 2, 12, AlignCenter, AlignTop, model->event_title);
+
+    char total_ram_str[20];
+    snprintf(
+        total_ram_str,
+        sizeof(total_ram_str),
+        "of %.2lf MiB",
+        (double)(model->total_ram / 1024.0 / 1024.0));
+
+    char transfered_ram_str[20];
+    snprintf(
+        transfered_ram_str,
+        sizeof(transfered_ram_str),
+        "%.2lf MiB",
+        (double)(model->transfered / 1024.0 / 1024.0));
+
+    // Calcula la Tasa de Transferencia en KiB/s
+    char transfer_rate_str[20];
+    if(model->transfered > 0 && model->elapsed_time > 0) {
+        double transfer_rate_kibps =
+            (double)model->transfered / ((double)model->elapsed_time) / (double)1024.0;
+        snprintf(transfer_rate_str, sizeof(transfer_rate_str), "%.2lf KiB/s", transfer_rate_kibps);
+    } else {
+        snprintf(transfer_rate_str, sizeof(transfer_rate_str), "0 KiB/s");
+    }
+
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 22 + 2, AlignLeft, AlignTop, transfered_ram_str);
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 30 + 2, AlignLeft, AlignTop, total_ram_str);
+    canvas_draw_str_aligned(
+        canvas, (128 / 2) + UI_PADDING, 38 + 2, AlignLeft, AlignTop, transfer_rate_str);
+
+    if(model->rx_active) {
+        canvas_draw_icon_ex(canvas, UI_PADDING, 28, &I_ArrowUpFilled_14x15, IconRotation180);
+    } else {
+        canvas_draw_icon_ex(canvas, UI_PADDING, 28, &I_ArrowUpEmpty_14x15, IconRotation180);
+    }
+
+    char totalText[42];
+    snprintf(totalText, sizeof(totalText), "%d", model->total_ram);
+
+    drawProgressBar(
+        canvas,
+        (progress * UI_PROGRESS_ROWS * UI_PROGRESS_COLS) /
+            100); // Pinta las primeras 10 cajas de negro
+
+    elements_button_center(canvas, "Write");
+}
+
+static void gb_cartridge_scene_5_model_init(GameBoyCartridgeRAMWriteModel* const model) {
+    model->progress = 0;
+    model->total_ram = 0;
+    model->transfered = 0;
+    model->ramBanks = 0;
+    model->elapsed_time = 0;
+    model->start_time = 0;
+    model->event_title = "...";
+}
+void gameboy_handle_rx_data_cb(uint8_t* buf, size_t len, void* context) {
+    furi_assert(context);
+    UNUSED(len);
+    UNUSED(buf);
+    GBCartridge* instance = context;
+
+    with_view_model(
+        instance->gb_cartridge_scene_5->view,
+        GameBoyCartridgeRAMWriteModel * model,
+        {
+            UNUSED(model);
+
+            uint64_t current_time = furi_hal_rtc_get_timestamp();
+            model->elapsed_time = current_time - model->start_time;
+            if(current_time - last_toggle_time >= 0.2) {
+                model->rx_active = !model->rx_active;
+                last_toggle_time = current_time;
+            }
+
+            cJSON* json = cJSON_Parse((char*)buf);
+            if(json == NULL) {
+            } else {
+                cJSON* type = cJSON_GetObjectItemCaseSensitive(json, "type");
+                if(cJSON_IsString(type) && (type->valuestring != NULL)) {
+                    model->event_type = strdup(type->valuestring);
+                } else {
+                    model->event_type = "None";
+                }
+
+                //  offset
+                cJSON* offset = cJSON_GetObjectItemCaseSensitive(json, "offset");
+                if(cJSON_IsNumber(offset)) {
+                    model->offset = offset->valueint;
+                } else {
+                    model->offset = 0;
+                }
+
+                //  value
+                cJSON* value = cJSON_GetObjectItemCaseSensitive(json, "value");
+                if(cJSON_IsNumber(value)) {
+                    model->value = value->valueint;
+                } else {
+                    model->value = 0;
+                }
+            }
+            if(strcmp(model->event_type, "progress") == 0) {
+                //  progress
+                cJSON* progress = cJSON_GetObjectItemCaseSensitive(json, "progress");
+                if(cJSON_IsNumber(progress)) {
+                    model->transfered += progress->valueint;
+                }
+            }
+            if(strcmp(model->event_type, "success") == 0) {
+                notification_success(instance->notification);
+                model->transfered = model->total_ram;
+                model->event_title = "Done!";
+            }
+        },
+        true);
+}
+bool gb_cartridge_scene_5_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    GBCartridgeScene5* instance = context;
+
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                instance->view,
+                GameBoyCartridgeRAMWriteModel * model,
+                {
+                    UNUSED(model);
+                    GBCartridge* app = (GBCartridge*)instance->context;
+                    // Unregister rx callback
+                    uart_set_handle_rx_data_cb(app->uart, NULL);
+                    uart_set_handle_rx_data_cb(app->lp_uart, NULL);
+
+                    instance->callback(GBCartridgeCustomEventScene5Back, instance->context);
+                },
+                true);
+            break;
+        case InputKeyUp:
+        case InputKeyDown:
+        case InputKeyLeft:
+        case InputKeyRight:
+            break;
+        case InputKeyOk: {
+            GBCartridge* app = ((GBCartridge*)instance->context);
+            uart_set_handle_rx_data_cb(app->uart, gameboy_handle_rx_data_cb);
+            cartridge_writting_worker_thread(app);
+        } break;
+        case InputKeyMAX:
+            break;
+        }
+    }
+
+    return true;
+}
+
+void gb_cartridge_scene_5_exit(void* context) {
+    furi_assert(context);
+    GBCartridge* app = context;
+    gb_cartridge_stop_all_sound(app);
+}
+
+void gb_cartridge_scene_5_enter(void* context) {
+    furi_assert(context);
+    GBCartridgeScene5* instance = context;
+    GBCartridge* app = (GBCartridge*)instance->context;
+
+    UNUSED(app);
+    with_view_model(
+        app->gb_cartridge_scene_5->view,
+        GameBoyCartridgeRAMWriteModel * model,
+        {
+            UNUSED(model);
+            gb_cartridge_scene_5_model_init(model);
+        },
+        false);
+}
+
+GBCartridgeScene5* gb_cartridge_scene_5_alloc() {
+    GBCartridgeScene5* instance = malloc(sizeof(GBCartridgeScene5));
+    instance->view = view_alloc();
+    view_allocate_model(
+        instance->view, ViewModelTypeLocking, sizeof(GameBoyCartridgeRAMWriteModel));
+
+    view_set_context(instance->view, instance);
+    view_set_draw_callback(instance->view, (ViewDrawCallback)gb_cartridge_scene_5_draw);
+    view_set_input_callback(instance->view, gb_cartridge_scene_5_input);
+    view_set_enter_callback(instance->view, gb_cartridge_scene_5_enter);
+    view_set_exit_callback(instance->view, gb_cartridge_scene_5_exit);
+
+    with_view_model(
+        instance->view,
+        GameBoyCartridgeRAMWriteModel * model,
+        { gb_cartridge_scene_5_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void gb_cartridge_scene_5_free(GBCartridgeScene5* instance) {
+    GBCartridge* app = instance->context;
+    UNUSED(app);
+    furi_assert(instance);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* gb_cartridge_scene_5_get_view(GBCartridgeScene5* instance) {
+    furi_assert(instance);
+
+    return instance->view;
+}

+ 19 - 0
malveke_gb_cartridge/views/gb_cartridge_scene_5.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/gb_cartridge_custom_event.h"
+
+typedef struct GBCartridgeScene5 GBCartridgeScene5;
+
+typedef void (*GBCartridgeScene5Callback)(GBCartridgeCustomEvent event, void* context);
+
+void gb_cartridge_scene_5_set_callback(
+    GBCartridgeScene5* instance,
+    GBCartridgeScene5Callback callback,
+    void* context);
+
+GBCartridgeScene5* gb_cartridge_scene_5_alloc();
+
+void gb_cartridge_scene_5_free(GBCartridgeScene5* gb_cartridge_static);
+
+View* gb_cartridge_scene_5_get_view(GBCartridgeScene5* boilerpate_static);

+ 155 - 0
malveke_gb_cartridge/views/gb_cartridge_startscreen.c

@@ -0,0 +1,155 @@
+#include "../gb_cartridge_app.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <notification/notification_messages.h>
+
+struct GBCartridgeStartscreen {
+    View* view;
+    GBCartridgeStartscreenCallback callback;
+    void* context;
+};
+
+typedef struct {
+    int some_value;
+} GBCartridgeStartscreenModel;
+
+static const NotificationSequence sequence_alarm = {
+    // &message_note_c5,
+    // &message_delay_100,
+    &message_sound_off,
+    // &message_note_c7,
+    // &message_delay_500,
+    &message_sound_off,
+    NULL,
+};
+
+void gb_cartridge_startscreen_set_callback(
+    GBCartridgeStartscreen* instance,
+    GBCartridgeStartscreenCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+// void screen_gameboy_connect(Canvas* const canvas) {
+//     canvas_draw_frame(canvas, 0, 0, 128, 64);
+//     canvas_draw_icon(canvas, 1, 21, &I_Connect_me_62x31);
+//     canvas_draw_icon(canvas, 0, 53, &I_Background_128x11);
+//     canvas_draw_icon(canvas, 80, 0, &I_game_boy);
+//     canvas_draw_icon(canvas, 8, 2, &I_Space_65x18);
+//     canvas_draw_str(canvas, 18, 13, "Connect GB");
+// }
+// void screen_gameboy_connected(Canvas* const canvas) {
+//     canvas_draw_frame(canvas, 0, 0, 128, 64);
+//     canvas_draw_icon(canvas, 1, 21, &I_Connected_62x31);
+//     canvas_draw_icon(canvas, 0, 53, &I_Background_128x11);
+//     canvas_draw_icon(canvas, 80, 0, &I_game_boy);
+//     canvas_draw_icon(canvas, 8, 2, &I_Space_65x18);
+//     canvas_draw_str(canvas, 18, 13, "Connected!");
+// }
+void gb_cartridge_startscreen_draw(Canvas* canvas, GBCartridgeStartscreenModel* model) {
+    UNUSED(model);
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_draw_icon(canvas, 0 /*128/2 - (42/2)*/, 64 / 2 - (64 / 2), &I_cartridge_42x64);
+
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 64 + 20, 6, AlignCenter, AlignTop, "Connect Malveke");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(canvas, 64 + 20, 18, AlignCenter, AlignTop, "to the Flipper and");
+    canvas_draw_str_aligned(canvas, 64 + 20, 28, AlignCenter, AlignTop, "and Insert Game Boy");
+    canvas_draw_str_aligned(canvas, 64 + 20, 38, AlignCenter, AlignTop, "Cartridge");
+    elements_button_center(canvas, "Ok");
+}
+
+static void gb_cartridge_startscreen_model_init(GBCartridgeStartscreenModel* const model) {
+    model->some_value = 1;
+}
+
+bool gb_cartridge_startscreen_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    GBCartridgeStartscreen* instance = context;
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                instance->view,
+                GBCartridgeStartscreenModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(GBCartridgeCustomEventStartscreenBack, instance->context);
+                },
+                true);
+            break;
+        case InputKeyLeft:
+        case InputKeyRight:
+        case InputKeyUp:
+        case InputKeyDown:
+        case InputKeyOk:
+            notification_message(((GBCartridge*)instance->context)->notification, &sequence_alarm);
+            with_view_model(
+                instance->view,
+                GBCartridgeStartscreenModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(GBCartridgeCustomEventStartscreenOk, instance->context);
+                },
+                true);
+            break;
+        case InputKeyMAX:
+            break;
+        }
+    }
+    return true;
+}
+
+void gb_cartridge_startscreen_exit(void* context) {
+    furi_assert(context);
+}
+
+void gb_cartridge_startscreen_enter(void* context) {
+    furi_assert(context);
+    GBCartridgeStartscreen* instance = (GBCartridgeStartscreen*)context;
+    with_view_model(
+        instance->view,
+        GBCartridgeStartscreenModel * model,
+        { gb_cartridge_startscreen_model_init(model); },
+        true);
+}
+
+GBCartridgeStartscreen* gb_cartridge_startscreen_alloc() {
+    GBCartridgeStartscreen* instance = malloc(sizeof(GBCartridgeStartscreen));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(GBCartridgeStartscreenModel));
+    view_set_context(instance->view, instance); // furi_assert crashes in events without this
+    view_set_draw_callback(instance->view, (ViewDrawCallback)gb_cartridge_startscreen_draw);
+    view_set_input_callback(instance->view, gb_cartridge_startscreen_input);
+    //view_set_enter_callback(instance->view, gb_cartridge_startscreen_enter);
+    //view_set_exit_callback(instance->view, gb_cartridge_startscreen_exit);
+
+    with_view_model(
+        instance->view,
+        GBCartridgeStartscreenModel * model,
+        { gb_cartridge_startscreen_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void gb_cartridge_startscreen_free(GBCartridgeStartscreen* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, GBCartridgeStartscreenModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* gb_cartridge_startscreen_get_view(GBCartridgeStartscreen* instance) {
+    furi_assert(instance);
+    return instance->view;
+}

+ 22 - 0
malveke_gb_cartridge/views/gb_cartridge_startscreen.h

@@ -0,0 +1,22 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/gb_cartridge_custom_event.h"
+
+// #include <assets_icons.h>
+#include <malveke_gb_cartridge_icons.h>
+
+typedef struct GBCartridgeStartscreen GBCartridgeStartscreen;
+
+typedef void (*GBCartridgeStartscreenCallback)(GBCartridgeCustomEvent event, void* context);
+
+void gb_cartridge_startscreen_set_callback(
+    GBCartridgeStartscreen* gb_cartridge_startscreen,
+    GBCartridgeStartscreenCallback callback,
+    void* context);
+
+View* gb_cartridge_startscreen_get_view(GBCartridgeStartscreen* gb_cartridge_static);
+
+GBCartridgeStartscreen* gb_cartridge_startscreen_alloc();
+
+void gb_cartridge_startscreen_free(GBCartridgeStartscreen* gb_cartridge_static);

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.