diff --git a/README.md b/README.md index 3ecbd19b8..b3d23b7e7 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Many apps included in Momentum are modified (some lots more than others). This i - Removing/tweaking icons and/or their usages to **support our Asset Packs system** - Removing duplicate keyboard implementations to **use our extended system keyboard** - With our system keyboard also **support our CLI command `input keyboard`** to type with PC keyboard +- Tweak UART/SPI usage to **support our GPIO Pins mapping settings** - **Moving location of save files** to a more appropriate location or changing how they are saved - **Changing application display names** to fit our naming scheme - **Changing how some menus work/look** or adding **new exclusive menus and features** diff --git a/avr_isp/.gitsubtree b/avr_isp/.gitsubtree index e0fb94c9b..397afbfed 100644 --- a/avr_isp/.gitsubtree +++ b/avr_isp/.gitsubtree @@ -1,2 +1,2 @@ -https://github.com/xMasterX/all-the-plugins dev base_pack/avr_isp_programmer 4558d74c9da36abc851edd96a95d18f7d5511a75 +https://github.com/xMasterX/all-the-plugins dev base_pack/avr_isp_programmer 17bec3e26b8250c59acebd4fa52b6b08f68152d7 https://github.com/flipperdevices/flipperzero-good-faps dev avr_isp_programmer b791dea234f855155027bb46215dc60f3ddeb243 diff --git a/avr_isp/helpers/avr_isp_worker.c b/avr_isp/helpers/avr_isp_worker.c index acd05c619..89a1bda77 100644 --- a/avr_isp/helpers/avr_isp_worker.c +++ b/avr_isp/helpers/avr_isp_worker.c @@ -55,6 +55,7 @@ static const CdcCallbacks cdc_cb = { vcp_state_callback, vcp_on_cdc_control_line, vcp_on_line_config, + NULL, }; /* VCP callbacks */ diff --git a/esp_flasher/application.fam b/esp_flasher/application.fam index 0129093fb..b26275be6 100644 --- a/esp_flasher/application.fam +++ b/esp_flasher/application.fam @@ -1,7 +1,7 @@ App( appid="esp_flasher", name="[ESP] ESP Flasher", - fap_version=(1, 6), + fap_version=(1, 7), apptype=FlipperAppType.EXTERNAL, entry_point="esp_flasher_app", requires=["gui"], diff --git a/esp_flasher/resources/apps_data/esp_flasher/assets/flipperhttp/s2/flipper_http_firmware_a.bin b/esp_flasher/resources/apps_data/esp_flasher/assets/flipperhttp/s2/flipper_http_firmware_a.bin index caab9f3f0..c10bca3a6 100644 Binary files a/esp_flasher/resources/apps_data/esp_flasher/assets/flipperhttp/s2/flipper_http_firmware_a.bin and b/esp_flasher/resources/apps_data/esp_flasher/assets/flipperhttp/s2/flipper_http_firmware_a.bin differ diff --git a/esp_flasher/scenes/esp_flasher_scene_browse.c b/esp_flasher/scenes/esp_flasher_scene_browse.c index c8a67fcdc..5460be620 100644 --- a/esp_flasher/scenes/esp_flasher_scene_browse.c +++ b/esp_flasher/scenes/esp_flasher_scene_browse.c @@ -180,9 +180,9 @@ static void esp_flasher_scene_browse_callback(void* context, uint32_t index) { #define STR_APP_A "FirmwareA(" TOSTRING(ESP_ADDR_APP_A) ")" #define STR_APP_B "FirmwareB(" TOSTRING(ESP_ADDR_APP_B) ")" #define STR_CUSTOM "Custom" -#define STR_FLASH_S3 "[>] FLASH - slow (S3)" +#define STR_FLASH_S3 "[>] FLASH - slow (0x0)" #define STR_FLASH "[>] FLASH - slow" -#define STR_FLASH_TURBO_S3 "[>] FLASH - fast (S3)" +#define STR_FLASH_TURBO_S3 "[>] FLASH - fast (0x0)" #define STR_FLASH_TURBO "[>] FLASH - fast" static void _refresh_submenu(EspFlasherApp* app) { Submenu* submenu = app->submenu; @@ -192,8 +192,8 @@ static void _refresh_submenu(EspFlasherApp* app) { submenu_set_header(submenu, "Browse for files to flash"); submenu_add_item( submenu, - app->selected_flash_options[SelectedFlashS3Mode] ? "[x] Using ESP32-S3" : - "[ ] Select if using S3", + app->selected_flash_options[SelectedFlashS3Mode] ? "[x] Using S3, C3 or C6" : + "[ ] Select for S3, C3, C6", SubmenuIndexS3Mode, esp_flasher_scene_browse_callback, app); diff --git a/esp_flasher/scenes/esp_flasher_scene_quick.c b/esp_flasher/scenes/esp_flasher_scene_quick.c index 9fa995ff0..5cce341d3 100644 --- a/esp_flasher/scenes/esp_flasher_scene_quick.c +++ b/esp_flasher/scenes/esp_flasher_scene_quick.c @@ -16,8 +16,8 @@ enum QuickState { QuickStart, QuickS2Boot, QuickS2Boot_Marauder, - QuickS2Boot_Blackmagic, QuickS2Boot_Flipperhttp, + QuickS2Boot_Blackmagic, QuickWROOMBoot, QuickWROOMBoot_Marauder, QuickWROOMBoot_Wardriver, @@ -29,8 +29,8 @@ enum QuickState { QuickWROOM_Wardriver, QuickS2, QuickS2_Marauder, - QuickS2_Blackmagic, QuickS2_Flipperhttp, + QuickS2_Blackmagic, QuickS3, QuickS3_Marauder, QuickS3_Wardriver, @@ -91,11 +91,11 @@ void esp_flasher_scene_quick_on_enter(void* context) { submenu, "Other ESP32-S3", QuickS3, esp_flasher_scene_quick_submenu_callback, app); break; case QuickS2Boot_Marauder: - case QuickS2Boot_Blackmagic: case QuickS2Boot_Flipperhttp: + case QuickS2Boot_Blackmagic: case QuickS2_Marauder: - case QuickS2_Blackmagic: case QuickS2_Flipperhttp: + case QuickS2_Blackmagic: submenu_set_header(submenu, "Choose Firmware:"); submenu_add_item( submenu, @@ -105,14 +105,14 @@ void esp_flasher_scene_quick_on_enter(void* context) { app); submenu_add_item( submenu, - "Black Magic (FZ debugger)", - state > QuickS2 ? QuickS2_Blackmagic : QuickS2Boot_Blackmagic, + "FlipperHTTP (web access)", + state > QuickS2 ? QuickS2_Flipperhttp : QuickS2Boot_Flipperhttp, esp_flasher_scene_quick_submenu_callback, app); submenu_add_item( submenu, - "FlipperHTTP (web access)", - state > QuickS2 ? QuickS2_Flipperhttp : QuickS2Boot_Flipperhttp, + "Black Magic (FZ debugger)", + state > QuickS2 ? QuickS2_Blackmagic : QuickS2Boot_Blackmagic, esp_flasher_scene_quick_submenu_callback, app); break; @@ -199,15 +199,6 @@ bool esp_flasher_scene_quick_on_event(void* context, SceneManagerEvent event) { firm = APP_DATA_PATH("assets/marauder/s2/esp32_marauder.flipper.bin"); break; - case QuickS2Boot_Blackmagic: - enter_bootloader = true; - /* fallthrough */ - case QuickS2_Blackmagic: - boot = APP_DATA_PATH("assets/blackmagic/s2/bootloader.bin"); - part = APP_DATA_PATH("assets/blackmagic/s2/partition-table.bin"); - firm = APP_DATA_PATH("assets/blackmagic/s2/blackmagic.bin"); - break; - case QuickS2Boot_Flipperhttp: enter_bootloader = true; /* fallthrough */ @@ -217,6 +208,15 @@ bool esp_flasher_scene_quick_on_event(void* context, SceneManagerEvent event) { firm = APP_DATA_PATH("assets/flipperhttp/s2/flipper_http_firmware_a.bin"); break; + case QuickS2Boot_Blackmagic: + enter_bootloader = true; + /* fallthrough */ + case QuickS2_Blackmagic: + boot = APP_DATA_PATH("assets/blackmagic/s2/bootloader.bin"); + part = APP_DATA_PATH("assets/blackmagic/s2/partition-table.bin"); + firm = APP_DATA_PATH("assets/blackmagic/s2/blackmagic.bin"); + break; + case QuickWROOMBoot_Marauder: enter_bootloader = true; /* fallthrough */ diff --git a/flip_library/.DS_Store b/flip_library/.DS_Store deleted file mode 100644 index f691a6fa3..000000000 Binary files a/flip_library/.DS_Store and /dev/null differ diff --git a/flip_library/CHANGELOG.md b/flip_library/CHANGELOG.md index c518c4946..028b49637 100644 --- a/flip_library/CHANGELOG.md +++ b/flip_library/CHANGELOG.md @@ -1,3 +1,20 @@ +## v1.3 +Refactored by Derek Jamison: +- Improved progress display. +- Added connectivity check on startup. +- Added Wikipedia API. + +## v1.2 +- Improved memory allocation. +- Added in Dog Facts. +- Added in Random Quotes. + +## v1.1 +- Update for app catalog. + +## v1.0 +- Initial Release. + ## v1.2 - Improved memory allocation. - Added in Dog Facts. @@ -7,4 +24,4 @@ - Update for app catalog. ## v1.0 -- Initial Release. \ No newline at end of file +- Initial Release. diff --git a/flip_library/README.md b/flip_library/README.md index 4c19e7407..4b5ff439f 100644 --- a/flip_library/README.md +++ b/flip_library/README.md @@ -1,12 +1,12 @@ The **FlipLibrary** app for Flipper Zero is a versatile and user-friendly application that offers a combination of useful features to enhance your Flipper Zero experience. -The app includes a **dictionary**, **random facts**, and additional functionalities, all accessible directly from your Flipper Zero device. It is designed for easy navigation and quick access to information, making it a handy companion for on-the-go learning and entertainment. +The app includes a **dictionary**, **random facts**, and additional functionalities, all accessible directly from your Flipper Zero device. It is designed for easy navigation and quick access to information, making it a handy companion for on-the-go learning and entertainment. Big shout out to [Derek Jamison](https://github.com/jamisonderek) for his contributions. FlipLibrary uses the FlipperHTTP flash for the WiFi Devboard, first introduced in the WebCrawler app: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP ## Requirements -- WiFi Dev Board or Raspberry Pi Pico W for Flipper Zero with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP +- WiFi Developer Board or Raspberry Pi Pico W with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP - WiFi Access Point @@ -34,3 +34,8 @@ The app automatically allocates necessary resources and initializes settings. If - Visit **Random Facts** to read interesting trivia. - Configure **WiFi settings** if network-related features are required in the future. - Check the **About** section to learn more about the app. + +# Known Bugs + +1. **Screen Delay**: Occasionally, the Defition or Random Facts screen may get stuck on "Loading". + - Update to version 1.3 or higher. diff --git a/flip_library/flip_library_i.h b/flip_library/alloc/flip_library_alloc.c similarity index 81% rename from flip_library/flip_library_i.h rename to flip_library/alloc/flip_library_alloc.c index a4251eff1..b817e6c12 100644 --- a/flip_library/flip_library_i.h +++ b/flip_library/alloc/flip_library_alloc.c @@ -1,8 +1,7 @@ -#ifndef FLIP_LIBRARY_I_H -#define FLIP_LIBRARY_I_H +#include "alloc/flip_library_alloc.h" // Function to allocate resources for the FlipLibraryApp -static FlipLibraryApp* flip_library_app_alloc() { +FlipLibraryApp* flip_library_app_alloc() { FlipLibraryApp* app = (FlipLibraryApp*)malloc(sizeof(FlipLibraryApp)); Gui* gui = furi_record_open(RECORD_GUI); @@ -15,7 +14,7 @@ static FlipLibraryApp* flip_library_app_alloc() { // Allocate the text input buffer app->uart_text_input_buffer_size_ssid = 64; app->uart_text_input_buffer_size_password = 64; - app->uart_text_input_buffer_size_dictionary = 64; + app->uart_text_input_buffer_size_query = 64; if(!easy_flipper_set_buffer( &app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid)) { return NULL; @@ -34,12 +33,11 @@ static FlipLibraryApp* flip_library_app_alloc() { return NULL; } if(!easy_flipper_set_buffer( - &app->uart_text_input_buffer_dictionary, app->uart_text_input_buffer_size_dictionary)) { + &app->uart_text_input_buffer_query, app->uart_text_input_buffer_size_query)) { return NULL; } if(!easy_flipper_set_buffer( - &app->uart_text_input_temp_buffer_dictionary, - app->uart_text_input_buffer_size_dictionary)) { + &app->uart_text_input_temp_buffer_query, app->uart_text_input_buffer_size_query)) { return NULL; } @@ -47,54 +45,39 @@ static FlipLibraryApp* flip_library_app_alloc() { if(!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) { return NULL; } + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, flip_library_custom_event_callback); // Main view if(!easy_flipper_set_view( - &app->view_random_facts, - FlipLibraryViewRandomFactsRun, - view_draw_callback_random_facts, + &app->view_loader, + FlipLibraryViewLoader, + flip_library_loader_draw_callback, NULL, callback_to_random_facts, &app->view_dispatcher, app)) { return NULL; } - if(!easy_flipper_set_view( - &app->view_dictionary, - FlipLibraryViewDictionaryRun, - view_draw_callback_dictionary_run, - NULL, - callback_to_submenu, - &app->view_dispatcher, - app)) { - return NULL; - } + flip_library_loader_init(app->view_loader); // Widget if(!easy_flipper_set_widget( - &app->widget, + &app->widget_about, FlipLibraryViewAbout, - "FlipLibrary v1.2\n-----\nDictionary, random facts, and\nmore.\n-----\nwww.github.com/jblanked", + "FlipLibrary v1.3\n-----\nDictionary, random facts, and\nmore.\n-----\nwww.github.com/jblanked", callback_to_submenu, &app->view_dispatcher)) { return NULL; } if(!easy_flipper_set_widget( - &app->widget_random_fact, - FlipLibraryViewRandomFactWidget, + &app->widget_result, + FlipLibraryViewWidgetResult, "Error, try again.", callback_to_random_facts, &app->view_dispatcher)) { return NULL; } - if(!easy_flipper_set_widget( - &app->widget_dictionary, - FlipLibraryViewDictionaryWidget, - "Error, try again.", - callback_to_submenu, - &app->view_dispatcher)) { - return NULL; - } // Text Input if(!easy_flipper_set_uart_text_input( @@ -122,12 +105,12 @@ static FlipLibraryApp* flip_library_app_alloc() { return NULL; } if(!easy_flipper_set_uart_text_input( - &app->uart_text_input_dictionary, - FlipLibraryViewDictionaryTextInput, - "Enter a word", - app->uart_text_input_temp_buffer_dictionary, - app->uart_text_input_buffer_size_dictionary, - text_updated_dictionary, + &app->uart_text_input_query, + FlipLibraryViewTextInputQuery, + "Enter Query", + app->uart_text_input_temp_buffer_query, + app->uart_text_input_buffer_size_query, + text_updated_query, callback_to_submenu, &app->view_dispatcher, app)) { @@ -156,7 +139,7 @@ static FlipLibraryApp* flip_library_app_alloc() { if(!easy_flipper_set_submenu( &app->submenu_main, FlipLibraryViewSubmenuMain, - "FlipLibrary v1.2", + "FlipLibrary v1.3", callback_exit_app, &app->view_dispatcher)) { return NULL; @@ -176,6 +159,12 @@ static FlipLibraryApp* flip_library_app_alloc() { FlipLibrarySubmenuIndexRandomFacts, callback_submenu_choices, app); + submenu_add_item( + app->submenu_main, + "Wikipedia", + FlipLibrarySubmenuIndexRandomFactsWiki, + callback_submenu_choices, + app); submenu_add_item( app->submenu_main, "Dictionary", @@ -219,9 +208,10 @@ static FlipLibraryApp* flip_library_app_alloc() { app->uart_text_input_buffer_password, app->uart_text_input_buffer_size_password)) { // Update variable items - if(app->variable_item_ssid) + if(app->variable_item_ssid) { variable_item_set_current_value_text( app->variable_item_ssid, app->uart_text_input_buffer_ssid); + } // dont show password // Copy items into their temp buffers with safety checks @@ -251,5 +241,3 @@ static FlipLibraryApp* flip_library_app_alloc() { return app; } - -#endif // FLIP_LIBRARY_I_H diff --git a/flip_library/alloc/flip_library_alloc.h b/flip_library/alloc/flip_library_alloc.h new file mode 100644 index 000000000..f5d4c86e2 --- /dev/null +++ b/flip_library/alloc/flip_library_alloc.h @@ -0,0 +1,10 @@ +#ifndef FLIP_LIBRARY_I_H +#define FLIP_LIBRARY_I_H + +#include +#include + +// Function to allocate resources for the FlipLibraryApp +FlipLibraryApp* flip_library_app_alloc(); + +#endif // FLIP_LIBRARY_I_H diff --git a/flip_library/app.c b/flip_library/app.c index 2cb759f01..8f4e60e27 100644 --- a/flip_library/app.c +++ b/flip_library/app.c @@ -1,8 +1,5 @@ -#include -#include -#include -#include -#include +#include +#include // Entry point for the FlipLibrary application int32_t flip_library_app(void* p) { @@ -10,8 +7,8 @@ int32_t flip_library_app(void* p) { UNUSED(p); // Initialize the FlipLibrary application - FlipLibraryApp* app = flip_library_app_alloc(); - if(!app) { + app_instance = flip_library_app_alloc(); + if(!app_instance) { FURI_LOG_E(TAG, "Failed to allocate FlipLibraryApp"); return -1; } @@ -21,11 +18,38 @@ int32_t flip_library_app(void* p) { return -1; } + if(app_instance->uart_text_input_buffer_ssid != NULL && + app_instance->uart_text_input_buffer_password != NULL) { + // Try to wait for pong response. + uint8_t counter = 10; + while(fhttp.state == INACTIVE && --counter > 0) { + FURI_LOG_D(TAG, "Waiting for PONG"); + furi_delay_ms(100); + } + + if(counter == 0) { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header( + message, "[FlipperHTTP Error]", 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + message, + "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.", + 0, + 63, + AlignLeft, + AlignBottom); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); + } + } + // Run the view dispatcher - view_dispatcher_run(app->view_dispatcher); + view_dispatcher_run(app_instance->view_dispatcher); // Free the resources used by the FlipLibrary application - flip_library_app_free(app); + flip_library_app_free(app_instance); // Return 0 to indicate success return 0; diff --git a/flip_library/application.fam b/flip_library/application.fam index 24b4a0997..6af5a8d89 100644 --- a/flip_library/application.fam +++ b/flip_library/application.fam @@ -10,5 +10,5 @@ App( fap_description="Dictionary, random facts, and more.", fap_author="JBlanked", fap_weburl="https://github.com/jblanked/FlipLibrary", - fap_version="1.1", + fap_version="1.3.1", ) diff --git a/flip_library/assets/01-main.png b/flip_library/assets/01-main.png index 962059c99..74b2d7d27 100644 Binary files a/flip_library/assets/01-main.png and b/flip_library/assets/01-main.png differ diff --git a/flip_library/assets/02-random-facts.png b/flip_library/assets/02-random-facts.png index 2cd30e8e3..0e851d6d3 100644 Binary files a/flip_library/assets/02-random-facts.png and b/flip_library/assets/02-random-facts.png differ diff --git a/flip_library/callback/flip_library_callback.c b/flip_library/callback/flip_library_callback.c new file mode 100644 index 000000000..f85c0cef8 --- /dev/null +++ b/flip_library/callback/flip_library_callback.c @@ -0,0 +1,818 @@ +#include + +// FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port. +#ifdef DEVELOPMENT +#define FURI_LOG_DEV(tag, format, ...) \ + furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__) +#define DEV_CRASH() furi_crash() +#else +#define FURI_LOG_DEV(tag, format, ...) +#define DEV_CRASH() +#endif + +static bool flip_library_wiki_fetch(FactLoaderModel* model) { + UNUSED(model); + snprintf( + fhttp.file_path, + sizeof(fhttp.file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_library/wiki.json"); + + // encode spaces for url + for(size_t i = 0; i < strlen(app_instance->uart_text_input_buffer_query); i++) { + if(app_instance->uart_text_input_buffer_query[i] == ' ') { + app_instance->uart_text_input_buffer_query[i] = '_'; + } + } + + char url[260]; + snprintf( + url, + sizeof(url), + "https://api.wikimedia.org/core/v1/wikipedia/en/search/title?q=%s&limit=1", + app_instance->uart_text_input_buffer_query); + Storage* storage = furi_record_open(RECORD_STORAGE); + storage_simply_remove_recursive(storage, fhttp.file_path); + fhttp.save_received_data = true; + + return flipper_http_get_request_with_headers(url, "{\"Content-Type\":\"application/json\"}"); +} + +static char* flip_library_wiki_parse(FactLoaderModel* model) { + UNUSED(model); + FuriString* data = flipper_http_load_from_file(fhttp.file_path); + if(data == NULL) { + FURI_LOG_E(TAG, "Failed to load received data from file."); + return "Failed to load received data from file."; + } + char* data_cstr = (char*)furi_string_get_cstr(data); + if(data_cstr == NULL) { + FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); + furi_string_free(data); + return "Failed to get C-string from FuriString."; + } + char* pages = get_json_array_value("pages", 0, data_cstr, MAX_TOKENS); + if(pages == NULL) { + furi_string_free(data); + return data_cstr; + } + char* description = get_json_value("description", pages, 64); + if(description == NULL) { + furi_string_free(data); + return data_cstr; + } + return description; +} + +static void flip_library_wiki_switch_to_view(FlipLibraryApp* app) { + text_input_set_header_text(app->uart_text_input_query, "Search Wikipedia"); + flip_library_generic_switch_to_view( + app, + "Searching..", + flip_library_wiki_fetch, + flip_library_wiki_parse, + 1, + callback_to_submenu, + FlipLibraryViewTextInputQuery); +} + +static bool flip_library_random_fact_fetch(FactLoaderModel* model) { + UNUSED(model); + return flipper_http_get_request("https://uselessfacts.jsph.pl/api/v2/facts/random"); +} + +static char* flip_library_random_fact_parse(FactLoaderModel* model) { + UNUSED(model); + return get_json_value("text", fhttp.last_response, 128); +} + +static void flip_library_random_fact_switch_to_view(FlipLibraryApp* app) { + flip_library_generic_switch_to_view( + app, + "Random Fact", + flip_library_random_fact_fetch, + flip_library_random_fact_parse, + 1, + callback_to_random_facts, + FlipLibraryViewLoader); +} + +static bool flip_library_cat_fact_fetch(FactLoaderModel* model) { + UNUSED(model); + return flipper_http_get_request_with_headers( + "https://catfact.ninja/fact", "{\"Content-Type\":\"application/json\"}"); +} + +static char* flip_library_cat_fact_parse(FactLoaderModel* model) { + UNUSED(model); + return get_json_value("fact", fhttp.last_response, 128); +} + +static void flip_library_cat_fact_switch_to_view(FlipLibraryApp* app) { + flip_library_generic_switch_to_view( + app, + "Random Cat Fact", + flip_library_cat_fact_fetch, + flip_library_cat_fact_parse, + 1, + callback_to_random_facts, + FlipLibraryViewLoader); +} + +static bool flip_library_dog_fact_fetch(FactLoaderModel* model) { + UNUSED(model); + return flipper_http_get_request_with_headers( + "https://dog-api.kinduff.com/api/facts", "{\"Content-Type\":\"application/json\"}"); +} + +static char* flip_library_dog_fact_parse(FactLoaderModel* model) { + UNUSED(model); + return get_json_array_value("facts", 0, fhttp.last_response, 256); +} + +static void flip_library_dog_fact_switch_to_view(FlipLibraryApp* app) { + flip_library_generic_switch_to_view( + app, + "Random Dog Fact", + flip_library_dog_fact_fetch, + flip_library_dog_fact_parse, + 1, + callback_to_random_facts, + FlipLibraryViewLoader); +} + +static bool flip_library_quote_fetch(FactLoaderModel* model) { + UNUSED(model); + return flipper_http_get_request("https://zenquotes.io/api/random"); +} + +static char* flip_library_quote_parse(FactLoaderModel* model) { + UNUSED(model); + // remove [ and ] from the start and end of the string + char* response = fhttp.last_response; + if(response[0] == '[') { + response++; + } + if(response[strlen(response) - 1] == ']') { + response[strlen(response) - 1] = '\0'; + } + // remove white space from both sides + while(response[0] == ' ') { + response++; + } + while(response[strlen(response) - 1] == ' ') { + response[strlen(response) - 1] = '\0'; + } + return get_json_value("q", response, 128); +} + +static void flip_library_quote_switch_to_view(FlipLibraryApp* app) { + flip_library_generic_switch_to_view( + app, + "Random Quote", + flip_library_quote_fetch, + flip_library_quote_parse, + 1, + callback_to_random_facts, + FlipLibraryViewLoader); +} + +static bool flip_library_dictionary_fetch(FactLoaderModel* model) { + UNUSED(model); + char payload[128]; + snprintf( + payload, sizeof(payload), "{\"word\":\"%s\"}", app_instance->uart_text_input_buffer_query); + + return flipper_http_post_request_with_headers( + "https://www.flipsocial.net/api/define/", + "{\"Content-Type\":\"application/json\"}", + payload); +} + +static char* flip_library_dictionary_parse(FactLoaderModel* model) { + UNUSED(model); + char* defn = get_json_value("definition", fhttp.last_response, 16); + if(defn == NULL) { + defn = get_json_value("[ERROR]", fhttp.last_response, 16); + } + return defn; +} + +static void flip_library_dictionary_switch_to_view(FlipLibraryApp* app) { + text_input_set_header_text(app->uart_text_input_query, "Enter a word"); + flip_library_generic_switch_to_view( + app, + "Defining", + flip_library_dictionary_fetch, + flip_library_dictionary_parse, + 1, + callback_to_submenu, + FlipLibraryViewTextInputQuery); +} + +static void flip_library_request_error_draw(Canvas* canvas) { + if(canvas == NULL) { + FURI_LOG_E(TAG, "flip_library_request_error_draw - canvas is NULL"); + DEV_CRASH(); + return; + } + if(fhttp.last_response != NULL) { + if(strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != + NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); + canvas_draw_str(canvas, 0, 22, "Failed to reconnect."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } else if(strstr(fhttp.last_response, "[PONG]") != NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[STATUS]Connecting to AP..."); + } else { + canvas_clear(canvas); + FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response); + canvas_draw_str(canvas, 0, 10, "[ERROR] Unusual error..."); + canvas_draw_str(canvas, 0, 60, "Press BACK and retry."); + } + } else { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Failed to receive data."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } +} + +static void flip_library_widget_set_text(char* message, Widget** widget) { + if(widget == NULL) { + FURI_LOG_E(TAG, "flip_library_set_widget_text - widget is NULL"); + DEV_CRASH(); + return; + } + if(message == NULL) { + FURI_LOG_E(TAG, "flip_library_set_widget_text - message is NULL"); + DEV_CRASH(); + return; + } + widget_reset(*widget); + + uint32_t message_length = strlen(message); // Length of the message + uint32_t i = 0; // Index tracker + uint32_t formatted_index = 0; // Tracker for where we are in the formatted message + char* formatted_message; // Buffer to hold the final formatted message + if(!easy_flipper_set_buffer(&formatted_message, message_length * 2 + 1)) { + return; + } + + while(i < message_length) { + // TODO: Use canvas_glyph_width to calculate the maximum characters for the line + uint32_t max_line_length = 29; // Maximum characters per line + uint32_t remaining_length = message_length - i; // Remaining characters + uint32_t line_length = (remaining_length < max_line_length) ? remaining_length : + max_line_length; + + // Temporary buffer to hold the current line + char line[30]; + strncpy(line, message + i, line_length); + line[line_length] = '\0'; + + // Check if the line ends in the middle of a word and adjust accordingly + if(line_length == 29 && message[i + line_length] != '\0' && + message[i + line_length] != ' ') { + // Find the last space within the 30-character segment + char* last_space = strrchr(line, ' '); + if(last_space != NULL) { + // Adjust the line length to avoid cutting the word + line_length = last_space - line; + line[line_length] = '\0'; // Null-terminate at the space + } + } + + // Manually copy the fixed line into the formatted_message buffer + for(uint32_t j = 0; j < line_length; j++) { + formatted_message[formatted_index++] = line[j]; + } + + // Add a newline character for line spacing + formatted_message[formatted_index++] = '\n'; + + // Move i forward to the start of the next word + i += line_length; + + // Skip spaces at the beginning of the next line + while(message[i] == ' ') { + i++; + } + } + + // Add the formatted message to the widget + widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message); +} + +void flip_library_loader_draw_callback(Canvas* canvas, void* model) { + if(!canvas || !model) { + FURI_LOG_E(TAG, "flip_library_loader_draw_callback - canvas or model is NULL"); + return; + } + + SerialState http_state = fhttp.state; + FactLoaderModel* fact_loader_model = (FactLoaderModel*)model; + FactState fact_state = fact_loader_model->fact_state; + char* title = fact_loader_model->title; + + canvas_set_font(canvas, FontSecondary); + + if(http_state == INACTIVE) { + canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); + canvas_draw_str(canvas, 0, 17, "Please connect to the board."); + canvas_draw_str(canvas, 0, 32, "If your board is connected,"); + canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); + canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); + canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); + return; + } + + if(fact_state == FactStateError || fact_state == FactStateParseError) { + flip_library_request_error_draw(canvas); + return; + } + + canvas_draw_str(canvas, 0, 7, title); + canvas_draw_str(canvas, 0, 15, "Loading..."); + + if(fact_state == FactStateInitial) { + return; + } + + if(http_state == SENDING) { + canvas_draw_str(canvas, 0, 22, "Sending..."); + return; + } + + if(http_state == RECEIVING || fact_state == FactStateRequested) { + canvas_draw_str(canvas, 0, 22, "Receiving..."); + return; + } + + if(http_state == IDLE && fact_state == FactStateReceived) { + canvas_draw_str(canvas, 0, 22, "Processing..."); + return; + } + + if(http_state == IDLE && fact_state == FactStateParsed) { + canvas_draw_str(canvas, 0, 22, "Processed..."); + return; + } +} + +static void flip_library_loader_process_callback(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_library_loader_process_callback - context is NULL"); + DEV_CRASH(); + return; + } + + FlipLibraryApp* app = (FlipLibraryApp*)context; + View* view = app->view_loader; + + FactState current_fact_state; + with_view_model( + view, FactLoaderModel * model, { current_fact_state = model->fact_state; }, false); + + if(current_fact_state == FactStateInitial) { + with_view_model( + view, + FactLoaderModel * model, + { + model->fact_state = FactStateRequested; + FactLoaderFetch fetch = model->fetcher; + if(fetch == NULL) { + FURI_LOG_E(TAG, "Model doesn't have Fetch function assigned."); + model->fact_state = FactStateError; + return; + } + + // Clear any previous responses + strncpy(fhttp.last_response, "", 1); + bool request_status = fetch(model); + if(!request_status) { + model->fact_state = FactStateError; + } + }, + true); + } else if(current_fact_state == FactStateRequested || current_fact_state == FactStateError) { + if(fhttp.state == IDLE && fhttp.last_response != NULL) { + if(strstr(fhttp.last_response, "[PONG]") != NULL) { + FURI_LOG_DEV(TAG, "PONG received."); + } else if(strncmp(fhttp.last_response, "[SUCCESS]", 9) == 0) { + FURI_LOG_DEV( + TAG, + "SUCCESS received. %s", + fhttp.last_response ? fhttp.last_response : "NULL"); + } else if(strncmp(fhttp.last_response, "[ERROR]", 9) == 0) { + FURI_LOG_DEV( + TAG, "ERROR received. %s", fhttp.last_response ? fhttp.last_response : "NULL"); + } else if(strlen(fhttp.last_response) == 0) { + // Still waiting on response + } else { + with_view_model( + view, + FactLoaderModel * model, + { model->fact_state = FactStateReceived; }, + true); + } + } else if(fhttp.state == SENDING || fhttp.state == RECEIVING) { + // continue waiting + } else if(fhttp.state == INACTIVE) { + // inactive. try again + } else if(fhttp.state == ISSUE) { + with_view_model( + view, FactLoaderModel * model, { model->fact_state = FactStateError; }, true); + } else { + FURI_LOG_DEV( + TAG, + "Unexpected state: %d lastresp: %s", + fhttp.state, + fhttp.last_response ? fhttp.last_response : "NULL"); + DEV_CRASH(); + } + } else if(current_fact_state == FactStateReceived) { + with_view_model( + view, + FactLoaderModel * model, + { + char* fact_text; + if(model->parser == NULL) { + fact_text = NULL; + FURI_LOG_DEV(TAG, "Parser is NULL"); + DEV_CRASH(); + } else { + fact_text = model->parser(model); + } + FURI_LOG_DEV( + TAG, + "Parsed fact: %s\r\ntext: %s", + fhttp.last_response ? fhttp.last_response : "NULL", + fact_text ? fact_text : "NULL"); + model->fact_text = fact_text; + if(fact_text == NULL) { + model->fact_state = FactStateParseError; + } else { + model->fact_state = FactStateParsed; + } + }, + true); + } else if(current_fact_state == FactStateParsed) { + with_view_model( + view, + FactLoaderModel * model, + { + if(++model->request_index < model->request_count) { + model->fact_state = FactStateInitial; + } else { + flip_library_widget_set_text( + model->fact_text != NULL ? model->fact_text : "Empty result", + &app_instance->widget_result); + if(model->fact_text != NULL) { + free(model->fact_text); + model->fact_text = NULL; + } + view_set_previous_callback( + widget_get_view(app_instance->widget_result), model->back_callback); + view_dispatcher_switch_to_view( + app_instance->view_dispatcher, FlipLibraryViewWidgetResult); + } + }, + true); + } +} + +static void flip_library_loader_timer_callback(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_library_loader_timer_callback - context is NULL"); + DEV_CRASH(); + return; + } + FlipLibraryApp* app = (FlipLibraryApp*)context; + view_dispatcher_send_custom_event(app->view_dispatcher, FlipLibraryCustomEventProcess); +} + +static void flip_library_loader_on_enter(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_library_loader_on_enter - context is NULL"); + DEV_CRASH(); + return; + } + FlipLibraryApp* app = (FlipLibraryApp*)context; + View* view = app->view_loader; + with_view_model( + view, + FactLoaderModel * model, + { + view_set_previous_callback(view, model->back_callback); + if(model->timer == NULL) { + model->timer = furi_timer_alloc( + flip_library_loader_timer_callback, FuriTimerTypePeriodic, app); + } + furi_timer_start(model->timer, 250); + }, + true); +} + +static void flip_library_loader_on_exit(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_library_loader_on_exit - context is NULL"); + DEV_CRASH(); + return; + } + FlipLibraryApp* app = (FlipLibraryApp*)context; + View* view = app->view_loader; + with_view_model( + view, + FactLoaderModel * model, + { + if(model->timer) { + furi_timer_stop(model->timer); + } + }, + false); +} + +void flip_library_loader_init(View* view) { + if(view == NULL) { + FURI_LOG_E(TAG, "flip_library_loader_init - view is NULL"); + DEV_CRASH(); + return; + } + view_allocate_model(view, ViewModelTypeLocking, sizeof(FactLoaderModel)); + view_set_enter_callback(view, flip_library_loader_on_enter); + view_set_exit_callback(view, flip_library_loader_on_exit); +} + +void flip_library_loader_free_model(View* view) { + if(view == NULL) { + FURI_LOG_E(TAG, "flip_library_loader_free_model - view is NULL"); + DEV_CRASH(); + return; + } + with_view_model( + view, + FactLoaderModel * model, + { + if(model->timer) { + furi_timer_free(model->timer); + model->timer = NULL; + } + if(model->parser_context) { + free(model->parser_context); + model->parser_context = NULL; + } + }, + false); +} + +bool flip_library_custom_event_callback(void* context, uint32_t index) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_library_custom_event_callback - context is NULL"); + DEV_CRASH(); + return false; + } + + switch(index) { + case FlipLibraryCustomEventProcess: + flip_library_loader_process_callback(context); + return true; + default: + FURI_LOG_DEV(TAG, "flip_library_custom_event_callback. Unknown index: %ld", index); + return false; + } +} + +void callback_submenu_choices(void* context, uint32_t index) { + if(context == NULL) { + FURI_LOG_E(TAG, "callback_submenu_choices - context is NULL"); + DEV_CRASH(); + return; + } + FlipLibraryApp* app = (FlipLibraryApp*)context; + switch(index) { + case FlipLibrarySubmenuIndexRandomFacts: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewRandomFacts); + break; + case FlipLibrarySubmenuIndexAbout: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewAbout); + break; + case FlipLibrarySubmenuIndexSettings: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewSettings); + break; + case FlipLibrarySubmenuIndexDictionary: + flip_library_dictionary_switch_to_view(app); + break; + case FlipLibrarySubmenuIndexRandomFactsCats: + flip_library_cat_fact_switch_to_view(app); + break; + case FlipLibrarySubmenuIndexRandomFactsDogs: + flip_library_dog_fact_switch_to_view(app); + break; + case FlipLibrarySubmenuIndexRandomFactsQuotes: + flip_library_quote_switch_to_view(app); + break; + case FlipLibrarySubmenuIndexRandomFactsAll: + flip_library_random_fact_switch_to_view(app); + break; + case FlipLibrarySubmenuIndexRandomFactsWiki: + flip_library_wiki_switch_to_view(app); + break; + default: + break; + } +} + +void text_updated_ssid(void* context) { + FlipLibraryApp* app = (FlipLibraryApp*)context; + if(!app) { + FURI_LOG_E(TAG, "text_updated_ssid - FlipLibraryApp is NULL"); + DEV_CRASH(); + return; + } + + // store the entered text + strncpy( + app->uart_text_input_buffer_ssid, + app->uart_text_input_temp_buffer_ssid, + app->uart_text_input_buffer_size_ssid); + + // Ensure null-termination + app->uart_text_input_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; + + // update the variable item text + if(app->variable_item_ssid) { + variable_item_set_current_value_text( + app->variable_item_ssid, app->uart_text_input_buffer_ssid); + } + + // save settings + save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); + + // save wifi settings to devboard + if(strlen(app->uart_text_input_buffer_ssid) > 0 && + strlen(app->uart_text_input_buffer_password) > 0) { + if(!flipper_http_save_wifi( + app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { + FURI_LOG_E(TAG, "Failed to save wifi settings."); + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewSettings); +} + +void text_updated_password(void* context) { + FlipLibraryApp* app = (FlipLibraryApp*)context; + if(!app) { + FURI_LOG_E(TAG, "text_updated_password - FlipLibraryApp is NULL"); + DEV_CRASH(); + return; + } + + // store the entered text + strncpy( + app->uart_text_input_buffer_password, + app->uart_text_input_temp_buffer_password, + app->uart_text_input_buffer_size_password); + + // Ensure null-termination + app->uart_text_input_buffer_password[app->uart_text_input_buffer_size_password - 1] = '\0'; + + // update the variable item text + if(app->variable_item_password) { + variable_item_set_current_value_text( + app->variable_item_password, app->uart_text_input_buffer_password); + } + + // save settings + save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); + + // save wifi settings to devboard + if(strlen(app->uart_text_input_buffer_ssid) > 0 && + strlen(app->uart_text_input_buffer_password) > 0) { + if(!flipper_http_save_wifi( + app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { + FURI_LOG_E(TAG, "Failed to save wifi settings"); + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewSettings); +} + +void text_updated_query(void* context) { + FlipLibraryApp* app = (FlipLibraryApp*)context; + if(!app) { + FURI_LOG_E(TAG, "text_updated_query - FlipLibraryApp is NULL"); + DEV_CRASH(); + return; + } + + // store the entered text + strncpy( + app->uart_text_input_buffer_query, + app->uart_text_input_temp_buffer_query, + app->uart_text_input_buffer_size_query); + + // Ensure null-termination + app->uart_text_input_buffer_query[app->uart_text_input_buffer_size_query - 1] = '\0'; + + // switch to the loader view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewLoader); +} + +uint32_t callback_to_submenu(void* context) { + UNUSED(context); + return FlipLibraryViewSubmenuMain; +} + +uint32_t callback_to_wifi_settings(void* context) { + UNUSED(context); + return FlipLibraryViewSettings; +} + +uint32_t callback_to_random_facts(void* context) { + UNUSED(context); + return FlipLibraryViewRandomFacts; +} + +void settings_item_selected(void* context, uint32_t index) { + FlipLibraryApp* app = (FlipLibraryApp*)context; + if(!app) { + FURI_LOG_E(TAG, "settings_item_selected - FlipLibraryApp is NULL"); + DEV_CRASH(); + return; + } + switch(index) { + case 0: // Input SSID + view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewTextInputSSID); + break; + case 1: // Input Password + view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewTextInputPassword); + break; + default: + FURI_LOG_E(TAG, "Unknown configuration item index"); + break; + } +} + +/** + * @brief Navigation callback for exiting the application + * @param context The context - unused + * @return next view id (VIEW_NONE to exit the app) + */ +uint32_t callback_exit_app(void* context) { + // Exit the application + if(!context) { + FURI_LOG_E(TAG, "callback_exit_app - Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + return VIEW_NONE; // Return VIEW_NONE to exit the app +} + +void flip_library_generic_switch_to_view( + FlipLibraryApp* app, + char* title, + FactLoaderFetch fetcher, + FactLoaderParser parser, + size_t request_count, + ViewNavigationCallback back, + uint32_t view_id) { + if(app == NULL) { + FURI_LOG_E(TAG, "flip_library_generic_switch_to_view - app is NULL"); + DEV_CRASH(); + return; + } + + View* view = app->view_loader; + if(view == NULL) { + FURI_LOG_E(TAG, "flip_library_generic_switch_to_view - view is NULL"); + DEV_CRASH(); + return; + } + + with_view_model( + view, + FactLoaderModel * model, + { + model->title = title; + model->fetcher = fetcher; + model->parser = parser; + model->request_index = 0; + model->request_count = request_count; + model->back_callback = back; + model->fact_state = FactStateInitial; + model->fact_text = NULL; + }, + true); + + view_dispatcher_switch_to_view(app->view_dispatcher, view_id); +} diff --git a/flip_library/callback/flip_library_callback.h b/flip_library/callback/flip_library_callback.h new file mode 100644 index 000000000..e331b55d4 --- /dev/null +++ b/flip_library/callback/flip_library_callback.h @@ -0,0 +1,85 @@ +#ifndef FLIP_LIBRARY_CALLBACK_H +#define FLIP_LIBRARY_CALLBACK_H +#include +#include + +#define MAX_TOKENS 512 // Adjust based on expected JSON size + +typedef enum FactState FactState; +enum FactState { + FactStateInitial, + FactStateRequested, + FactStateReceived, + FactStateParsed, + FactStateParseError, + FactStateError, +}; + +typedef enum FlipLibraryCustomEvent FlipLibraryCustomEvent; +enum FlipLibraryCustomEvent { + FlipLibraryCustomEventProcess, +}; + +typedef struct FactLoaderModel FactLoaderModel; +typedef bool (*FactLoaderFetch)(FactLoaderModel* model); +typedef char* (*FactLoaderParser)(FactLoaderModel* model); +struct FactLoaderModel { + char* title; + char* fact_text; + FactState fact_state; + FactLoaderFetch fetcher; + FactLoaderParser parser; + void* parser_context; + size_t request_index; + size_t request_count; + ViewNavigationCallback back_callback; + FuriTimer* timer; +}; + +extern uint32_t random_facts_index; +extern bool sent_random_fact_request; +extern bool random_fact_request_success; +extern bool random_fact_request_success_all; +extern char* random_fact; + +void flip_library_generic_switch_to_view( + FlipLibraryApp* app, + char* title, + FactLoaderFetch fetcher, + FactLoaderParser parser, + size_t request_count, + ViewNavigationCallback back, + uint32_t view_id); + +void flip_library_loader_draw_callback(Canvas* canvas, void* model); + +void flip_library_loader_init(View* view); + +void flip_library_loader_free_model(View* view); + +bool flip_library_custom_event_callback(void* context, uint32_t index); + +void callback_submenu_choices(void* context, uint32_t index); + +void text_updated_ssid(void* context); + +void text_updated_password(void* context); + +void text_updated_query(void* context); + +uint32_t callback_to_submenu(void* context); + +uint32_t callback_to_wifi_settings(void* context); + +uint32_t callback_to_random_facts(void* context); + +void settings_item_selected(void* context, uint32_t index); + +/** + * @brief Navigation callback for exiting the application + * @param context The context - unused + * @return next view id (VIEW_NONE to exit the app) + */ +uint32_t callback_exit_app(void* context); + +#endif // FLIP_LIBRARY_CALLBACK_H diff --git a/flip_trader/easy_flipper.h b/flip_library/easy_flipper/easy_flipper.c similarity index 96% rename from flip_trader/easy_flipper.h rename to flip_library/easy_flipper/easy_flipper.c index e8f0ad796..8b98e1a1b 100644 --- a/flip_trader/easy_flipper.h +++ b/flip_library/easy_flipper/easy_flipper.c @@ -1,24 +1,4 @@ -#ifndef EASY_FLIPPER_H -#define EASY_FLIPPER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define EASY_TAG "EasyFlipper" +#include /** * @brief Navigation callback for exiting the application @@ -530,5 +510,3 @@ bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer furi_string_set_str(*furi_string, buffer); return true; } - -#endif // EASY_FLIPPER_H diff --git a/flip_library/easy_flipper/easy_flipper.h b/flip_library/easy_flipper/easy_flipper.h new file mode 100644 index 000000000..1d6dbe430 --- /dev/null +++ b/flip_library/easy_flipper/easy_flipper.h @@ -0,0 +1,261 @@ +#ifndef EASY_FLIPPER_H +#define EASY_FLIPPER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EASY_TAG "EasyFlipper" + +/** + * @brief Navigation callback for exiting the application + * @param context The context - unused + * @return next view id (VIEW_NONE to exit the app) + */ +uint32_t easy_flipper_callback_exit_app(void* context); +/** + * @brief Initialize a buffer + * @param buffer The buffer to initialize + * @param buffer_size The size of the buffer + * @return true if successful, false otherwise + */ +bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size); +/** + * @brief Initialize a View object + * @param view The View object to initialize + * @param view_id The ID/Index of the view + * @param draw_callback The draw callback function (set to NULL if not needed) + * @param input_callback The input callback function (set to NULL if not needed) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view( + View** view, + int32_t view_id, + void draw_callback(Canvas*, void*), + bool input_callback(InputEvent*, void*), + uint32_t (*previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a ViewDispatcher object + * @param view_dispatcher The ViewDispatcher object to initialize + * @param gui The GUI object + * @param context The context to pass to the event callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui, void* context); + +/** + * @brief Initialize a Submenu object + * @note This does not set the items in the submenu + * @param submenu The Submenu object to initialize + * @param view_id The ID/Index of the view + * @param title The title/header of the submenu + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_submenu( + Submenu** submenu, + int32_t view_id, + char* title, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a Menu object + * @note This does not set the items in the menu + * @param menu The Menu object to initialize + * @param view_id The ID/Index of the view + * @param item_callback The item callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_menu( + Menu** menu, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a Widget object + * @param widget The Widget object to initialize + * @param view_id The ID/Index of the view + * @param text The text to display in the widget + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_widget( + Widget** widget, + int32_t view_id, + char* text, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a VariableItemList object + * @note This does not set the items in the VariableItemList + * @param variable_item_list The VariableItemList object to initialize + * @param view_id The ID/Index of the view + * @param enter_callback The enter callback function (can be set to NULL) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the enter callback (usually the app) + * @return true if successful, false otherwise + */ +bool easy_flipper_set_variable_item_list( + VariableItemList** variable_item_list, + int32_t view_id, + void (*enter_callback)(void*, uint32_t), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a TextInput object + * @param text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_text_input( + TextInput** text_input, + int32_t view_id, + char* header_text, + char* text_input_temp_buffer, + uint32_t text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a TextInput object with extra symbols + * @param uart_text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_uart_text_input( + TextInput** uart_text_input, + int32_t view_id, + char* header_text, + char* uart_text_input_temp_buffer, + uint32_t uart_text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a DialogEx object + * @param dialog_ex The DialogEx object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param left_button_text The text of the left button + * @param right_button_text The text of the right button + * @param center_button_text The text of the center button + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_dialog_ex( + DialogEx** dialog_ex, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + char* left_button_text, + char* right_button_text, + char* center_button_text, + void (*result_callback)(DialogExResult, void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a Popup object + * @param popup The Popup object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_popup( + Popup** popup, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a Loading object + * @param loading The Loading object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_loading( + Loading** loading, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Set a char butter to a FuriString + * @param furi_string The FuriString object + * @param buffer The buffer to copy the string to + * @return true if successful, false otherwise + */ +bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer); + +#endif diff --git a/flip_library/flip_library_free.h b/flip_library/flip_library.c similarity index 65% rename from flip_library/flip_library_free.h rename to flip_library/flip_library.c index 6c1f18e77..573ceb626 100644 --- a/flip_library/flip_library_free.h +++ b/flip_library/flip_library.c @@ -1,21 +1,21 @@ -#ifndef FLIP_LIBRARY_FREE_H -#define FLIP_LIBRARY_FREE_H +#include "flip_library.h" + +FlipLibraryApp* app_instance = NULL; + +void flip_library_loader_free_model(View* view); // Function to free the resources used by FlipLibraryApp -static void flip_library_app_free(FlipLibraryApp* app) { +void flip_library_app_free(FlipLibraryApp* app) { if(!app) { FURI_LOG_E(TAG, "FlipLibraryApp is NULL"); return; } // Free View(s) - if(app->view_random_facts) { - view_dispatcher_remove_view(app->view_dispatcher, FlipLibraryViewRandomFactsRun); - view_free(app->view_random_facts); - } - if(app->view_dictionary) { - view_dispatcher_remove_view(app->view_dispatcher, FlipLibraryViewDictionaryRun); - view_free(app->view_dictionary); + if(app->view_loader) { + view_dispatcher_remove_view(app->view_dispatcher, FlipLibraryViewLoader); + flip_library_loader_free_model(app->view_loader); + view_free(app->view_loader); } // Free Submenu(s) @@ -29,17 +29,13 @@ static void flip_library_app_free(FlipLibraryApp* app) { } // Free Widget(s) - if(app->widget) { + if(app->widget_about) { view_dispatcher_remove_view(app->view_dispatcher, FlipLibraryViewAbout); - widget_free(app->widget); + widget_free(app->widget_about); } - if(app->widget_random_fact) { - view_dispatcher_remove_view(app->view_dispatcher, FlipLibraryViewRandomFactWidget); - widget_free(app->widget_random_fact); - } - if(app->widget_dictionary) { - view_dispatcher_remove_view(app->view_dispatcher, FlipLibraryViewDictionaryWidget); - widget_free(app->widget_dictionary); + if(app->widget_result) { + view_dispatcher_remove_view(app->view_dispatcher, FlipLibraryViewWidgetResult); + widget_free(app->widget_result); } // Free Variable Item List(s) @@ -57,16 +53,18 @@ static void flip_library_app_free(FlipLibraryApp* app) { view_dispatcher_remove_view(app->view_dispatcher, FlipLibraryViewTextInputPassword); text_input_free(app->uart_text_input_password); } - if(app->uart_text_input_dictionary) { - view_dispatcher_remove_view(app->view_dispatcher, FlipLibraryViewDictionaryTextInput); - text_input_free(app->uart_text_input_dictionary); + if(app->uart_text_input_query) { + view_dispatcher_remove_view(app->view_dispatcher, FlipLibraryViewTextInputQuery); + text_input_free(app->uart_text_input_query); } // deinitalize flipper http flipper_http_deinit(); // free the view dispatcher - if(app->view_dispatcher) view_dispatcher_free(app->view_dispatcher); + if(app->view_dispatcher) { + view_dispatcher_free(app->view_dispatcher); + } // close the gui furi_record_close(RECORD_GUI); @@ -79,5 +77,3 @@ static void flip_library_app_free(FlipLibraryApp* app) { // free the app free(app); } - -#endif // FLIP_LIBRARY_FREE_H diff --git a/flip_library/flip_library_e.h b/flip_library/flip_library.h similarity index 59% rename from flip_library/flip_library_e.h rename to flip_library/flip_library.h index 12bd1c69d..f54612502 100644 --- a/flip_library/flip_library_e.h +++ b/flip_library/flip_library.h @@ -1,8 +1,8 @@ #ifndef FLIP_LIBRARY_E_H #define FLIP_LIBRARY_E_H -#include -#include +#include +#include #include #include #include @@ -11,7 +11,7 @@ #include #include #include -#include +#include #define TAG "FlipLibrary" @@ -26,48 +26,37 @@ typedef enum { FlipLibrarySubmenuIndexRandomFactsDogs, // Click to view the random facts (dogs) FlipLibrarySubmenuIndexRandomFactsQuotes, // Click to view the random facts (quotes) FlipLibrarySubmenuIndexRandomFactsAll, // Click to view the random facts (all) + FlipLibrarySubmenuIndexRandomFactsWiki, // Click to view the random facts (wiki) } FlipLibrarySubmenuIndex; // Define a single view for our FlipLibrary application typedef enum { FlipLibraryViewRandomFacts = 7, // The random facts main screen - FlipLibraryViewRandomFactsRun = 8, // The random facts widget that displays the random fact - FlipLibraryViewSubmenuMain = 9, // The submenu screen - FlipLibraryViewAbout = 10, // The about screen - FlipLibraryViewSettings = 11, // The settings screen - FlipLibraryViewTextInputSSID = 12, // The text input screen (SSID) - FlipLibraryViewTextInputPassword = 13, // The text input screen (password) - FlipLibraryViewDictionary = 14, // The dictionary submenu screen - // - FlipLibraryViewDictionaryTextInput = 15, - FlipLibraryViewDictionaryRun = 16, - // - FlipLibraryViewRandomFactsCats = 17, - FlipLibraryViewRandomFactsDogs = 18, - FlipLibraryViewRandomFactsQuotes = 19, - FlipLibraryViewRandomFactsAll = 20, - // - FlipLibraryViewRandomFactWidget = 21, // The text box that displays the random fact - FlipLibraryViewDictionaryWidget = 22, // The text box that displays the dictionary + FlipLibraryViewLoader, // The loader screen retrieves data from the internet + FlipLibraryViewSubmenuMain, // The submenu screen + FlipLibraryViewAbout, // The about screen + FlipLibraryViewSettings, // The settings screen + FlipLibraryViewTextInputSSID, // The text input screen (SSID) + FlipLibraryViewTextInputPassword, // The text input screen (password) + FlipLibraryViewTextInputQuery, // Query the user for information + FlipLibraryViewWidgetResult, // The text box that displays the random fact } FlipLibraryView; // Each screen will have its own view typedef struct { ViewDispatcher* view_dispatcher; // Switches between our views - View* view_random_facts; // The main screen that displays the random fact - View* view_dictionary; // The dictionary screen + View* view_loader; // The screen that loads data from internet Submenu* submenu_main; // The submenu for the main screen Submenu* submenu_random_facts; // The submenu for the random facts screen - Widget* widget; // The widget + Widget* widget_about; // The widget for the about screen VariableItemList* variable_item_list_wifi; // The variable item list (WiFi settings) VariableItem* variable_item_ssid; // The variable item (SSID) VariableItem* variable_item_password; // The variable item (password) TextInput* uart_text_input_ssid; // The text input for the SSID TextInput* uart_text_input_password; // The text input for the password - TextInput* uart_text_input_dictionary; // The text input for the dictionary + TextInput* uart_text_input_query; // The text input for querying information // - Widget* widget_random_fact; // The text box that displays the random fact - Widget* widget_dictionary; // The text box that displays the dictionary + Widget* widget_result; // The text box that displays the result char* uart_text_input_buffer_ssid; // Buffer for the text input (SSID) char* uart_text_input_temp_buffer_ssid; // Temporary buffer for the text input (SSID) @@ -77,9 +66,12 @@ typedef struct { char* uart_text_input_temp_buffer_password; // Temporary buffer for the text input (password) uint32_t uart_text_input_buffer_size_password; // Size of the text input buffer (password) - char* uart_text_input_buffer_dictionary; // Buffer for the text input (dictionary) - char* uart_text_input_temp_buffer_dictionary; // Temporary buffer for the text input (dictionary) - uint32_t uart_text_input_buffer_size_dictionary; // Size of the text input buffer (dictionary) + char* uart_text_input_buffer_query; // Buffer for the text input (query) + char* uart_text_input_temp_buffer_query; // Temporary buffer for the text input (query) + uint32_t uart_text_input_buffer_size_query; // Size of the text input buffer (query) } FlipLibraryApp; +// Function to free the resources used by FlipLibraryApp +void flip_library_app_free(FlipLibraryApp* app); +extern FlipLibraryApp* app_instance; #endif // FLIP_LIBRARY_E_H diff --git a/flip_library/flip_library_callback.h b/flip_library/flip_library_callback.h deleted file mode 100644 index b08a492f1..000000000 --- a/flip_library/flip_library_callback.h +++ /dev/null @@ -1,819 +0,0 @@ -#ifndef FLIP_LIBRARY_CALLBACK_H -#define FLIP_LIBRARY_CALLBACK_H -static uint32_t random_facts_index = 0; -static bool sent_random_fact_request = false; -static bool random_fact_request_success = false; -static bool random_fact_request_success_all = false; -char* random_fact = NULL; -static FlipLibraryApp* app_instance = NULL; - -#define MAX_TOKENS 512 // Adjust based on expected JSON size - -// Parse JSON to find the "text" key -char* flip_library_parse_random_fact() { - return get_json_value("text", fhttp.last_response, 128); -} - -char* flip_library_parse_cat_fact() { - return get_json_value("fact", fhttp.last_response, 128); -} - -char* flip_library_parse_dog_fact() { - return get_json_array_value("facts", 0, fhttp.last_response, 128); -} - -char* flip_library_parse_quote() { - // remove [ and ] from the start and end of the string - char* response = fhttp.last_response; - if(response[0] == '[') { - response++; - } - if(response[strlen(response) - 1] == ']') { - response[strlen(response) - 1] = '\0'; - } - // remove white space from both sides - while(response[0] == ' ') { - response++; - } - while(response[strlen(response) - 1] == ' ') { - response[strlen(response) - 1] = '\0'; - } - return get_json_value("q", response, 128); -} - -char* flip_library_parse_dictionary() { - return get_json_value("definition", fhttp.last_response, 16); -} - -static void flip_library_request_error(Canvas* canvas) { - if(fhttp.last_response == NULL) { - if(fhttp.last_response != NULL) { - if(strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != - NULL) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } else { - canvas_clear(canvas); - FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response); - canvas_draw_str(canvas, 0, 10, "[ERROR] Unusual error..."); - canvas_draw_str(canvas, 0, 60, "Press BACK and retry."); - } - } else { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - } else { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Failed to receive data."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } -} - -static void flip_library_draw_fact(char* message, Widget** widget) { - if(app_instance == NULL) { - FURI_LOG_E(TAG, "App instance is NULL"); - return; - } - widget_reset(*widget); - - uint32_t fact_length = strlen(message); // Length of the message - uint32_t i = 0; // Index tracker - uint32_t formatted_index = 0; // Tracker for where we are in the formatted message - char* formatted_message; // Buffer to hold the final formatted message - if(!easy_flipper_set_buffer(&formatted_message, fact_length * 2 + 1)) { - return; - } - - while(i < fact_length) { - uint32_t max_line_length = 29; // Maximum characters per line - uint32_t remaining_length = fact_length - i; // Remaining characters - uint32_t line_length = (remaining_length < max_line_length) ? remaining_length : - max_line_length; - - // Temporary buffer to hold the current line - char fact_line[30]; - strncpy(fact_line, message + i, line_length); - fact_line[line_length] = '\0'; - - // Check if the line ends in the middle of a word and adjust accordingly - if(line_length == 29 && message[i + line_length] != '\0' && - message[i + line_length] != ' ') { - // Find the last space within the 30-character segment - char* last_space = strrchr(fact_line, ' '); - if(last_space != NULL) { - // Adjust the line length to avoid cutting the word - line_length = last_space - fact_line; - fact_line[line_length] = '\0'; // Null-terminate at the space - } - } - - // Manually copy the fixed line into the formatted_message buffer - for(uint32_t j = 0; j < line_length; j++) { - formatted_message[formatted_index++] = fact_line[j]; - } - - // Add a newline character for line spacing - formatted_message[formatted_index++] = '\n'; - - // Move i forward to the start of the next word - i += line_length; - - // Skip spaces at the beginning of the next line - while(message[i] == ' ') { - i++; - } - } - - // Add the formatted message to the widget - widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message); -} - -// Callback for drawing the main screen -static void view_draw_callback_random_facts(Canvas* canvas, void* model) { - if(!canvas || !app_instance) { - return; - } - UNUSED(model); - - canvas_set_font(canvas, FontSecondary); - - if(fhttp.state == INACTIVE) { - canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); - canvas_draw_str(canvas, 0, 17, "Please connect to the board."); - canvas_draw_str(canvas, 0, 32, "If your board is connected,"); - canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); - canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); - canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); - return; - } - // Cats - if(random_facts_index == FlipLibrarySubmenuIndexRandomFactsCats) { - canvas_draw_str(canvas, 0, 7, "Random Cat Fact"); - canvas_draw_str(canvas, 0, 15, "Loading..."); - - if(!sent_random_fact_request) { - sent_random_fact_request = true; - random_fact_request_success = flipper_http_get_request_with_headers( - "https://catfact.ninja/fact", "{\"Content-Type\":\"application/json\"}"); - if(!random_fact_request_success) { - FURI_LOG_E(TAG, "Failed to send request"); - flip_library_request_error(canvas); - return; - } - fhttp.state = RECEIVING; - } else { - if(fhttp.state == RECEIVING) { - canvas_draw_str(canvas, 0, 22, "Receiving..."); - return; - } - // check status - else if(fhttp.state == ISSUE || !random_fact_request_success) { - flip_library_request_error(canvas); - } else if( - fhttp.state == IDLE && fhttp.last_response != NULL && - !random_fact_request_success_all) { - canvas_draw_str(canvas, 0, 22, "Processing..."); - // success - // check status - // unnecessary check - if(fhttp.state == ISSUE || fhttp.last_response == NULL) { - flip_library_request_error(canvas); - FURI_LOG_E(TAG, "HTTP request failed or received data is NULL"); - return; - } else if(!random_fact_request_success_all) { - random_fact = flip_library_parse_cat_fact(); - - if(random_fact == NULL) { - flip_library_request_error(canvas); - fhttp.state = ISSUE; - return; - } - - // Mark success - random_fact_request_success_all = true; - - // draw random facts - flip_library_draw_fact(random_fact, &app_instance->widget_random_fact); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewRandomFactWidget); - } - } - // likely redundant but just in case - else if(fhttp.state == IDLE && random_fact_request_success_all && random_fact != NULL) { - flip_library_draw_fact(random_fact, &app_instance->widget_random_fact); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewRandomFactWidget); - } else // handle weird scenarios - { - // if received data isnt NULL - if(fhttp.last_response != NULL) { - // parse json to find the text key - random_fact = flip_library_parse_cat_fact(); - - if(random_fact == NULL) { - flip_library_request_error(canvas); - fhttp.state = ISSUE; - return; - } - } - } - } - } - // Dogs - else if(random_facts_index == FlipLibrarySubmenuIndexRandomFactsDogs) { - canvas_draw_str(canvas, 0, 7, "Random Dog Fact"); - canvas_draw_str(canvas, 0, 15, "Loading..."); - - if(!sent_random_fact_request) { - sent_random_fact_request = true; - random_fact_request_success = flipper_http_get_request_with_headers( - "https://dog-api.kinduff.com/api/facts", - "{\"Content-Type\":\"application/json\"}"); - if(!random_fact_request_success) { - FURI_LOG_E(TAG, "Failed to send request"); - flip_library_request_error(canvas); - return; - } - fhttp.state = RECEIVING; - } else { - if(fhttp.state == RECEIVING) { - canvas_draw_str(canvas, 0, 22, "Receiving..."); - return; - } - // check status - else if(fhttp.state == ISSUE || !random_fact_request_success) { - flip_library_request_error(canvas); - } else if( - fhttp.state == IDLE && fhttp.last_response != NULL && - !random_fact_request_success_all) { - canvas_draw_str(canvas, 0, 22, "Processing..."); - // success - // check status - // unnecessary check - if(fhttp.state == ISSUE || fhttp.last_response == NULL) { - flip_library_request_error(canvas); - FURI_LOG_E(TAG, "HTTP request failed or received data is NULL"); - return; - } else if(!random_fact_request_success_all) { - random_fact = flip_library_parse_dog_fact(); - - if(random_fact == NULL) { - flip_library_request_error(canvas); - fhttp.state = ISSUE; - return; - } - - // Mark success - random_fact_request_success_all = true; - - // draw random facts - flip_library_draw_fact(random_fact, &app_instance->widget_random_fact); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewRandomFactWidget); - } - } - // likely redundant but just in case - else if(fhttp.state == IDLE && random_fact_request_success_all && random_fact != NULL) { - flip_library_draw_fact(random_fact, &app_instance->widget_random_fact); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewRandomFactWidget); - } else // handle weird scenarios - { - // if received data isnt NULL - if(fhttp.last_response != NULL) { - // parse json to find the text key - random_fact = flip_library_parse_cat_fact(); - - if(random_fact == NULL) { - flip_library_request_error(canvas); - fhttp.state = ISSUE; - return; - } - } - } - } - } - // Quotes - else if(random_facts_index == FlipLibrarySubmenuIndexRandomFactsQuotes) { - canvas_draw_str(canvas, 0, 7, "Random Quote"); - canvas_draw_str(canvas, 0, 15, "Loading..."); - - if(!sent_random_fact_request) { - sent_random_fact_request = true; - random_fact_request_success = - flipper_http_get_request("https://zenquotes.io/api/random"); - if(!random_fact_request_success) { - FURI_LOG_E(TAG, "Failed to send request"); - flip_library_request_error(canvas); - return; - } - fhttp.state = RECEIVING; - } else { - if(fhttp.state == RECEIVING) { - canvas_draw_str(canvas, 0, 22, "Receiving..."); - return; - } - // check status - else if(fhttp.state == ISSUE || !random_fact_request_success) { - flip_library_request_error(canvas); - } else if( - fhttp.state == IDLE && fhttp.last_response != NULL && - !random_fact_request_success_all) { - canvas_draw_str(canvas, 0, 22, "Processing..."); - // success - // check status - // unnecessary check - if(fhttp.state == ISSUE || fhttp.last_response == NULL) { - flip_library_request_error(canvas); - FURI_LOG_E(TAG, "HTTP request failed or received data is NULL"); - return; - } else if(!random_fact_request_success_all) { - random_fact = flip_library_parse_quote(); - - if(random_fact == NULL) { - flip_library_request_error(canvas); - fhttp.state = ISSUE; - return; - } - - // Mark success - random_fact_request_success_all = true; - - // draw random facts - flip_library_draw_fact(random_fact, &app_instance->widget_random_fact); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewRandomFactWidget); - } - } - // likely redundant but just in case - else if(fhttp.state == IDLE && random_fact_request_success_all && random_fact != NULL) { - flip_library_draw_fact(random_fact, &app_instance->widget_random_fact); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewRandomFactWidget); - } else // handle weird scenarios - { - // if received data isnt NULL - if(fhttp.last_response != NULL) { - // parse json to find the text key - random_fact = flip_library_parse_quote(); - - if(random_fact == NULL) { - flip_library_request_error(canvas); - fhttp.state = ISSUE; - return; - } - } - } - } - } - // All Random Facts - else if(random_facts_index == FlipLibrarySubmenuIndexRandomFactsAll) { - canvas_draw_str(canvas, 0, 10, "Random Fact"); - canvas_set_font(canvas, FontSecondary); - canvas_draw_str(canvas, 0, 20, "Loading..."); - - if(!sent_random_fact_request) { - sent_random_fact_request = true; - - random_fact_request_success = - flipper_http_get_request("https://uselessfacts.jsph.pl/api/v2/facts/random"); - if(!random_fact_request_success) { - FURI_LOG_E(TAG, "Failed to send request"); - return; - } - fhttp.state = RECEIVING; - } else { - // check status - if(fhttp.state == RECEIVING) { - canvas_draw_str(canvas, 0, 30, "Receiving..."); - return; - } - // check status - else if(fhttp.state == ISSUE || !random_fact_request_success) { - flip_library_request_error(canvas); - return; - } else if( - fhttp.state == IDLE && fhttp.last_response != NULL && - !random_fact_request_success_all) { - canvas_draw_str(canvas, 0, 30, "Processing..."); - // success - // check status - if(fhttp.state == ISSUE || fhttp.last_response == NULL) { - flip_library_request_error(canvas); - FURI_LOG_E(TAG, "HTTP request failed or received data is NULL"); - return; - } - - // parse json to find the text key - random_fact = flip_library_parse_random_fact(); - - if(random_fact == NULL) { - flip_library_request_error(canvas); - fhttp.state = ISSUE; - return; - } - - // Mark success - random_fact_request_success_all = true; - - // draw random facts - flip_library_draw_fact(random_fact, &app_instance->widget_random_fact); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewRandomFactWidget); - } - // likely redundant but just in case - else if(fhttp.state == IDLE && random_fact_request_success_all && random_fact != NULL) { - // draw random facts - flip_library_draw_fact(random_fact, &app_instance->widget_random_fact); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewRandomFactWidget); - } else // handle weird scenarios - { - // if received data isnt NULL - if(fhttp.last_response != NULL) { - // parse json to find the text key - random_fact = flip_library_parse_random_fact(); - - if(random_fact == NULL) { - flip_library_request_error(canvas); - fhttp.state = ISSUE; - return; - } - } - } - } - } else { - canvas_draw_str(canvas, 0, 7, "Random Fact"); - } -} - -static void view_draw_callback_dictionary_run(Canvas* canvas, void* model) { - if(!canvas || !app_instance || app_instance->uart_text_input_buffer_dictionary == NULL) { - return; - } - - UNUSED(model); - - canvas_set_font(canvas, FontSecondary); - - if(fhttp.state == INACTIVE) { - canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); - canvas_draw_str(canvas, 0, 17, "Please connect to the board."); - canvas_draw_str(canvas, 0, 32, "If your board is connected,"); - canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); - canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); - canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); - return; - } - - canvas_draw_str(canvas, 0, 10, "Defining, please wait..."); - - if(!sent_random_fact_request) { - sent_random_fact_request = true; - - char payload[128]; - snprintf( - payload, - sizeof(payload), - "{\"word\":\"%s\"}", - app_instance->uart_text_input_buffer_dictionary); - - random_fact_request_success = flipper_http_post_request_with_headers( - "https://www.flipsocial.net/api/define/", - "{\"Content-Type\":\"application/json\"}", - payload); - if(!random_fact_request_success) { - FURI_LOG_E(TAG, "Failed to send request"); - return; - } - fhttp.state = RECEIVING; - } else { - // check status - if(fhttp.state == RECEIVING) { - canvas_draw_str(canvas, 0, 20, "Receiving..."); - return; - } - // check status - else if(fhttp.state == ISSUE || !random_fact_request_success) { - flip_library_request_error(canvas); - return; - } else if( - fhttp.state == IDLE && fhttp.last_response != NULL && - !random_fact_request_success_all) { - canvas_draw_str(canvas, 0, 20, "Processing..."); - // success - // check status - if(fhttp.state == ISSUE || fhttp.last_response == NULL) { - flip_library_request_error(canvas); - FURI_LOG_E(TAG, "HTTP request failed or received data is NULL"); - return; - } - - // parse json to find the text key - char* definition = flip_library_parse_dictionary(); - - if(definition == NULL) { - flip_library_request_error(canvas); - fhttp.state = ISSUE; - return; - } - - // Mark success - random_fact_request_success_all = true; - - // draw random facts - flip_library_draw_fact(definition, &app_instance->widget_dictionary); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewDictionaryWidget); - } - // likely redundant but just in case - else if(fhttp.state == IDLE && random_fact_request_success_all && random_fact != NULL) { - // draw random facts - flip_library_draw_fact(random_fact, &app_instance->widget_dictionary); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewDictionaryWidget); - } else // handle weird scenarios - { - // if received data isnt NULL - if(fhttp.last_response != NULL) { - // parse json to find the text key - char* definition = flip_library_parse_dictionary(); - - if(definition == NULL) { - flip_library_request_error(canvas); - fhttp.state = ISSUE; - return; - } - - // draw random facts - flip_library_draw_fact(definition, &app_instance->widget_dictionary); - - // go to random facts widget - view_dispatcher_switch_to_view( - app_instance->view_dispatcher, FlipLibraryViewDictionaryWidget); - - free(definition); - - return; - } - } - } -} - -// Input callback for the view (async input handling) -bool view_input_callback_random_facts(InputEvent* event, void* context) { - if(!event || !context) { - return false; - } - FlipLibraryApp* app = (FlipLibraryApp*)context; - if(event->type == InputTypePress && event->key == InputKeyBack) { - // Exit the app when the back button is pressed - view_dispatcher_stop(app->view_dispatcher); - return true; - } - return false; -} - -static void callback_submenu_choices(void* context, uint32_t index) { - FlipLibraryApp* app = (FlipLibraryApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipLibraryApp is NULL"); - return; - } - switch(index) { - case FlipLibrarySubmenuIndexRandomFacts: - random_facts_index = 0; - sent_random_fact_request = false; - random_fact = NULL; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewRandomFacts); - break; - case FlipLibrarySubmenuIndexAbout: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewAbout); - break; - case FlipLibrarySubmenuIndexSettings: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewSettings); - break; - case FlipLibrarySubmenuIndexDictionary: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewDictionaryTextInput); - break; - case FlipLibrarySubmenuIndexRandomFactsCats: - random_facts_index = FlipLibrarySubmenuIndexRandomFactsCats; - sent_random_fact_request = false; - random_fact = NULL; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewRandomFactsRun); - break; - case FlipLibrarySubmenuIndexRandomFactsDogs: - random_facts_index = FlipLibrarySubmenuIndexRandomFactsDogs; - sent_random_fact_request = false; - random_fact = NULL; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewRandomFactsRun); - break; - case FlipLibrarySubmenuIndexRandomFactsQuotes: - random_facts_index = FlipLibrarySubmenuIndexRandomFactsQuotes; - sent_random_fact_request = false; - random_fact = NULL; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewRandomFactsRun); - break; - case FlipLibrarySubmenuIndexRandomFactsAll: - random_facts_index = FlipLibrarySubmenuIndexRandomFactsAll; - sent_random_fact_request = false; - random_fact = NULL; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewRandomFactsRun); - break; - default: - break; - } -} - -static void text_updated_ssid(void* context) { - FlipLibraryApp* app = (FlipLibraryApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipLibraryApp is NULL"); - return; - } - - // store the entered text - strncpy( - app->uart_text_input_buffer_ssid, - app->uart_text_input_temp_buffer_ssid, - app->uart_text_input_buffer_size_ssid); - - // Ensure null-termination - app->uart_text_input_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; - - // update the variable item text - if(app->variable_item_ssid) { - variable_item_set_current_value_text( - app->variable_item_ssid, app->uart_text_input_buffer_ssid); - } - - // save settings - save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); - - // save wifi settings to devboard - if(strlen(app->uart_text_input_buffer_ssid) > 0 && - strlen(app->uart_text_input_buffer_password) > 0) { - if(!flipper_http_save_wifi( - app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { - FURI_LOG_E(TAG, "Failed to save wifi settings"); - } - } - - // switch to the settings view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewSettings); -} - -static void text_updated_password(void* context) { - FlipLibraryApp* app = (FlipLibraryApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipLibraryApp is NULL"); - return; - } - - // store the entered text - strncpy( - app->uart_text_input_buffer_password, - app->uart_text_input_temp_buffer_password, - app->uart_text_input_buffer_size_password); - - // Ensure null-termination - app->uart_text_input_buffer_password[app->uart_text_input_buffer_size_password - 1] = '\0'; - - // update the variable item text - if(app->variable_item_password) { - variable_item_set_current_value_text( - app->variable_item_password, app->uart_text_input_buffer_password); - } - - // save settings - save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); - - // save wifi settings to devboard - if(strlen(app->uart_text_input_buffer_ssid) > 0 && - strlen(app->uart_text_input_buffer_password) > 0) { - if(!flipper_http_save_wifi( - app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { - FURI_LOG_E(TAG, "Failed to save wifi settings"); - } - } - - // switch to the settings view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewSettings); -} - -static void text_updated_dictionary(void* context) { - FlipLibraryApp* app = (FlipLibraryApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipLibraryApp is NULL"); - return; - } - - // store the entered text - strncpy( - app->uart_text_input_buffer_dictionary, - app->uart_text_input_temp_buffer_dictionary, - app->uart_text_input_buffer_size_dictionary); - - // Ensure null-termination - app->uart_text_input_buffer_dictionary[app->uart_text_input_buffer_size_dictionary - 1] = '\0'; - - // switch to the dictionary view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewDictionaryRun); -} - -static uint32_t callback_to_submenu(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - random_facts_index = 0; - sent_random_fact_request = false; - random_fact_request_success = false; - random_fact_request_success_all = false; - random_fact = NULL; - return FlipLibraryViewSubmenuMain; -} - -static uint32_t callback_to_wifi_settings(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - return FlipLibraryViewSettings; -} - -static uint32_t callback_to_random_facts(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - return FlipLibraryViewRandomFacts; -} - -static void settings_item_selected(void* context, uint32_t index) { - FlipLibraryApp* app = (FlipLibraryApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipLibraryApp is NULL"); - return; - } - switch(index) { - case 0: // Input SSID - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewTextInputSSID); - break; - case 1: // Input Password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipLibraryViewTextInputPassword); - break; - default: - FURI_LOG_E(TAG, "Unknown configuration item index"); - break; - } -} - -/** - * @brief Navigation callback for exiting the application - * @param context The context - unused - * @return next view id (VIEW_NONE to exit the app) - */ -static uint32_t callback_exit_app(void* context) { - // Exit the application - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - return VIEW_NONE; // Return VIEW_NONE to exit the app -} - -#endif // FLIP_LIBRARY_CALLBACK_H diff --git a/flip_library/flip_library_storage.h b/flip_library/flip_storage/flip_library_storage.c similarity index 88% rename from flip_library/flip_library_storage.h rename to flip_library/flip_storage/flip_library_storage.c index b84ed0edd..946c7b5a7 100644 --- a/flip_library/flip_library_storage.h +++ b/flip_library/flip_storage/flip_library_storage.c @@ -1,12 +1,6 @@ -#ifndef FLIP_LIBRARY_STORAGE_H -#define FLIP_LIBRARY_STORAGE_H +#include -#include -#include - -#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_library/settings.bin" - -static void save_settings(const char* ssid, const char* password) { +void save_settings(const char* ssid, const char* password) { // Create the directory for saving settings char directory_path[256]; snprintf( @@ -44,7 +38,7 @@ static void save_settings(const char* ssid, const char* password) { furi_record_close(RECORD_STORAGE); } -static bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size) { +bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size) { Storage* storage = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(storage); @@ -86,5 +80,3 @@ static bool load_settings(char* ssid, size_t ssid_size, char* password, size_t p return true; } - -#endif // FLIP_LIBRARY_STORAGE_H diff --git a/flip_library/flip_storage/flip_library_storage.h b/flip_library/flip_storage/flip_library_storage.h new file mode 100644 index 000000000..18467636b --- /dev/null +++ b/flip_library/flip_storage/flip_library_storage.h @@ -0,0 +1,13 @@ +#ifndef FLIP_LIBRARY_STORAGE_H +#define FLIP_LIBRARY_STORAGE_H + +#include +#include +#include +#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_library/settings.bin" + +void save_settings(const char* ssid, const char* password); + +bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size); + +#endif // FLIP_LIBRARY_STORAGE_H diff --git a/flip_weather/flipper_http.h b/flip_library/flipper_http/flipper_http.c similarity index 89% rename from flip_weather/flipper_http.h rename to flip_library/flipper_http/flipper_http.c index 9f14e83ed..ed3b216de 100644 --- a/flip_weather/flipper_http.h +++ b/flip_library/flipper_http/flipper_http.c @@ -1,137 +1,7 @@ -// flipper_http.h -#ifndef FLIPPER_HTTP_H -#define FLIPPER_HTTP_H - -#include -#include -#include -#include -#include - -// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext - -#define HTTP_TAG "FlipWeather" // change this to your app name -#define http_tag "flip_weather" // change this to your app id -#define UART_CH (FuriHalSerialIdUsart) // UART channel -#define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds -#define BAUDRATE (115200) // UART baudrate -#define RX_BUF_SIZE 1024 // UART RX buffer size -#define RX_LINE_BUFFER_SIZE 4096 // UART RX line buffer size (increase for large responses) -#define MAX_FILE_SHOW 4096 // Maximum data from file to show - -// Forward declaration for callback -typedef void (*FlipperHTTP_Callback)(const char* line, void* context); - -// Functions -bool flipper_http_init(FlipperHTTP_Callback callback, void* context); -void flipper_http_deinit(); -//--- -void flipper_http_rx_callback(const char* line, void* context); -bool flipper_http_send_data(const char* data); -//--- -bool flipper_http_connect_wifi(); -bool flipper_http_disconnect_wifi(); -bool flipper_http_ping(); -bool flipper_http_scan_wifi(); -bool flipper_http_save_wifi(const char* ssid, const char* password); -bool flipper_http_ip_wifi(); -bool flipper_http_ip_address(); -//--- -bool flipper_http_list_commands(); -bool flipper_http_led_on(); -bool flipper_http_led_off(); -bool flipper_http_parse_json(const char* key, const char* json_data); -bool flipper_http_parse_json_array(const char* key, int index, const char* json_data); -//--- -bool flipper_http_get_request(const char* url); -bool flipper_http_get_request_with_headers(const char* url, const char* headers); -bool flipper_http_post_request_with_headers( - const char* url, - const char* headers, - const char* payload); -bool flipper_http_put_request_with_headers( - const char* url, - const char* headers, - const char* payload); -bool flipper_http_delete_request_with_headers( - const char* url, - const char* headers, - const char* payload); -//--- -bool flipper_http_get_request_bytes(const char* url, const char* headers); -bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload); -// -bool flipper_http_append_to_file( - const void* data, - size_t data_size, - bool start_new_file, - char* file_path); - -FuriString* flipper_http_load_from_file(char* file_path); -static char* trim(const char* str); -// -bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)); - -// State variable to track the UART state -typedef enum { - INACTIVE, // Inactive state - IDLE, // Default state - RECEIVING, // Receiving data - SENDING, // Sending data - ISSUE, // Issue with connection -} SerialState; - -// Event Flags for UART Worker Thread -typedef enum { - WorkerEvtStop = (1 << 0), - WorkerEvtRxDone = (1 << 1), -} WorkerEvtFlags; - -// FlipperHTTP Structure -typedef struct { - FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication - FuriHalSerialHandle* serial_handle; // Serial handle for UART communication - FuriThread* rx_thread; // Worker thread for UART - FuriThreadId rx_thread_id; // Worker thread ID - FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines - void* callback_context; // Context for the callback - SerialState state; // State of the UART - - // variable to store the last received data from the UART - char* last_response; - char file_path[256]; // Path to save the received data - - // Timer-related members - FuriTimer* get_timeout_timer; // Timer for HTTP request timeout - - bool started_receiving_get; // Indicates if a GET request has started - bool just_started_get; // Indicates if GET data reception has just started - - bool started_receiving_post; // Indicates if a POST request has started - bool just_started_post; // Indicates if POST data reception has just started - - bool started_receiving_put; // Indicates if a PUT request has started - bool just_started_put; // Indicates if PUT data reception has just started - - bool started_receiving_delete; // Indicates if a DELETE request has started - bool just_started_delete; // Indicates if DELETE data reception has just started - - // Buffer to hold the raw bytes received from the UART - uint8_t* received_bytes; - size_t received_bytes_len; // Length of the received bytes - bool is_bytes_request; // Flag to indicate if the request is for bytes - bool save_bytes; // Flag to save the received data to a file - bool save_received_data; // Flag to save the received data to a file -} FlipperHTTP; - -static FlipperHTTP fhttp; -// Global static array for the line buffer -static char rx_line_buffer[RX_LINE_BUFFER_SIZE]; -#define FILE_BUFFER_SIZE 512 -static uint8_t file_buffer[FILE_BUFFER_SIZE]; - -// fhttp.last_response holds the last received data from the UART - +#include +FlipperHTTP fhttp; +char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +uint8_t file_buffer[FILE_BUFFER_SIZE]; // Function to append received data to file // make sure to initialize the file path before calling this function bool flipper_http_append_to_file( @@ -143,6 +13,15 @@ bool flipper_http_append_to_file( File* file = storage_file_alloc(storage); if(start_new_file) { + // Delete the file if it already exists + if(storage_file_exists(storage, file_path)) { + if(!storage_simply_remove_recursive(storage, file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to delete file: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + } // Open the file in write mode if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path); @@ -258,7 +137,7 @@ FuriString* flipper_http_load_from_file(char* file_path) { * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -static int32_t flipper_http_worker(void* context) { +int32_t flipper_http_worker(void* context) { UNUSED(context); size_t rx_line_pos = 0; static size_t file_buffer_len = 0; @@ -286,10 +165,14 @@ static int32_t flipper_http_worker(void* context) { // Write to file if buffer is full if(file_buffer_len >= FILE_BUFFER_SIZE) { if(!flipper_http_append_to_file( - file_buffer, file_buffer_len, false, fhttp.file_path)) { + file_buffer, + file_buffer_len, + fhttp.just_started_bytes, + fhttp.file_path)) { FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); } file_buffer_len = 0; + fhttp.just_started_bytes = false; } } @@ -302,8 +185,6 @@ static int32_t flipper_http_worker(void* context) { // Invoke the callback with the complete line fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context); - // save the received data - // Reset the line buffer position rx_line_pos = 0; } else { @@ -376,7 +257,7 @@ void get_timeout_timer_callback(void* context) { * @param context The context to pass to the callback. * @note This function will handle received data asynchronously via the callback. */ -static void _flipper_http_rx_callback( +void _flipper_http_rx_callback( FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* context) { @@ -1281,6 +1162,7 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.state = RECEIVING; // for GET request, save data only if it's a bytes request fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; return; } else if(strstr(line, "[POST/SUCCESS]") != NULL) { FURI_LOG_I(HTTP_TAG, "POST request succeeded."); @@ -1289,6 +1171,7 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.state = RECEIVING; // for POST request, save data only if it's a bytes request fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; return; } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { FURI_LOG_I(HTTP_TAG, "PUT request succeeded."); @@ -1388,5 +1271,3 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars } return true; } - -#endif // FLIPPER_HTTP_H diff --git a/flip_library/flipper_http/flipper_http.h b/flip_library/flipper_http/flipper_http.h new file mode 100644 index 000000000..23a56041b --- /dev/null +++ b/flip_library/flipper_http/flipper_http.h @@ -0,0 +1,363 @@ +// flipper_http.h +#ifndef FLIPPER_HTTP_H +#define FLIPPER_HTTP_H + +#include +#include +#include +#include +#include +#include + +// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext + +#define HTTP_TAG "FlipLibrary" // change this to your app name +#define http_tag "flip_library" // change this to your app id +#define UART_CH (momentum_settings.uart_esp_channel) // UART channel +#define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds +#define BAUDRATE (115200) // UART baudrate +#define RX_BUF_SIZE 1024 // UART RX buffer size +#define RX_LINE_BUFFER_SIZE 4096 // UART RX line buffer size (increase for large responses) +#define MAX_FILE_SHOW 4096 // Maximum data from file to show +#define FILE_BUFFER_SIZE 512 // File buffer size + +// Forward declaration for callback +typedef void (*FlipperHTTP_Callback)(const char* line, void* context); + +// State variable to track the UART state +typedef enum { + INACTIVE, // Inactive state + IDLE, // Default state + RECEIVING, // Receiving data + SENDING, // Sending data + ISSUE, // Issue with connection +} SerialState; + +// Event Flags for UART Worker Thread +typedef enum { + WorkerEvtStop = (1 << 0), + WorkerEvtRxDone = (1 << 1), +} WorkerEvtFlags; + +// FlipperHTTP Structure +typedef struct { + FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication + FuriHalSerialHandle* serial_handle; // Serial handle for UART communication + FuriThread* rx_thread; // Worker thread for UART + FuriThreadId rx_thread_id; // Worker thread ID + FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines + void* callback_context; // Context for the callback + SerialState state; // State of the UART + + // variable to store the last received data from the UART + char* last_response; + char file_path[256]; // Path to save the received data + + // Timer-related members + FuriTimer* get_timeout_timer; // Timer for HTTP request timeout + + bool started_receiving_get; // Indicates if a GET request has started + bool just_started_get; // Indicates if GET data reception has just started + + bool started_receiving_post; // Indicates if a POST request has started + bool just_started_post; // Indicates if POST data reception has just started + + bool started_receiving_put; // Indicates if a PUT request has started + bool just_started_put; // Indicates if PUT data reception has just started + + bool started_receiving_delete; // Indicates if a DELETE request has started + bool just_started_delete; // Indicates if DELETE data reception has just started + + // Buffer to hold the raw bytes received from the UART + uint8_t* received_bytes; + size_t received_bytes_len; // Length of the received bytes + bool is_bytes_request; // Flag to indicate if the request is for bytes + bool save_bytes; // Flag to save the received data to a file + bool save_received_data; // Flag to save the received data to a file + + bool just_started_bytes; // Indicates if bytes data reception has just started +} FlipperHTTP; + +extern FlipperHTTP fhttp; +// Global static array for the line buffer +extern char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +extern uint8_t file_buffer[FILE_BUFFER_SIZE]; + +// fhttp.last_response holds the last received data from the UART + +// Function to append received data to file +// make sure to initialize the file path before calling this function +bool flipper_http_append_to_file( + const void* data, + size_t data_size, + bool start_new_file, + char* file_path); + +FuriString* flipper_http_load_from_file(char* file_path); + +// UART worker thread +/** + * @brief Worker thread to handle UART data asynchronously. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +// UART worker thread +int32_t flipper_http_worker(void* context); + +// Timer callback function +/** + * @brief Callback function for the GET timeout timer. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will be called when the GET request times out. + */ +void get_timeout_timer_callback(void* context); + +// UART RX Handler Callback (Interrupt Context) +/** + * @brief A private callback function to handle received data asynchronously. + * @return void + * @param handle The UART handle. + * @param event The event type. + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +void _flipper_http_rx_callback( + FuriHalSerialHandle* handle, + FuriHalSerialRxEvent event, + void* context); + +// UART initialization function +/** + * @brief Initialize UART. + * @return true if the UART was initialized successfully, false otherwise. + * @param callback The callback function to handle received data (ex. flipper_http_rx_callback). + * @param context The context to pass to the callback. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_init(FlipperHTTP_Callback callback, void* context); + +// Deinitialize UART +/** + * @brief Deinitialize UART. + * @return void + * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. + */ +void flipper_http_deinit(); + +// Function to send data over UART with newline termination +/** + * @brief Send data over UART with newline termination. + * @return true if the data was sent successfully, false otherwise. + * @param data The data to send over UART. + * @note The data will be sent over UART with a newline character appended. + */ +bool flipper_http_send_data(const char* data); + +// Function to send a PING request +/** + * @brief Send a PING request to check if the Wifi Dev Board is connected. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + * @note This is best used to check if the Wifi Dev Board is connected. + * @note The state will remain INACTIVE until a PONG is received. + */ +bool flipper_http_ping(); + +// Function to list available commands +/** + * @brief Send a command to list available commands. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_list_commands(); + +// Function to turn on the LED +/** + * @brief Allow the LED to display while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_on(); + +// Function to turn off the LED +/** + * @brief Disable the LED from displaying while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_off(); + +// Function to parse JSON data +/** + * @brief Parse JSON data. + * @return true if the JSON data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON data. + * @param json_data The JSON data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json(const char* key, const char* json_data); + +// Function to parse JSON array data +/** + * @brief Parse JSON array data. + * @return true if the JSON array data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON array data. + * @param index The index to parse from the JSON array data. + * @param json_data The JSON array data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json_array(const char* key, int index, const char* json_data); + +// Function to scan for WiFi networks +/** + * @brief Send a command to scan for WiFi networks. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_scan_wifi(); + +// Function to save WiFi settings (returns true if successful) +/** + * @brief Send a command to save WiFi settings. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_save_wifi(const char* ssid, const char* password); + +// Function to get IP address of WiFi Devboard +/** + * @brief Send a command to get the IP address of the WiFi Devboard + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_address(); + +// Function to get IP address of the connected WiFi network +/** + * @brief Send a command to get the IP address of the connected WiFi network. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_wifi(); + +// Function to disconnect from WiFi (returns true if successful) +/** + * @brief Send a command to disconnect from WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_disconnect_wifi(); + +// Function to connect to WiFi (returns true if successful) +/** + * @brief Send a command to connect to WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_connect_wifi(); + +// Function to send a GET request +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request(const char* url); + +// Function to send a GET request with headers +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_with_headers(const char* url, const char* headers); + +// Function to send a GET request with headers and return bytes +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_bytes(const char* url, const char* headers); + +// Function to send a POST request with headers +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param data The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to send a POST request with headers and return bytes +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param payload The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload); + +// Function to send a PUT request with headers +/** + * @brief Send a PUT request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the PUT request to. + * @param headers The headers to send with the PUT request. + * @param data The data to send with the PUT request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_put_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to send a DELETE request with headers +/** + * @brief Send a DELETE request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the DELETE request to. + * @param headers The headers to send with the DELETE request. + * @param data The data to send with the DELETE request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_delete_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to handle received data asynchronously +/** + * @brief Callback function to handle received data asynchronously. + * @return void + * @param line The received line. + * @param context The context passed to the callback. + * @note The received data will be handled asynchronously via the callback and handles the state of the UART. + */ +void flipper_http_rx_callback(const char* line, void* context); + +// Function to trim leading and trailing spaces and newlines from a constant string +char* trim(const char* str); +/** + * @brief Process requests and parse JSON data asynchronously + * @param http_request The function to send the request + * @param parse_json The function to parse the JSON + * @return true if successful, false otherwise + */ +bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)); + +#endif // FLIPPER_HTTP_H diff --git a/flip_weather/jsmn.h b/flip_library/jsmn/jsmn.c similarity index 84% rename from flip_weather/jsmn.h rename to flip_library/jsmn/jsmn.c index 26312c613..eb33b3cc7 100644 --- a/flip_weather/jsmn.h +++ b/flip_library/jsmn/jsmn.c @@ -3,113 +3,20 @@ * * Copyright (c) 2010 Serge Zaitsev * - * 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. + * [License text continues...] */ -#ifndef JSMN_H -#define JSMN_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef JSMN_STATIC -#define JSMN_API static -#else -#define JSMN_API extern -#endif - -/** - * JSON type identifier. Basic types are: - * o Object - * o Array - * o String - * o Other primitive: number, boolean (true/false) or null - */ -typedef enum { - JSMN_UNDEFINED = 0, - JSMN_OBJECT = 1 << 0, - JSMN_ARRAY = 1 << 1, - JSMN_STRING = 1 << 2, - JSMN_PRIMITIVE = 1 << 3 -} jsmntype_t; - -enum jsmnerr { - /* Not enough tokens were provided */ - JSMN_ERROR_NOMEM = -1, - /* Invalid character inside JSON string */ - JSMN_ERROR_INVAL = -2, - /* The string is not a full JSON packet, more bytes expected */ - JSMN_ERROR_PART = -3 -}; - -/** - * JSON token description. - * type type (object, array, string etc.) - * start start position in JSON data string - * end end position in JSON data string - */ -typedef struct jsmntok { - jsmntype_t type; - int start; - int end; - int size; -#ifdef JSMN_PARENT_LINKS - int parent; -#endif -} jsmntok_t; - -/** - * JSON parser. Contains an array of token blocks available. Also stores - * the string being parsed now and current position in that string. - */ -typedef struct jsmn_parser { - unsigned int pos; /* offset in the JSON string */ - unsigned int toknext; /* next token to allocate */ - int toksuper; /* superior token node, e.g. parent object or array */ -} jsmn_parser; -/** - * Create JSON parser over an array of tokens - */ -JSMN_API void jsmn_init(jsmn_parser* parser); - -/** - * Run JSON parser. It parses a JSON data string into and array of tokens, each - * describing - * a single JSON object. - */ -JSMN_API int jsmn_parse( - jsmn_parser* parser, - const char* js, - const size_t len, - jsmntok_t* tokens, - const unsigned int num_tokens); +#include +#include +#include -#ifndef JSMN_HEADER /** - * Allocates a fresh unused token from the token pool. - */ + * Allocates a fresh unused token from the token pool. + */ static jsmntok_t* jsmn_alloc_token(jsmn_parser* parser, jsmntok_t* tokens, const size_t num_tokens) { jsmntok_t* tok; + if(parser->toknext >= num_tokens) { return NULL; } @@ -123,8 +30,8 @@ static jsmntok_t* } /** - * Fills token type and boundaries. - */ + * Fills token type and boundaries. + */ static void jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { token->type = type; @@ -134,8 +41,8 @@ static void } /** - * Fills next available token with JSON primitive. - */ + * Fills next available token with JSON primitive. + */ static int jsmn_parse_primitive( jsmn_parser* parser, const char* js, @@ -195,8 +102,8 @@ static int jsmn_parse_primitive( } /** - * Fills next token with JSON string. - */ + * Fills next token with JSON string. + */ static int jsmn_parse_string( jsmn_parser* parser, const char* js, @@ -272,9 +179,18 @@ static int jsmn_parse_string( } /** - * Parse JSON string and fill tokens. - */ -JSMN_API int jsmn_parse( + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser* parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +/** + * Parse JSON string and fill tokens. + */ +int jsmn_parse( jsmn_parser* parser, const char* js, const size_t len, @@ -464,34 +380,6 @@ JSMN_API int jsmn_parse( return count; } -/** - * Creates a new parser based over a given buffer with an array of tokens - * available. - */ -JSMN_API void jsmn_init(jsmn_parser* parser) { - parser->pos = 0; - parser->toknext = 0; - parser->toksuper = -1; -} - -#endif /* JSMN_HEADER */ - -#ifdef __cplusplus -} -#endif - -#endif /* JSMN_H */ - -#ifndef JB_JSMN_EDIT -#define JB_JSMN_EDIT -/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ - -#include -#include -#include -#include -#include - // Helper function to create a JSON object char* jsmn(const char* key, const char* value) { int length = strlen(key) + strlen(value) + 8; // Calculate required length @@ -512,7 +400,7 @@ int jsoneq(const char* json, jsmntok_t* tok, const char* s) { return -1; } -// return the value of the key in the JSON data +// Return the value of the key in the JSON data char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { // Parse the JSON feed if(json_data != NULL) { @@ -776,5 +664,3 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in free(array_str); return values; } - -#endif /* JB_JSMN_EDIT */ diff --git a/flip_library/jsmn/jsmn.h b/flip_library/jsmn/jsmn.h new file mode 100644 index 000000000..cd95a0e58 --- /dev/null +++ b/flip_library/jsmn/jsmn.h @@ -0,0 +1,131 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * 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: + * + * [License text continues...] + */ + +#ifndef JSMN_H +#define JSMN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser* parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing a single JSON object. + */ +JSMN_API int jsmn_parse( + jsmn_parser* parser, + const char* js, + const size_t len, + jsmntok_t* tokens, + const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/* Implementation has been moved to jsmn.c */ +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */ + +/* Custom Helper Functions */ +#ifndef JB_JSMN_EDIT +#define JB_JSMN_EDIT +/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ + +#include +#include +#include +#include +#include + +// Helper function to create a JSON object +char* jsmn(const char* key, const char* value); +// Helper function to compare JSON keys +int jsoneq(const char* json, jsmntok_t* tok, const char* s); + +// Return the value of the key in the JSON data +char* get_json_value(char* key, char* json_data, uint32_t max_tokens); + +// Revised get_json_array_value function +char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t max_tokens); + +// Revised get_json_array_values function with correct token skipping +char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, int* num_values); +#endif /* JB_JSMN_EDIT */ diff --git a/flip_social/README.md b/flip_social/README.md index cee9b4982..e3e0759fa 100644 --- a/flip_social/README.md +++ b/flip_social/README.md @@ -6,7 +6,7 @@ The highlight of this app is customizable pre-saves, which, as explained below, FlipSocial uses the FlipperHTTP flash for the WiFi Devboard, first introduced in the WebCrawler app: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP ## Requirements -- WiFi Dev Board or Raspberry Pi Pico W for Flipper Zero with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP +- WiFi Developer Board or Raspberry Pi Pico W with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP - WiFi Access Point @@ -88,4 +88,8 @@ This is a big project, and I welcome all contributors, especially developers int - **Solution 3:** Ensure your WiFi Devboard is plugged in, then restart your Flipper device. 5. Out of memory when starting the app or after visiting the feed and post views back-to-back. -- **Solution:** Restart your Flipper device. \ No newline at end of file +- **Solution:** Restart your Flipper device. + +6. I can no longer access my Direct Messages. +- **Solution 1:** Uppdate the app to version 0.6.3 (or higher) +- **Solution 2:** Click the logout button then login again. diff --git a/flip_social/alloc/flip_social_alloc.c b/flip_social/alloc/flip_social_alloc.c index e96acb61b..3e6d42f39 100644 --- a/flip_social/alloc/flip_social_alloc.c +++ b/flip_social/alloc/flip_social_alloc.c @@ -918,6 +918,8 @@ FlipSocialApp* flip_social_app_alloc() { '\0'; } + auth_headers_alloc(); + // set variable item text (ommit the passwords) variable_item_set_current_value_text( app->variable_item_logged_in_wifi_settings_ssid, app->wifi_ssid_logged_in); diff --git a/flip_social/application.fam b/flip_social/application.fam index a45e50426..97ffe28ca 100644 --- a/flip_social/application.fam +++ b/flip_social/application.fam @@ -9,6 +9,6 @@ App( fap_icon_assets="assets", fap_author="jblanked", fap_weburl="https://github.com/jblanked/FlipSocial", - fap_version="0.6", + fap_version="0.6.3", fap_description="Social media platform for the Flipper Zero.", ) diff --git a/flip_social/assets/README.md b/flip_social/assets/README.md index cee9b4982..e3e0759fa 100644 --- a/flip_social/assets/README.md +++ b/flip_social/assets/README.md @@ -6,7 +6,7 @@ The highlight of this app is customizable pre-saves, which, as explained below, FlipSocial uses the FlipperHTTP flash for the WiFi Devboard, first introduced in the WebCrawler app: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP ## Requirements -- WiFi Dev Board or Raspberry Pi Pico W for Flipper Zero with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP +- WiFi Developer Board or Raspberry Pi Pico W with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP - WiFi Access Point @@ -88,4 +88,8 @@ This is a big project, and I welcome all contributors, especially developers int - **Solution 3:** Ensure your WiFi Devboard is plugged in, then restart your Flipper device. 5. Out of memory when starting the app or after visiting the feed and post views back-to-back. -- **Solution:** Restart your Flipper device. \ No newline at end of file +- **Solution:** Restart your Flipper device. + +6. I can no longer access my Direct Messages. +- **Solution 1:** Uppdate the app to version 0.6.3 (or higher) +- **Solution 2:** Click the logout button then login again. diff --git a/flip_social/callback/flip_social_callback.c b/flip_social/callback/flip_social_callback.c index 58005c3d5..d765d1f4c 100644 --- a/flip_social/callback/flip_social_callback.c +++ b/flip_social/callback/flip_social_callback.c @@ -967,6 +967,7 @@ void flip_social_logged_in_profile_change_password_updated(void* context) { } // send post request to change password + auth_headers_alloc(); char payload[256]; snprintf( payload, @@ -975,15 +976,12 @@ void flip_social_logged_in_profile_change_password_updated(void* context) { app->login_username_logged_out, old_password, app->change_password_logged_in); - char* headers = jsmn("Content-Type", "application/json"); if(!flipper_http_post_request_with_headers( - "https://www.flipsocial.net/api/user/change-password/", headers, payload)) { + "https://www.flipsocial.net/api/user/change-password/", auth_headers, payload)) { FURI_LOG_E(TAG, "Failed to send post request to change password"); FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); - free(headers); return; } - free(headers); // Save the settings save_settings( app_instance->wifi_ssid_logged_out, @@ -1098,6 +1096,7 @@ void flip_social_logged_in_messages_user_choice_message_updated(void* context) { '\0'; // send post request to send message + auth_headers_alloc(); char url[128]; char payload[256]; snprintf( @@ -1111,17 +1110,15 @@ void flip_social_logged_in_messages_user_choice_message_updated(void* context) { "{\"receiver\":\"%s\",\"content\":\"%s\"}", flip_social_explore->usernames[flip_social_explore->index], app->message_user_choice_logged_in); - char* headers = jsmn("Content-Type", "application/json"); - if(flipper_http_post_request_with_headers(url, headers, payload)) // start the async request + if(flipper_http_post_request_with_headers( + url, auth_headers, payload)) // start the async request { furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); fhttp.state = RECEIVING; - free(headers); } else { FURI_LOG_E(TAG, "Failed to send post request to send message"); FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); - free(headers); return; } while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { @@ -1173,6 +1170,7 @@ void flip_social_logged_in_messages_new_message_updated(void* context) { '\0'; // send post request to send message + auth_headers_alloc(); char url[128]; char payload[256]; snprintf( @@ -1186,17 +1184,15 @@ void flip_social_logged_in_messages_new_message_updated(void* context) { "{\"receiver\":\"%s\",\"content\":\"%s\"}", flip_social_message_users->usernames[flip_social_message_users->index], app->messages_new_message_logged_in); - char* headers = jsmn("Content-Type", "application/json"); - if(flipper_http_post_request_with_headers(url, headers, payload)) // start the async request + if(flipper_http_post_request_with_headers( + url, auth_headers, payload)) // start the async request { furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); fhttp.state = RECEIVING; - free(headers); } else { FURI_LOG_E(TAG, "Failed to send post request to send message"); FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); - free(headers); return; } while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { diff --git a/flip_social/draw/flip_social_draw.c b/flip_social/draw/flip_social_draw.c index f412f3f37..13abe7195 100644 --- a/flip_social/draw/flip_social_draw.c +++ b/flip_social/draw/flip_social_draw.c @@ -132,12 +132,22 @@ void flip_social_callback_draw_compose(Canvas* canvas, void* model) { FURI_LOG_E(TAG, "FlipSocialApp is NULL"); return; } + if(!selected_message) { + FURI_LOG_E(TAG, "Selected message is NULL"); + return; + } + + if(strlen(selected_message) > MAX_MESSAGE_LENGTH) { + FURI_LOG_E(TAG, "Message is too long"); + return; + } if(!flip_social_dialog_shown) { flip_social_dialog_shown = true; app_instance->input_event_queue = furi_record_open(RECORD_INPUT_EVENTS); app_instance->input_event = furi_pubsub_subscribe(app_instance->input_event_queue, on_input, NULL); + auth_headers_alloc(); } draw_user_message(canvas, selected_message, 0, 2); @@ -159,6 +169,10 @@ void flip_social_callback_draw_compose(Canvas* canvas, void* model) { case ActionNext: // send selected_message if(selected_message && app_instance->login_username_logged_in) { + if(strlen(selected_message) > MAX_MESSAGE_LENGTH) { + FURI_LOG_E(TAG, "Message is too long"); + return; + } // Send the selected_message char command[256]; snprintf( @@ -168,12 +182,8 @@ void flip_social_callback_draw_compose(Canvas* canvas, void* model) { app_instance->login_username_logged_in, selected_message); - bool success = flipper_http_post_request_with_headers( - "https://www.flipsocial.net/api/feed/post/", - "{\"Content-Type\":\"application/json\"}", - command); - - if(!success) { + if(!flipper_http_post_request_with_headers( + "https://www.flipsocial.net/api/feed/post/", auth_headers, command)) { FURI_LOG_E(TAG, "Failed to send HTTP request for feed"); fhttp.state = ISSUE; return; @@ -185,23 +195,12 @@ void flip_social_callback_draw_compose(Canvas* canvas, void* model) { FURI_LOG_E(TAG, "Message or username is NULL"); return; } - - int i = 0; while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { // Wait for the feed to be received furi_delay_ms(100); - char dots_str[64] = "Receiving"; - - // Append dots to the string based on the value of i - int dot_count = i % 4; - int len = strlen(dots_str); - snprintf(dots_str + len, sizeof(dots_str) - len, "%.*s", dot_count, "...."); - // Draw the resulting string on the canvas - canvas_draw_str(canvas, 0, 30, dots_str); - - i++; + canvas_draw_str(canvas, 0, 30, "Receiving.."); } flip_social_dialog_stop = true; furi_timer_stop(fhttp.get_timeout_timer); @@ -372,6 +371,7 @@ void flip_social_callback_draw_feed(Canvas* canvas, void* model) { app_instance->input_event_queue = furi_record_open(RECORD_INPUT_EVENTS); app_instance->input_event = furi_pubsub_subscribe(app_instance->input_event_queue, on_input, NULL); + auth_headers_alloc(); } // handle action @@ -442,9 +442,7 @@ void flip_social_callback_draw_feed(Canvas* canvas, void* model) { app_instance->login_username_logged_in, flip_social_feed->ids[flip_social_feed->index]); flipper_http_post_request_with_headers( - "https://www.flipsocial.net/api/feed/flip/", - "{\"Content-Type\":\"application/json\"}", - payload); + "https://www.flipsocial.net/api/feed/flip/", auth_headers, payload); flip_social_canvas_draw_message( canvas, flip_social_feed->usernames[flip_social_feed->index], @@ -513,10 +511,9 @@ void flip_social_callback_draw_login(Canvas* canvas, void* model) { "{\"username\":\"%s\",\"password\":\"%s\"}", app_instance->login_username_logged_out, app_instance->login_password_logged_out); + auth_headers_alloc(); flip_social_login_success = flipper_http_post_request_with_headers( - "https://www.flipsocial.net/api/user/login/", - "{\"Content-Type\":\"application/json\"}", - buffer); + "https://www.flipsocial.net/api/user/login/", auth_headers, buffer); if(flip_social_login_success) { fhttp.state = RECEIVING; return; @@ -679,6 +676,9 @@ void flip_social_callback_draw_register(Canvas* canvas, void* model) { app_instance->is_logged_in = "true"; + // update header credentials + auth_headers_alloc(); + // save the credentials save_settings( app_instance->wifi_ssid_logged_out, @@ -755,6 +755,7 @@ void flip_social_callback_draw_explore(Canvas* canvas, void* model) { app_instance->input_event_queue = furi_record_open(RECORD_INPUT_EVENTS); app_instance->input_event = furi_pubsub_subscribe(app_instance->input_event_queue, on_input, NULL); + auth_headers_alloc(); } flip_social_canvas_draw_explore( canvas, flip_social_explore->usernames[flip_social_explore->index], last_explore_response); @@ -771,9 +772,7 @@ void flip_social_callback_draw_explore(Canvas* canvas, void* model) { app_instance->login_username_logged_in, flip_social_explore->usernames[flip_social_explore->index]); flipper_http_post_request_with_headers( - "https://www.flipsocial.net/api/user/add-friend/", - "{\"Content-Type\":\"application/json\"}", - add_payload); + "https://www.flipsocial.net/api/user/add-friend/", auth_headers, add_payload); canvas_clear(canvas); flip_social_canvas_draw_explore( canvas, flip_social_explore->usernames[flip_social_explore->index], "Added!"); @@ -789,9 +788,7 @@ void flip_social_callback_draw_explore(Canvas* canvas, void* model) { app_instance->login_username_logged_in, flip_social_explore->usernames[flip_social_explore->index]); flipper_http_post_request_with_headers( - "https://www.flipsocial.net/api/user/remove-friend/", - "{\"Content-Type\":\"application/json\"}", - remove_payload); + "https://www.flipsocial.net/api/user/remove-friend/", auth_headers, remove_payload); canvas_clear(canvas); flip_social_canvas_draw_explore( canvas, flip_social_explore->usernames[flip_social_explore->index], "Removed!"); @@ -834,6 +831,7 @@ void flip_social_callback_draw_friends(Canvas* canvas, void* model) { app_instance->input_event_queue = furi_record_open(RECORD_INPUT_EVENTS); app_instance->input_event = furi_pubsub_subscribe(app_instance->input_event_queue, on_input, NULL); + auth_headers_alloc(); } flip_social_canvas_draw_explore( canvas, flip_social_friends->usernames[flip_social_friends->index], last_explore_response); @@ -850,9 +848,7 @@ void flip_social_callback_draw_friends(Canvas* canvas, void* model) { app_instance->login_username_logged_in, flip_social_friends->usernames[flip_social_friends->index]); if(flipper_http_post_request_with_headers( - "https://www.flipsocial.net/api/user/add-friend/", - "{\"Content-Type\":\"application/json\"}", - add_payload)) { + "https://www.flipsocial.net/api/user/add-friend/", auth_headers, add_payload)) { canvas_clear(canvas); flip_social_canvas_draw_explore( canvas, flip_social_friends->usernames[flip_social_friends->index], "Added!"); @@ -878,7 +874,7 @@ void flip_social_callback_draw_friends(Canvas* canvas, void* model) { flip_social_friends->usernames[flip_social_friends->index]); if(flipper_http_post_request_with_headers( "https://www.flipsocial.net/api/user/remove-friend/", - "{\"Content-Type\":\"application/json\"}", + auth_headers, remove_payload)) { canvas_clear(canvas); flip_social_canvas_draw_explore( diff --git a/flip_social/explore/flip_social_explore.c b/flip_social/explore/flip_social_explore.c index 8121873cc..2fdb6a703 100644 --- a/flip_social/explore/flip_social_explore.c +++ b/flip_social/explore/flip_social_explore.c @@ -40,12 +40,9 @@ bool flip_social_get_explore() { STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/users.txt"); fhttp.save_received_data = true; - char* headers = jsmn("Content-Type", "application/json"); - // will return true unless the devboard is not connected - bool success = flipper_http_get_request_with_headers( - "https://www.flipsocial.net/api/user/users/", headers); - free(headers); - if(!success) { + auth_headers_alloc(); + if(!flipper_http_get_request_with_headers( + "https://www.flipsocial.net/api/user/users/", auth_headers)) { FURI_LOG_E(TAG, "Failed to send HTTP request for explore"); fhttp.state = ISSUE; return false; diff --git a/flip_social/feed/flip_social_feed.c b/flip_social/feed/flip_social_feed.c index 9be35b451..de095d859 100644 --- a/flip_social/feed/flip_social_feed.c +++ b/flip_social/feed/flip_social_feed.c @@ -92,27 +92,29 @@ void flip_social_free_feed() { } bool flip_social_get_feed() { + if(!app_instance) { + FURI_LOG_E(TAG, "FlipSocialApp is NULL"); + return false; + } // Get the feed from the server if(app_instance->login_username_logged_out == NULL) { FURI_LOG_E(TAG, "Username is NULL"); return false; } - char command[128]; snprintf( fhttp.file_path, sizeof(fhttp.file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/feed.txt"); fhttp.save_received_data = true; - char* headers = jsmn("Content-Type", "application/json"); + auth_headers_alloc(); + char command[96]; snprintf( command, - 128, + 96, "https://www.flipsocial.net/api/feed/40/%s/extended/", app_instance->login_username_logged_out); - bool success = flipper_http_get_request_with_headers(command, headers); - free(headers); - if(!success) { + if(!flipper_http_get_request_with_headers(command, auth_headers)) { FURI_LOG_E(TAG, "Failed to send HTTP request for feed"); fhttp.state = ISSUE; return false; diff --git a/flip_social/flip_social.c b/flip_social/flip_social.c index bd9d1ad39..ab1d9597b 100644 --- a/flip_social/flip_social.c +++ b/flip_social/flip_social.c @@ -20,6 +20,8 @@ bool flip_social_send_message = false; char* last_explore_response = NULL; char* selected_message = NULL; +char auth_headers[256] = {0}; + /** * @brief Function to free the resources used by FlipSocialApp. * @details Cleans up all allocated resources before exiting the application. @@ -241,6 +243,8 @@ void flip_social_app_free(FlipSocialApp* app) { if(app->message_user_choice_logged_in) free(app->message_user_choice_logged_in); if(app->message_user_choice_logged_in_temp_buffer) free(app->message_user_choice_logged_in_temp_buffer); + if(last_explore_response) free(last_explore_response); + if(selected_message) free(selected_message); if(app->input_event && app->input_event_queue) furi_pubsub_unsubscribe(app->input_event_queue, app->input_event); @@ -251,3 +255,27 @@ void flip_social_app_free(FlipSocialApp* app) { // Free the app structure if(app_instance) free(app_instance); } + +void auth_headers_alloc(void) { + if(!app_instance) { + return; + } + + if(app_instance->login_username_logged_out && app_instance->login_password_logged_out) { + snprintf( + auth_headers, + sizeof(auth_headers), + "{\"Content-Type\":\"application/json\",\"username\":\"%s\",\"password\":\"%s\"}", + app_instance->login_username_logged_out, + app_instance->login_password_logged_out); + } else if(app_instance->login_username_logged_in && app_instance->change_password_logged_in) { + snprintf( + auth_headers, + sizeof(auth_headers), + "{\"Content-Type\":\"application/json\",\"username\":\"%s\",\"password\":\"%s\"}", + app_instance->login_username_logged_in, + app_instance->change_password_logged_in); + } else { + snprintf(auth_headers, sizeof(auth_headers), "{\"Content-Type\":\"application/json\"}"); + } +} diff --git a/flip_social/flip_social.h b/flip_social/flip_social.h index 57a1d8d73..c5cd9312a 100644 --- a/flip_social/flip_social.h +++ b/flip_social/flip_social.h @@ -325,5 +325,7 @@ extern bool flip_social_dialog_stop; extern bool flip_social_send_message; extern char* last_explore_response; extern char* selected_message; +extern char auth_headers[256]; +void auth_headers_alloc(void); #endif diff --git a/flip_social/flipper_http/flipper_http.c b/flip_social/flipper_http/flipper_http.c index 63d6f9f3c..c0182f693 100644 --- a/flip_social/flipper_http/flipper_http.c +++ b/flip_social/flipper_http/flipper_http.c @@ -1,7 +1,8 @@ -#include +#include // change this to where flipper_http.h is located FlipperHTTP fhttp; char rx_line_buffer[RX_LINE_BUFFER_SIZE]; uint8_t file_buffer[FILE_BUFFER_SIZE]; +size_t file_buffer_len = 0; // Function to append received data to file // make sure to initialize the file path before calling this function bool flipper_http_append_to_file( @@ -13,6 +14,15 @@ bool flipper_http_append_to_file( File* file = storage_file_alloc(storage); if(start_new_file) { + // Delete the file if it already exists + if(storage_file_exists(storage, file_path)) { + if(!storage_simply_remove_recursive(storage, file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to delete file: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + } // Open the file in write mode if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path); @@ -131,12 +141,13 @@ FuriString* flipper_http_load_from_file(char* file_path) { int32_t flipper_http_worker(void* context) { UNUSED(context); size_t rx_line_pos = 0; - static size_t file_buffer_len = 0; while(1) { uint32_t events = furi_thread_flags_wait( WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever); - if(events & WorkerEvtStop) break; + if(events & WorkerEvtStop) { + break; + } if(events & WorkerEvtRxDone) { // Continuously read from the stream buffer until it's empty while(!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) { @@ -156,10 +167,14 @@ int32_t flipper_http_worker(void* context) { // Write to file if buffer is full if(file_buffer_len >= FILE_BUFFER_SIZE) { if(!flipper_http_append_to_file( - file_buffer, file_buffer_len, false, fhttp.file_path)) { + file_buffer, + file_buffer_len, + fhttp.just_started_bytes, + fhttp.file_path)) { FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); } file_buffer_len = 0; + fhttp.just_started_bytes = false; } } @@ -182,36 +197,6 @@ int32_t flipper_http_worker(void* context) { } } - if(fhttp.save_bytes) { - // Write the remaining data to the file - if(file_buffer_len > 0) { - if(!flipper_http_append_to_file(file_buffer, file_buffer_len, false, fhttp.file_path)) { - FURI_LOG_E(HTTP_TAG, "Failed to append remaining data to file"); - } - } - } - - // remove [POST/END] and/or [GET/END] from the file - if(fhttp.save_bytes) { - char* end = NULL; - if((end = strstr(fhttp.file_path, "[POST/END]")) != NULL) { - *end = '\0'; - } else if((end = strstr(fhttp.file_path, "[GET/END]")) != NULL) { - *end = '\0'; - } - } - - // remove newline from the from the end of the file - if(fhttp.save_bytes) { - char* end = NULL; - if((end = strstr(fhttp.file_path, "\n")) != NULL) { - *end = '\0'; - } - } - - // Reset the file buffer length - file_buffer_len = 0; - return 0; } // Timer callback function @@ -418,13 +403,13 @@ bool flipper_http_send_data(const char* data) { // Create a buffer with data + '\n' size_t send_length = data_length + 1; // +1 for '\n' - if(send_length > 256) { // Ensure buffer size is sufficient + if(send_length > 512) { // Ensure buffer size is sufficient FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP."); return false; } - char send_buffer[257]; // 256 + 1 for safety - strncpy(send_buffer, data, 256); + char send_buffer[513]; // 512 + 1 for safety + strncpy(send_buffer, data, 512); send_buffer[data_length] = '\n'; // Append newline send_buffer[data_length + 1] = '\0'; // Null-terminate @@ -710,7 +695,7 @@ bool flipper_http_get_request(const char* url) { } // Prepare GET request command - char command[256]; + char command[512]; int ret = snprintf(command, sizeof(command), "[GET]%s", url); if(ret < 0 || ret >= (int)sizeof(command)) { FURI_LOG_E("FlipperHTTP", "Failed to format GET request command."); @@ -742,7 +727,7 @@ bool flipper_http_get_request_with_headers(const char* url, const char* headers) } // Prepare GET request command with headers - char command[256]; + char command[512]; int ret = snprintf( command, sizeof(command), "[GET/HTTP]{\"url\":\"%s\",\"headers\":%s}", url, headers); if(ret < 0 || ret >= (int)sizeof(command)) { @@ -774,7 +759,7 @@ bool flipper_http_get_request_bytes(const char* url, const char* headers) { } // Prepare GET request command with headers - char command[256]; + char command[512]; int ret = snprintf( command, sizeof(command), "[GET/BYTES]{\"url\":\"%s\",\"headers\":%s}", url, headers); if(ret < 0 || ret >= (int)sizeof(command)) { @@ -812,7 +797,7 @@ bool flipper_http_post_request_with_headers( } // Prepare POST request command with headers and data - char command[256]; + char command[512]; int ret = snprintf( command, sizeof(command), @@ -851,7 +836,7 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const } // Prepare POST request command with headers and data - char command[256]; + char command[512]; int ret = snprintf( command, sizeof(command), @@ -1006,8 +991,35 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.just_started_get = false; fhttp.state = IDLE; fhttp.save_bytes = false; - fhttp.is_bytes_request = false; fhttp.save_received_data = false; + + if(fhttp.is_bytes_request) { + // Search for the binary marker `[GET/END]` in the file buffer + const char marker[] = "[GET/END]"; + const size_t marker_len = sizeof(marker) - 1; // Exclude null terminator + + for(size_t i = 0; i <= file_buffer_len - marker_len; i++) { + // Check if the marker is found + if(memcmp(&file_buffer[i], marker, marker_len) == 0) { + // Remove the marker by shifting the remaining data left + size_t remaining_len = file_buffer_len - (i + marker_len); + memmove(&file_buffer[i], &file_buffer[i + marker_len], remaining_len); + file_buffer_len -= marker_len; + break; + } + } + + // If there is data left in the buffer, append it to the file + if(file_buffer_len > 0) { + if(!flipper_http_append_to_file( + file_buffer, file_buffer_len, false, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + } + file_buffer_len = 0; + } + } + + fhttp.is_bytes_request = false; return; } @@ -1041,8 +1053,35 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.just_started_post = false; fhttp.state = IDLE; fhttp.save_bytes = false; - fhttp.is_bytes_request = false; fhttp.save_received_data = false; + + if(fhttp.is_bytes_request) { + // Search for the binary marker `[POST/END]` in the file buffer + const char marker[] = "[POST/END]"; + const size_t marker_len = sizeof(marker) - 1; // Exclude null terminator + + for(size_t i = 0; i <= file_buffer_len - marker_len; i++) { + // Check if the marker is found + if(memcmp(&file_buffer[i], marker, marker_len) == 0) { + // Remove the marker by shifting the remaining data left + size_t remaining_len = file_buffer_len - (i + marker_len); + memmove(&file_buffer[i], &file_buffer[i + marker_len], remaining_len); + file_buffer_len -= marker_len; + break; + } + } + + // If there is data left in the buffer, append it to the file + if(file_buffer_len > 0) { + if(!flipper_http_append_to_file( + file_buffer, file_buffer_len, false, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + } + file_buffer_len = 0; + } + } + + fhttp.is_bytes_request = false; return; } @@ -1149,6 +1188,8 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.state = RECEIVING; // for GET request, save data only if it's a bytes request fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; + file_buffer_len = 0; return; } else if(strstr(line, "[POST/SUCCESS]") != NULL) { FURI_LOG_I(HTTP_TAG, "POST request succeeded."); @@ -1157,6 +1198,8 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.state = RECEIVING; // for POST request, save data only if it's a bytes request fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; + file_buffer_len = 0; return; } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { FURI_LOG_I(HTTP_TAG, "PUT request succeeded."); diff --git a/flip_social/flipper_http/flipper_http.h b/flip_social/flipper_http/flipper_http.h index 3e589c39b..8901fe8cb 100644 --- a/flip_social/flipper_http/flipper_http.h +++ b/flip_social/flipper_http/flipper_http.h @@ -7,12 +7,13 @@ #include #include #include +#include // STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext #define HTTP_TAG "FlipSocial" // change this to your app name #define http_tag "flip_social" // change this to your app id -#define UART_CH (FuriHalSerialIdUsart) // UART channel +#define UART_CH (momentum_settings.uart_esp_channel) // UART channel #define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds #define BAUDRATE (115200) // UART baudrate #define RX_BUF_SIZE 1024 // UART RX buffer size @@ -73,12 +74,15 @@ typedef struct { bool is_bytes_request; // Flag to indicate if the request is for bytes bool save_bytes; // Flag to save the received data to a file bool save_received_data; // Flag to save the received data to a file + + bool just_started_bytes; // Indicates if bytes data reception has just started } FlipperHTTP; extern FlipperHTTP fhttp; // Global static array for the line buffer extern char rx_line_buffer[RX_LINE_BUFFER_SIZE]; extern uint8_t file_buffer[FILE_BUFFER_SIZE]; +extern size_t file_buffer_len; // fhttp.last_response holds the last received data from the UART diff --git a/flip_social/friends/flip_social_friends.c b/flip_social/friends/flip_social_friends.c index 51cbe2eb1..972420ff8 100644 --- a/flip_social/friends/flip_social_friends.c +++ b/flip_social/friends/flip_social_friends.c @@ -38,16 +38,15 @@ bool flip_social_get_friends() { STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/friends.txt"); fhttp.save_received_data = true; - char* headers = jsmn("Content-Type", "application/json"); + auth_headers_alloc(); snprintf( url, 100, "https://www.flipsocial.net/api/user/friends/%s/", app_instance->login_username_logged_in); - bool success = flipper_http_get_request_with_headers(url, headers); - free(headers); - if(!success) { + if(!flipper_http_get_request_with_headers(url, auth_headers)) { FURI_LOG_E(TAG, "Failed to send HTTP request for friends"); + fhttp.state = ISSUE; return false; } fhttp.state = RECEIVING; diff --git a/flip_social/messages/flip_social_messages.c b/flip_social/messages/flip_social_messages.c index 8ae833a39..e164ea938 100644 --- a/flip_social/messages/flip_social_messages.c +++ b/flip_social/messages/flip_social_messages.c @@ -135,15 +135,13 @@ bool flip_social_get_message_users() { STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/message_users.txt"); fhttp.save_received_data = true; - char* headers = jsmn("Content-Type", "application/json"); + auth_headers_alloc(); snprintf( command, 128, "https://www.flipsocial.net/api/messages/%s/get/list/", app_instance->login_username_logged_out); - bool success = flipper_http_get_request_with_headers(command, headers); - free(headers); - if(!success) { + if(!flipper_http_get_request_with_headers(command, auth_headers)) { FURI_LOG_E(TAG, "Failed to send HTTP request for messages"); fhttp.state = ISSUE; return false; @@ -158,6 +156,11 @@ bool flip_social_get_messages_with_user() { FURI_LOG_E(TAG, "Username is NULL"); return false; } + if(!flip_social_message_users->usernames[flip_social_message_users->index] || + strlen(flip_social_message_users->usernames[flip_social_message_users->index]) == 0) { + FURI_LOG_E(TAG, "Username is NULL"); + return false; + } char command[128]; snprintf( fhttp.file_path, @@ -165,16 +168,14 @@ bool flip_social_get_messages_with_user() { STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/messages.txt"); fhttp.save_received_data = true; - char* headers = jsmn("Content-Type", "application/json"); + auth_headers_alloc(); snprintf( command, 128, "https://www.flipsocial.net/api/messages/%s/get/%s/", app_instance->login_username_logged_out, flip_social_message_users->usernames[flip_social_message_users->index]); - bool success = flipper_http_get_request_with_headers(command, headers); - free(headers); - if(!success) { + if(!flipper_http_get_request_with_headers(command, auth_headers)) { FURI_LOG_E(TAG, "Failed to send HTTP request for messages"); fhttp.state = ISSUE; return false; diff --git a/flip_store/.DS_Store b/flip_store/.DS_Store deleted file mode 100644 index 9cd75921d..000000000 Binary files a/flip_store/.DS_Store and /dev/null differ diff --git a/flip_store/README.md b/flip_store/README.md index ecbda9b1c..971d76ddc 100644 --- a/flip_store/README.md +++ b/flip_store/README.md @@ -4,13 +4,13 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long ## Features - App Catalog - Install Apps -- Delete Apps (coming soon) +- Delete Apps +- Install Developer Board Flashes - Install Custom Apps (coming soon) -- Install Devboard Flashes (coming soon) - Install Official Firmware (coming soon) ## Installation -1. Flash your WiFi Devboard: https://github.com/jblanked/FlipperHTTP +1. Flash your WiFi Dveloper Board or Raspberry Pi Pico W: https://github.com/jblanked/FlipperHTTP 2. Install the app. 3. Enjoy :D @@ -20,7 +20,7 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long - App Categories **v0.3** -- Caching +- Improved memory allocation - Stability Patch 2 - App Catalog Patch (add in required functionalility) @@ -47,7 +47,9 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long This is a big task, and I welcome all contributors, especially developers interested in animations and graphics. Fork the repository, create a pull request, and I will review your edits. ## Known Bugs -1. Clicking the catalog results in an "Out of Memory" error. +1. Clicking a category in the app catalog results in an "Out of Memory" error. - This issue has been addressed, but it may still occur. If it does, restart the app. 2. The app file is corrupted. - This is likely due to an error parsing the data. Restart the app and wait until the green LED light turns off after downloading the app before exiting the view. If this happens more than three times, the current version of FlipStore may not be able to download that app successfully. +3. The app is frozen on the "Installing", "Loading", or "Receiving data" screen. + - If it there LED is not on and it's been more than 5 seconds, restart your Flipper Zero with the devboard plugged in. diff --git a/flip_store/flip_store_i.h b/flip_store/alloc/flip_store_alloc.c similarity index 83% rename from flip_store/flip_store_i.h rename to flip_store/alloc/flip_store_alloc.c index accddcbec..dc115bec8 100644 --- a/flip_store/flip_store_i.h +++ b/flip_store/alloc/flip_store_alloc.c @@ -1,8 +1,6 @@ -#ifndef FLIP_STORE_I_H -#define FLIP_STORE_I_H - +#include // Function to allocate resources for the FlipStoreApp -static FlipStoreApp* flip_store_app_alloc() { +FlipStoreApp* flip_store_app_alloc() { FlipStoreApp* app = (FlipStoreApp*)malloc(sizeof(FlipStoreApp)); Gui* gui = furi_record_open(RECORD_GUI); @@ -58,6 +56,16 @@ static FlipStoreApp* flip_store_app_alloc() { app)) { return NULL; } + if(!easy_flipper_set_view( + &app->view_firmware_download, + FlipStoreViewFirmwareDownload, + flip_store_view_draw_callback_firmware, + NULL, + callback_to_firmware_list, + &app->view_dispatcher, + app)) { + return NULL; + } // Widget if(!easy_flipper_set_widget( @@ -99,13 +107,32 @@ static FlipStoreApp* flip_store_app_alloc() { "No", "Yes", NULL, - dialog_callback, + dialog_delete_callback, callback_to_app_list, &app->view_dispatcher, app)) { return NULL; } + if(!easy_flipper_set_dialog_ex( + &app->dialog_firmware, + FlipStoreViewFirmwareDialog, + "Download Firmware", + 0, + 0, + "Are you sure you want to\ndownload this firmware?", + 0, + 10, + "No", + "Yes", + NULL, + dialog_firmware_callback, + callback_to_firmware_list, + &app->view_dispatcher, + app)) { + return NULL; + } + // Text Input if(!easy_flipper_set_uart_text_input( &app->uart_text_input_ssid, @@ -149,18 +176,34 @@ static FlipStoreApp* flip_store_app_alloc() { // Submenu if(!easy_flipper_set_submenu( - &app->submenu, + &app->submenu_main, FlipStoreViewSubmenu, - "FlipStore", + "FlipStore v0.6", callback_exit_app, &app->view_dispatcher)) { return NULL; } + if(!easy_flipper_set_submenu( + &app->submenu_options, + FlipStoreViewSubmenuOptions, + "Browse", + callback_to_submenu, + &app->view_dispatcher)) { + return NULL; + } if(!easy_flipper_set_submenu( &app->submenu_app_list, FlipStoreViewAppList, "App Catalog", - callback_to_submenu, + callback_to_submenu_options, + &app->view_dispatcher)) { + return NULL; + } + if(!easy_flipper_set_submenu( + &app->submenu_firmwares, + FlipStoreViewFirmwares, + "ESP32 Firmwares", + callback_to_submenu_options, &app->view_dispatcher)) { return NULL; } @@ -252,12 +295,31 @@ static FlipStoreApp* flip_store_app_alloc() { &app->view_dispatcher)) { return NULL; } + // submenu_add_item( - app->submenu, "Catalog", FlipStoreSubmenuIndexAppList, callback_submenu_choices, app); + app->submenu_main, "Browse", FlipStoreSubmenuIndexOptions, callback_submenu_choices, app); submenu_add_item( - app->submenu, "About", FlipStoreSubmenuIndexAbout, callback_submenu_choices, app); + app->submenu_main, "About", FlipStoreSubmenuIndexAbout, callback_submenu_choices, app); submenu_add_item( - app->submenu, "Settings", FlipStoreSubmenuIndexSettings, callback_submenu_choices, app); + app->submenu_main, + "Settings", + FlipStoreSubmenuIndexSettings, + callback_submenu_choices, + app); + // + submenu_add_item( + app->submenu_options, + "App Catalog", + FlipStoreSubmenuIndexAppList, + callback_submenu_choices, + app); + submenu_add_item( + app->submenu_options, + "ESP32 Firmwares", + FlipStoreSubmenuIndexFirmwares, + callback_submenu_choices, + app); + // submenu_add_item( app->submenu_app_list, @@ -364,5 +426,3 @@ static FlipStoreApp* flip_store_app_alloc() { return app; } - -#endif // FLIP_STORE_I_H diff --git a/flip_store/alloc/flip_store_alloc.h b/flip_store/alloc/flip_store_alloc.h new file mode 100644 index 000000000..700a8b98e --- /dev/null +++ b/flip_store/alloc/flip_store_alloc.h @@ -0,0 +1,10 @@ +#ifndef FLIP_STORE_I_H +#define FLIP_STORE_I_H + +#include +#include + +// Function to allocate resources for the FlipStoreApp +FlipStoreApp* flip_store_app_alloc(); + +#endif // FLIP_STORE_I_H diff --git a/flip_store/app.c b/flip_store/app.c index 08b5d64ad..73f201926 100644 --- a/flip_store/app.c +++ b/flip_store/app.c @@ -1,8 +1,6 @@ -#include -#include -#include -#include -#include +#include +#include +#include // Entry point for the Hello World application int32_t main_flip_store(void* p) { @@ -26,6 +24,7 @@ int32_t main_flip_store(void* p) { // Free the resources used by the Hello World application flip_store_app_free(app); + flip_catalog_free(); // Return 0 to indicate success return 0; diff --git a/flip_store/application.fam b/flip_store/application.fam index 00b6e36da..8cf17bdc3 100644 --- a/flip_store/application.fam +++ b/flip_store/application.fam @@ -10,5 +10,5 @@ App( fap_description="Download apps via WiFi directly to your Flipper Zero", fap_author="JBlanked", fap_weburl="https://github.com/jblanked/FlipStore", - fap_version="0.2", + fap_version="0.6.1", ) diff --git a/flip_store/apps/flip_store_apps.c b/flip_store/apps/flip_store_apps.c new file mode 100644 index 000000000..6edbdcf8f --- /dev/null +++ b/flip_store/apps/flip_store_apps.c @@ -0,0 +1,446 @@ +#include + +FlipStoreAppInfo* flip_catalog = NULL; + +uint32_t app_selected_index = 0; +bool flip_store_sent_request = false; +bool flip_store_success = false; +bool flip_store_saved_data = false; +bool flip_store_saved_success = false; +uint32_t flip_store_category_index = 0; + +// define the list of categories +char* categories[] = { + "Bluetooth", + "Games", + "GPIO", + "Infrared", + "iButton", + "Media", + "NFC", + "RFID", + "Sub-GHz", + "Tools", + "USB", +}; + +FlipStoreAppInfo* flip_catalog_alloc() { + FlipStoreAppInfo* app_catalog = + (FlipStoreAppInfo*)malloc(MAX_APP_COUNT * sizeof(FlipStoreAppInfo)); + if(!app_catalog) { + FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog."); + return NULL; + } + for(int i = 0; i < MAX_APP_COUNT; i++) { + app_catalog[i].app_name = (char*)malloc(MAX_APP_NAME_LENGTH * sizeof(char)); + if(!app_catalog[i].app_name) { + FURI_LOG_E(TAG, "Failed to allocate memory for app_name."); + return NULL; + } + app_catalog[i].app_id = (char*)malloc(MAX_APP_NAME_LENGTH * sizeof(char)); + if(!app_catalog[i].app_id) { + FURI_LOG_E(TAG, "Failed to allocate memory for app_id."); + return NULL; + } + app_catalog[i].app_build_id = (char*)malloc(MAX_ID_LENGTH * sizeof(char)); + if(!app_catalog[i].app_build_id) { + FURI_LOG_E(TAG, "Failed to allocate memory for app_build_id."); + return NULL; + } + app_catalog[i].app_version = (char*)malloc(MAX_APP_VERSION_LENGTH * sizeof(char)); + if(!app_catalog[i].app_version) { + FURI_LOG_E(TAG, "Failed to allocate memory for app_version."); + return NULL; + } + app_catalog[i].app_description = (char*)malloc(MAX_APP_DESCRIPTION_LENGTH * sizeof(char)); + if(!app_catalog[i].app_description) { + FURI_LOG_E(TAG, "Failed to allocate memory for app_description."); + return NULL; + } + } + return app_catalog; +} +void flip_catalog_free() { + if(!flip_catalog) { + return; + } + for(int i = 0; i < MAX_APP_COUNT; i++) { + if(flip_catalog[i].app_name) { + free(flip_catalog[i].app_name); + } + if(flip_catalog[i].app_id) { + free(flip_catalog[i].app_id); + } + if(flip_catalog[i].app_build_id) { + free(flip_catalog[i].app_build_id); + } + if(flip_catalog[i].app_version) { + free(flip_catalog[i].app_version); + } + if(flip_catalog[i].app_description) { + free(flip_catalog[i].app_description); + } + } +} + +bool flip_store_process_app_list() { + // Initialize the flip_catalog + flip_catalog = flip_catalog_alloc(); + if(!flip_catalog) { + FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog."); + return false; + } + + FuriString* feed_data = flipper_http_load_from_file(fhttp.file_path); + if(feed_data == NULL) { + FURI_LOG_E(TAG, "Failed to load received data from file."); + return false; + } + + char* data_cstr = (char*)furi_string_get_cstr(feed_data); + if(data_cstr == NULL) { + FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); + furi_string_free(feed_data); + return false; + } + + // Parser state variables + bool in_string = false; + bool is_escaped = false; + bool reading_key = false; + bool reading_value = false; + bool inside_app_object = false; + bool found_name = false, found_id = false, found_build_id = false, found_version = false, + found_description = false; + char current_key[MAX_KEY_LENGTH] = {0}; + size_t key_index = 0; + char current_value[MAX_VALUE_LENGTH] = {0}; + size_t value_index = 0; + int app_count = 0; + enum ObjectState object_state = OBJECT_EXPECT_KEY; + enum { + STATE_SEARCH_APPS_KEY, + STATE_SEARCH_ARRAY_START, + STATE_READ_ARRAY_ELEMENTS, + STATE_DONE + } state = STATE_SEARCH_APPS_KEY; + + // Iterate through the data + for(size_t i = 0; data_cstr[i] != '\0' && state != STATE_DONE; ++i) { + char c = data_cstr[i]; + + if(is_escaped) { + is_escaped = false; + if(reading_key && key_index < MAX_KEY_LENGTH - 1) { + current_key[key_index++] = c; + } else if(reading_value && value_index < MAX_VALUE_LENGTH - 1) { + current_value[value_index++] = c; + } + continue; + } + + if(c == '\\') { + is_escaped = true; + continue; + } + + if(c == '\"') { + in_string = !in_string; + if(in_string) { + if(!reading_key && !reading_value) { + if(state == STATE_SEARCH_APPS_KEY || object_state == OBJECT_EXPECT_KEY) { + reading_key = true; + key_index = 0; + current_key[0] = '\0'; + } else if(object_state == OBJECT_EXPECT_VALUE) { + reading_value = true; + value_index = 0; + current_value[0] = '\0'; + } + } + } else { + if(reading_key) { + reading_key = false; + current_key[key_index] = '\0'; + if(state == STATE_SEARCH_APPS_KEY && strcmp(current_key, "apps") == 0) { + state = STATE_SEARCH_ARRAY_START; + } else if(inside_app_object) { + object_state = OBJECT_EXPECT_COLON; + } + } else if(reading_value) { + reading_value = false; + current_value[value_index] = '\0'; + + if(inside_app_object) { + if(strcmp(current_key, "name") == 0) { + snprintf( + flip_catalog[app_count].app_name, + MAX_APP_NAME_LENGTH, + "%.31s", + current_value); + found_name = true; + } else if(strcmp(current_key, "id") == 0) { + snprintf( + flip_catalog[app_count].app_id, + MAX_ID_LENGTH, + "%.31s", + current_value); + found_id = true; + } else if(strcmp(current_key, "build_id") == 0) { + snprintf( + flip_catalog[app_count].app_build_id, + MAX_ID_LENGTH, + "%.31s", + current_value); + found_build_id = true; + } else if(strcmp(current_key, "version") == 0) { + snprintf( + flip_catalog[app_count].app_version, + MAX_APP_VERSION_LENGTH, + "%.3s", + current_value); + found_version = true; + } else if(strcmp(current_key, "description") == 0) { + snprintf( + flip_catalog[app_count].app_description, + MAX_APP_DESCRIPTION_LENGTH, + "%.99s", + current_value); + found_description = true; + } + + if(found_name && found_id && found_build_id && found_version && + found_description) { + app_count++; + if(app_count >= MAX_APP_COUNT) { + FURI_LOG_I(TAG, "Reached maximum app count."); + state = STATE_DONE; + break; + } + + found_name = found_id = found_build_id = found_version = + found_description = false; + } + + object_state = OBJECT_EXPECT_COMMA_OR_END; + } + } + } + continue; + } + + if(in_string) { + if(reading_key && key_index < MAX_KEY_LENGTH - 1) { + current_key[key_index++] = c; + } else if(reading_value && value_index < MAX_VALUE_LENGTH - 1) { + current_value[value_index++] = c; + } + continue; + } + + if(state == STATE_SEARCH_ARRAY_START && c == '[') { + state = STATE_READ_ARRAY_ELEMENTS; + continue; + } + + if(state == STATE_READ_ARRAY_ELEMENTS) { + if(c == '{') { + inside_app_object = true; + object_state = OBJECT_EXPECT_KEY; + } else if(c == '}') { + inside_app_object = false; + } else if(c == ':') { + object_state = OBJECT_EXPECT_VALUE; + } else if(c == ',') { + object_state = OBJECT_EXPECT_KEY; + } else if(c == ']') { + state = STATE_DONE; + break; + } + } + } + + // Clean up + furi_string_free(feed_data); + free(data_cstr); + return app_count > 0; +} + +bool flip_store_get_fap_file( + char* build_id, + uint8_t target, + uint16_t api_major, + uint16_t api_minor) { + char url[128]; + fhttp.save_received_data = false; + fhttp.is_bytes_request = true; + snprintf( + url, + sizeof(url), + "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d", + build_id, + target, + api_major, + api_minor); + char* headers = jsmn("Content-Type", "application/octet-stream"); + bool sent_request = flipper_http_get_request_bytes(url, headers); + free(headers); + return sent_request; +} + +// function to handle the entire installation process "asynchronously" +bool flip_store_install_app(Canvas* canvas, char* category) { + // create /apps/FlipStore directory if it doesn't exist + char directory_path[128]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps/%s", category); + + // Create the directory + Storage* storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + + // Adjusted to access flip_catalog as an array of structures + char installation_text[64]; + snprintf( + installation_text, + sizeof(installation_text), + "Installing %s", + flip_catalog[app_selected_index].app_name); + snprintf( + fhttp.file_path, + sizeof(fhttp.file_path), + STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", + category, + flip_catalog[app_selected_index].app_id); + canvas_draw_str(canvas, 0, 10, installation_text); + canvas_draw_str(canvas, 0, 20, "Sending request.."); + uint8_t target = furi_hal_version_get_hw_target(); + uint16_t api_major, api_minor; + furi_hal_info_get_api_version(&api_major, &api_minor); + if(fhttp.state != INACTIVE && + flip_store_get_fap_file( + flip_catalog[app_selected_index].app_build_id, target, api_major, api_minor)) { + canvas_draw_str(canvas, 0, 30, "Request sent."); + fhttp.state = RECEIVING; + canvas_draw_str(canvas, 0, 40, "Receiving..."); + } else { + FURI_LOG_E(TAG, "Failed to send the request"); + flip_store_success = false; + return false; + } + while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { + // Wait for the feed to be received + furi_delay_ms(10); + } + // furi_timer_stop(fhttp.get_timeout_timer); + if(fhttp.state == ISSUE) { + flip_store_request_error(canvas); + flip_store_success = false; + return false; + } + flip_store_success = true; + return true; +} + +// process the app list and return view +int32_t flip_store_handle_app_list( + FlipStoreApp* app, + int32_t success_view, + char* category, + Submenu** submenu) { + // reset the flip_catalog + flip_catalog_free(); + + if(!app) { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return FlipStoreViewPopup; + } + snprintf( + fhttp.file_path, + sizeof(fhttp.file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s.json", + category); + + fhttp.save_received_data = true; + fhttp.is_bytes_request = false; + char url[128]; + snprintf(url, sizeof(url), "https://www.flipsocial.net/api/flipper/apps/%s/max/", category); + // async call to the app list with timer + if(fhttp.state != INACTIVE && + flipper_http_get_request_with_headers(url, "{\"Content-Type\":\"application/json\"}")) { + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + } else { + FURI_LOG_E(TAG, "Failed to send the request"); + fhttp.state = ISSUE; + return FlipStoreViewPopup; + } + while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { + // Wait for the feed to be received + furi_delay_ms(10); + } + furi_timer_stop(fhttp.get_timeout_timer); + if(fhttp.state == ISSUE) { + FURI_LOG_E(TAG, "Failed to receive data"); + if(fhttp.last_response == NULL) { + if(fhttp.last_response != NULL) { + if(strstr( + fhttp.last_response, + "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL) { + popup_set_text( + app->popup, + "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", + 0, + 10, + AlignLeft, + AlignTop); + } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { + popup_set_text( + app->popup, + "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", + 0, + 10, + AlignLeft, + AlignTop); + } else { + popup_set_text(app->popup, fhttp.last_response, 0, 50, AlignLeft, AlignTop); + } + } else { + popup_set_text( + app->popup, + "[ERROR] Unknown Error.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", + 0, + 10, + AlignLeft, + AlignTop); + } + return FlipStoreViewPopup; + } else { + popup_set_text(app->popup, "Failed to received data.", 0, 50, AlignLeft, AlignTop); + return FlipStoreViewPopup; + } + } else { + // process the app list + if(flip_store_process_app_list() && submenu && flip_catalog) { + submenu_reset(*submenu); + // add each app name to submenu + for(int i = 0; i < MAX_APP_COUNT; i++) { + if(strlen(flip_catalog[i].app_name) > 0) { + submenu_add_item( + *submenu, + flip_catalog[i].app_name, + FlipStoreSubmenuIndexStartAppList + i, + callback_submenu_choices, + app); + } else { + break; + } + } + return success_view; + } else { + FURI_LOG_E(TAG, "Failed to process the app list"); + popup_set_text( + app->popup, "Failed to process the app list", 0, 10, AlignLeft, AlignTop); + return FlipStoreViewPopup; + } + } +} diff --git a/flip_store/apps/flip_store_apps.h b/flip_store/apps/flip_store_apps.h new file mode 100644 index 000000000..3ab1999a3 --- /dev/null +++ b/flip_store/apps/flip_store_apps.h @@ -0,0 +1,65 @@ +#ifndef FLIP_STORE_APPS_H +#define FLIP_STORE_APPS_H + +#include +#include +#include + +// Define maximum limits +#define MAX_APP_NAME_LENGTH 32 +#define MAX_ID_LENGTH 32 +#define MAX_APP_COUNT 100 +#define MAX_APP_DESCRIPTION_LENGTH 100 +#define MAX_APP_VERSION_LENGTH 5 + +// define the list of categories +extern char* categories[]; + +typedef struct { + char* app_name; + char* app_id; + char* app_build_id; + char* app_version; + char* app_description; +} FlipStoreAppInfo; + +extern FlipStoreAppInfo* flip_catalog; + +extern uint32_t app_selected_index; +extern bool flip_store_sent_request; +extern bool flip_store_success; +extern bool flip_store_saved_data; +extern bool flip_store_saved_success; +extern uint32_t flip_store_category_index; + +enum ObjectState { + OBJECT_EXPECT_KEY, + OBJECT_EXPECT_COLON, + OBJECT_EXPECT_VALUE, + OBJECT_EXPECT_COMMA_OR_END +}; + +FlipStoreAppInfo* flip_catalog_alloc(); + +void flip_catalog_free(); + +// Utility function to parse JSON incrementally from a file +bool flip_store_process_app_list(); + +bool flip_store_get_fap_file( + char* build_id, + uint8_t target, + uint16_t api_major, + uint16_t api_minor); + +// function to handle the entire installation process "asynchronously" +bool flip_store_install_app(Canvas* canvas, char* category); + +// process the app list and return view +int32_t flip_store_handle_app_list( + FlipStoreApp* app, + int32_t success_view, + char* category, + Submenu** submenu); + +#endif // FLIP_STORE_APPS_H diff --git a/flip_store/assets/01-main-menu.png b/flip_store/assets/01-main-menu.png new file mode 100644 index 000000000..2e069bf93 Binary files /dev/null and b/flip_store/assets/01-main-menu.png differ diff --git a/flip_store/assets/01-main.png b/flip_store/assets/01-main.png deleted file mode 100644 index e43d88ea8..000000000 Binary files a/flip_store/assets/01-main.png and /dev/null differ diff --git a/flip_store/assets/02-catalog.png b/flip_store/assets/02-catalog.png new file mode 100644 index 000000000..dc8ccb801 Binary files /dev/null and b/flip_store/assets/02-catalog.png differ diff --git a/flip_store/assets/02-list.png b/flip_store/assets/03-list.png similarity index 100% rename from flip_store/assets/02-list.png rename to flip_store/assets/03-list.png diff --git a/flip_store/assets/04-app-folder.png b/flip_store/assets/04-app-folder.png deleted file mode 100644 index 041cce6f6..000000000 Binary files a/flip_store/assets/04-app-folder.png and /dev/null differ diff --git a/flip_store/assets/03-success.png b/flip_store/assets/04-success.png similarity index 100% rename from flip_store/assets/03-success.png rename to flip_store/assets/04-success.png diff --git a/flip_store/assets/05-browse.png b/flip_store/assets/05-browse.png new file mode 100644 index 000000000..5093697d9 Binary files /dev/null and b/flip_store/assets/05-browse.png differ diff --git a/flip_store/assets/CHANGELOG.md b/flip_store/assets/CHANGELOG.md index 2b2c700ee..db4688cd2 100644 --- a/flip_store/assets/CHANGELOG.md +++ b/flip_store/assets/CHANGELOG.md @@ -1,6 +1,21 @@ +## v0.6 +- Updated app layout +- Added an option to download Developer Board firmware (Black Magic, FlipperHTTP, and Marauder) + +## v0.5 +- Added app descriptions and versioning + +## v0.4 +- Added an option to delete apps +- Edits by Willy-JL + +## v0.3 +- Edits by Willy-JL +- Improved memory allocation +- Stability patch + ## v0.2 -- Refactored using the Easy Flipper library. -- Added categories: Users can now navigate through categories, and when FlipStore installs a selected app, it will download directly to the corresponding category's folder in the apps directory. +- Added categories: Users can now navigate through categories, and when FlipStore installs a selected app, it downloads directly to the corresponding category folder in the apps directory - Improved memory allocation to prevent "Out of Memory" warnings - Updated installation messages diff --git a/flip_store/assets/README.md b/flip_store/assets/README.md index 7c796020b..971d76ddc 100644 --- a/flip_store/assets/README.md +++ b/flip_store/assets/README.md @@ -4,13 +4,13 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long ## Features - App Catalog - Install Apps -- Delete Apps (coming soon) +- Delete Apps +- Install Developer Board Flashes - Install Custom Apps (coming soon) -- Install Devboard Flashes (coming soon) - Install Official Firmware (coming soon) ## Installation -1. Flash your WiFi Devboard: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP +1. Flash your WiFi Dveloper Board or Raspberry Pi Pico W: https://github.com/jblanked/FlipperHTTP 2. Install the app. 3. Enjoy :D @@ -20,7 +20,8 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long - App Categories **v0.3** -- Caching +- Improved memory allocation +- Stability Patch 2 - App Catalog Patch (add in required functionalility) **v0.4** @@ -46,9 +47,9 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long This is a big task, and I welcome all contributors, especially developers interested in animations and graphics. Fork the repository, create a pull request, and I will review your edits. ## Known Bugs -1. When clicking the Catalog, I get an "out of memory" error. - - This has been addressed but may still occur. If it does, just restart the app. +1. Clicking a category in the app catalog results in an "Out of Memory" error. + - This issue has been addressed, but it may still occur. If it does, restart the app. 2. The app file is corrupted. - - It's likely there was an error parsing the data. Restart the app and wait until the green LED light turns off after downloading the app before exiting the view. -3. The app is stuck on "receiving". - - Restart your Flipper Zero with your WiFi Devboard plugged in. + - This is likely due to an error parsing the data. Restart the app and wait until the green LED light turns off after downloading the app before exiting the view. If this happens more than three times, the current version of FlipStore may not be able to download that app successfully. +3. The app is frozen on the "Installing", "Loading", or "Receiving data" screen. + - If it there LED is not on and it's been more than 5 seconds, restart your Flipper Zero with the devboard plugged in. diff --git a/flip_store/callback/flip_store_callback.c b/flip_store/callback/flip_store_callback.c new file mode 100644 index 000000000..b23e8ade3 --- /dev/null +++ b/flip_store/callback/flip_store_callback.c @@ -0,0 +1,691 @@ +#include + +bool flip_store_app_does_exist = false; +uint32_t selected_firmware_index = 0; + +// Callback for drawing the main screen +void flip_store_view_draw_callback_main(Canvas* canvas, void* model) { + UNUSED(model); + canvas_set_font(canvas, FontSecondary); + + if(fhttp.state == INACTIVE) { + canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); + canvas_draw_str(canvas, 0, 17, "Please connect to the board."); + canvas_draw_str(canvas, 0, 32, "If your board is connected,"); + canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); + canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); + canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); + return; + } + + if(!flip_store_sent_request) { + flip_store_sent_request = true; + + if(!flip_store_install_app(canvas, categories[flip_store_category_index])) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Failed to install app."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } + } else { + if(flip_store_success) { + if(fhttp.state == RECEIVING) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Downloading app..."); + canvas_draw_str(canvas, 0, 60, "Please wait..."); + return; + } else if(fhttp.state == IDLE) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "App installed successfully."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } + } else { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Failed to install app."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } + } +} + +// Function to draw the firmware download screen +void flip_store_view_draw_callback_firmware(Canvas* canvas, void* model) { + UNUSED(model); + + // Check if the HTTP state is inactive + if(fhttp.state == INACTIVE) { + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); + canvas_draw_str(canvas, 0, 17, "Please connect to the board."); + canvas_draw_str(canvas, 0, 32, "If your board is connected,"); + canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); + canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); + canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); + return; + } + + // Set font and clear the canvas for the loading state + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Loading..."); + + // Handle first firmware file + if(!sent_firmware_request) { + sent_firmware_request = true; + firmware_request_success = flip_store_get_firmware_file( + firmwares[selected_firmware_index].links[0], + firmwares[selected_firmware_index].name, + strrchr(firmwares[selected_firmware_index].links[0], '/') + 1); + + if(!firmware_request_success) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + flip_store_request_error(canvas); + } + return; + } else if(sent_firmware_request && !firmware_download_success) { + if(!firmware_request_success || fhttp.state == ISSUE) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + flip_store_request_error(canvas); + } else if(fhttp.state == RECEIVING) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Downloading file 1..."); + canvas_draw_str(canvas, 0, 60, "Please wait..."); + } else if(fhttp.state == IDLE) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Success"); + canvas_draw_str(canvas, 0, 60, "Downloading the next file now."); + firmware_download_success = true; + } + return; + } + + // Handle second firmware file + if(firmware_download_success && !sent_firmware_request_2) { + sent_firmware_request_2 = true; + firmware_request_success_2 = flip_store_get_firmware_file( + firmwares[selected_firmware_index].links[1], + firmwares[selected_firmware_index].name, + strrchr(firmwares[selected_firmware_index].links[1], '/') + 1); + + if(!firmware_request_success_2) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + flip_store_request_error(canvas); + } + return; + } else if(sent_firmware_request_2 && !firmware_download_success_2) { + if(!firmware_request_success_2 || fhttp.state == ISSUE) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + flip_store_request_error(canvas); + } else if(fhttp.state == RECEIVING) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Downloading file 2..."); + canvas_draw_str(canvas, 0, 60, "Please wait..."); + } else if(fhttp.state == IDLE) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Success"); + canvas_draw_str(canvas, 0, 60, "Downloading the next file now."); + firmware_download_success_2 = true; + } + return; + } + + // Handle third firmware file + if(firmware_download_success && firmware_download_success_2 && !sent_firmware_request_3) { + sent_firmware_request_3 = true; + firmware_request_success_3 = flip_store_get_firmware_file( + firmwares[selected_firmware_index].links[2], + firmwares[selected_firmware_index].name, + strrchr(firmwares[selected_firmware_index].links[2], '/') + 1); + + if(!firmware_request_success_3) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + flip_store_request_error(canvas); + } + return; + } else if(sent_firmware_request_3 && !firmware_download_success_3) { + if(!firmware_request_success_3 || fhttp.state == ISSUE) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + flip_store_request_error(canvas); + } else if(fhttp.state == RECEIVING) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Downloading file 3..."); + canvas_draw_str(canvas, 0, 60, "Please wait..."); + } else if(fhttp.state == IDLE) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Success"); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + firmware_download_success_3 = true; + } + return; + } + + // All files downloaded successfully + if(firmware_download_success && firmware_download_success_2 && firmware_download_success_3) { + canvas_set_font(canvas, FontSecondary); + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Files downloaded successfully."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } +} + +// Function to draw the message on the canvas with word wrapping +void draw_description(Canvas* canvas, const char* description, int x, int y) { + if(description == NULL || strlen(description) == 0) { + FURI_LOG_E(TAG, "User message is NULL."); + return; + } + if(!canvas) { + FURI_LOG_E(TAG, "Canvas is NULL."); + return; + } + + size_t msg_length = strlen(description); + size_t start = 0; + int line_num = 0; + char line[MAX_LINE_LENGTH + 1]; // Buffer for the current line (+1 for null terminator) + + while(start < msg_length && line_num < 4) { + size_t remaining = msg_length - start; + size_t len = (remaining > MAX_LINE_LENGTH) ? MAX_LINE_LENGTH : remaining; + + if(remaining > MAX_LINE_LENGTH) { + // Find the last space within the first 'len' characters + size_t last_space = len; + while(last_space > 0 && description[start + last_space - 1] != ' ') { + last_space--; + } + + if(last_space > 0) { + len = last_space; // Adjust len to the position of the last space + } + } + + // Copy the substring to 'line' and null-terminate it + memcpy(line, description + start, len); + line[len] = '\0'; // Ensure the string is null-terminated + + // Draw the string on the canvas + // Adjust the y-coordinate based on the line number + canvas_draw_str_aligned(canvas, x, y + line_num * 10, AlignLeft, AlignTop, line); + + // Update the start position for the next line + start += len; + + // Skip any spaces to avoid leading spaces on the next line + while(start < msg_length && description[start] == ' ') { + start++; + } + + // Increment the line number + line_num++; + } +} + +void flip_store_view_draw_callback_app_list(Canvas* canvas, void* model) { + UNUSED(model); + canvas_clear(canvas); + canvas_set_font(canvas, FontPrimary); + char title[30]; + snprintf( + title, + 30, + "%s (v.%s)", + flip_catalog[app_selected_index].app_name, + flip_catalog[app_selected_index].app_version); + canvas_draw_str(canvas, 0, 10, title); + canvas_set_font(canvas, FontSecondary); + draw_description(canvas, flip_catalog[app_selected_index].app_description, 0, 13); + if(flip_store_app_does_exist) { + canvas_draw_icon(canvas, 0, 53, &I_ButtonLeft_4x7); + canvas_draw_str_aligned(canvas, 7, 54, AlignLeft, AlignTop, "Delete"); + canvas_draw_icon(canvas, 45, 53, &I_ButtonBACK_10x8); + canvas_draw_str_aligned(canvas, 57, 54, AlignLeft, AlignTop, "Back"); + } else { + canvas_draw_icon(canvas, 0, 53, &I_ButtonBACK_10x8); + canvas_draw_str_aligned(canvas, 12, 54, AlignLeft, AlignTop, "Back"); + } + canvas_draw_icon(canvas, 90, 53, &I_ButtonRight_4x7); + canvas_draw_str_aligned(canvas, 97, 54, AlignLeft, AlignTop, "Install"); +} + +bool flip_store_input_callback(InputEvent* event, void* context) { + FlipStoreApp* app = (FlipStoreApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return false; + } + if(event->type == InputTypeShort) { + if(event->key == InputKeyLeft && flip_store_app_does_exist) { + // Left button clicked, delete the app + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppDelete); + return true; + } + if(event->key == InputKeyRight) { + // Right button clicked, download the app + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewMain); + return true; + } + } else if(event->type == InputTypePress) { + if(event->key == InputKeyBack) { + // Back button clicked, switch to the previous view. + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); + return true; + } + } + + return false; +} + +void flip_store_text_updated_ssid(void* context) { + FlipStoreApp* app = (FlipStoreApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + + // store the entered text + strncpy( + app->uart_text_input_buffer_ssid, + app->uart_text_input_temp_buffer_ssid, + app->uart_text_input_buffer_size_ssid); + + // Ensure null-termination + app->uart_text_input_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; + + // update the variable item text + if(app->variable_item_ssid) { + variable_item_set_current_value_text( + app->variable_item_ssid, app->uart_text_input_buffer_ssid); + } + + // save the settings + save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass); + + // if SSID and PASS are not empty, connect to the WiFi + if(strlen(app->uart_text_input_buffer_ssid) > 0 && + strlen(app->uart_text_input_buffer_pass) > 0) { + // save wifi settings + if(!flipper_http_save_wifi( + app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass)) { + FURI_LOG_E(TAG, "Failed to save WiFi settings"); + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); +} +void flip_store_text_updated_pass(void* context) { + FlipStoreApp* app = (FlipStoreApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + + // store the entered text + strncpy( + app->uart_text_input_buffer_pass, + app->uart_text_input_temp_buffer_pass, + app->uart_text_input_buffer_size_pass); + + // Ensure null-termination + app->uart_text_input_buffer_pass[app->uart_text_input_buffer_size_pass - 1] = '\0'; + + // update the variable item text + if(app->variable_item_pass) { + variable_item_set_current_value_text( + app->variable_item_pass, app->uart_text_input_buffer_pass); + } + + // save the settings + save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass); + + // if SSID and PASS are not empty, connect to the WiFi + if(strlen(app->uart_text_input_buffer_ssid) > 0 && + strlen(app->uart_text_input_buffer_pass) > 0) { + // save wifi settings + if(!flipper_http_save_wifi( + app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass)) { + FURI_LOG_E(TAG, "Failed to save WiFi settings"); + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); +} + +uint32_t callback_to_submenu(void* context) { + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + firmware_free(); + return FlipStoreViewSubmenu; +} + +uint32_t callback_to_submenu_options(void* context) { + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + firmware_free(); + return FlipStoreViewSubmenuOptions; +} + +uint32_t callback_to_firmware_list(void* context) { + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + sent_firmware_request = false; + sent_firmware_request_2 = false; + sent_firmware_request_3 = false; + // + firmware_request_success = false; + firmware_request_success_2 = false; + firmware_request_success_3 = false; + // + firmware_download_success = false; + firmware_download_success_2 = false; + firmware_download_success_3 = false; + return FlipStoreViewFirmwares; +} + +uint32_t callback_to_app_list(void* context) { + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + flip_store_sent_request = false; + flip_store_success = false; + flip_store_saved_data = false; + flip_store_saved_success = false; + flip_store_app_does_exist = false; + sent_firmware_request = false; + return FlipStoreViewAppList; +} + +void settings_item_selected(void* context, uint32_t index) { + FlipStoreApp* app = (FlipStoreApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + switch(index) { + case 0: // Input SSID + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewTextInputSSID); + break; + case 1: // Input Password + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewTextInputPass); + break; + default: + FURI_LOG_E(TAG, "Unknown configuration item index"); + break; + } +} + +void dialog_delete_callback(DialogExResult result, void* context) { + furi_assert(context); + FlipStoreApp* app = (FlipStoreApp*)context; + if(result == DialogExResultLeft) // No + { + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); + } else if(result == DialogExResultRight) { + // delete the app then return to the app list + if(!delete_app( + flip_catalog[app_selected_index].app_id, categories[flip_store_category_index])) { + // pop up a message + popup_set_header(app->popup, "[ERROR]", 0, 0, AlignLeft, AlignTop); + popup_set_text(app->popup, "Issue deleting app.", 0, 50, AlignLeft, AlignTop); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup); + furi_delay_ms(2000); // delay for 2 seconds + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); + } else { + // pop up a message + popup_set_header(app->popup, "[SUCCESS]", 0, 0, AlignLeft, AlignTop); + popup_set_text(app->popup, "App deleted successfully.", 0, 50, AlignLeft, AlignTop); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup); + furi_delay_ms(2000); // delay for 2 seconds + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); + } + } +} + +void dialog_firmware_callback(DialogExResult result, void* context) { + furi_assert(context); + FlipStoreApp* app = (FlipStoreApp*)context; + if(result == DialogExResultLeft) // No + { + // switch to the firmware list + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwares); + } else if(result == DialogExResultRight) { + // download the firmware then return to the firmware list + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwareDownload); + } +} + +void popup_callback(void* context) { + FlipStoreApp* app = (FlipStoreApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenu); +} + +uint32_t callback_exit_app(void* context) { + // Exit the application + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + return VIEW_NONE; // Return VIEW_NONE to exit the app +} + +void callback_submenu_choices(void* context, uint32_t index) { + FlipStoreApp* app = (FlipStoreApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + switch(index) { + case FlipStoreSubmenuIndexMain: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewMain); + break; + case FlipStoreSubmenuIndexAbout: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAbout); + break; + case FlipStoreSubmenuIndexSettings: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); + break; + case FlipStoreSubmenuIndexOptions: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenuOptions); + break; + case FlipStoreSubmenuIndexAppList: + flip_store_category_index = 0; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); + break; + case FlipStoreSubmenuIndexFirmwares: + if(!app->submenu_firmwares) { + FURI_LOG_E(TAG, "Submenu firmwares is NULL"); + return; + } + firmwares = firmware_alloc(); + if(firmwares == NULL) { + FURI_LOG_E(TAG, "Failed to allocate memory for firmwares"); + return; + } + submenu_reset(app->submenu_firmwares); + submenu_set_header(app->submenu_firmwares, "ESP32 Firmwares"); + for(int i = 0; i < FIRMWARE_COUNT; i++) { + submenu_add_item( + app->submenu_firmwares, + firmwares[i].name, + FlipStoreSubmenuIndexStartFirmwares + i, + callback_submenu_choices, + app); + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwares); + break; + case FlipStoreSubmenuIndexAppListBluetooth: + flip_store_category_index = 0; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListBluetooth, "Bluetooth", &app->submenu_app_list_bluetooth)); + break; + case FlipStoreSubmenuIndexAppListGames: + flip_store_category_index = 1; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListGames, "Games", &app->submenu_app_list_games)); + break; + case FlipStoreSubmenuIndexAppListGPIO: + flip_store_category_index = 2; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListGPIO, "GPIO", &app->submenu_app_list_gpio)); + break; + case FlipStoreSubmenuIndexAppListInfrared: + flip_store_category_index = 3; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListInfrared, "Infrared", &app->submenu_app_list_infrared)); + break; + case FlipStoreSubmenuIndexAppListiButton: + flip_store_category_index = 4; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListiButton, "iButton", &app->submenu_app_list_ibutton)); + break; + case FlipStoreSubmenuIndexAppListMedia: + flip_store_category_index = 5; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListMedia, "Media", &app->submenu_app_list_media)); + break; + case FlipStoreSubmenuIndexAppListNFC: + flip_store_category_index = 6; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListNFC, "NFC", &app->submenu_app_list_nfc)); + break; + case FlipStoreSubmenuIndexAppListRFID: + flip_store_category_index = 7; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListRFID, "RFID", &app->submenu_app_list_rfid)); + break; + case FlipStoreSubmenuIndexAppListSubGHz: + flip_store_category_index = 8; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListSubGHz, "Sub-GHz", &app->submenu_app_list_subghz)); + break; + case FlipStoreSubmenuIndexAppListTools: + flip_store_category_index = 9; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListTools, "Tools", &app->submenu_app_list_tools)); + break; + case FlipStoreSubmenuIndexAppListUSB: + flip_store_category_index = 10; + flip_store_app_does_exist = false; + view_dispatcher_switch_to_view( + app->view_dispatcher, + flip_store_handle_app_list( + app, FlipStoreViewAppListUSB, "USB", &app->submenu_app_list_usb)); + break; + default: + // Check if the index is within the firmwares list range + if(index >= FlipStoreSubmenuIndexStartFirmwares && + index < FlipStoreSubmenuIndexStartFirmwares + 3) { + // Get the firmware index + uint32_t firmware_index = index - FlipStoreSubmenuIndexStartFirmwares; + + // Check if the firmware index is valid + if((int)firmware_index >= 0 && firmware_index < FIRMWARE_COUNT) { + // Get the firmware name + selected_firmware_index = firmware_index; + + // Switch to the firmware download view + dialog_ex_set_header( + app->dialog_firmware, + firmwares[firmware_index].name, + 0, + 0, + AlignLeft, + AlignTop); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwareDialog); + } else { + FURI_LOG_E(TAG, "Invalid firmware index"); + popup_set_header(app->popup, "[ERROR]", 0, 0, AlignLeft, AlignTop); + popup_set_text(app->popup, "Issue parsing firmwarex", 0, 50, AlignLeft, AlignTop); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup); + } + } + // Check if the index is within the app list range + else if( + index >= FlipStoreSubmenuIndexStartAppList && + index < FlipStoreSubmenuIndexStartAppList + MAX_APP_COUNT) { + // Get the app index + uint32_t app_index = index - FlipStoreSubmenuIndexStartAppList; + + // Check if the app index is valid + if((int)app_index >= 0 && app_index < MAX_APP_COUNT) { + // Get the app name + char* app_name = flip_catalog[app_index].app_name; + + // Check if the app name is valid + if(app_name != NULL && strlen(app_name) > 0) { + app_selected_index = app_index; + flip_store_app_does_exist = app_exists( + flip_catalog[app_selected_index].app_id, + categories[flip_store_category_index]); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppInfo); + } else { + FURI_LOG_E(TAG, "Invalid app name"); + } + } else { + FURI_LOG_E(TAG, "Invalid app index"); + } + } else { + FURI_LOG_E(TAG, "Unknown submenu index"); + } + break; + } +} diff --git a/flip_store/callback/flip_store_callback.h b/flip_store/callback/flip_store_callback.h new file mode 100644 index 000000000..283736b87 --- /dev/null +++ b/flip_store/callback/flip_store_callback.h @@ -0,0 +1,52 @@ +#ifndef FLIP_STORE_CALLBACK_H +#define FLIP_STORE_CALLBACK_H +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_LINE_LENGTH 30 + +extern bool flip_store_app_does_exist; +extern uint32_t selected_firmware_index; + +// Callback for drawing the main screen +void flip_store_view_draw_callback_main(Canvas* canvas, void* model); + +void flip_store_view_draw_callback_firmware(Canvas* canvas, void* model); + +// Function to draw the description on the canvas with word wrapping +void draw_description(Canvas* canvas, const char* user_message, int x, int y); + +void flip_store_view_draw_callback_app_list(Canvas* canvas, void* model); + +bool flip_store_input_callback(InputEvent* event, void* context); + +void flip_store_text_updated_ssid(void* context); + +void flip_store_text_updated_pass(void* context); + +uint32_t callback_to_submenu(void* context); + +uint32_t callback_to_submenu_options(void* context); + +uint32_t callback_to_firmware_list(void* context); + +uint32_t callback_to_app_list(void* context); + +void settings_item_selected(void* context, uint32_t index); + +void dialog_delete_callback(DialogExResult result, void* context); +void dialog_firmware_callback(DialogExResult result, void* context); + +void popup_callback(void* context); + +uint32_t callback_exit_app(void* context); +void callback_submenu_choices(void* context, uint32_t index); + +#endif // FLIP_STORE_CALLBACK_H diff --git a/flip_store/easy_flipper.h b/flip_store/easy_flipper/easy_flipper.c similarity index 96% rename from flip_store/easy_flipper.h rename to flip_store/easy_flipper/easy_flipper.c index e8f0ad796..8b98e1a1b 100644 --- a/flip_store/easy_flipper.h +++ b/flip_store/easy_flipper/easy_flipper.c @@ -1,24 +1,4 @@ -#ifndef EASY_FLIPPER_H -#define EASY_FLIPPER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define EASY_TAG "EasyFlipper" +#include /** * @brief Navigation callback for exiting the application @@ -530,5 +510,3 @@ bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer furi_string_set_str(*furi_string, buffer); return true; } - -#endif // EASY_FLIPPER_H diff --git a/flip_store/easy_flipper/easy_flipper.h b/flip_store/easy_flipper/easy_flipper.h new file mode 100644 index 000000000..1d6dbe430 --- /dev/null +++ b/flip_store/easy_flipper/easy_flipper.h @@ -0,0 +1,261 @@ +#ifndef EASY_FLIPPER_H +#define EASY_FLIPPER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EASY_TAG "EasyFlipper" + +/** + * @brief Navigation callback for exiting the application + * @param context The context - unused + * @return next view id (VIEW_NONE to exit the app) + */ +uint32_t easy_flipper_callback_exit_app(void* context); +/** + * @brief Initialize a buffer + * @param buffer The buffer to initialize + * @param buffer_size The size of the buffer + * @return true if successful, false otherwise + */ +bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size); +/** + * @brief Initialize a View object + * @param view The View object to initialize + * @param view_id The ID/Index of the view + * @param draw_callback The draw callback function (set to NULL if not needed) + * @param input_callback The input callback function (set to NULL if not needed) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view( + View** view, + int32_t view_id, + void draw_callback(Canvas*, void*), + bool input_callback(InputEvent*, void*), + uint32_t (*previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a ViewDispatcher object + * @param view_dispatcher The ViewDispatcher object to initialize + * @param gui The GUI object + * @param context The context to pass to the event callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui, void* context); + +/** + * @brief Initialize a Submenu object + * @note This does not set the items in the submenu + * @param submenu The Submenu object to initialize + * @param view_id The ID/Index of the view + * @param title The title/header of the submenu + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_submenu( + Submenu** submenu, + int32_t view_id, + char* title, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a Menu object + * @note This does not set the items in the menu + * @param menu The Menu object to initialize + * @param view_id The ID/Index of the view + * @param item_callback The item callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_menu( + Menu** menu, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a Widget object + * @param widget The Widget object to initialize + * @param view_id The ID/Index of the view + * @param text The text to display in the widget + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_widget( + Widget** widget, + int32_t view_id, + char* text, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a VariableItemList object + * @note This does not set the items in the VariableItemList + * @param variable_item_list The VariableItemList object to initialize + * @param view_id The ID/Index of the view + * @param enter_callback The enter callback function (can be set to NULL) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the enter callback (usually the app) + * @return true if successful, false otherwise + */ +bool easy_flipper_set_variable_item_list( + VariableItemList** variable_item_list, + int32_t view_id, + void (*enter_callback)(void*, uint32_t), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a TextInput object + * @param text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_text_input( + TextInput** text_input, + int32_t view_id, + char* header_text, + char* text_input_temp_buffer, + uint32_t text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a TextInput object with extra symbols + * @param uart_text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_uart_text_input( + TextInput** uart_text_input, + int32_t view_id, + char* header_text, + char* uart_text_input_temp_buffer, + uint32_t uart_text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a DialogEx object + * @param dialog_ex The DialogEx object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param left_button_text The text of the left button + * @param right_button_text The text of the right button + * @param center_button_text The text of the center button + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_dialog_ex( + DialogEx** dialog_ex, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + char* left_button_text, + char* right_button_text, + char* center_button_text, + void (*result_callback)(DialogExResult, void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a Popup object + * @param popup The Popup object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_popup( + Popup** popup, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a Loading object + * @param loading The Loading object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_loading( + Loading** loading, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Set a char butter to a FuriString + * @param furi_string The FuriString object + * @param buffer The buffer to copy the string to + * @return true if successful, false otherwise + */ +bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer); + +#endif diff --git a/flip_store/firmwares/flip_store_firmwares.c b/flip_store/firmwares/flip_store_firmwares.c new file mode 100644 index 000000000..b5a7dc4c1 --- /dev/null +++ b/flip_store/firmwares/flip_store_firmwares.c @@ -0,0 +1,119 @@ +#include + +Firmware* firmwares = NULL; +bool sent_firmware_request = false; +bool sent_firmware_request_2 = false; +bool sent_firmware_request_3 = false; +// +bool firmware_request_success = false; +bool firmware_request_success_2 = false; +bool firmware_request_success_3 = false; +// +bool firmware_download_success = false; +bool firmware_download_success_2 = false; +bool firmware_download_success_3 = false; + +Firmware* firmware_alloc() { + Firmware* fw = (Firmware*)malloc(FIRMWARE_COUNT * sizeof(Firmware)); + if(!fw) { + FURI_LOG_E(TAG, "Failed to allocate memory for Firmware"); + return NULL; + } + for(int i = 0; i < FIRMWARE_COUNT; i++) { + if(fw[i].name == NULL) { + fw[i].name = (char*)malloc(16); + if(!fw[i].name) { + FURI_LOG_E(TAG, "Failed to allocate memory for Firmware name"); + return NULL; + } + } + for(int j = 0; j < FIRMWARE_LINKS; j++) { + if(fw[i].links[j] == NULL) { + fw[i].links[j] = (char*)malloc(256); + if(!fw[i].links[j]) { + FURI_LOG_E(TAG, "Failed to allocate memory for Firmware links"); + return NULL; + } + } + } + } + + // Black Magic + fw[0].name = "Black Magic"; + fw[0].links[0] = + "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/bootloader.bin"; + fw[0].links[1] = + "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/partition-table.bin"; + fw[0].links[2] = + "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/blackmagic.bin"; + + // FlipperHTTP + fw[1].name = "FlipperHTTP"; + fw[1].links[0] = + "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_bootloader.bin"; + fw[1].links[1] = + "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_firmware_a.bin"; + fw[1].links[2] = + "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_partitions.bin"; + + // Marauder + fw[2].name = "Marauder"; + fw[2].links[0] = + "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.bootloader.bin"; + fw[2].links[1] = + "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.partitions.bin"; + fw[2].links[2] = + "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/CURRENT/esp32_marauder_v1_0_0_20240626_flipper.bin"; + + // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/STATIC/BM/bootloader.bin + // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/STATIC/BM/partition-table.bin + // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/STATIC/BM/blackmagic.bin + + // https://api.github.com/repos/jblanked/FlipperHTTP/contents/flipper_http_bootloader.bin + // https://api.github.com/repos/jblanked/FlipperHTTP/contents/flipper_http_firmware_a.bin + // https://api.github.com/repos/jblanked/FlipperHTTP/contents/flipper_http_partitions.bin + + // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.bootloader.bin + // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.partitions.bin + // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/CURRENT/esp32_marauder_v1_0_0_20240626_flipper.bin + return fw; +} +void firmware_free() { + if(firmwares) { + free(firmwares); + } +} + +bool flip_store_get_firmware_file(char* link, char* name, char* filename) { + if(fhttp.state == INACTIVE) { + return false; + } + Storage* storage = furi_record_open(RECORD_STORAGE); + char directory_path[64]; + snprintf( + directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/esp_flasher"); + storage_common_mkdir(storage, directory_path); + snprintf( + directory_path, + sizeof(directory_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/esp_flasher/%s", + firmwares[selected_firmware_index].name); + storage_common_mkdir(storage, directory_path); + snprintf( + fhttp.file_path, + sizeof(fhttp.file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/esp_flasher/%s/%s", + name, + filename); + fhttp.save_received_data = false; + fhttp.is_bytes_request = true; + char* headers = jsmn("Content-Type", "application/octet-stream"); + bool sent_request = flipper_http_get_request_bytes(link, headers); + free(headers); + if(sent_request) { + fhttp.state = RECEIVING; + return true; + } + fhttp.state = ISSUE; + return false; +} diff --git a/flip_store/firmwares/flip_store_firmwares.h b/flip_store/firmwares/flip_store_firmwares.h new file mode 100644 index 000000000..937074e59 --- /dev/null +++ b/flip_store/firmwares/flip_store_firmwares.h @@ -0,0 +1,30 @@ +#ifndef FLIP_STORE_FIRMWARES_H +#define FLIP_STORE_FIRMWARES_H + +#include +#include +#include + +typedef struct { + char* name; + char* links[FIRMWARE_LINKS]; +} Firmware; + +extern Firmware* firmwares; +Firmware* firmware_alloc(); +void firmware_free(); + +// download and waiting process +bool flip_store_get_firmware_file(char* link, char* name, char* filename); + +extern bool sent_firmware_request; +extern bool sent_firmware_request_2; +extern bool sent_firmware_request_3; +extern bool firmware_request_success; +extern bool firmware_request_success_2; +extern bool firmware_request_success_3; +extern bool firmware_download_success; +extern bool firmware_download_success_2; +extern bool firmware_download_success_3; + +#endif // FLIP_STORE_FIRMWARES_H diff --git a/flip_store/flip_store_storage.h b/flip_store/flip_storage/flip_store_storage.c similarity index 92% rename from flip_store/flip_store_storage.h rename to flip_store/flip_storage/flip_store_storage.c index 547783b26..2c2530ae1 100644 --- a/flip_store/flip_store_storage.h +++ b/flip_store/flip_storage/flip_store_storage.c @@ -1,12 +1,6 @@ -#ifndef FLIP_STORE_STORAGE_H -#define FLIP_STORE_STORAGE_H +#include "flip_storage/flip_store_storage.h" -#include -#include - -#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/settings.bin" - -static void save_settings(const char* ssid, const char* password) { +void save_settings(const char* ssid, const char* password) { // Create the directory for saving settings char directory_path[128]; snprintf( @@ -44,7 +38,7 @@ static void save_settings(const char* ssid, const char* password) { furi_record_close(RECORD_STORAGE); } -static bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size) { +bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size) { Storage* storage = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(storage); @@ -110,9 +104,22 @@ bool delete_app(const char* app_id, const char* app_category) { return true; } -#define BUFFER_SIZE 64 -#define MAX_KEY_LENGTH 32 -#define MAX_VALUE_LENGTH 64 +bool app_exists(const char* app_id, const char* app_category) { + // Check if the app exists + char directory_path[128]; + snprintf( + directory_path, + sizeof(directory_path), + STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", + app_category, + app_id); + + Storage* storage = furi_record_open(RECORD_STORAGE); + bool exists = storage_common_exists(storage, directory_path); + furi_record_close(RECORD_STORAGE); + + return exists; +} // Function to parse JSON incrementally from a file bool parse_json_incrementally( @@ -271,5 +278,3 @@ bool parse_json_incrementally( } return false; } - -#endif diff --git a/flip_store/flip_storage/flip_store_storage.h b/flip_store/flip_storage/flip_store_storage.h new file mode 100644 index 000000000..25758d481 --- /dev/null +++ b/flip_store/flip_storage/flip_store_storage.h @@ -0,0 +1,28 @@ +#ifndef FLIP_STORE_STORAGE_H +#define FLIP_STORE_STORAGE_H + +#include +#include +#include + +#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/settings.bin" +#define BUFFER_SIZE 64 +#define MAX_KEY_LENGTH 16 +#define MAX_VALUE_LENGTH 100 + +void save_settings(const char* ssid, const char* password); + +bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size); + +bool delete_app(const char* app_id, const char* app_category); + +bool app_exists(const char* app_id, const char* app_category); + +// Function to parse JSON incrementally from a file +bool parse_json_incrementally( + const char* file_path, + const char* target_key, + char* value_buffer, + size_t value_buffer_size); + +#endif diff --git a/flip_store/flip_store_free.h b/flip_store/flip_store.c similarity index 65% rename from flip_store/flip_store_free.h rename to flip_store/flip_store.c index 6b2f9dc02..41cba1a43 100644 --- a/flip_store/flip_store_free.h +++ b/flip_store/flip_store.c @@ -1,8 +1,7 @@ -#ifndef FLIP_STORE_FREE_H -#define FLIP_STORE_FREE_H +#include // Function to free the resources used by FlipStoreApp -static void flip_store_app_free(FlipStoreApp* app) { +void flip_store_app_free(FlipStoreApp* app) { if(!app) { FURI_LOG_E(TAG, "FlipStoreApp is NULL"); return; @@ -17,16 +16,28 @@ static void flip_store_app_free(FlipStoreApp* app) { view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppInfo); view_free(app->view_app_info); } + if(app->view_firmware_download) { + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewFirmwareDownload); + view_free(app->view_firmware_download); + } // Free Submenu(s) - if(app->submenu) { + if(app->submenu_main) { view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSubmenu); - submenu_free(app->submenu); + submenu_free(app->submenu_main); + } + if(app->submenu_options) { + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSubmenuOptions); + submenu_free(app->submenu_options); } if(app->submenu_app_list) { view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppList); submenu_free(app->submenu_app_list); } + if(app->submenu_firmwares) { + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewFirmwares); + submenu_free(app->submenu_firmwares); + } if(app->submenu_app_list_bluetooth) { view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListBluetooth); submenu_free(app->submenu_app_list_bluetooth); @@ -105,9 +116,10 @@ static void flip_store_app_free(FlipStoreApp* app) { view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppDelete); dialog_ex_free(app->dialog_delete); } - - // Free the flip catalog - flip_catalog_free(); + if(app->dialog_firmware) { + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewFirmwareDialog); + dialog_ex_free(app->dialog_firmware); + } // deinitalize flipper http flipper_http_deinit(); @@ -122,4 +134,29 @@ static void flip_store_app_free(FlipStoreApp* app) { free(app); } -#endif // FLIP_STORE_FREE_H +void flip_store_request_error(Canvas* canvas) { + if(fhttp.last_response != NULL) { + if(strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != + NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } else { + FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response); + canvas_draw_str(canvas, 0, 42, "Unusual error..."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } + } else { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } +} diff --git a/flip_store/flip_store_e.h b/flip_store/flip_store.h similarity index 78% rename from flip_store/flip_store_e.h rename to flip_store/flip_store.h index c13f88626..ff86370a2 100644 --- a/flip_store/flip_store_e.h +++ b/flip_store/flip_store.h @@ -1,7 +1,7 @@ #ifndef FLIP_STORE_E_H #define FLIP_STORE_E_H -#include -#include +#include +#include #include #include #include @@ -10,30 +10,22 @@ #include #include #include -#include -#define TAG "FlipStore" - -// define the list of categories -char* categories[] = { - "Bluetooth", - "Games", - "GPIO", - "Infrared", - "iButton", - "Media", - "NFC", - "RFID", - "Sub-GHz", - "Tools", - "USB", -}; +#include +#include +#define TAG "FlipStore" +#define FIRMWARE_COUNT 3 +#define FIRMWARE_LINKS 3 // Define the submenu items for our FlipStore application typedef enum { FlipStoreSubmenuIndexMain, // Click to start downloading the selected app FlipStoreSubmenuIndexAbout, FlipStoreSubmenuIndexSettings, + // + FlipStoreSubmenuIndexOptions, // Click to view the options + // FlipStoreSubmenuIndexAppList, + FlipStoreSubmenuIndexFirmwares, // FlipStoreSubmenuIndexAppListBluetooth, FlipStoreSubmenuIndexAppListGames, @@ -47,13 +39,18 @@ typedef enum { FlipStoreSubmenuIndexAppListTools, FlipStoreSubmenuIndexAppListUSB, // - FlipStoreSubmenuIndexStartAppList + FlipStoreSubmenuIndexStartFirmwares, + // + FlipStoreSubmenuIndexStartAppList = 100, } FlipStoreSubmenuIndex; // Define a single view for our FlipStore application typedef enum { - FlipStoreViewMain, // The main screen + FlipStoreViewMain, // The main screen for downloading apps + // FlipStoreViewSubmenu, // The submenu + FlipStoreViewSubmenuOptions, // The submenu options + // FlipStoreViewAbout, // The about screen FlipStoreViewSettings, // The settings screen FlipStoreViewTextInputSSID, // The text input screen for SSID @@ -62,6 +59,10 @@ typedef enum { FlipStoreViewPopup, // The popup screen // FlipStoreViewAppList, // The app list screen + FlipStoreViewFirmwares, // The firmwares screen (submenu) + FlipStoreViewFirmwareDialog, // The firmware view (DialogEx) of the selected firmware + FlipStoreViewFirmwareDownload, // The firmware download screen + // FlipStoreViewAppInfo, // The app info screen (widget) of the selected app FlipStoreViewAppDownload, // The app download screen (widget) of the selected app FlipStoreViewAppDelete, // The app delete screen (DialogEx) of the selected app @@ -84,9 +85,15 @@ typedef struct { ViewDispatcher* view_dispatcher; // Switches between our views View* view_main; // The main screen for downloading apps View* view_app_info; // The app info screen (view) of the selected app - Submenu* submenu; // The submenu (main) // + DialogEx* dialog_firmware; // The dialog for installing a firmware + View* view_firmware_download; // The firmware download screen (view) of the selected firmware + // + Submenu* submenu_main; // The submenu (main) + // + Submenu* submenu_options; // The submenu (options) Submenu* submenu_app_list; // The submenu (app list) for the selected category + Submenu* submenu_firmwares; // The submenu (firmwares) // Submenu* submenu_app_list_bluetooth; // The submenu (app list) for Bluetooth Submenu* submenu_app_list_games; // The submenu (app list) for Games @@ -118,20 +125,8 @@ typedef struct { uint32_t uart_text_input_buffer_size_pass; // Size of the text input buffer } FlipStoreApp; -// include strndup (otherwise NULL pointer dereference) -char* strndup(const char* s, size_t n) { - char* result; - size_t len = strlen(s); - - if(n < len) len = n; - - result = (char*)malloc(len + 1); - if(!result) return NULL; - - result[len] = '\0'; - return (char*)memcpy(result, s, len); -} +void flip_store_app_free(FlipStoreApp* app); -static void callback_submenu_choices(void* context, uint32_t index); +void flip_store_request_error(Canvas* canvas); #endif // FLIP_STORE_E_H diff --git a/flip_store/flip_store_apps.h b/flip_store/flip_store_apps.h deleted file mode 100644 index e2ea36ee0..000000000 --- a/flip_store/flip_store_apps.h +++ /dev/null @@ -1,488 +0,0 @@ -#ifndef FLIP_STORE_APPS_H -#define FLIP_STORE_APPS_H - -// Define maximum limits -#define MAX_APP_NAME_LENGTH 32 -#define MAX_ID_LENGTH 32 -#define MAX_APP_COUNT 100 - -typedef struct { - char app_name[MAX_APP_NAME_LENGTH]; - char app_id[MAX_APP_NAME_LENGTH]; - char app_build_id[MAX_ID_LENGTH]; -} FlipStoreAppInfo; - -static FlipStoreAppInfo* flip_catalog = NULL; - -static uint32_t app_selected_index = 0; -static bool flip_store_sent_request = false; -static bool flip_store_success = false; -static bool flip_store_saved_data = false; -static bool flip_store_saved_success = false; -static uint32_t flip_store_category_index = 0; - -enum ObjectState { - OBJECT_EXPECT_KEY, - OBJECT_EXPECT_COLON, - OBJECT_EXPECT_VALUE, - OBJECT_EXPECT_COMMA_OR_END -}; - -static void flip_catalog_free() { - if(flip_catalog) { - free(flip_catalog); - flip_catalog = NULL; - } -} - -static bool flip_catalog_alloc() { - if(!flip_catalog) { - flip_catalog = (FlipStoreAppInfo*)malloc(MAX_APP_COUNT * sizeof(FlipStoreAppInfo)); - } - if(!flip_catalog) { - FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog."); - return false; - } - return true; -} - -// Utility function to parse JSON incrementally from a file -static bool flip_store_process_app_list(const char* file_path) { - if(file_path == NULL) { - FURI_LOG_E(TAG, "JSON file path is NULL."); - return false; - } - - // initialize the flip_catalog - if(!flip_catalog_alloc()) { - FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog."); - return false; - } - - Storage* _storage = NULL; - File* _file = NULL; - char buffer[BUFFER_SIZE]; - size_t bytes_read; - bool in_string = false; - bool is_escaped = false; - bool reading_key = false; - bool reading_value = false; - bool inside_app_object = false; - bool found_name = false; - bool found_id = false; - bool found_build_id = false; - char current_key[MAX_KEY_LENGTH] = {0}; - size_t key_index = 0; - char current_value[MAX_VALUE_LENGTH] = {0}; - size_t value_index = 0; - int app_count = 0; - enum ObjectState object_state = OBJECT_EXPECT_KEY; // Initialize object_state - - // Initialize parser state - enum { - STATE_SEARCH_APPS_KEY, - STATE_SEARCH_ARRAY_START, - STATE_READ_ARRAY_ELEMENTS, - STATE_DONE - } state = STATE_SEARCH_APPS_KEY; - - // Open storage and file - _storage = furi_record_open(RECORD_STORAGE); - if(!_storage) { - FURI_LOG_E(TAG, "Failed to open storage."); - return false; - } - - _file = storage_file_alloc(_storage); - if(!_file) { - FURI_LOG_E(TAG, "Failed to allocate file."); - furi_record_close(RECORD_STORAGE); - return false; - } - - if(!storage_file_open(_file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { - FURI_LOG_E(TAG, "Failed to open JSON file for reading."); - storage_file_free(_file); - furi_record_close(RECORD_STORAGE); - return false; - } - - while((bytes_read = storage_file_read(_file, buffer, BUFFER_SIZE)) > 0 && - state != STATE_DONE) { - for(size_t i = 0; i < bytes_read; ++i) { - char c = buffer[i]; - - if(is_escaped) { - is_escaped = false; - if(reading_key) { - if(key_index < MAX_KEY_LENGTH - 1) { - current_key[key_index++] = c; - } - } else if(reading_value) { - if(value_index < MAX_VALUE_LENGTH - 1) { - current_value[value_index++] = c; - } - } - continue; - } - - if(c == '\\') { - is_escaped = true; - continue; - } - - if(c == '\"') { - in_string = !in_string; - - if(in_string) { - // Start of a string - if(!reading_key && !reading_value) { - if(state == STATE_SEARCH_APPS_KEY) { - reading_key = true; - key_index = 0; - current_key[0] = '\0'; - } else if(inside_app_object) { - if(object_state == OBJECT_EXPECT_KEY) { - reading_key = true; - key_index = 0; - current_key[0] = '\0'; - } else if(object_state == OBJECT_EXPECT_VALUE) { - reading_value = true; - value_index = 0; - current_value[0] = '\0'; - } - } - } - } else { - // End of a string - if(reading_key) { - reading_key = false; - current_key[key_index] = '\0'; - - if(state == STATE_SEARCH_APPS_KEY && strcmp(current_key, "apps") == 0) { - state = STATE_SEARCH_ARRAY_START; - } else if(inside_app_object) { - object_state = OBJECT_EXPECT_COLON; - } - } else if(reading_value) { - reading_value = false; - current_value[value_index] = '\0'; - - if(inside_app_object) { - if(strcmp(current_key, "name") == 0) { - strncpy( - flip_catalog[app_count].app_name, - current_value, - MAX_APP_NAME_LENGTH - 1); - flip_catalog[app_count].app_name[MAX_APP_NAME_LENGTH - 1] = '\0'; - found_name = true; - } else if(strcmp(current_key, "id") == 0) { - strncpy( - flip_catalog[app_count].app_id, - current_value, - MAX_APP_NAME_LENGTH - 1); - flip_catalog[app_count].app_id[MAX_APP_NAME_LENGTH - 1] = '\0'; - found_id = true; - } else if(strcmp(current_key, "build_id") == 0) { - strncpy( - flip_catalog[app_count].app_build_id, - current_value, - MAX_APP_NAME_LENGTH - 1); - flip_catalog[app_count].app_build_id[MAX_ID_LENGTH - 1] = '\0'; - found_build_id = true; - } - - // After processing value, expect comma or end - object_state = OBJECT_EXPECT_COMMA_OR_END; - - // Check if both name and id are found - if(found_name && found_id && found_build_id) { - app_count++; - if(app_count >= MAX_APP_COUNT) { - FURI_LOG_I(TAG, "Reached maximum app count."); - state = STATE_DONE; - break; - } - - // Reset for next app - found_name = false; - found_id = false; - found_build_id = false; - } - } - } - } - continue; - } - - if(in_string) { - if(reading_key) { - if(key_index < MAX_KEY_LENGTH - 1) { - current_key[key_index++] = c; - } - } else if(reading_value) { - if(value_index < MAX_VALUE_LENGTH - 1) { - current_value[value_index++] = c; - } - } - continue; - } - - // Not inside a string - - if(state == STATE_SEARCH_ARRAY_START) { - if(c == '[') { - state = STATE_READ_ARRAY_ELEMENTS; - } - continue; - } - - if(state == STATE_READ_ARRAY_ELEMENTS) { - if(c == '{') { - inside_app_object = true; - found_name = false; - found_id = false; - found_build_id = false; - object_state = OBJECT_EXPECT_KEY; - } else if(c == '}') { - inside_app_object = false; - object_state = OBJECT_EXPECT_KEY; - } else if(c == ']') { - state = STATE_DONE; - break; - } else if(c == ':') { - if(inside_app_object && object_state == OBJECT_EXPECT_COLON) { - object_state = OBJECT_EXPECT_VALUE; - } - } else if(c == ',') { - if(inside_app_object && object_state == OBJECT_EXPECT_COMMA_OR_END) { - object_state = OBJECT_EXPECT_KEY; - } - // Else, separator between objects or values - } - // Ignore other characters like whitespace, etc. - continue; - } - } - } - - // Clean up - storage_file_close(_file); - storage_file_free(_file); - furi_record_close(RECORD_STORAGE); - - if(app_count == 0) { - FURI_LOG_E(TAG, "No valid apps were parsed."); - return false; - } - return true; -} - -static bool flip_store_get_fap_file( - char* build_id, - uint8_t target, - uint16_t api_major, - uint16_t api_minor) { - is_compile_app_request = true; - char url[164]; - snprintf( - url, - sizeof(url), - "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d", - build_id, - target, - api_major, - api_minor); - char* headers = jsmn("Content-Type", "application/octet-stream"); - bool sent_request = flipper_http_get_request_bytes(url, headers); - free(headers); - return sent_request; -} - -static void flip_store_request_error(Canvas* canvas) { - if(fhttp.received_data == NULL) { - if(fhttp.last_response != NULL) { - if(strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != - NULL) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } else { - FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response); - canvas_draw_str(canvas, 0, 42, "Unusual error..."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - } else { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - } else { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Failed to receive data."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } -} -// function to handle the entire installation process "asynchronously" -static bool flip_store_install_app(Canvas* canvas, char* category) { - // create /apps/FlipStore directory if it doesn't exist - char directory_path[128]; - snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps/%s", category); - - // Create the directory - Storage* storage = furi_record_open(RECORD_STORAGE); - storage_common_mkdir(storage, directory_path); - - // Adjusted to access flip_catalog as an array of structures - char* app_name = flip_catalog[app_selected_index].app_name; - char installing_text[128]; - snprintf(installing_text, sizeof(installing_text), "Installing %s", app_name); - char bin_path[256]; - snprintf( - bin_path, - sizeof(bin_path), - STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", - category, - flip_catalog[app_selected_index].app_id); - strncpy(fhttp.file_path, bin_path, sizeof(fhttp.file_path) - 1); - canvas_draw_str(canvas, 0, 10, installing_text); - canvas_draw_str(canvas, 0, 20, "Sending request.."); - uint8_t target = furi_hal_version_get_hw_target(); - uint16_t api_major, api_minor; - furi_hal_info_get_api_version(&api_major, &api_minor); - if(fhttp.state != INACTIVE && - flip_store_get_fap_file( - flip_catalog[app_selected_index].app_build_id, target, api_major, api_minor)) { - canvas_draw_str(canvas, 0, 30, "Request sent."); - fhttp.state = RECEIVING; - // furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - canvas_draw_str(canvas, 0, 40, "Receiving..."); - } else { - FURI_LOG_E(TAG, "Failed to send the request"); - flip_store_success = false; - return false; - } - while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { - // Wait for the feed to be received - furi_delay_ms(10); - } - // furi_timer_stop(fhttp.get_timeout_timer); - if(fhttp.state == ISSUE || fhttp.received_data == NULL) { - flip_store_request_error(canvas); - flip_store_success = false; - return false; - } - flip_store_success = true; - return true; -} - -// process the app list and return view -static int32_t flip_store_handle_app_list( - FlipStoreApp* app, - int32_t success_view, - char* category, - Submenu** submenu) { - // reset the flip_catalog - if(flip_catalog) { - flip_catalog_free(); - } - - if(!app) { - FURI_LOG_E(TAG, "FlipStoreApp is NULL"); - return FlipStoreViewPopup; - } - char url[128]; - is_compile_app_request = false; - // append the category to the end of the url - snprintf( - url, sizeof(url), "https://www.flipsocial.net/api/flipper/apps/%s/extended/", category); - // async call to the app list with timer - if(fhttp.state != INACTIVE && - flipper_http_get_request_with_headers(url, jsmn("Content-Type", "application/json"))) { - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - } else { - FURI_LOG_E(TAG, "Failed to send the request"); - return FlipStoreViewPopup; - } - while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { - // Wait for the feed to be received - furi_delay_ms(100); - } - furi_timer_stop(fhttp.get_timeout_timer); - if(fhttp.state == ISSUE || fhttp.received_data == NULL) { - if(fhttp.received_data == NULL) { - FURI_LOG_E(TAG, "Failed to receive data"); - if(fhttp.last_response != NULL) { - if(strstr( - fhttp.last_response, - "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL) { - popup_set_text( - app->popup, - "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", - 0, - 10, - AlignLeft, - AlignTop); - } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { - popup_set_text( - app->popup, - "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", - 0, - 10, - AlignLeft, - AlignTop); - } else { - popup_set_text(app->popup, fhttp.last_response, 0, 10, AlignLeft, AlignTop); - } - } else { - popup_set_text( - app->popup, - "[ERROR] Unknown Error.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", - 0, - 10, - AlignLeft, - AlignTop); - } - return FlipStoreViewPopup; - } else { - FURI_LOG_E(TAG, "Failed to receive data"); - popup_set_text(app->popup, "Failed to received data.", 0, 10, AlignLeft, AlignTop); - return FlipStoreViewPopup; - } - } else { - // process the app list - const char* output_file_path = STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag - "/received_data.txt"; - if(flip_store_process_app_list(output_file_path)) { - submenu_reset(*submenu); - // add each app name to submenu - for(int i = 0; i < MAX_APP_COUNT; i++) { - if(strlen(flip_catalog[i].app_name) > 0) { - submenu_add_item( - *submenu, - flip_catalog[i].app_name, - FlipStoreSubmenuIndexStartAppList + i, - callback_submenu_choices, - app); - } - } - return success_view; - } else { - FURI_LOG_E(TAG, "Failed to process the app list"); - popup_set_text( - app->popup, "Failed to process the app list", 0, 10, AlignLeft, AlignTop); - return FlipStoreViewPopup; - } - } -} - -#endif // FLIP_STORE_APPS_H diff --git a/flip_store/flip_store_callback.h b/flip_store/flip_store_callback.h deleted file mode 100644 index c34a4cd5a..000000000 --- a/flip_store/flip_store_callback.h +++ /dev/null @@ -1,384 +0,0 @@ -#ifndef FLIP_STORE_CALLBACK_H -#define FLIP_STORE_CALLBACK_H -#include -#include -#include -#include -#include -#include -#include "flip_store_icons.h" - -// Callback for drawing the main screen -static void flip_store_view_draw_callback_main(Canvas* canvas, void* model) { - UNUSED(model); - canvas_set_font(canvas, FontSecondary); - - if(fhttp.state == INACTIVE) { - canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); - canvas_draw_str(canvas, 0, 17, "Please connect to the board."); - canvas_draw_str(canvas, 0, 32, "If your board is connected,"); - canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); - canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); - canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); - return; - } - - if(!flip_store_sent_request) { - flip_store_sent_request = true; - - if(!flip_store_install_app(canvas, categories[flip_store_category_index])) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Failed to install app."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - } else { - if(flip_store_success) { - if(fhttp.state == RECEIVING) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Downloading app..."); - canvas_draw_str(canvas, 0, 60, "Please wait..."); - return; - } else if(fhttp.state == IDLE) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "App installed successfully."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - } else { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Failed to install app."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - } -} - -static void flip_store_view_draw_callback_app_list(Canvas* canvas, void* model) { - UNUSED(model); - canvas_clear(canvas); - canvas_set_font(canvas, FontPrimary); - // Adjusted to access flip_catalog as an array of structures - canvas_draw_str(canvas, 0, 10, flip_catalog[app_selected_index].app_name); - // canvas_draw_icon(canvas, 0, 53, &I_ButtonLeft_4x7); (future implementation) - // canvas_draw_str_aligned(canvas, 7, 54, AlignLeft, AlignTop, "Delete"); (future implementation) - canvas_draw_icon(canvas, 0, 53, &I_ButtonBACK_10x8); - canvas_draw_str_aligned(canvas, 12, 54, AlignLeft, AlignTop, "Back"); - canvas_draw_icon(canvas, 90, 53, &I_ButtonRight_4x7); - canvas_draw_str_aligned(canvas, 97, 54, AlignLeft, AlignTop, "Install"); -} - -static bool flip_store_input_callback(InputEvent* event, void* context) { - FlipStoreApp* app = (FlipStoreApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipStoreApp is NULL"); - return false; - } - if(event->type == InputTypeShort) { - // Future implementation - // if (event->key == InputKeyLeft) - //{ - // Left button clicked, delete the app with DialogEx confirmation - // view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppDelete); - // return true; - //} - if(event->key == InputKeyRight) { - // Right button clicked, download the app - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewMain); - return true; - } - } else if(event->type == InputTypePress) { - if(event->key == InputKeyBack) { - // Back button clicked, switch to the previous view. - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); - return true; - } - } - - return false; -} - -static void flip_store_text_updated_ssid(void* context) { - FlipStoreApp* app = (FlipStoreApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipStoreApp is NULL"); - return; - } - - // store the entered text - strncpy( - app->uart_text_input_buffer_ssid, - app->uart_text_input_temp_buffer_ssid, - app->uart_text_input_buffer_size_ssid); - - // Ensure null-termination - app->uart_text_input_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; - - // update the variable item text - if(app->variable_item_ssid) { - variable_item_set_current_value_text( - app->variable_item_ssid, app->uart_text_input_buffer_ssid); - } - - // save the settings - save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass); - - // if SSID and PASS are not empty, connect to the WiFi - if(strlen(app->uart_text_input_buffer_ssid) > 0 && - strlen(app->uart_text_input_buffer_pass) > 0) { - // save wifi settings - if(!flipper_http_save_wifi( - app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass)) { - FURI_LOG_E(TAG, "Failed to save WiFi settings"); - } - } - - // switch to the settings view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); -} -static void flip_store_text_updated_pass(void* context) { - FlipStoreApp* app = (FlipStoreApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipStoreApp is NULL"); - return; - } - - // store the entered text - strncpy( - app->uart_text_input_buffer_pass, - app->uart_text_input_temp_buffer_pass, - app->uart_text_input_buffer_size_pass); - - // Ensure null-termination - app->uart_text_input_buffer_pass[app->uart_text_input_buffer_size_pass - 1] = '\0'; - - // update the variable item text - if(app->variable_item_pass) { - variable_item_set_current_value_text( - app->variable_item_pass, app->uart_text_input_buffer_pass); - } - - // save the settings - save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass); - - // if SSID and PASS are not empty, connect to the WiFi - if(strlen(app->uart_text_input_buffer_ssid) > 0 && - strlen(app->uart_text_input_buffer_pass) > 0) { - // save wifi settings - if(!flipper_http_save_wifi( - app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass)) { - FURI_LOG_E(TAG, "Failed to save WiFi settings"); - } - } - - // switch to the settings view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); -} - -static uint32_t callback_to_submenu(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - return FlipStoreViewSubmenu; -} - -static uint32_t callback_to_app_list(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - flip_store_sent_request = false; - flip_store_success = false; - flip_store_saved_data = false; - flip_store_saved_success = false; - return FlipStoreViewAppList; -} - -static void settings_item_selected(void* context, uint32_t index) { - FlipStoreApp* app = (FlipStoreApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipStoreApp is NULL"); - return; - } - switch(index) { - case 0: // Input SSID - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewTextInputSSID); - break; - case 1: // Input Password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewTextInputPass); - break; - default: - FURI_LOG_E(TAG, "Unknown configuration item index"); - break; - } -} - -void dialog_callback(DialogExResult result, void* context) { - furi_assert(context); - FlipStoreApp* app = (FlipStoreApp*)context; - if(result == DialogExResultLeft) // No - { - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); - } else if(result == DialogExResultRight) { - // delete the app then return to the app list - - // pop up a message - popup_set_header(app->popup, "Success", 0, 0, AlignLeft, AlignTop); - popup_set_text(app->popup, "App deleted successfully.", 0, 60, AlignLeft, AlignTop); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup); - furi_delay_ms(2000); // delay for 2 seconds - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); - } -} - -void popup_callback(void* context) { - FlipStoreApp* app = (FlipStoreApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipStoreApp is NULL"); - return; - } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenu); -} - -/** - * @brief Navigation callback for exiting the application - * @param context The context - unused - * @return next view id (VIEW_NONE to exit the app) - */ -static uint32_t callback_exit_app(void* context) { - // Exit the application - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - return VIEW_NONE; // Return VIEW_NONE to exit the app -} - -static void callback_submenu_choices(void* context, uint32_t index) { - FlipStoreApp* app = (FlipStoreApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipStoreApp is NULL"); - return; - } - switch(index) { - case FlipStoreSubmenuIndexMain: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewMain); - break; - case FlipStoreSubmenuIndexAbout: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAbout); - break; - case FlipStoreSubmenuIndexSettings: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); - break; - case FlipStoreSubmenuIndexAppList: - flip_store_category_index = 0; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); - break; - case FlipStoreSubmenuIndexAppListBluetooth: - flip_store_category_index = 0; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListBluetooth, "Bluetooth", &app->submenu_app_list_bluetooth)); - break; - case FlipStoreSubmenuIndexAppListGames: - flip_store_category_index = 1; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListGames, "Games", &app->submenu_app_list_games)); - break; - case FlipStoreSubmenuIndexAppListGPIO: - flip_store_category_index = 2; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListGPIO, "GPIO", &app->submenu_app_list_gpio)); - break; - case FlipStoreSubmenuIndexAppListInfrared: - flip_store_category_index = 3; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListInfrared, "Infrared", &app->submenu_app_list_infrared)); - break; - case FlipStoreSubmenuIndexAppListiButton: - flip_store_category_index = 4; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListiButton, "iButton", &app->submenu_app_list_ibutton)); - break; - case FlipStoreSubmenuIndexAppListMedia: - flip_store_category_index = 5; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListMedia, "Media", &app->submenu_app_list_media)); - break; - case FlipStoreSubmenuIndexAppListNFC: - flip_store_category_index = 6; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListNFC, "NFC", &app->submenu_app_list_nfc)); - break; - case FlipStoreSubmenuIndexAppListRFID: - flip_store_category_index = 7; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListRFID, "RFID", &app->submenu_app_list_rfid)); - break; - case FlipStoreSubmenuIndexAppListSubGHz: - flip_store_category_index = 8; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListSubGHz, "Sub-GHz", &app->submenu_app_list_subghz)); - break; - case FlipStoreSubmenuIndexAppListTools: - flip_store_category_index = 9; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListTools, "Tools", &app->submenu_app_list_tools)); - break; - case FlipStoreSubmenuIndexAppListUSB: - flip_store_category_index = 10; - view_dispatcher_switch_to_view( - app->view_dispatcher, - flip_store_handle_app_list( - app, FlipStoreViewAppListUSB, "USB", &app->submenu_app_list_usb)); - break; - default: - // Check if the index is within the app list range - if(index >= FlipStoreSubmenuIndexStartAppList && - index < FlipStoreSubmenuIndexStartAppList + MAX_APP_COUNT) { - // Get the app index - uint32_t app_index = index - FlipStoreSubmenuIndexStartAppList; - - // Check if the app index is valid - if((int)app_index >= 0 && app_index < MAX_APP_COUNT) { - // Get the app name - char* app_name = flip_catalog[app_index].app_name; - - // Check if the app name is valid - if(app_name != NULL && strlen(app_name) > 0) { - app_selected_index = app_index; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppInfo); - } else { - FURI_LOG_E(TAG, "Invalid app name"); - } - } else { - FURI_LOG_E(TAG, "Invalid app index"); - } - } else { - FURI_LOG_E(TAG, "Unknown submenu index"); - } - break; - } -} - -#endif // FLIP_STORE_CALLBACK_H diff --git a/flip_store/flipper_http.h b/flip_store/flipper_http.h deleted file mode 100644 index ed6bb4d2a..000000000 --- a/flip_store/flipper_http.h +++ /dev/null @@ -1,1252 +0,0 @@ -// flipper_http.h - Flipper HTTP Library (www.github.com/jblanked) -// Author: JBlanked -#ifndef FLIPPER_HTTP_H -#define FLIPPER_HTTP_H - -#include -#include -#include -#include -#include -#include - -// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext - -#define HTTP_TAG "FlipStore" // change this to your app name -#define http_tag "flip_store" // change this to your app id -#define UART_CH (FuriHalSerialIdUsart) // UART channel -#define TIMEOUT_DURATION_TICKS (6 * 1000) // 6 seconds -#define BAUDRATE (115200) // UART baudrate -#define RX_BUF_SIZE 1024 // UART RX buffer size -#define RX_LINE_BUFFER_SIZE 5000 // UART RX line buffer size (increase for large responses) - -// Forward declaration for callback -typedef void (*FlipperHTTP_Callback)(const char* line, void* context); - -// Functions -bool flipper_http_init(FlipperHTTP_Callback callback, void* context); -void flipper_http_deinit(); -//--- -void flipper_http_rx_callback(const char* line, void* context); -bool flipper_http_send_data(const char* data); -//--- -bool flipper_http_connect_wifi(); -bool flipper_http_disconnect_wifi(); -bool flipper_http_ping(); -bool flipper_http_scan_wifi(); -bool flipper_http_save_wifi(const char* ssid, const char* password); -//--- -bool flipper_http_get_request(const char* url); -bool flipper_http_get_request_with_headers(const char* url, const char* headers); -bool flipper_http_post_request_with_headers( - const char* url, - const char* headers, - const char* payload); -bool flipper_http_put_request_with_headers( - const char* url, - const char* headers, - const char* payload); -bool flipper_http_delete_request_with_headers( - const char* url, - const char* headers, - const char* payload); -//--- -bool flipper_http_get_request_bytes(const char* url, const char* headers); -bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload); -//--- -bool flipper_http_save_received_data(size_t bytes_received, const char line_buffer[]); -char* trim(const char* str); - -// State variable to track the UART state -typedef enum { - INACTIVE, // Inactive state - IDLE, // Default state - RECEIVING, // Receiving data - SENDING, // Sending data - ISSUE, // Issue with connection -} SerialState; - -// Event Flags for UART Worker Thread -typedef enum { - WorkerEvtStop = (1 << 0), - WorkerEvtRxDone = (1 << 1), -} WorkerEvtFlags; - -static bool is_compile_app_request = false; // personal use in flip_store_apps.h - -// FlipperHTTP Structure -typedef struct { - FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication - FuriHalSerialHandle* serial_handle; // Serial handle for UART communication - FuriThread* rx_thread; // Worker thread for UART - // uint8_t rx_buf[RX_BUF_SIZE]; // Buffer for received data - FuriThreadId rx_thread_id; // Worker thread ID - FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines - void* callback_context; // Context for the callback - SerialState state; // State of the UART - - // variable to store the last received data from the UART - char* last_response; - - // Timer-related members - FuriTimer* get_timeout_timer; // Timer for HTTP request timeout - char* received_data; // Buffer to store received data - - bool started_receiving_get; // Indicates if a GET request has started - bool just_started_get; // Indicates if GET data reception has just started - - bool started_receiving_post; // Indicates if a POST request has started - bool just_started_post; // Indicates if POST data reception has just started - - bool started_receiving_put; // Indicates if a PUT request has started - bool just_started_put; // Indicates if PUT data reception has just started - - bool started_receiving_delete; // Indicates if a DELETE request has started - bool just_started_delete; // Indicates if DELETE data reception has just started - - // Buffer to hold the raw bytes received from the UART - uint8_t* received_bytes; - size_t received_bytes_len; // Length of the received bytes - - // File path to save the bytes received - char file_path[256]; - - bool save_data; // Flag to save the received data - -} FlipperHTTP; - -static FlipperHTTP fhttp; - -// Function to append received data to file -static bool append_to_file(const char* file_path, const void* data, size_t data_size) { - Storage* storage = furi_record_open(RECORD_STORAGE); - File* file = storage_file_alloc(storage); - - // Open the file in append mode - if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_OPEN_APPEND)) { - FURI_LOG_E(HTTP_TAG, "Failed to open file for appending: %s", file_path); - storage_file_free(file); - furi_record_close(RECORD_STORAGE); - return false; - } - - // Write the data to the file - if(storage_file_write(file, data, data_size) != data_size) { - FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); - storage_file_close(file); - storage_file_free(file); - furi_record_close(RECORD_STORAGE); - return false; - } - - storage_file_close(file); - storage_file_free(file); - furi_record_close(RECORD_STORAGE); - - return true; -} -// Global static array for the line buffer -static char rx_line_buffer[RX_LINE_BUFFER_SIZE]; -#define FILE_BUFFER_SIZE 512 -static uint8_t file_buffer[FILE_BUFFER_SIZE]; - -// UART worker thread -/** - * @brief Worker thread to handle UART data asynchronously. - * @return 0 - * @param context The context to pass to the callback. - * @note This function will handle received data asynchronously via the callback. - */ -// UART worker thread -static int32_t flipper_http_worker(void* context) { - UNUSED(context); - size_t rx_line_pos = 0; - static size_t file_buffer_len = 0; - - while(1) { - uint32_t events = furi_thread_flags_wait( - WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever); - if(events & WorkerEvtStop) break; - if(events & WorkerEvtRxDone) { - // Continuously read from the stream buffer until it's empty - while(!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) { - // Read one byte at a time - char c = 0; - size_t received = furi_stream_buffer_receive(fhttp.flipper_http_stream, &c, 1, 0); - - if(received == 0) { - // No more data to read - break; - } - - // Append the received byte to the file if saving is enabled - if(fhttp.save_data) { - // Add byte to the buffer - file_buffer[file_buffer_len++] = c; - // Write to file if buffer is full - if(file_buffer_len >= FILE_BUFFER_SIZE) { - if(!append_to_file(fhttp.file_path, file_buffer, file_buffer_len)) { - FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); - } - file_buffer_len = 0; - } - } - - // Handle line buffering only if callback is set (text data) - if(fhttp.handle_rx_line_cb) { - // Handle line buffering - if(c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1) { - rx_line_buffer[rx_line_pos] = '\0'; // Null-terminate the line - - // Invoke the callback with the complete line - fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context); - - // Reset the line buffer position - rx_line_pos = 0; - } else { - rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer - } - } - } - } - } - - if(fhttp.save_data) { - // Write the remaining data to the file - if(file_buffer_len > 0) { - if(!append_to_file(fhttp.file_path, file_buffer, file_buffer_len)) { - FURI_LOG_E(HTTP_TAG, "Failed to append remaining data to file"); - } - } - } - - // remove [POST/END] and/or [GET/END] from the file - if(fhttp.save_data) { - char* end = NULL; - if((end = strstr(fhttp.file_path, "[POST/END]")) != NULL) { - *end = '\0'; - } else if((end = strstr(fhttp.file_path, "[GET/END]")) != NULL) { - *end = '\0'; - } - } - - // remove newline from the from the end of the file - if(fhttp.save_data) { - char* end = NULL; - if((end = strstr(fhttp.file_path, "\n")) != NULL) { - *end = '\0'; - } - } - - // Reset the file buffer length - file_buffer_len = 0; - - return 0; -} - -// Timer callback function -/** - * @brief Callback function for the GET timeout timer. - * @return 0 - * @param context The context to pass to the callback. - * @note This function will be called when the GET request times out. - */ -void get_timeout_timer_callback(void* context) { - UNUSED(context); - FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving the end."); - - // Reset the state - fhttp.started_receiving_get = false; - fhttp.started_receiving_post = false; - fhttp.started_receiving_put = false; - fhttp.started_receiving_delete = false; - - // Free received data if any - if(fhttp.received_data) { - free(fhttp.received_data); - fhttp.received_data = NULL; - } - - // Update UART state - fhttp.state = ISSUE; -} - -// UART RX Handler Callback (Interrupt Context) -/** - * @brief A private callback function to handle received data asynchronously. - * @return void - * @param handle The UART handle. - * @param event The event type. - * @param context The context to pass to the callback. - * @note This function will handle received data asynchronously via the callback. - */ -static void _flipper_http_rx_callback( - FuriHalSerialHandle* handle, - FuriHalSerialRxEvent event, - void* context) { - UNUSED(context); - if(event == FuriHalSerialRxEventData) { - uint8_t data = furi_hal_serial_async_rx(handle); - furi_stream_buffer_send(fhttp.flipper_http_stream, &data, 1, 0); - furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtRxDone); - } -} - -// UART initialization function -/** - * @brief Initialize UART. - * @return true if the UART was initialized successfully, false otherwise. - * @param callback The callback function to handle received data (ex. flipper_http_rx_callback). - * @param context The context to pass to the callback. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { - if(!context) { - FURI_LOG_E(HTTP_TAG, "Invalid context provided to flipper_http_init."); - return false; - } - if(!callback) { - FURI_LOG_E(HTTP_TAG, "Invalid callback provided to flipper_http_init."); - return false; - } - fhttp.flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1); - if(!fhttp.flipper_http_stream) { - FURI_LOG_E(HTTP_TAG, "Failed to allocate UART stream buffer."); - return false; - } - - fhttp.rx_thread = furi_thread_alloc(); - if(!fhttp.rx_thread) { - FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread."); - furi_stream_buffer_free(fhttp.flipper_http_stream); - return false; - } - - furi_thread_set_name(fhttp.rx_thread, "FlipperHTTP_RxThread"); - furi_thread_set_stack_size(fhttp.rx_thread, 1024); - furi_thread_set_context(fhttp.rx_thread, &fhttp); - furi_thread_set_callback(fhttp.rx_thread, flipper_http_worker); - - fhttp.handle_rx_line_cb = callback; - fhttp.callback_context = context; - - furi_thread_start(fhttp.rx_thread); - fhttp.rx_thread_id = furi_thread_get_id(fhttp.rx_thread); - - // handle when the UART control is busy to avoid furi_check failed - if(furi_hal_serial_control_is_busy(UART_CH)) { - FURI_LOG_E(HTTP_TAG, "UART control is busy."); - return false; - } - - fhttp.serial_handle = furi_hal_serial_control_acquire(UART_CH); - if(!fhttp.serial_handle) { - FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL"); - // Cleanup resources - furi_thread_free(fhttp.rx_thread); - furi_stream_buffer_free(fhttp.flipper_http_stream); - return false; - } - - // Initialize UART with acquired handle - furi_hal_serial_init(fhttp.serial_handle, BAUDRATE); - - // Enable RX direction - furi_hal_serial_enable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); - - // Start asynchronous RX with the callback - furi_hal_serial_async_rx_start(fhttp.serial_handle, _flipper_http_rx_callback, &fhttp, false); - - // Wait for the TX to complete to ensure UART is ready - furi_hal_serial_tx_wait_complete(fhttp.serial_handle); - - // Allocate the timer for handling timeouts - fhttp.get_timeout_timer = furi_timer_alloc( - get_timeout_timer_callback, // Callback function - FuriTimerTypeOnce, // One-shot timer - &fhttp // Context passed to callback - ); - - if(!fhttp.get_timeout_timer) { - FURI_LOG_E(HTTP_TAG, "Failed to allocate HTTP request timeout timer."); - // Cleanup resources - furi_hal_serial_async_rx_stop(fhttp.serial_handle); - furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); - furi_hal_serial_control_release(fhttp.serial_handle); - furi_hal_serial_deinit(fhttp.serial_handle); - furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop); - furi_thread_join(fhttp.rx_thread); - furi_thread_free(fhttp.rx_thread); - furi_stream_buffer_free(fhttp.flipper_http_stream); - return false; - } - - // Set the timer thread priority if needed - furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated); - - fhttp.file_path[0] = '\0'; // Null-terminate the file path - fhttp.received_data = NULL; - fhttp.received_bytes = NULL; - fhttp.received_bytes_len = 0; - return true; -} - -// Deinitialize UART -/** - * @brief Deinitialize UART. - * @return void - * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. - */ -void flipper_http_deinit() { - if(fhttp.serial_handle == NULL) { - FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?"); - return; - } - // Stop asynchronous RX - furi_hal_serial_async_rx_stop(fhttp.serial_handle); - - // Release and deinitialize the serial handle - furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); - furi_hal_serial_control_release(fhttp.serial_handle); - furi_hal_serial_deinit(fhttp.serial_handle); - - // Signal the worker thread to stop - furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop); - // Wait for the thread to finish - furi_thread_join(fhttp.rx_thread); - // Free the thread resources - furi_thread_free(fhttp.rx_thread); - - // Free the stream buffer - furi_stream_buffer_free(fhttp.flipper_http_stream); - - // Free the timer - if(fhttp.get_timeout_timer) { - furi_timer_free(fhttp.get_timeout_timer); - fhttp.get_timeout_timer = NULL; - } - - // Free received data if any - if(fhttp.received_data) { - free(fhttp.received_data); - fhttp.received_data = NULL; - } - - // Free the last response - if(fhttp.last_response) { - free(fhttp.last_response); - fhttp.last_response = NULL; - } - - // FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully."); -} - -// Function to send data over UART with newline termination -/** - * @brief Send data over UART with newline termination. - * @return true if the data was sent successfully, false otherwise. - * @param data The data to send over UART. - * @note The data will be sent over UART with a newline character appended. - */ -bool flipper_http_send_data(const char* data) { - size_t data_length = strlen(data); - if(data_length == 0) { - FURI_LOG_E("FlipperHTTP", "Attempted to send empty data."); - return false; - } - - // Create a buffer with data + '\n' - size_t send_length = data_length + 1; // +1 for '\n' - if(send_length > 256) { // Ensure buffer size is sufficient - FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP."); - return false; - } - - char send_buffer[257]; // 256 + 1 for safety - strncpy(send_buffer, data, 256); - send_buffer[data_length] = '\n'; // Append newline - send_buffer[data_length + 1] = '\0'; // Null-terminate - - if(fhttp.state == INACTIVE && ((strstr(send_buffer, "[PING]") == NULL) && - (strstr(send_buffer, "[WIFI/CONNECT]") == NULL))) { - FURI_LOG_E("FlipperHTTP", "Cannot send data while INACTIVE."); - fhttp.last_response = "Cannot send data while INACTIVE."; - return false; - } - - fhttp.state = SENDING; - furi_hal_serial_tx(fhttp.serial_handle, (const uint8_t*)send_buffer, send_length); - - // Uncomment below line to log the data sent over UART - // FURI_LOG_I("FlipperHTTP", "Sent data over UART: %s", send_buffer); - fhttp.state = IDLE; - return true; -} - -// Function to send a PING request -/** - * @brief Send a PING request to check the connection status. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - * @note This is best used to check if the Wifi Dev Board is connected. - * @note The state will remain INACTIVE until a PONG is received. - */ -bool flipper_http_ping() { - const char* command = "[PING]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send PING command."); - return false; - } - // set state as INACTIVE to be made IDLE if PONG is received - fhttp.state = INACTIVE; - // The response will be handled asynchronously via the callback - return true; -} - -// Function to scan for WiFi networks -/** - * @brief Send a command to scan for WiFi networks. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_scan_wifi() { - const char* command = "[WIFI/SCAN]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send WiFi scan command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to save WiFi settings (returns true if successful) -/** - * @brief Send a command to save WiFi settings. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_save_wifi(const char* ssid, const char* password) { - if(!ssid || !password) { - FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_save_wifi."); - return false; - } - char buffer[256]; - int ret = snprintf( - buffer, sizeof(buffer), "[WIFI/SAVE]{\"ssid\":\"%s\",\"password\":\"%s\"}", ssid, password); - if(ret < 0 || ret >= (int)sizeof(buffer)) { - FURI_LOG_E("FlipperHTTP", "Failed to format WiFi save command."); - return false; - } - - if(!flipper_http_send_data(buffer)) { - FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to disconnect from WiFi (returns true if successful) -/** - * @brief Send a command to disconnect from WiFi. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_disconnect_wifi() { - const char* command = "[WIFI/DISCONNECT]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to connect to WiFi (returns true if successful) -/** - * @brief Send a command to connect to WiFi. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_connect_wifi() { - const char* command = "[WIFI/CONNECT]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to send a GET request -/** - * @brief Send a GET request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the GET request to. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_get_request(const char* url) { - if(!url) { - FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request."); - return false; - } - - // Prepare GET request command - char command[256]; - int ret = snprintf(command, sizeof(command), "[GET]%s", url); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to format GET request command."); - return false; - } - - // Send GET request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send GET request command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to send a GET request with headers -/** - * @brief Send a GET request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the GET request to. - * @param headers The headers to send with the GET request. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_get_request_with_headers(const char* url, const char* headers) { - if(!url || !headers) { - FURI_LOG_E( - "FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_with_headers."); - return false; - } - - // Prepare GET request command with headers - char command[256]; - int ret = snprintf( - command, sizeof(command), "[GET/HTTP]{\"url\":\"%s\",\"headers\":%s}", url, headers); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers."); - return false; - } - - // Send GET request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to send a GET request with headers and return bytes -/** - * @brief Send a GET request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the GET request to. - * @param headers The headers to send with the GET request. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_get_request_bytes(const char* url, const char* headers) { - if(!url || !headers) { - FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_bytes."); - return false; - } - - // Prepare GET request command with headers - char command[256]; - int ret = snprintf( - command, sizeof(command), "[GET/BYTES]{\"url\":\"%s\",\"headers\":%s}", url, headers); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers."); - return false; - } - - // Send GET request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to send a POST request with headers -/** - * @brief Send a POST request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the POST request to. - * @param headers The headers to send with the POST request. - * @param payload The data to send with the POST request. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_post_request_with_headers( - const char* url, - const char* headers, - const char* payload) { - if(!url || !headers || !payload) { - FURI_LOG_E( - "FlipperHTTP", - "Invalid arguments provided to flipper_http_post_request_with_headers."); - return false; - } - - // Prepare POST request command with headers and data - char command[256]; - int ret = snprintf( - command, - sizeof(command), - "[POST/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", - url, - headers, - payload); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data."); - return false; - } - - // Send POST request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to send a POST request with headers and return bytes -/** - * @brief Send a POST request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the POST request to. - * @param headers The headers to send with the POST request. - * @param payload The data to send with the POST request. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload) { - if(!url || !headers || !payload) { - FURI_LOG_E( - "FlipperHTTP", "Invalid arguments provided to flipper_http_post_request_bytes."); - return false; - } - - // Prepare POST request command with headers and data - char command[256]; - int ret = snprintf( - command, - sizeof(command), - "[POST/BYTES]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", - url, - headers, - payload); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data."); - return false; - } - - // Send POST request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to send a PUT request with headers -/** - * @brief Send a PUT request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the PUT request to. - * @param headers The headers to send with the PUT request. - * @param payload The data to send with the PUT request. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_put_request_with_headers( - const char* url, - const char* headers, - const char* payload) { - if(!url || !headers || !payload) { - FURI_LOG_E( - "FlipperHTTP", "Invalid arguments provided to flipper_http_put_request_with_headers."); - return false; - } - - // Prepare PUT request command with headers and data - char command[256]; - int ret = snprintf( - command, - sizeof(command), - "[PUT/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", - url, - headers, - payload); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to format PUT request command with headers and data."); - return false; - } - - // Send PUT request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to send a DELETE request with headers -/** - * @brief Send a DELETE request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the DELETE request to. - * @param headers The headers to send with the DELETE request. - * @param payload The data to send with the DELETE request. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_delete_request_with_headers( - const char* url, - const char* headers, - const char* payload) { - if(!url || !headers || !payload) { - FURI_LOG_E( - "FlipperHTTP", - "Invalid arguments provided to flipper_http_delete_request_with_headers."); - return false; - } - - // Prepare DELETE request command with headers and data - char command[256]; - int ret = snprintf( - command, - sizeof(command), - "[DELETE/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", - url, - headers, - payload); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E( - "FlipperHTTP", "Failed to format DELETE request command with headers and data."); - return false; - } - - // Send DELETE request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to handle received data asynchronously -/** - * @brief Callback function to handle received data asynchronously. - * @return void - * @param line The received line. - * @param context The context passed to the callback. - * @note The received data will be handled asynchronously via the callback and handles the state of the UART. - */ -void flipper_http_rx_callback(const char* line, void* context) { - if(!line || !context) { - FURI_LOG_E(HTTP_TAG, "Invalid arguments provided to flipper_http_rx_callback."); - return; - } - - // Trim the received line to check if it's empty - char* trimmed_line = trim(line); - if(trimmed_line != NULL && trimmed_line[0] != '\0') { - fhttp.last_response = (char*)line; - } - free(trimmed_line); // Free the allocated memory for trimmed_line - - if(fhttp.state != INACTIVE && fhttp.state != ISSUE) { - fhttp.state = RECEIVING; - } - - // Uncomment below line to log the data received over UART - FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line); - - // Check if we've started receiving data from a GET request - if(fhttp.started_receiving_get) { - // Restart the timeout timer each time new data is received - furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - - if(strstr(line, "[GET/END]") != NULL) { - FURI_LOG_I(HTTP_TAG, "GET request completed."); - // Stop the timer since we've completed the GET request - furi_timer_stop(fhttp.get_timeout_timer); - - if(fhttp.received_data) { - // uncomment if you want to save the received data to the external storage - flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data); - fhttp.started_receiving_get = false; - fhttp.just_started_get = false; - fhttp.state = IDLE; - return; - } else { - FURI_LOG_E(HTTP_TAG, "No data received."); - fhttp.started_receiving_get = false; - fhttp.just_started_get = false; - fhttp.state = IDLE; - return; - } - } - - // Append the new line to the existing data - if(fhttp.received_data == NULL) { - fhttp.received_data = - (char*)malloc(strlen(line) + 2); // +2 for newline and null terminator - if(fhttp.received_data) { - strcpy(fhttp.received_data, line); - fhttp.received_data[strlen(line)] = '\n'; // Add newline - fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator - } - } else { - size_t current_len = strlen(fhttp.received_data); - size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator - fhttp.received_data = (char*)realloc(fhttp.received_data, new_size); - if(fhttp.received_data) { - memcpy( - fhttp.received_data + current_len, - line, - strlen(line)); // Copy line at the end of the current data - fhttp.received_data[current_len + strlen(line)] = '\n'; // Add newline - fhttp.received_data[current_len + strlen(line) + 1] = '\0'; // Null terminator - } - } - - if(!fhttp.just_started_get) { - fhttp.just_started_get = true; - } - return; - } - - // Check if we've started receiving data from a POST request - else if(fhttp.started_receiving_post) { - // Restart the timeout timer each time new data is received - furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - - if(strstr(line, "[POST/END]") != NULL) { - FURI_LOG_I(HTTP_TAG, "POST request completed."); - fhttp.save_data = false; - // Stop the timer since we've completed the POST request - furi_timer_stop(fhttp.get_timeout_timer); - - if(fhttp.received_data) { - // uncomment if you want to save the received data to the external storage - // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data); - fhttp.started_receiving_post = false; - fhttp.just_started_post = false; - fhttp.state = IDLE; - return; - } else { - FURI_LOG_E(HTTP_TAG, "No data received."); - fhttp.started_receiving_post = false; - fhttp.just_started_post = false; - fhttp.state = IDLE; - return; - } - } - - // Append the new line to the existing data - if(fhttp.received_data == NULL) { - fhttp.received_data = - (char*)malloc(strlen(line) + 2); // +2 for newline and null terminator - if(fhttp.received_data) { - strcpy(fhttp.received_data, line); - fhttp.received_data[strlen(line)] = '\n'; // Add newline - fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator - } - } else { - size_t current_len = strlen(fhttp.received_data); - size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator - fhttp.received_data = (char*)realloc(fhttp.received_data, new_size); - if(fhttp.received_data) { - memcpy( - fhttp.received_data + current_len, - line, - strlen(line)); // Copy line at the end of the current data - fhttp.received_data[current_len + strlen(line)] = '\n'; // Add newline - fhttp.received_data[current_len + strlen(line) + 1] = '\0'; // Null terminator - } - } - - if(!fhttp.just_started_post) { - fhttp.just_started_post = true; - } - return; - } - - // Check if we've started receiving data from a PUT request - else if(fhttp.started_receiving_put) { - // Restart the timeout timer each time new data is received - furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - - if(strstr(line, "[PUT/END]") != NULL) { - FURI_LOG_I(HTTP_TAG, "PUT request completed."); - // Stop the timer since we've completed the PUT request - furi_timer_stop(fhttp.get_timeout_timer); - - if(fhttp.received_data) { - // uncomment if you want to save the received data to the external storage - // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data); - fhttp.started_receiving_put = false; - fhttp.just_started_put = false; - fhttp.state = IDLE; - return; - } else { - FURI_LOG_E(HTTP_TAG, "No data received."); - fhttp.started_receiving_put = false; - fhttp.just_started_put = false; - fhttp.state = IDLE; - return; - } - } - - // Append the new line to the existing data - if(fhttp.received_data == NULL) { - fhttp.received_data = - (char*)malloc(strlen(line) + 2); // +2 for newline and null terminator - if(fhttp.received_data) { - strcpy(fhttp.received_data, line); - fhttp.received_data[strlen(line)] = '\n'; // Add newline - fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator - } - } else { - size_t current_len = strlen(fhttp.received_data); - size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator - fhttp.received_data = (char*)realloc(fhttp.received_data, new_size); - if(fhttp.received_data) { - memcpy( - fhttp.received_data + current_len, - line, - strlen(line)); // Copy line at the end of the current data - fhttp.received_data[current_len + strlen(line)] = '\n'; // Add newline - fhttp.received_data[current_len + strlen(line) + 1] = '\0'; // Null terminator - } - } - - if(!fhttp.just_started_put) { - fhttp.just_started_put = true; - } - return; - } - - // Check if we've started receiving data from a DELETE request - else if(fhttp.started_receiving_delete) { - // Restart the timeout timer each time new data is received - furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - - if(strstr(line, "[DELETE/END]") != NULL) { - FURI_LOG_I(HTTP_TAG, "DELETE request completed."); - // Stop the timer since we've completed the DELETE request - furi_timer_stop(fhttp.get_timeout_timer); - - if(fhttp.received_data) { - // uncomment if you want to save the received data to the external storage - // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data); - fhttp.started_receiving_delete = false; - fhttp.just_started_delete = false; - fhttp.state = IDLE; - return; - } else { - FURI_LOG_E(HTTP_TAG, "No data received."); - fhttp.started_receiving_delete = false; - fhttp.just_started_delete = false; - fhttp.state = IDLE; - return; - } - } - - // Append the new line to the existing data - if(fhttp.received_data == NULL) { - fhttp.received_data = - (char*)malloc(strlen(line) + 2); // +2 for newline and null terminator - if(fhttp.received_data) { - strcpy(fhttp.received_data, line); - fhttp.received_data[strlen(line)] = '\n'; // Add newline - fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator - } - } else { - size_t current_len = strlen(fhttp.received_data); - size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator - fhttp.received_data = (char*)realloc(fhttp.received_data, new_size); - if(fhttp.received_data) { - memcpy( - fhttp.received_data + current_len, - line, - strlen(line)); // Copy line at the end of the current data - fhttp.received_data[current_len + strlen(line)] = '\n'; // Add newline - fhttp.received_data[current_len + strlen(line) + 1] = '\0'; // Null terminator - } - } - - if(!fhttp.just_started_delete) { - fhttp.just_started_delete = true; - } - return; - } - - // Handle different types of responses - if(strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL) { - FURI_LOG_I(HTTP_TAG, "Operation succeeded."); - } else if(strstr(line, "[INFO]") != NULL) { - FURI_LOG_I(HTTP_TAG, "Received info: %s", line); - - if(fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL) { - fhttp.state = IDLE; - } - } else if(strstr(line, "[GET/SUCCESS]") != NULL) { - FURI_LOG_I(HTTP_TAG, "GET request succeeded."); - fhttp.started_receiving_get = true; - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - fhttp.received_data = NULL; - if(is_compile_app_request) { - fhttp.save_data = true; - } - return; - } else if(strstr(line, "[POST/SUCCESS]") != NULL) { - FURI_LOG_I(HTTP_TAG, "POST request succeeded."); - fhttp.started_receiving_post = true; - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - fhttp.received_data = NULL; - if(is_compile_app_request) { - fhttp.save_data = true; - } - return; - } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { - FURI_LOG_I(HTTP_TAG, "PUT request succeeded."); - fhttp.started_receiving_put = true; - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - fhttp.received_data = NULL; - return; - } else if(strstr(line, "[DELETE/SUCCESS]") != NULL) { - FURI_LOG_I(HTTP_TAG, "DELETE request succeeded."); - fhttp.started_receiving_delete = true; - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - fhttp.received_data = NULL; - return; - } else if(strstr(line, "[DISCONNECTED]") != NULL) { - FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully."); - } else if(strstr(line, "[ERROR]") != NULL) { - FURI_LOG_E(HTTP_TAG, "Received error: %s", line); - fhttp.state = ISSUE; - return; - } else if(strstr(line, "[PONG]") != NULL) { - FURI_LOG_I(HTTP_TAG, "Received PONG response: Wifi Dev Board is still alive."); - - // send command to connect to WiFi - if(fhttp.state == INACTIVE) { - fhttp.state = IDLE; - return; - } - } - - if(fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) { - fhttp.state = IDLE; - } else if(fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) { - fhttp.state = INACTIVE; - } else { - fhttp.state = IDLE; - } -} -// Function to save received data to a file -/** - * @brief Save the received data to a file. - * @return true if the data was saved successfully, false otherwise. - * @param bytes_received The number of bytes received. - * @param line_buffer The buffer containing the received data. - * @note The data will be saved to a file in the STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/received_data.txt" directory. - */ -bool flipper_http_save_received_data(size_t bytes_received, const char line_buffer[]) { - const char* output_file_path = STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag - "/received_data.txt"; - - // Ensure the directory exists - char directory_path[128]; - snprintf( - directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag); - - Storage* _storage = NULL; - File* _file = NULL; - // Open the storage if not opened already - // Initialize storage and create the directory if it doesn't exist - _storage = furi_record_open(RECORD_STORAGE); - storage_common_mkdir(_storage, directory_path); // Create directory if it doesn't exist - _file = storage_file_alloc(_storage); - - // Open file for writing and append data line by line - if(!storage_file_open(_file, output_file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { - FURI_LOG_E(HTTP_TAG, "Failed to open output file for writing."); - storage_file_free(_file); - furi_record_close(RECORD_STORAGE); - return false; - } - - // Write each line received from the UART to the file - if(bytes_received > 0 && _file) { - storage_file_write(_file, line_buffer, bytes_received); - storage_file_write(_file, "\n", 1); // Add a newline after each line - } else { - FURI_LOG_E(HTTP_TAG, "No data received."); - return false; - } - - if(_file) { - storage_file_close(_file); - storage_file_free(_file); - _file = NULL; - } - if(_storage) { - furi_record_close(RECORD_STORAGE); - _storage = NULL; - } - - return true; -} -// Function to trim leading and trailing spaces and newlines from a constant string -char* trim(const char* str) { - const char* end; - char* trimmed_str; - size_t len; - - // Trim leading space - while(isspace((unsigned char)*str)) - str++; - - // All spaces? - if(*str == 0) return strdup(""); // Return an empty string if all spaces - - // Trim trailing space - end = str + strlen(str) - 1; - while(end > str && isspace((unsigned char)*end)) - end--; - - // Set length for the trimmed string - len = end - str + 1; - - // Allocate space for the trimmed string and null terminator - trimmed_str = (char*)malloc(len + 1); - if(trimmed_str == NULL) { - return NULL; // Handle memory allocation failure - } - - // Copy the trimmed part of the string into trimmed_str - strncpy(trimmed_str, str, len); - trimmed_str[len] = '\0'; // Null terminate the string - - return trimmed_str; -} - -#endif // FLIPPER_HTTP_H diff --git a/web_crawler/flipper_http.h b/flip_store/flipper_http/flipper_http.c similarity index 87% rename from web_crawler/flipper_http.h rename to flip_store/flipper_http/flipper_http.c index d9dfd002d..1c689aaa2 100644 --- a/web_crawler/flipper_http.h +++ b/flip_store/flipper_http/flipper_http.c @@ -1,137 +1,8 @@ -// flipper_http.h -#ifndef FLIPPER_HTTP_H -#define FLIPPER_HTTP_H - -#include -#include -#include -#include -#include - -// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext - -#define HTTP_TAG "WebCrawler" // change this to your app name -#define http_tag "web_crawler" // change this to your app id -#define UART_CH (FuriHalSerialIdUsart) // UART channel -#define TIMEOUT_DURATION_TICKS (3 * 1000) // 3 seconds -#define BAUDRATE (115200) // UART baudrate -#define RX_BUF_SIZE 2048 // UART RX buffer size -#define RX_LINE_BUFFER_SIZE 2048 // UART RX line buffer size (increase for large responses) -#define MAX_FILE_SHOW 5000 // Maximum data from file to show - -// Forward declaration for callback -typedef void (*FlipperHTTP_Callback)(const char* line, void* context); - -// Functions -bool flipper_http_init(FlipperHTTP_Callback callback, void* context); -void flipper_http_deinit(); -//--- -void flipper_http_rx_callback(const char* line, void* context); -bool flipper_http_send_data(const char* data); -//--- -bool flipper_http_connect_wifi(); -bool flipper_http_disconnect_wifi(); -bool flipper_http_ping(); -bool flipper_http_scan_wifi(); -bool flipper_http_save_wifi(const char* ssid, const char* password); -bool flipper_http_ip_wifi(); -bool flipper_http_ip_address(); -//--- -bool flipper_http_list_commands(); -bool flipper_http_led_on(); -bool flipper_http_led_off(); -bool flipper_http_parse_json(const char* key, const char* json_data); -bool flipper_http_parse_json_array(const char* key, int index, const char* json_data); -//--- -bool flipper_http_get_request(const char* url); -bool flipper_http_get_request_with_headers(const char* url, const char* headers); -bool flipper_http_post_request_with_headers( - const char* url, - const char* headers, - const char* payload); -bool flipper_http_put_request_with_headers( - const char* url, - const char* headers, - const char* payload); -bool flipper_http_delete_request_with_headers( - const char* url, - const char* headers, - const char* payload); -//--- -bool flipper_http_get_request_bytes(const char* url, const char* headers); -bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload); -// -bool flipper_http_append_to_file( - const void* data, - size_t data_size, - bool start_new_file, - char* file_path); - -FuriString* flipper_http_load_from_file(char* file_path); -static char* trim(const char* str); -// -bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)); - -// State variable to track the UART state -typedef enum { - INACTIVE, // Inactive state - IDLE, // Default state - RECEIVING, // Receiving data - SENDING, // Sending data - ISSUE, // Issue with connection -} SerialState; - -// Event Flags for UART Worker Thread -typedef enum { - WorkerEvtStop = (1 << 0), - WorkerEvtRxDone = (1 << 1), -} WorkerEvtFlags; - -// FlipperHTTP Structure -typedef struct { - FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication - FuriHalSerialHandle* serial_handle; // Serial handle for UART communication - FuriThread* rx_thread; // Worker thread for UART - FuriThreadId rx_thread_id; // Worker thread ID - FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines - void* callback_context; // Context for the callback - SerialState state; // State of the UART - - // variable to store the last received data from the UART - char* last_response; - char file_path[256]; // Path to save the received data - - // Timer-related members - FuriTimer* get_timeout_timer; // Timer for HTTP request timeout - - bool started_receiving_get; // Indicates if a GET request has started - bool just_started_get; // Indicates if GET data reception has just started - - bool started_receiving_post; // Indicates if a POST request has started - bool just_started_post; // Indicates if POST data reception has just started - - bool started_receiving_put; // Indicates if a PUT request has started - bool just_started_put; // Indicates if PUT data reception has just started - - bool started_receiving_delete; // Indicates if a DELETE request has started - bool just_started_delete; // Indicates if DELETE data reception has just started - - // Buffer to hold the raw bytes received from the UART - uint8_t* received_bytes; - size_t received_bytes_len; // Length of the received bytes - bool is_bytes_request; // Flag to indicate if the request is for bytes - bool save_bytes; // Flag to save the received data to a file - bool save_received_data; // Flag to save the received data to a file -} FlipperHTTP; - -static FlipperHTTP fhttp; -// Global static array for the line buffer -static char rx_line_buffer[RX_LINE_BUFFER_SIZE]; -#define FILE_BUFFER_SIZE 512 -static uint8_t file_buffer[FILE_BUFFER_SIZE]; - -// fhttp.last_response holds the last received data from the UART, which could be [GET/END], [POST/END], [PUT/END], [DELETE/END], etc - +#include +FlipperHTTP fhttp; +char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +uint8_t file_buffer[FILE_BUFFER_SIZE]; +size_t file_buffer_len = 0; // Function to append received data to file // make sure to initialize the file path before calling this function bool flipper_http_append_to_file( @@ -143,6 +14,15 @@ bool flipper_http_append_to_file( File* file = storage_file_alloc(storage); if(start_new_file) { + // Delete the file if it already exists + if(storage_file_exists(storage, file_path)) { + if(!storage_simply_remove_recursive(storage, file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to delete file: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + } // Open the file in write mode if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path); @@ -258,15 +138,16 @@ FuriString* flipper_http_load_from_file(char* file_path) { * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -static int32_t flipper_http_worker(void* context) { +int32_t flipper_http_worker(void* context) { UNUSED(context); size_t rx_line_pos = 0; - static size_t file_buffer_len = 0; while(1) { uint32_t events = furi_thread_flags_wait( WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever); - if(events & WorkerEvtStop) break; + if(events & WorkerEvtStop) { + break; + } if(events & WorkerEvtRxDone) { // Continuously read from the stream buffer until it's empty while(!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) { @@ -286,10 +167,14 @@ static int32_t flipper_http_worker(void* context) { // Write to file if buffer is full if(file_buffer_len >= FILE_BUFFER_SIZE) { if(!flipper_http_append_to_file( - file_buffer, file_buffer_len, false, fhttp.file_path)) { + file_buffer, + file_buffer_len, + fhttp.just_started_bytes, + fhttp.file_path)) { FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); } file_buffer_len = 0; + fhttp.just_started_bytes = false; } } @@ -302,8 +187,6 @@ static int32_t flipper_http_worker(void* context) { // Invoke the callback with the complete line fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context); - // save the received data - // Reset the line buffer position rx_line_pos = 0; } else { @@ -314,36 +197,6 @@ static int32_t flipper_http_worker(void* context) { } } - if(fhttp.save_bytes) { - // Write the remaining data to the file - if(file_buffer_len > 0) { - if(!flipper_http_append_to_file(file_buffer, file_buffer_len, false, fhttp.file_path)) { - FURI_LOG_E(HTTP_TAG, "Failed to append remaining data to file"); - } - } - } - - // remove [POST/END] and/or [GET/END] from the file - if(fhttp.save_bytes) { - char* end = NULL; - if((end = strstr(fhttp.file_path, "[POST/END]")) != NULL) { - *end = '\0'; - } else if((end = strstr(fhttp.file_path, "[GET/END]")) != NULL) { - *end = '\0'; - } - } - - // remove newline from the from the end of the file - if(fhttp.save_bytes) { - char* end = NULL; - if((end = strstr(fhttp.file_path, "\n")) != NULL) { - *end = '\0'; - } - } - - // Reset the file buffer length - file_buffer_len = 0; - return 0; } // Timer callback function @@ -376,7 +229,7 @@ void get_timeout_timer_callback(void* context) { * @param context The context to pass to the callback. * @note This function will handle received data asynchronously via the callback. */ -static void _flipper_http_rx_callback( +void _flipper_http_rx_callback( FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* context) { @@ -1123,7 +976,7 @@ void flipper_http_rx_callback(const char* line, void* context) { } // Uncomment below line to log the data received over UART - FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line); + // FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line); // Check if we've started receiving data from a GET request if(fhttp.started_receiving_get) { @@ -1138,8 +991,35 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.just_started_get = false; fhttp.state = IDLE; fhttp.save_bytes = false; - fhttp.is_bytes_request = false; fhttp.save_received_data = false; + + if(fhttp.is_bytes_request) { + // Search for the binary marker `[GET/END]` in the file buffer + const char marker[] = "[GET/END]"; + const size_t marker_len = sizeof(marker) - 1; // Exclude null terminator + + for(size_t i = 0; i <= file_buffer_len - marker_len; i++) { + // Check if the marker is found + if(memcmp(&file_buffer[i], marker, marker_len) == 0) { + // Remove the marker by shifting the remaining data left + size_t remaining_len = file_buffer_len - (i + marker_len); + memmove(&file_buffer[i], &file_buffer[i + marker_len], remaining_len); + file_buffer_len -= marker_len; + break; + } + } + + // If there is data left in the buffer, append it to the file + if(file_buffer_len > 0) { + if(!flipper_http_append_to_file( + file_buffer, file_buffer_len, false, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + } + file_buffer_len = 0; + } + } + + fhttp.is_bytes_request = false; return; } @@ -1173,8 +1053,35 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.just_started_post = false; fhttp.state = IDLE; fhttp.save_bytes = false; - fhttp.is_bytes_request = false; fhttp.save_received_data = false; + + if(fhttp.is_bytes_request) { + // Search for the binary marker `[POST/END]` in the file buffer + const char marker[] = "[POST/END]"; + const size_t marker_len = sizeof(marker) - 1; // Exclude null terminator + + for(size_t i = 0; i <= file_buffer_len - marker_len; i++) { + // Check if the marker is found + if(memcmp(&file_buffer[i], marker, marker_len) == 0) { + // Remove the marker by shifting the remaining data left + size_t remaining_len = file_buffer_len - (i + marker_len); + memmove(&file_buffer[i], &file_buffer[i + marker_len], remaining_len); + file_buffer_len -= marker_len; + break; + } + } + + // If there is data left in the buffer, append it to the file + if(file_buffer_len > 0) { + if(!flipper_http_append_to_file( + file_buffer, file_buffer_len, false, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + } + file_buffer_len = 0; + } + } + + fhttp.is_bytes_request = false; return; } @@ -1281,6 +1188,8 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.state = RECEIVING; // for GET request, save data only if it's a bytes request fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; + file_buffer_len = 0; return; } else if(strstr(line, "[POST/SUCCESS]") != NULL) { FURI_LOG_I(HTTP_TAG, "POST request succeeded."); @@ -1289,6 +1198,8 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.state = RECEIVING; // for POST request, save data only if it's a bytes request fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; + file_buffer_len = 0; return; } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { FURI_LOG_I(HTTP_TAG, "PUT request succeeded."); @@ -1388,5 +1299,3 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars } return true; } - -#endif // FLIPPER_HTTP_H diff --git a/flip_store/flipper_http/flipper_http.h b/flip_store/flipper_http/flipper_http.h new file mode 100644 index 000000000..5c13a2079 --- /dev/null +++ b/flip_store/flipper_http/flipper_http.h @@ -0,0 +1,364 @@ +// flipper_http.h +#ifndef FLIPPER_HTTP_H +#define FLIPPER_HTTP_H + +#include +#include +#include +#include +#include +#include + +// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext + +#define HTTP_TAG "FlipStore" // change this to your app name +#define http_tag "flip_store" // change this to your app id +#define UART_CH (momentum_settings.uart_esp_channel) // UART channel +#define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds +#define BAUDRATE (115200) // UART baudrate +#define RX_BUF_SIZE 2048 // UART RX buffer size +#define RX_LINE_BUFFER_SIZE 8192 // UART RX line buffer size (increase for large responses) +#define MAX_FILE_SHOW 8192 // Maximum data from file to show +#define FILE_BUFFER_SIZE 1024 // File buffer size + +// Forward declaration for callback +typedef void (*FlipperHTTP_Callback)(const char* line, void* context); + +// State variable to track the UART state +typedef enum { + INACTIVE, // Inactive state + IDLE, // Default state + RECEIVING, // Receiving data + SENDING, // Sending data + ISSUE, // Issue with connection +} SerialState; + +// Event Flags for UART Worker Thread +typedef enum { + WorkerEvtStop = (1 << 0), + WorkerEvtRxDone = (1 << 1), +} WorkerEvtFlags; + +// FlipperHTTP Structure +typedef struct { + FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication + FuriHalSerialHandle* serial_handle; // Serial handle for UART communication + FuriThread* rx_thread; // Worker thread for UART + FuriThreadId rx_thread_id; // Worker thread ID + FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines + void* callback_context; // Context for the callback + SerialState state; // State of the UART + + // variable to store the last received data from the UART + char* last_response; + char file_path[256]; // Path to save the received data + + // Timer-related members + FuriTimer* get_timeout_timer; // Timer for HTTP request timeout + + bool started_receiving_get; // Indicates if a GET request has started + bool just_started_get; // Indicates if GET data reception has just started + + bool started_receiving_post; // Indicates if a POST request has started + bool just_started_post; // Indicates if POST data reception has just started + + bool started_receiving_put; // Indicates if a PUT request has started + bool just_started_put; // Indicates if PUT data reception has just started + + bool started_receiving_delete; // Indicates if a DELETE request has started + bool just_started_delete; // Indicates if DELETE data reception has just started + + // Buffer to hold the raw bytes received from the UART + uint8_t* received_bytes; + size_t received_bytes_len; // Length of the received bytes + bool is_bytes_request; // Flag to indicate if the request is for bytes + bool save_bytes; // Flag to save the received data to a file + bool save_received_data; // Flag to save the received data to a file + + bool just_started_bytes; // Indicates if bytes data reception has just started +} FlipperHTTP; + +extern FlipperHTTP fhttp; +// Global static array for the line buffer +extern char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +extern uint8_t file_buffer[FILE_BUFFER_SIZE]; +extern size_t file_buffer_len; + +// fhttp.last_response holds the last received data from the UART + +// Function to append received data to file +// make sure to initialize the file path before calling this function +bool flipper_http_append_to_file( + const void* data, + size_t data_size, + bool start_new_file, + char* file_path); + +FuriString* flipper_http_load_from_file(char* file_path); + +// UART worker thread +/** + * @brief Worker thread to handle UART data asynchronously. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +// UART worker thread +int32_t flipper_http_worker(void* context); + +// Timer callback function +/** + * @brief Callback function for the GET timeout timer. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will be called when the GET request times out. + */ +void get_timeout_timer_callback(void* context); + +// UART RX Handler Callback (Interrupt Context) +/** + * @brief A private callback function to handle received data asynchronously. + * @return void + * @param handle The UART handle. + * @param event The event type. + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +void _flipper_http_rx_callback( + FuriHalSerialHandle* handle, + FuriHalSerialRxEvent event, + void* context); + +// UART initialization function +/** + * @brief Initialize UART. + * @return true if the UART was initialized successfully, false otherwise. + * @param callback The callback function to handle received data (ex. flipper_http_rx_callback). + * @param context The context to pass to the callback. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_init(FlipperHTTP_Callback callback, void* context); + +// Deinitialize UART +/** + * @brief Deinitialize UART. + * @return void + * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. + */ +void flipper_http_deinit(); + +// Function to send data over UART with newline termination +/** + * @brief Send data over UART with newline termination. + * @return true if the data was sent successfully, false otherwise. + * @param data The data to send over UART. + * @note The data will be sent over UART with a newline character appended. + */ +bool flipper_http_send_data(const char* data); + +// Function to send a PING request +/** + * @brief Send a PING request to check if the Wifi Dev Board is connected. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + * @note This is best used to check if the Wifi Dev Board is connected. + * @note The state will remain INACTIVE until a PONG is received. + */ +bool flipper_http_ping(); + +// Function to list available commands +/** + * @brief Send a command to list available commands. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_list_commands(); + +// Function to turn on the LED +/** + * @brief Allow the LED to display while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_on(); + +// Function to turn off the LED +/** + * @brief Disable the LED from displaying while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_off(); + +// Function to parse JSON data +/** + * @brief Parse JSON data. + * @return true if the JSON data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON data. + * @param json_data The JSON data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json(const char* key, const char* json_data); + +// Function to parse JSON array data +/** + * @brief Parse JSON array data. + * @return true if the JSON array data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON array data. + * @param index The index to parse from the JSON array data. + * @param json_data The JSON array data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json_array(const char* key, int index, const char* json_data); + +// Function to scan for WiFi networks +/** + * @brief Send a command to scan for WiFi networks. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_scan_wifi(); + +// Function to save WiFi settings (returns true if successful) +/** + * @brief Send a command to save WiFi settings. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_save_wifi(const char* ssid, const char* password); + +// Function to get IP address of WiFi Devboard +/** + * @brief Send a command to get the IP address of the WiFi Devboard + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_address(); + +// Function to get IP address of the connected WiFi network +/** + * @brief Send a command to get the IP address of the connected WiFi network. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_wifi(); + +// Function to disconnect from WiFi (returns true if successful) +/** + * @brief Send a command to disconnect from WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_disconnect_wifi(); + +// Function to connect to WiFi (returns true if successful) +/** + * @brief Send a command to connect to WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_connect_wifi(); + +// Function to send a GET request +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request(const char* url); + +// Function to send a GET request with headers +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_with_headers(const char* url, const char* headers); + +// Function to send a GET request with headers and return bytes +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_bytes(const char* url, const char* headers); + +// Function to send a POST request with headers +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param data The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to send a POST request with headers and return bytes +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param payload The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload); + +// Function to send a PUT request with headers +/** + * @brief Send a PUT request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the PUT request to. + * @param headers The headers to send with the PUT request. + * @param data The data to send with the PUT request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_put_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to send a DELETE request with headers +/** + * @brief Send a DELETE request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the DELETE request to. + * @param headers The headers to send with the DELETE request. + * @param data The data to send with the DELETE request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_delete_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to handle received data asynchronously +/** + * @brief Callback function to handle received data asynchronously. + * @return void + * @param line The received line. + * @param context The context passed to the callback. + * @note The received data will be handled asynchronously via the callback and handles the state of the UART. + */ +void flipper_http_rx_callback(const char* line, void* context); + +// Function to trim leading and trailing spaces and newlines from a constant string +char* trim(const char* str); +/** + * @brief Process requests and parse JSON data asynchronously + * @param http_request The function to send the request + * @param parse_json The function to parse the JSON + * @return true if successful, false otherwise + */ +bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)); + +#endif // FLIPPER_HTTP_H diff --git a/flip_store/jsmn.h b/flip_store/jsmn/jsmn.c similarity index 84% rename from flip_store/jsmn.h rename to flip_store/jsmn/jsmn.c index 26312c613..eb33b3cc7 100644 --- a/flip_store/jsmn.h +++ b/flip_store/jsmn/jsmn.c @@ -3,113 +3,20 @@ * * Copyright (c) 2010 Serge Zaitsev * - * 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. + * [License text continues...] */ -#ifndef JSMN_H -#define JSMN_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef JSMN_STATIC -#define JSMN_API static -#else -#define JSMN_API extern -#endif - -/** - * JSON type identifier. Basic types are: - * o Object - * o Array - * o String - * o Other primitive: number, boolean (true/false) or null - */ -typedef enum { - JSMN_UNDEFINED = 0, - JSMN_OBJECT = 1 << 0, - JSMN_ARRAY = 1 << 1, - JSMN_STRING = 1 << 2, - JSMN_PRIMITIVE = 1 << 3 -} jsmntype_t; - -enum jsmnerr { - /* Not enough tokens were provided */ - JSMN_ERROR_NOMEM = -1, - /* Invalid character inside JSON string */ - JSMN_ERROR_INVAL = -2, - /* The string is not a full JSON packet, more bytes expected */ - JSMN_ERROR_PART = -3 -}; - -/** - * JSON token description. - * type type (object, array, string etc.) - * start start position in JSON data string - * end end position in JSON data string - */ -typedef struct jsmntok { - jsmntype_t type; - int start; - int end; - int size; -#ifdef JSMN_PARENT_LINKS - int parent; -#endif -} jsmntok_t; - -/** - * JSON parser. Contains an array of token blocks available. Also stores - * the string being parsed now and current position in that string. - */ -typedef struct jsmn_parser { - unsigned int pos; /* offset in the JSON string */ - unsigned int toknext; /* next token to allocate */ - int toksuper; /* superior token node, e.g. parent object or array */ -} jsmn_parser; -/** - * Create JSON parser over an array of tokens - */ -JSMN_API void jsmn_init(jsmn_parser* parser); - -/** - * Run JSON parser. It parses a JSON data string into and array of tokens, each - * describing - * a single JSON object. - */ -JSMN_API int jsmn_parse( - jsmn_parser* parser, - const char* js, - const size_t len, - jsmntok_t* tokens, - const unsigned int num_tokens); +#include +#include +#include -#ifndef JSMN_HEADER /** - * Allocates a fresh unused token from the token pool. - */ + * Allocates a fresh unused token from the token pool. + */ static jsmntok_t* jsmn_alloc_token(jsmn_parser* parser, jsmntok_t* tokens, const size_t num_tokens) { jsmntok_t* tok; + if(parser->toknext >= num_tokens) { return NULL; } @@ -123,8 +30,8 @@ static jsmntok_t* } /** - * Fills token type and boundaries. - */ + * Fills token type and boundaries. + */ static void jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { token->type = type; @@ -134,8 +41,8 @@ static void } /** - * Fills next available token with JSON primitive. - */ + * Fills next available token with JSON primitive. + */ static int jsmn_parse_primitive( jsmn_parser* parser, const char* js, @@ -195,8 +102,8 @@ static int jsmn_parse_primitive( } /** - * Fills next token with JSON string. - */ + * Fills next token with JSON string. + */ static int jsmn_parse_string( jsmn_parser* parser, const char* js, @@ -272,9 +179,18 @@ static int jsmn_parse_string( } /** - * Parse JSON string and fill tokens. - */ -JSMN_API int jsmn_parse( + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser* parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +/** + * Parse JSON string and fill tokens. + */ +int jsmn_parse( jsmn_parser* parser, const char* js, const size_t len, @@ -464,34 +380,6 @@ JSMN_API int jsmn_parse( return count; } -/** - * Creates a new parser based over a given buffer with an array of tokens - * available. - */ -JSMN_API void jsmn_init(jsmn_parser* parser) { - parser->pos = 0; - parser->toknext = 0; - parser->toksuper = -1; -} - -#endif /* JSMN_HEADER */ - -#ifdef __cplusplus -} -#endif - -#endif /* JSMN_H */ - -#ifndef JB_JSMN_EDIT -#define JB_JSMN_EDIT -/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ - -#include -#include -#include -#include -#include - // Helper function to create a JSON object char* jsmn(const char* key, const char* value) { int length = strlen(key) + strlen(value) + 8; // Calculate required length @@ -512,7 +400,7 @@ int jsoneq(const char* json, jsmntok_t* tok, const char* s) { return -1; } -// return the value of the key in the JSON data +// Return the value of the key in the JSON data char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { // Parse the JSON feed if(json_data != NULL) { @@ -776,5 +664,3 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in free(array_str); return values; } - -#endif /* JB_JSMN_EDIT */ diff --git a/flip_store/jsmn/jsmn.h b/flip_store/jsmn/jsmn.h new file mode 100644 index 000000000..cd95a0e58 --- /dev/null +++ b/flip_store/jsmn/jsmn.h @@ -0,0 +1,131 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * 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: + * + * [License text continues...] + */ + +#ifndef JSMN_H +#define JSMN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser* parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing a single JSON object. + */ +JSMN_API int jsmn_parse( + jsmn_parser* parser, + const char* js, + const size_t len, + jsmntok_t* tokens, + const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/* Implementation has been moved to jsmn.c */ +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */ + +/* Custom Helper Functions */ +#ifndef JB_JSMN_EDIT +#define JB_JSMN_EDIT +/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ + +#include +#include +#include +#include +#include + +// Helper function to create a JSON object +char* jsmn(const char* key, const char* value); +// Helper function to compare JSON keys +int jsoneq(const char* json, jsmntok_t* tok, const char* s); + +// Return the value of the key in the JSON data +char* get_json_value(char* key, char* json_data, uint32_t max_tokens); + +// Revised get_json_array_value function +char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t max_tokens); + +// Revised get_json_array_values function with correct token skipping +char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, int* num_values); +#endif /* JB_JSMN_EDIT */ diff --git a/flip_trader/.DS_Store b/flip_trader/.DS_Store deleted file mode 100644 index cb3b5b143..000000000 Binary files a/flip_trader/.DS_Store and /dev/null differ diff --git a/flip_trader/CHANGELOG.md b/flip_trader/CHANGELOG.md index 7db40410f..d505d2d44 100644 --- a/flip_trader/CHANGELOG.md +++ b/flip_trader/CHANGELOG.md @@ -1,2 +1,11 @@ +## v1.2 +Updates from Derek Jamison: +- Improved progress display. +- Added connectivity check on startup. + +## v1.1 +- Improved memory allocation +- Added more assets + ## v1.0 - Initial Release \ No newline at end of file diff --git a/flip_trader/README.md b/flip_trader/README.md index 5b82a6a13..3d4b071a6 100644 --- a/flip_trader/README.md +++ b/flip_trader/README.md @@ -61,14 +61,14 @@ FlipTrader is an app for the Flipper Zero that uses WiFi to fetch the prices of FlipTrader automatically allocates necessary resources and initializes settings upon launch. If WiFi settings have been previously configured, they are loaded automatically for convenience. ## How to Use -1. **Flash the WiFi Devboard**: Follow the instructions to flash the WiFi Devboard with FlipperHTTP: https://github.com/jblanked/FlipperHTTP. +1. **Flash the WiFi Devboard**: Follow the instructions to flash the WiFi Devboard with FlipperHTTP:https://github.com/jblanked/FlipperHTTP 2. **Install the App**: Download FlipTrader from the App Store. 3. **Launch FlipTrader**: Open the app on your Flipper Zero. -4. **Explore the Features**: +4. Click **Settings** to manage your network configurations. +5. **Explore the Features**: - Browse the **Assets** section and select an asset to fetch its current price. - Visit **About** for app information and version history. - - Use **WiFi Settings** to manage your network configurations. ## Known Issues -1. **Asset Screen Delay**: Occasionally, the Asset Price screen may get stuck on "Loading Data" or take up to 30 seconds to display information. - - **Solution**: Restart your Flipper Zero if this occurs. \ No newline at end of file +1. **Asset Screen Delay**: Occasionally, the Asset Price screen may get stuck on "Loading Data". + - Update to version 1.2 (or higher) diff --git a/flip_trader/flip_trader_i.h b/flip_trader/alloc/flip_trader_alloc.c similarity index 87% rename from flip_trader/flip_trader_i.h rename to flip_trader/alloc/flip_trader_alloc.c index e34f3923f..adfa6c02d 100644 --- a/flip_trader/flip_trader_i.h +++ b/flip_trader/alloc/flip_trader_alloc.c @@ -1,8 +1,7 @@ -#ifndef FLIP_TRADER_I_H -#define FLIP_TRADER_I_H +#include "alloc/flip_trader_alloc.h" // Function to allocate resources for the FlipTraderApp -static FlipTraderApp* flip_trader_app_alloc() { +FlipTraderApp* flip_trader_app_alloc() { FlipTraderApp* app = (FlipTraderApp*)malloc(sizeof(FlipTraderApp)); Gui* gui = furi_record_open(RECORD_GUI); @@ -34,32 +33,47 @@ static FlipTraderApp* flip_trader_app_alloc() { return NULL; } + asset_names = asset_names_alloc(); + if(!asset_names) { + return NULL; + } + // Allocate ViewDispatcher if(!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) { return NULL; } - + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, flip_trader_custom_event_callback); // Main view if(!easy_flipper_set_view( - &app->view_main, - FlipTraderViewMain, - flip_trader_view_draw_callback, + &app->view_loader, + FlipTraderViewLoader, + flip_trader_loader_draw_callback, NULL, callback_to_assets_submenu, &app->view_dispatcher, app)) { return NULL; } + flip_trader_loader_init(app->view_loader); // Widget if(!easy_flipper_set_widget( - &app->widget, + &app->widget_about, FlipTraderViewAbout, - "FlipTrader v1.1\n-----\nUse WiFi to get the price of\nstocks and currency pairs.\n-----\nwww.github.com/jblanked", + "FlipTrader v1.2\n-----\nUse WiFi to get the price of\nstocks and currency pairs.\n-----\nwww.github.com/jblanked", callback_to_submenu, &app->view_dispatcher)) { return NULL; } + if(!easy_flipper_set_widget( + &app->widget_result, + FlipTraderViewWidgetResult, + "Error, try again.", + callback_to_assets_submenu, + &app->view_dispatcher)) { + return NULL; + } // Text Input if(!easy_flipper_set_uart_text_input( @@ -108,7 +122,7 @@ static FlipTraderApp* flip_trader_app_alloc() { if(!easy_flipper_set_submenu( &app->submenu_main, FlipTraderViewMainSubmenu, - "FlipTrader v1.1", + "FlipTrader v1.2", easy_flipper_callback_exit_app, &app->view_dispatcher)) { return NULL; @@ -128,7 +142,7 @@ static FlipTraderApp* flip_trader_app_alloc() { submenu_add_item( app->submenu_main, "WiFi", FlipTraderSubmenuIndexSettings, callback_submenu_choices, app); // add the assets - for(uint32_t i = 0; i < sizeof(asset_names) / sizeof(asset_names[0]); i++) { + for(uint32_t i = 0; i < ASSET_COUNT; i++) { submenu_add_item( app->submenu_assets, asset_names[i], @@ -173,5 +187,3 @@ static FlipTraderApp* flip_trader_app_alloc() { return app; } - -#endif // FLIP_TRADER_I_H diff --git a/flip_trader/alloc/flip_trader_alloc.h b/flip_trader/alloc/flip_trader_alloc.h new file mode 100644 index 000000000..2108b6319 --- /dev/null +++ b/flip_trader/alloc/flip_trader_alloc.h @@ -0,0 +1,10 @@ +#ifndef FLIP_TRADER_I_H +#define FLIP_TRADER_I_H + +#include +#include + +// Function to allocate resources for the FlipTraderApp +FlipTraderApp* flip_trader_app_alloc(); + +#endif // FLIP_TRADER_I_H diff --git a/flip_trader/app.c b/flip_trader/app.c index d46ceb19e..ca631933d 100644 --- a/flip_trader/app.c +++ b/flip_trader/app.c @@ -1,8 +1,5 @@ -#include "flip_trader_e.h" -#include "flip_trader_storage.h" -#include "flip_trader_callback.h" -#include "flip_trader_i.h" -#include "flip_trader_free.h" +#include +#include // Entry point for the FlipTrader application int32_t flip_trader_app(void* p) { @@ -10,8 +7,8 @@ int32_t flip_trader_app(void* p) { UNUSED(p); // Initialize the FlipTrader application - FlipTraderApp* app = flip_trader_app_alloc(); - if(!app) { + app_instance = flip_trader_app_alloc(); + if(!app_instance) { FURI_LOG_E(TAG, "Failed to allocate FlipTraderApp"); return -1; } @@ -21,11 +18,38 @@ int32_t flip_trader_app(void* p) { return -1; } + if(app_instance->uart_text_input_buffer_ssid != NULL && + app_instance->uart_text_input_buffer_password != NULL) { + // Try to wait for pong response. + uint8_t counter = 10; + while(fhttp.state == INACTIVE && --counter > 0) { + FURI_LOG_D(TAG, "Waiting for PONG"); + furi_delay_ms(100); + } + + if(counter == 0) { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header( + message, "[FlipperHTTP Error]", 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + message, + "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.", + 0, + 63, + AlignLeft, + AlignBottom); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); + } + } + // Run the view dispatcher - view_dispatcher_run(app->view_dispatcher); + view_dispatcher_run(app_instance->view_dispatcher); // Free the resources used by the FlipTrader application - flip_trader_app_free(app); + flip_trader_app_free(app_instance); // Return 0 to indicate success return 0; diff --git a/flip_trader/application.fam b/flip_trader/application.fam index ab3bd0e2f..17d80e239 100644 --- a/flip_trader/application.fam +++ b/flip_trader/application.fam @@ -10,5 +10,5 @@ App( fap_description="Use WiFi to get the price of stocks and currency pairs on your Flipper Zero.", fap_author="JBlanked", fap_weburl="https://github.com/jblanked/FlipTrader", - fap_version="1.1", + fap_version="1.2", ) diff --git a/flip_trader/assets/01-main.png b/flip_trader/assets/01-main.png index 2449a105f..ac3e8fd9b 100644 Binary files a/flip_trader/assets/01-main.png and b/flip_trader/assets/01-main.png differ diff --git a/flip_trader/callback/flip_trader_callback.c b/flip_trader/callback/flip_trader_callback.c new file mode 100644 index 000000000..5b105d18c --- /dev/null +++ b/flip_trader/callback/flip_trader_callback.c @@ -0,0 +1,691 @@ +#include + +// Below added by Derek Jamison +// FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port. +#ifdef DEVELOPMENT +#define FURI_LOG_DEV(tag, format, ...) \ + furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__) +#define DEV_CRASH() furi_crash() +#else +#define FURI_LOG_DEV(tag, format, ...) +#define DEV_CRASH() +#endif + +// hold the price of the asset +char asset_price[64]; +bool sent_get_request = false; +bool get_request_success = false; +bool request_processed = false; + +static void flip_trader_request_error_draw(Canvas* canvas) { + if(canvas == NULL) { + FURI_LOG_E(TAG, "flip_trader_request_error_draw - canvas is NULL"); + DEV_CRASH(); + return; + } + if(fhttp.last_response != NULL) { + if(strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != + NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); + canvas_draw_str(canvas, 0, 22, "Failed to reconnect."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } else if(strstr(fhttp.last_response, "[PONG]") != NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[STATUS]Connecting to AP..."); + } else { + canvas_clear(canvas); + FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response); + canvas_draw_str(canvas, 0, 10, "[ERROR] Unusual error..."); + canvas_draw_str(canvas, 0, 60, "Press BACK and retry."); + } + } else { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Failed to receive data."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } +} + +static bool send_price_request(AssetLoaderModel* model) { + UNUSED(model); + if(fhttp.state == INACTIVE) { + return false; + } + if(!sent_get_request && fhttp.state == IDLE) { + sent_get_request = true; + char url[128]; + char* headers = jsmn("Content-Type", "application/json"); + snprintf( + fhttp.file_path, + sizeof(fhttp.file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_trader/price.txt"); + + fhttp.save_received_data = true; + snprintf( + url, + 128, + "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=%s&apikey=2X90WLEFMP43OJKE", + asset_names[asset_index]); + get_request_success = flipper_http_get_request_with_headers(url, headers); + free(headers); + if(!get_request_success) { + FURI_LOG_E(TAG, "Failed to send GET request"); + fhttp.state = ISSUE; + return false; + } + fhttp.state = RECEIVING; + } + return true; +} + +static char* process_asset_price(AssetLoaderModel* model) { + UNUSED(model); + if(!request_processed) { + // load the received data from the saved file + FuriString* price_data = flipper_http_load_from_file(fhttp.file_path); + if(price_data == NULL) { + FURI_LOG_E(TAG, "Failed to load received data from file."); + fhttp.state = ISSUE; + return NULL; + } + char* data_cstr = (char*)furi_string_get_cstr(price_data); + if(data_cstr == NULL) { + FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); + furi_string_free(price_data); + fhttp.state = ISSUE; + return NULL; + } + request_processed = true; + char* global_quote = get_json_value("Global Quote", data_cstr, MAX_TOKENS); + if(global_quote == NULL) { + FURI_LOG_E(TAG, "Failed to get Global Quote"); + fhttp.state = ISSUE; + furi_string_free(price_data); + free(global_quote); + free(data_cstr); + return NULL; + } + char* price = get_json_value("05. price", global_quote, MAX_TOKENS); + if(price == NULL) { + FURI_LOG_E(TAG, "Failed to get price"); + fhttp.state = ISSUE; + furi_string_free(price_data); + free(global_quote); + free(price); + free(data_cstr); + return NULL; + } + // store the price "Asset: $price" + snprintf(asset_price, 64, "%s: $%s", asset_names[asset_index], price); + + fhttp.state = IDLE; + furi_string_free(price_data); + free(global_quote); + free(price); + free(data_cstr); + } + return asset_price; +} + +static void flip_trader_asset_switch_to_view(FlipTraderApp* app) { + flip_trader_generic_switch_to_view( + app, + "Fetching..", + send_price_request, + process_asset_price, + 1, + callback_to_assets_submenu, + FlipTraderViewLoader); +} + +void callback_submenu_choices(void* context, uint32_t index) { + FlipTraderApp* app = (FlipTraderApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipTraderApp is NULL"); + return; + } + switch(index) { + // view the assets submenu + case FlipTradeSubmenuIndexAssets: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewAssetsSubmenu); + break; + // view the about screen + case FlipTraderSubmenuIndexAbout: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewAbout); + break; + // view the wifi settings screen + case FlipTraderSubmenuIndexSettings: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewWiFiSettings); + break; + default: + // handle FlipTraderSubmenuIndexAssetStartIndex + index + if(index >= FlipTraderSubmenuIndexAssetStartIndex) { + asset_index = index - FlipTraderSubmenuIndexAssetStartIndex; + flip_trader_asset_switch_to_view(app); + } else { + FURI_LOG_E(TAG, "Unknown submenu index"); + } + break; + } +} + +void text_updated_ssid(void* context) { + FlipTraderApp* app = (FlipTraderApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipTraderApp is NULL"); + return; + } + + // store the entered text + strncpy( + app->uart_text_input_buffer_ssid, + app->uart_text_input_temp_buffer_ssid, + app->uart_text_input_buffer_size_ssid); + + // Ensure null-termination + app->uart_text_input_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; + + // update the variable item text + if(app->variable_item_ssid) { + variable_item_set_current_value_text( + app->variable_item_ssid, app->uart_text_input_buffer_ssid); + } + + // save settings + save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); + + // save wifi settings to devboard + if(strlen(app->uart_text_input_buffer_ssid) > 0 && + strlen(app->uart_text_input_buffer_password) > 0) { + if(!flipper_http_save_wifi( + app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { + FURI_LOG_E(TAG, "Failed to save wifi settings"); + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewWiFiSettings); +} + +void text_updated_password(void* context) { + FlipTraderApp* app = (FlipTraderApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipTraderApp is NULL"); + return; + } + + // store the entered text + strncpy( + app->uart_text_input_buffer_password, + app->uart_text_input_temp_buffer_password, + app->uart_text_input_buffer_size_password); + + // Ensure null-termination + app->uart_text_input_buffer_password[app->uart_text_input_buffer_size_password - 1] = '\0'; + + // update the variable item text + if(app->variable_item_password) { + variable_item_set_current_value_text( + app->variable_item_password, app->uart_text_input_buffer_password); + } + + // save settings + save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); + + // save wifi settings to devboard + if(strlen(app->uart_text_input_buffer_ssid) > 0 && + strlen(app->uart_text_input_buffer_password) > 0) { + if(!flipper_http_save_wifi( + app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { + FURI_LOG_E(TAG, "Failed to save wifi settings"); + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewWiFiSettings); +} + +uint32_t callback_to_submenu(void* context) { + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + sent_get_request = false; + get_request_success = false; + request_processed = false; + asset_index = 0; + return FlipTraderViewMainSubmenu; +} + +uint32_t callback_to_wifi_settings(void* context) { + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + return FlipTraderViewWiFiSettings; +} + +uint32_t callback_to_assets_submenu(void* context) { + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + sent_get_request = false; + get_request_success = false; + request_processed = false; + asset_index = 0; + return FlipTraderViewAssetsSubmenu; +} + +void settings_item_selected(void* context, uint32_t index) { + FlipTraderApp* app = (FlipTraderApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipTraderApp is NULL"); + return; + } + switch(index) { + case 0: // Input SSID + view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewTextInputSSID); + break; + case 1: // Input Password + view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewTextInputPassword); + break; + default: + FURI_LOG_E(TAG, "Unknown configuration item index"); + break; + } +} + +static void flip_trader_widget_set_text(char* message, Widget** widget) { + if(widget == NULL) { + FURI_LOG_E(TAG, "flip_trader_set_widget_text - widget is NULL"); + DEV_CRASH(); + return; + } + if(message == NULL) { + FURI_LOG_E(TAG, "flip_trader_set_widget_text - message is NULL"); + DEV_CRASH(); + return; + } + widget_reset(*widget); + + uint32_t message_length = strlen(message); // Length of the message + uint32_t i = 0; // Index tracker + uint32_t formatted_index = 0; // Tracker for where we are in the formatted message + char* formatted_message; // Buffer to hold the final formatted message + if(!easy_flipper_set_buffer(&formatted_message, message_length * 2 + 1)) { + return; + } + + while(i < message_length) { + // TODO: Use canvas_glyph_width to calculate the maximum characters for the line + uint32_t max_line_length = 29; // Maximum characters per line + uint32_t remaining_length = message_length - i; // Remaining characters + uint32_t line_length = (remaining_length < max_line_length) ? remaining_length : + max_line_length; + + // Temporary buffer to hold the current line + char line[30]; + strncpy(line, message + i, line_length); + line[line_length] = '\0'; + + // Check if the line ends in the middle of a word and adjust accordingly + if(line_length == 29 && message[i + line_length] != '\0' && + message[i + line_length] != ' ') { + // Find the last space within the 30-character segment + char* last_space = strrchr(line, ' '); + if(last_space != NULL) { + // Adjust the line length to avoid cutting the word + line_length = last_space - line; + line[line_length] = '\0'; // Null-terminate at the space + } + } + + // Manually copy the fixed line into the formatted_message buffer + for(uint32_t j = 0; j < line_length; j++) { + formatted_message[formatted_index++] = line[j]; + } + + // Add a newline character for line spacing + formatted_message[formatted_index++] = '\n'; + + // Move i forward to the start of the next word + i += line_length; + + // Skip spaces at the beginning of the next line + while(message[i] == ' ') { + i++; + } + } + + // Add the formatted message to the widget + widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message); +} + +void flip_trader_loader_draw_callback(Canvas* canvas, void* model) { + if(!canvas || !model) { + FURI_LOG_E(TAG, "flip_trader_loader_draw_callback - canvas or model is NULL"); + return; + } + + SerialState http_state = fhttp.state; + AssetLoaderModel* asset_loader_model = (AssetLoaderModel*)model; + AssetState asset_state = asset_loader_model->asset_state; + char* title = asset_loader_model->title; + + canvas_set_font(canvas, FontSecondary); + + if(http_state == INACTIVE) { + canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); + canvas_draw_str(canvas, 0, 17, "Please connect to the board."); + canvas_draw_str(canvas, 0, 32, "If your board is connected,"); + canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); + canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); + canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); + return; + } + + if(asset_state == AssetStateError || asset_state == AssetStateParseError) { + flip_trader_request_error_draw(canvas); + return; + } + + canvas_draw_str(canvas, 0, 7, title); + canvas_draw_str(canvas, 0, 17, "Loading..."); + + if(asset_state == AssetStateInitial) { + return; + } + + if(http_state == SENDING) { + canvas_draw_str(canvas, 0, 27, "Sending..."); + return; + } + + if(http_state == RECEIVING || asset_state == AssetStateRequested) { + canvas_draw_str(canvas, 0, 27, "Receiving..."); + return; + } + + if(http_state == IDLE && asset_state == AssetStateReceived) { + canvas_draw_str(canvas, 0, 27, "Processing..."); + return; + } + + if(http_state == IDLE && asset_state == AssetStateParsed) { + canvas_draw_str(canvas, 0, 27, "Processed..."); + return; + } +} + +static void flip_trader_loader_process_callback(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_trader_loader_process_callback - context is NULL"); + DEV_CRASH(); + return; + } + + FlipTraderApp* app = (FlipTraderApp*)context; + View* view = app->view_loader; + + AssetState current_asset_state; + with_view_model( + view, AssetLoaderModel * model, { current_asset_state = model->asset_state; }, false); + + if(current_asset_state == AssetStateInitial) { + with_view_model( + view, + AssetLoaderModel * model, + { + model->asset_state = AssetStateRequested; + AssetLoaderFetch fetch = model->fetcher; + if(fetch == NULL) { + FURI_LOG_E(TAG, "Model doesn't have Fetch function assigned."); + model->asset_state = AssetStateError; + return; + } + + // Clear any previous responses + strncpy(fhttp.last_response, "", 1); + bool request_status = fetch(model); + if(!request_status) { + model->asset_state = AssetStateError; + } + }, + true); + } else if(current_asset_state == AssetStateRequested || current_asset_state == AssetStateError) { + if(fhttp.state == IDLE && fhttp.last_response != NULL) { + if(strstr(fhttp.last_response, "[PONG]") != NULL) { + FURI_LOG_DEV(TAG, "PONG received."); + } else if(strncmp(fhttp.last_response, "[SUCCESS]", 9) == 0) { + FURI_LOG_DEV( + TAG, + "SUCCESS received. %s", + fhttp.last_response ? fhttp.last_response : "NULL"); + } else if(strncmp(fhttp.last_response, "[ERROR]", 9) == 0) { + FURI_LOG_DEV( + TAG, "ERROR received. %s", fhttp.last_response ? fhttp.last_response : "NULL"); + } else if(strlen(fhttp.last_response) == 0) { + // Still waiting on response + } else { + with_view_model( + view, + AssetLoaderModel * model, + { model->asset_state = AssetStateReceived; }, + true); + } + } else if(fhttp.state == SENDING || fhttp.state == RECEIVING) { + // continue waiting + } else if(fhttp.state == INACTIVE) { + // inactive. try again + } else if(fhttp.state == ISSUE) { + with_view_model( + view, AssetLoaderModel * model, { model->asset_state = AssetStateError; }, true); + } else { + FURI_LOG_DEV( + TAG, + "Unexpected state: %d lastresp: %s", + fhttp.state, + fhttp.last_response ? fhttp.last_response : "NULL"); + DEV_CRASH(); + } + } else if(current_asset_state == AssetStateReceived) { + with_view_model( + view, + AssetLoaderModel * model, + { + char* asset_text; + if(model->parser == NULL) { + asset_text = NULL; + FURI_LOG_DEV(TAG, "Parser is NULL"); + DEV_CRASH(); + } else { + asset_text = model->parser(model); + } + FURI_LOG_DEV( + TAG, + "Parsed asset: %s\r\ntext: %s", + fhttp.last_response ? fhttp.last_response : "NULL", + asset_text ? asset_text : "NULL"); + model->asset_text = asset_text; + if(asset_text == NULL) { + model->asset_state = AssetStateParseError; + } else { + model->asset_state = AssetStateParsed; + } + }, + true); + } else if(current_asset_state == AssetStateParsed) { + with_view_model( + view, + AssetLoaderModel * model, + { + if(++model->request_index < model->request_count) { + model->asset_state = AssetStateInitial; + } else { + flip_trader_widget_set_text( + model->asset_text != NULL ? model->asset_text : "Empty result", + &app_instance->widget_result); + if(model->asset_text != NULL) { + free(model->asset_text); + model->asset_text = NULL; + } + view_set_previous_callback( + widget_get_view(app_instance->widget_result), model->back_callback); + view_dispatcher_switch_to_view( + app_instance->view_dispatcher, FlipTraderViewWidgetResult); + } + }, + true); + } +} + +static void flip_trader_loader_timer_callback(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_trader_loader_timer_callback - context is NULL"); + DEV_CRASH(); + return; + } + FlipTraderApp* app = (FlipTraderApp*)context; + view_dispatcher_send_custom_event(app->view_dispatcher, FlipTraderCustomEventProcess); +} + +static void flip_trader_loader_on_enter(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_trader_loader_on_enter - context is NULL"); + DEV_CRASH(); + return; + } + FlipTraderApp* app = (FlipTraderApp*)context; + View* view = app->view_loader; + with_view_model( + view, + AssetLoaderModel * model, + { + view_set_previous_callback(view, model->back_callback); + if(model->timer == NULL) { + model->timer = furi_timer_alloc( + flip_trader_loader_timer_callback, FuriTimerTypePeriodic, app); + } + furi_timer_start(model->timer, 250); + }, + true); +} + +static void flip_trader_loader_on_exit(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_trader_loader_on_exit - context is NULL"); + DEV_CRASH(); + return; + } + FlipTraderApp* app = (FlipTraderApp*)context; + View* view = app->view_loader; + with_view_model( + view, + AssetLoaderModel * model, + { + if(model->timer) { + furi_timer_stop(model->timer); + } + }, + false); +} + +void flip_trader_loader_init(View* view) { + if(view == NULL) { + FURI_LOG_E(TAG, "flip_trader_loader_init - view is NULL"); + DEV_CRASH(); + return; + } + view_allocate_model(view, ViewModelTypeLocking, sizeof(AssetLoaderModel)); + view_set_enter_callback(view, flip_trader_loader_on_enter); + view_set_exit_callback(view, flip_trader_loader_on_exit); +} + +void flip_trader_loader_free_model(View* view) { + if(view == NULL) { + FURI_LOG_E(TAG, "flip_trader_loader_free_model - view is NULL"); + DEV_CRASH(); + return; + } + with_view_model( + view, + AssetLoaderModel * model, + { + if(model->timer) { + furi_timer_free(model->timer); + model->timer = NULL; + } + if(model->parser_context) { + free(model->parser_context); + model->parser_context = NULL; + } + }, + false); +} + +bool flip_trader_custom_event_callback(void* context, uint32_t index) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_trader_custom_event_callback - context is NULL"); + DEV_CRASH(); + return false; + } + + switch(index) { + case FlipTraderCustomEventProcess: + flip_trader_loader_process_callback(context); + return true; + default: + FURI_LOG_DEV(TAG, "flip_trader_custom_event_callback. Unknown index: %ld", index); + return false; + } +} + +void flip_trader_generic_switch_to_view( + FlipTraderApp* app, + char* title, + AssetLoaderFetch fetcher, + AssetLoaderParser parser, + size_t request_count, + ViewNavigationCallback back, + uint32_t view_id) { + if(app == NULL) { + FURI_LOG_E(TAG, "flip_trader_generic_switch_to_view - app is NULL"); + DEV_CRASH(); + return; + } + + View* view = app->view_loader; + if(view == NULL) { + FURI_LOG_E(TAG, "flip_trader_generic_switch_to_view - view is NULL"); + DEV_CRASH(); + return; + } + + with_view_model( + view, + AssetLoaderModel * model, + { + model->title = title; + model->fetcher = fetcher; + model->parser = parser; + model->request_index = 0; + model->request_count = request_count; + model->back_callback = back; + model->asset_state = AssetStateInitial; + model->asset_text = NULL; + }, + true); + + view_dispatcher_switch_to_view(app->view_dispatcher, view_id); +} diff --git a/flip_trader/callback/flip_trader_callback.h b/flip_trader/callback/flip_trader_callback.h new file mode 100644 index 000000000..b69ecdd94 --- /dev/null +++ b/flip_trader/callback/flip_trader_callback.h @@ -0,0 +1,73 @@ +#ifndef FLIP_TRADER_CALLBACK_H +#define FLIP_TRADER_CALLBACK_H + +#define MAX_TOKENS 32 // Adjust based on expected JSON size (25) +#include +#include + +// hold the price of the asset +extern char asset_price[64]; +extern bool sent_get_request; +extern bool get_request_success; +extern bool request_processed; + +void callback_submenu_choices(void* context, uint32_t index); +void text_updated_ssid(void* context); + +void text_updated_password(void* context); + +uint32_t callback_to_submenu(void* context); + +uint32_t callback_to_wifi_settings(void* context); +uint32_t callback_to_assets_submenu(void* context); +void settings_item_selected(void* context, uint32_t index); + +// Add edits by Derek Jamison +typedef enum AssetState AssetState; +enum AssetState { + AssetStateInitial, + AssetStateRequested, + AssetStateReceived, + AssetStateParsed, + AssetStateParseError, + AssetStateError, +}; + +typedef enum FlipTraderCustomEvent FlipTraderCustomEvent; +enum FlipTraderCustomEvent { + FlipTraderCustomEventProcess, +}; + +typedef struct AssetLoaderModel AssetLoaderModel; +typedef bool (*AssetLoaderFetch)(AssetLoaderModel* model); +typedef char* (*AssetLoaderParser)(AssetLoaderModel* model); +struct AssetLoaderModel { + char* title; + char* asset_text; + AssetState asset_state; + AssetLoaderFetch fetcher; + AssetLoaderParser parser; + void* parser_context; + size_t request_index; + size_t request_count; + ViewNavigationCallback back_callback; + FuriTimer* timer; +}; + +void flip_trader_generic_switch_to_view( + FlipTraderApp* app, + char* title, + AssetLoaderFetch fetcher, + AssetLoaderParser parser, + size_t request_count, + ViewNavigationCallback back, + uint32_t view_id); + +void flip_trader_loader_draw_callback(Canvas* canvas, void* model); + +void flip_trader_loader_init(View* view); + +void flip_trader_loader_free_model(View* view); + +bool flip_trader_custom_event_callback(void* context, uint32_t index); +#endif // FLIP_TRADER_CALLBACK_H diff --git a/flip_library/easy_flipper.h b/flip_trader/easy_flipper/easy_flipper.c similarity index 96% rename from flip_library/easy_flipper.h rename to flip_trader/easy_flipper/easy_flipper.c index e8f0ad796..8b98e1a1b 100644 --- a/flip_library/easy_flipper.h +++ b/flip_trader/easy_flipper/easy_flipper.c @@ -1,24 +1,4 @@ -#ifndef EASY_FLIPPER_H -#define EASY_FLIPPER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define EASY_TAG "EasyFlipper" +#include /** * @brief Navigation callback for exiting the application @@ -530,5 +510,3 @@ bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer furi_string_set_str(*furi_string, buffer); return true; } - -#endif // EASY_FLIPPER_H diff --git a/flip_trader/easy_flipper/easy_flipper.h b/flip_trader/easy_flipper/easy_flipper.h new file mode 100644 index 000000000..1d6dbe430 --- /dev/null +++ b/flip_trader/easy_flipper/easy_flipper.h @@ -0,0 +1,261 @@ +#ifndef EASY_FLIPPER_H +#define EASY_FLIPPER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EASY_TAG "EasyFlipper" + +/** + * @brief Navigation callback for exiting the application + * @param context The context - unused + * @return next view id (VIEW_NONE to exit the app) + */ +uint32_t easy_flipper_callback_exit_app(void* context); +/** + * @brief Initialize a buffer + * @param buffer The buffer to initialize + * @param buffer_size The size of the buffer + * @return true if successful, false otherwise + */ +bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size); +/** + * @brief Initialize a View object + * @param view The View object to initialize + * @param view_id The ID/Index of the view + * @param draw_callback The draw callback function (set to NULL if not needed) + * @param input_callback The input callback function (set to NULL if not needed) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view( + View** view, + int32_t view_id, + void draw_callback(Canvas*, void*), + bool input_callback(InputEvent*, void*), + uint32_t (*previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a ViewDispatcher object + * @param view_dispatcher The ViewDispatcher object to initialize + * @param gui The GUI object + * @param context The context to pass to the event callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui, void* context); + +/** + * @brief Initialize a Submenu object + * @note This does not set the items in the submenu + * @param submenu The Submenu object to initialize + * @param view_id The ID/Index of the view + * @param title The title/header of the submenu + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_submenu( + Submenu** submenu, + int32_t view_id, + char* title, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a Menu object + * @note This does not set the items in the menu + * @param menu The Menu object to initialize + * @param view_id The ID/Index of the view + * @param item_callback The item callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_menu( + Menu** menu, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a Widget object + * @param widget The Widget object to initialize + * @param view_id The ID/Index of the view + * @param text The text to display in the widget + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_widget( + Widget** widget, + int32_t view_id, + char* text, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a VariableItemList object + * @note This does not set the items in the VariableItemList + * @param variable_item_list The VariableItemList object to initialize + * @param view_id The ID/Index of the view + * @param enter_callback The enter callback function (can be set to NULL) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the enter callback (usually the app) + * @return true if successful, false otherwise + */ +bool easy_flipper_set_variable_item_list( + VariableItemList** variable_item_list, + int32_t view_id, + void (*enter_callback)(void*, uint32_t), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a TextInput object + * @param text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_text_input( + TextInput** text_input, + int32_t view_id, + char* header_text, + char* text_input_temp_buffer, + uint32_t text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a TextInput object with extra symbols + * @param uart_text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_uart_text_input( + TextInput** uart_text_input, + int32_t view_id, + char* header_text, + char* uart_text_input_temp_buffer, + uint32_t uart_text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a DialogEx object + * @param dialog_ex The DialogEx object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param left_button_text The text of the left button + * @param right_button_text The text of the right button + * @param center_button_text The text of the center button + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_dialog_ex( + DialogEx** dialog_ex, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + char* left_button_text, + char* right_button_text, + char* center_button_text, + void (*result_callback)(DialogExResult, void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a Popup object + * @param popup The Popup object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_popup( + Popup** popup, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a Loading object + * @param loading The Loading object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_loading( + Loading** loading, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Set a char butter to a FuriString + * @param furi_string The FuriString object + * @param buffer The buffer to copy the string to + * @return true if successful, false otherwise + */ +bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer); + +#endif diff --git a/flip_trader/flip_trader_storage.h b/flip_trader/flip_storage/flip_trader_storage.c similarity index 88% rename from flip_trader/flip_trader_storage.h rename to flip_trader/flip_storage/flip_trader_storage.c index dd705756f..ce863f84e 100644 --- a/flip_trader/flip_trader_storage.h +++ b/flip_trader/flip_storage/flip_trader_storage.c @@ -1,12 +1,6 @@ -#ifndef FLIP_TRADER_STORAGE_H -#define FLIP_TRADER_STORAGE_H +#include "flip_storage/flip_trader_storage.h" -#include -#include - -#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_trader/settings.bin" - -static void save_settings(const char* ssid, const char* password) { +void save_settings(const char* ssid, const char* password) { // Create the directory for saving settings char directory_path[256]; snprintf( @@ -44,7 +38,7 @@ static void save_settings(const char* ssid, const char* password) { furi_record_close(RECORD_STORAGE); } -static bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size) { +bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size) { Storage* storage = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(storage); @@ -86,5 +80,3 @@ static bool load_settings(char* ssid, size_t ssid_size, char* password, size_t p return true; } - -#endif // FLIP_TRADER_STORAGE_H diff --git a/flip_trader/flip_storage/flip_trader_storage.h b/flip_trader/flip_storage/flip_trader_storage.h new file mode 100644 index 000000000..b2edac06a --- /dev/null +++ b/flip_trader/flip_storage/flip_trader_storage.h @@ -0,0 +1,14 @@ +#ifndef FLIP_TRADER_STORAGE_H +#define FLIP_TRADER_STORAGE_H + +#include +#include +#include + +#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_trader/settings.bin" + +void save_settings(const char* ssid, const char* password); + +bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size); + +#endif // FLIP_TRADER_STORAGE_H diff --git a/flip_trader/flip_trader.c b/flip_trader/flip_trader.c new file mode 100644 index 000000000..cc078af04 --- /dev/null +++ b/flip_trader/flip_trader.c @@ -0,0 +1,135 @@ +#include +void flip_trader_loader_free_model(View* view); + +void asset_names_free(char** names) { + if(names) { + for(int i = 0; i < ASSET_COUNT; i++) { + free(names[i]); + } + free(names); + } +} + +char** asset_names_alloc() { + // Allocate memory for an array of 42 string pointers + char** names = malloc(ASSET_COUNT * sizeof(char*)); + if(!names) { + FURI_LOG_E(TAG, "Failed to allocate memory for asset names"); + return NULL; + } + + // Assign each asset name to the array + names[0] = strdup("ETHUSD"); + names[1] = strdup("BTCUSD"); + names[2] = strdup("AAPL"); + names[3] = strdup("AMZN"); + names[4] = strdup("GOOGL"); + names[5] = strdup("MSFT"); + names[6] = strdup("TSLA"); + names[7] = strdup("NFLX"); + names[8] = strdup("META"); + names[9] = strdup("NVDA"); + names[10] = strdup("AMD"); + names[11] = strdup("INTC"); + names[12] = strdup("PLTR"); + names[13] = strdup("EURUSD"); + names[14] = strdup("GBPUSD"); + names[15] = strdup("AUDUSD"); + names[16] = strdup("NZDUSD"); + names[17] = strdup("XAUUSD"); + names[18] = strdup("USDJPY"); + names[19] = strdup("USDCHF"); + names[20] = strdup("USDCAD"); + names[21] = strdup("EURJPY"); + names[22] = strdup("EURGBP"); + names[23] = strdup("EURCHF"); + names[24] = strdup("EURCAD"); + names[25] = strdup("EURAUD"); + names[26] = strdup("EURNZD"); + names[27] = strdup("AUDJPY"); + names[28] = strdup("AUDCHF"); + names[29] = strdup("AUDCAD"); + names[30] = strdup("NZDJPY"); + names[31] = strdup("NZDCHF"); + names[32] = strdup("NZDCAD"); + names[33] = strdup("GBPJPY"); + names[34] = strdup("GBPCHF"); + names[35] = strdup("GBPCAD"); + names[36] = strdup("CHFJPY"); + names[37] = strdup("CADJPY"); + names[38] = strdup("CADCHF"); + names[39] = strdup("GBPAUD"); + names[40] = strdup("GBPNZD"); + names[41] = strdup("AUDNZD"); + + return names; +} + +char** asset_names = NULL; +// index +uint32_t asset_index = 0; +FlipTraderApp* app_instance = NULL; +// Function to free the resources used by FlipTraderApp +void flip_trader_app_free(FlipTraderApp* app) { + if(!app) { + FURI_LOG_E(TAG, "FlipTraderApp is NULL"); + return; + } + + // Free View(s) + if(app->view_loader) { + view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewLoader); + flip_trader_loader_free_model(app->view_loader); + view_free(app->view_loader); + } + + // Free Submenu(s) + if(app->submenu_main) { + view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewMainSubmenu); + submenu_free(app->submenu_main); + } + if(app->submenu_assets) { + view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewAssetsSubmenu); + submenu_free(app->submenu_assets); + } + + // Free Widget(s) + if(app->widget_about) { + view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewAbout); + widget_free(app->widget_about); + } + if(app->widget_result) { + view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewWidgetResult); + widget_free(app->widget_result); + } + + // Free Variable Item List(s) + if(app->variable_item_list_wifi) { + view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewWiFiSettings); + variable_item_list_free(app->variable_item_list_wifi); + } + + // Free Text Input(s) + if(app->uart_text_input_ssid) { + view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewTextInputSSID); + text_input_free(app->uart_text_input_ssid); + } + if(app->uart_text_input_password) { + view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewTextInputPassword); + text_input_free(app->uart_text_input_password); + } + + asset_names_free(asset_names); + + // deinitalize flipper http + flipper_http_deinit(); + + // free the view dispatcher + view_dispatcher_free(app->view_dispatcher); + + // close the gui + furi_record_close(RECORD_GUI); + + // free the app + free(app); +} diff --git a/flip_trader/flip_trader_e.h b/flip_trader/flip_trader.h similarity index 74% rename from flip_trader/flip_trader_e.h rename to flip_trader/flip_trader.h index 1fa81ddf2..35f28e828 100644 --- a/flip_trader/flip_trader_e.h +++ b/flip_trader/flip_trader.h @@ -1,8 +1,8 @@ #ifndef FLIP_TRADE_E_H #define FLIP_TRADE_E_H -#include -#include +#include +#include #include #include #include @@ -11,7 +11,7 @@ #include #include #include -#include +#include #define TAG "FlipTrader" @@ -35,15 +35,18 @@ typedef enum { FlipTraderViewTextInputPassword, // The text input screen for the password // FlipTraderViewAssetsSubmenu, // The submenu for the assets + FlipTraderViewWidgetResult, // The text box that displays the random fact + FlipTraderViewLoader, // The loader screen retrieves data from the internet } FlipTraderView; // Each screen will have its own view typedef struct { ViewDispatcher* view_dispatcher; // Switches between our views - View* view_main; // The main screen that displays "Hello, World!" + View* view_loader; // The screen that loads data from internet Submenu* submenu_main; // The submenu Submenu* submenu_assets; // The submenu for the assets - Widget* widget; // The widget + Widget* widget_about; // The widget + Widget* widget_result; // The widget that displays the result VariableItemList* variable_item_list_wifi; // The variable item list (settngs) VariableItem* variable_item_ssid; // The variable item for the SSID VariableItem* variable_item_password; // The variable item for the password @@ -60,53 +63,15 @@ typedef struct { } FlipTraderApp; -static char* asset_names[] = { - // Crypto pairs - "ETHUSD", - "BTCUSD", - // Stocks (will add mroe later) - "AAPL", - "AMZN", - "GOOGL", - "MSFT", - "TSLA", - "NFLX", - "META", - "NVDA", - "AMD", - // Forex pairs - "EURUSD", - "GBPUSD", - "AUDUSD", - "NZDUSD", - "XAUUSD", - "USDJPY", - "USDCHF", - "USDCAD", - "EURJPY", - "EURGBP", - "EURCHF", - "EURCAD", - "EURAUD", - "EURNZD", - "AUDJPY", - "AUDCHF", - "AUDCAD", - "NZDJPY", - "NZDCHF", - "NZDCAD", - "GBPJPY", - "GBPCHF", - "GBPCAD", - "CHFJPY", - "CADJPY", - "CADCHF", - "GBPAUD", - "GBPNZD", - "AUDNZD", -}; +#define ASSET_COUNT 42 +extern char** asset_names; // index -static uint32_t asset_index = 0; +extern uint32_t asset_index; +// Function to free the resources used by FlipTraderApp +void flip_trader_app_free(FlipTraderApp* app); +char** asset_names_alloc(); +void asset_names_free(char** names); +extern FlipTraderApp* app_instance; #endif // FLIP_TRADE_E_H diff --git a/flip_trader/flip_trader_callback.h b/flip_trader/flip_trader_callback.h deleted file mode 100644 index 8be75b4f2..000000000 --- a/flip_trader/flip_trader_callback.h +++ /dev/null @@ -1,307 +0,0 @@ -#ifndef FLIP_TRADER_CALLBACK_H -#define FLIP_TRADER_CALLBACK_H - -#define MAX_TOKENS 32 // Adjust based on expected JSON size (25) - -// hold the price of the asset -static char asset_price[64]; -static bool sent_get_request = false; -static bool get_request_success = false; -static bool request_processed = false; - -void flip_trader_request_error(Canvas* canvas) { - if(fhttp.received_data == NULL) { - if(fhttp.last_response != NULL) { - if(strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != - NULL) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } else if(strstr(fhttp.last_response, "[ERROR] WiFi SSID or Password is empty") != NULL) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } else { - canvas_clear(canvas); - FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response); - canvas_draw_str(canvas, 0, 10, "[ERROR] Unusual error..."); - canvas_draw_str(canvas, 0, 60, "Press BACK and retry."); - } - } else { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - } else { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Failed to receive data."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } -} - -static bool send_price_request() { - if(!sent_get_request && fhttp.state == IDLE) { - sent_get_request = true; - char url[128] = {0}; - snprintf( - url, - 128, - "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=%s&apikey=2X90WLEFMP43OJKE", - asset_names[asset_index]); - get_request_success = - flipper_http_get_request_with_headers(url, "{\"Content-Type\": \"application/json\"}"); - if(!get_request_success) { - FURI_LOG_E(TAG, "Failed to send GET request"); - return false; - } - fhttp.state = RECEIVING; - } - return true; -} - -static void process_asset_price() { - if(!request_processed && fhttp.received_data != NULL) { - request_processed = true; - char* global_quote = get_json_value("Global Quote", fhttp.received_data, MAX_TOKENS); - if(global_quote == NULL) { - FURI_LOG_E(TAG, "Failed to get Global Quote"); - return; - } - char* price = get_json_value("05. price", global_quote, MAX_TOKENS); - if(price == NULL) { - FURI_LOG_E(TAG, "Failed to get price"); - return; - } - // store the price "Asset: $price" - snprintf(asset_price, 64, "%s: $%s", asset_names[asset_index], price); - - fhttp.state = IDLE; - } else if(!request_processed && fhttp.received_data == NULL) { - request_processed = true; - // store an error message instead of the price - snprintf(asset_price, 64, "Failed. Update WiFi settings."); - fhttp.state = ISSUE; - } -} - -// Callback for drawing the main screen -static void flip_trader_view_draw_callback(Canvas* canvas, void* model) { - if(!canvas) { - return; - } - UNUSED(model); - - canvas_set_font(canvas, FontSecondary); - - if(fhttp.state == INACTIVE) { - canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); - canvas_draw_str(canvas, 0, 17, "Please connect to the board."); - canvas_draw_str(canvas, 0, 32, "If your board is connected,"); - canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); - canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); - canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); - return; - } - - canvas_draw_str(canvas, 0, 10, "Loading..."); - // canvas_draw_str(canvas, 0, 10, asset_names[asset_index]); - - // start the process - if(!send_price_request()) { - flip_trader_request_error(canvas); - } - // wait until the request is processed - if(!sent_get_request || !get_request_success || fhttp.state == RECEIVING) { - return; - } - // check status - if(fhttp.state == ISSUE || fhttp.received_data == NULL) { - flip_trader_request_error(canvas); - } - // success, process the data - process_asset_price(); - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, asset_price); -} - -// Input callback for the view (async input handling) -bool flip_trader_view_input_callback(InputEvent* event, void* context) { - FlipTraderApp* app = (FlipTraderApp*)context; - if(event->type == InputTypePress && event->key == InputKeyBack) { - // Exit the app when the back button is pressed - view_dispatcher_stop(app->view_dispatcher); - return true; - } - return false; -} - -static void callback_submenu_choices(void* context, uint32_t index) { - FlipTraderApp* app = (FlipTraderApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipTraderApp is NULL"); - return; - } - switch(index) { - // view the assets submenu - case FlipTradeSubmenuIndexAssets: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewAssetsSubmenu); - break; - // view the about screen - case FlipTraderSubmenuIndexAbout: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewAbout); - break; - // view the wifi settings screen - case FlipTraderSubmenuIndexSettings: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewWiFiSettings); - break; - default: - // handle FlipTraderSubmenuIndexAssetStartIndex + index - if(index >= FlipTraderSubmenuIndexAssetStartIndex) { - asset_index = index - FlipTraderSubmenuIndexAssetStartIndex; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewMain); - } else { - FURI_LOG_E(TAG, "Unknown submenu index"); - } - break; - } -} - -static void text_updated_ssid(void* context) { - FlipTraderApp* app = (FlipTraderApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipTraderApp is NULL"); - return; - } - - // store the entered text - strncpy( - app->uart_text_input_buffer_ssid, - app->uart_text_input_temp_buffer_ssid, - app->uart_text_input_buffer_size_ssid); - - // Ensure null-termination - app->uart_text_input_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; - - // update the variable item text - if(app->variable_item_ssid) { - variable_item_set_current_value_text( - app->variable_item_ssid, app->uart_text_input_buffer_ssid); - } - - // save settings - save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); - - // save wifi settings to devboard - if(strlen(app->uart_text_input_buffer_ssid) > 0 && - strlen(app->uart_text_input_buffer_password) > 0) { - if(!flipper_http_save_wifi( - app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { - FURI_LOG_E(TAG, "Failed to save wifi settings"); - } - } - - // switch to the settings view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewWiFiSettings); -} - -static void text_updated_password(void* context) { - FlipTraderApp* app = (FlipTraderApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipTraderApp is NULL"); - return; - } - - // store the entered text - strncpy( - app->uart_text_input_buffer_password, - app->uart_text_input_temp_buffer_password, - app->uart_text_input_buffer_size_password); - - // Ensure null-termination - app->uart_text_input_buffer_password[app->uart_text_input_buffer_size_password - 1] = '\0'; - - // update the variable item text - if(app->variable_item_password) { - variable_item_set_current_value_text( - app->variable_item_password, app->uart_text_input_buffer_password); - } - - // save settings - save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); - - // save wifi settings to devboard - if(strlen(app->uart_text_input_buffer_ssid) > 0 && - strlen(app->uart_text_input_buffer_password) > 0) { - if(!flipper_http_save_wifi( - app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { - FURI_LOG_E(TAG, "Failed to save wifi settings"); - } - } - - // switch to the settings view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewWiFiSettings); -} - -static uint32_t callback_to_submenu(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - sent_get_request = false; - get_request_success = false; - request_processed = false; - asset_index = 0; - return FlipTraderViewMainSubmenu; -} - -static uint32_t callback_to_wifi_settings(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - return FlipTraderViewWiFiSettings; -} - -static uint32_t callback_to_assets_submenu(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - sent_get_request = false; - get_request_success = false; - request_processed = false; - asset_index = 0; - return FlipTraderViewAssetsSubmenu; -} - -static void settings_item_selected(void* context, uint32_t index) { - FlipTraderApp* app = (FlipTraderApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipTraderApp is NULL"); - return; - } - switch(index) { - case 0: // Input SSID - view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewTextInputSSID); - break; - case 1: // Input Password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewTextInputPassword); - break; - default: - FURI_LOG_E(TAG, "Unknown configuration item index"); - break; - } -} - -#endif // FLIP_TRADER_CALLBACK_H diff --git a/flip_trader/flip_trader_free.h b/flip_trader/flip_trader_free.h deleted file mode 100644 index c25653717..000000000 --- a/flip_trader/flip_trader_free.h +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef FLIP_TRADER_FREE_H -#define FLIP_TRADER_FREE_H - -// Function to free the resources used by FlipTraderApp -static void flip_trader_app_free(FlipTraderApp* app) { - if(!app) { - FURI_LOG_E(TAG, "FlipTraderApp is NULL"); - return; - } - - if(!flipper_http_disconnect_wifi()) { - FURI_LOG_E(TAG, "Failed to disconnect from wifi"); - } - - // Free View(s) - if(app->view_main) { - view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewMain); - view_free(app->view_main); - } - - // Free Submenu(s) - if(app->submenu_main) { - view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewMainSubmenu); - submenu_free(app->submenu_main); - } - if(app->submenu_assets) { - view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewAssetsSubmenu); - submenu_free(app->submenu_assets); - } - - // Free Widget(s) - if(app->widget) { - view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewAbout); - widget_free(app->widget); - } - - // Free Variable Item List(s) - if(app->variable_item_list_wifi) { - view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewWiFiSettings); - variable_item_list_free(app->variable_item_list_wifi); - } - - // Free Text Input(s) - if(app->uart_text_input_ssid) { - view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewTextInputSSID); - text_input_free(app->uart_text_input_ssid); - } - if(app->uart_text_input_password) { - view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewTextInputPassword); - text_input_free(app->uart_text_input_password); - } - - // deinitalize flipper http - flipper_http_deinit(); - - // free the view dispatcher - view_dispatcher_free(app->view_dispatcher); - - // close the gui - furi_record_close(RECORD_GUI); - - // free the app - free(app); -} - -#endif // FLIP_TRADER_FREE_H diff --git a/flip_trader/flipper_http.h b/flip_trader/flipper_http.h deleted file mode 100644 index 788fc7e38..000000000 --- a/flip_trader/flipper_http.h +++ /dev/null @@ -1,1221 +0,0 @@ -// flipper_http.h -#ifndef FLIPPER_HTTP_H -#define FLIPPER_HTTP_H - -#include -#include -#include -#include -#include - -// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext - -#define HTTP_TAG "FlipperHTTP" // change this to your app name -#define http_tag "flipper_http" // change this to your app id -#define UART_CH (FuriHalSerialIdUsart) // UART channel -#define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds -#define BAUDRATE (115200) // UART baudrate -#define RX_BUF_SIZE 128 // UART RX buffer size -#define RX_LINE_BUFFER_SIZE 2048 // UART RX line buffer size (increase for large responses) - -// Forward declaration for callback -typedef void (*FlipperHTTP_Callback)(const char* line, void* context); - -// Functions -bool flipper_http_init(FlipperHTTP_Callback callback, void* context); -void flipper_http_deinit(); -//--- -void flipper_http_rx_callback(const char* line, void* context); -bool flipper_http_send_data(const char* data); -//--- -bool flipper_http_connect_wifi(); -bool flipper_http_disconnect_wifi(); -bool flipper_http_ping(); -bool flipper_http_scan_wifi(); -bool flipper_http_save_wifi(const char* ssid, const char* password); -bool flipper_http_ip_address(); -//--- -bool flipper_http_list_commands(); -bool flipper_http_led_on(); -bool flipper_http_led_off(); -bool flipper_http_parse_json(const char* key, const char* json_data); -bool flipper_http_parse_json_array(const char* key, int index, const char* json_data); -//--- -bool flipper_http_get_request(const char* url); -bool flipper_http_get_request_with_headers(const char* url, const char* headers); -bool flipper_http_post_request_with_headers( - const char* url, - const char* headers, - const char* payload); -bool flipper_http_put_request_with_headers( - const char* url, - const char* headers, - const char* payload); -bool flipper_http_delete_request_with_headers( - const char* url, - const char* headers, - const char* payload); -//--- -bool flipper_http_save_received_data(size_t bytes_received, const char line_buffer[]); -static char* trim(const char* str); - -// State variable to track the UART state -typedef enum { - INACTIVE, // Inactive state - IDLE, // Default state - RECEIVING, // Receiving data - SENDING, // Sending data - ISSUE, // Issue with connection -} SerialState; - -// Event Flags for UART Worker Thread -typedef enum { - WorkerEvtStop = (1 << 0), - WorkerEvtRxDone = (1 << 1), -} WorkerEvtFlags; - -// FlipperHTTP Structure -typedef struct { - FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication - FuriHalSerialHandle* serial_handle; // Serial handle for UART communication - FuriThread* rx_thread; // Worker thread for UART - uint8_t rx_buf[RX_BUF_SIZE]; // Buffer for received data - FuriThreadId rx_thread_id; // Worker thread ID - FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines - void* callback_context; // Context for the callback - SerialState state; // State of the UART - - // variable to store the last received data from the UART - char* last_response; - - // Timer-related members - FuriTimer* get_timeout_timer; // Timer for HTTP request timeout - char* received_data; // Buffer to store received data - - bool started_receiving_get; // Indicates if a GET request has started - bool just_started_get; // Indicates if GET data reception has just started - - bool started_receiving_post; // Indicates if a POST request has started - bool just_started_post; // Indicates if POST data reception has just started - - bool started_receiving_put; // Indicates if a PUT request has started - bool just_started_put; // Indicates if PUT data reception has just started - - bool started_receiving_delete; // Indicates if a DELETE request has started - bool just_started_delete; // Indicates if DELETE data reception has just started -} FlipperHTTP; - -FlipperHTTP fhttp; - -// fhttp.received_data holds the received data from HTTP requests -// fhttp.last_response holds the last received data from the UART, which could be [GET/END], [POST/END], [PUT/END], [DELETE/END], etc - -// Timer callback function -/** - * @brief Callback function for the GET timeout timer. - * @return 0 - * @param context The context to pass to the callback. - * @note This function will be called when the GET request times out. - */ -void get_timeout_timer_callback(void* context) { - UNUSED(context); - FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving the end."); - - // Reset the state - fhttp.started_receiving_get = false; - fhttp.started_receiving_post = false; - fhttp.started_receiving_put = false; - fhttp.started_receiving_delete = false; - - // Free received data if any - if(fhttp.received_data) { - free(fhttp.received_data); - fhttp.received_data = NULL; - } - - // Update UART state - fhttp.state = ISSUE; -} - -// UART RX Handler Callback (Interrupt Context) -/** - * @brief A private callback function to handle received data asynchronously. - * @return void - * @param handle The UART handle. - * @param event The event type. - * @param context The context to pass to the callback. - * @note This function will handle received data asynchronously via the callback. - */ -static void _flipper_http_rx_callback( - FuriHalSerialHandle* handle, - FuriHalSerialRxEvent event, - void* context) { - UNUSED(context); - if(event == FuriHalSerialRxEventData) { - uint8_t data = furi_hal_serial_async_rx(handle); - furi_stream_buffer_send(fhttp.flipper_http_stream, &data, 1, 0); - furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtRxDone); - } -} - -// UART worker thread -/** - * @brief Worker thread to handle UART data asynchronously. - * @return 0 - * @param context The context to pass to the callback. - * @note This function will handle received data asynchronously via the callback. - */ -static int32_t flipper_http_worker(void* context) { - UNUSED(context); - size_t rx_line_pos = 0; - char* rx_line_buffer = (char*)malloc(RX_LINE_BUFFER_SIZE); - - if(!rx_line_buffer) { - // Handle malloc failure - FURI_LOG_E(HTTP_TAG, "Failed to allocate memory for rx_line_buffer"); - return -1; - } - - while(1) { - uint32_t events = furi_thread_flags_wait( - WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever); - if(events & WorkerEvtStop) break; - if(events & WorkerEvtRxDone) { - size_t len = furi_stream_buffer_receive( - fhttp.flipper_http_stream, fhttp.rx_buf, RX_BUF_SIZE, 0); - for(size_t i = 0; i < len; i++) { - char c = fhttp.rx_buf[i]; - if(c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1) { - rx_line_buffer[rx_line_pos] = '\0'; - // Invoke the callback with the complete line - if(fhttp.handle_rx_line_cb) { - fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context); - } - // Reset the line buffer - rx_line_pos = 0; - } else { - rx_line_buffer[rx_line_pos++] = c; - } - } - } - } - - // Free the allocated memory before exiting the thread - free(rx_line_buffer); - - return 0; -} - -// UART initialization function -/** - * @brief Initialize UART. - * @return true if the UART was initialized successfully, false otherwise. - * @param callback The callback function to handle received data (ex. flipper_http_rx_callback). - * @param context The context to pass to the callback. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { - if(!context) { - FURI_LOG_E(HTTP_TAG, "Invalid context provided to flipper_http_init."); - return false; - } - if(!callback) { - FURI_LOG_E(HTTP_TAG, "Invalid callback provided to flipper_http_init."); - return false; - } - fhttp.flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1); - if(!fhttp.flipper_http_stream) { - FURI_LOG_E(HTTP_TAG, "Failed to allocate UART stream buffer."); - return false; - } - - fhttp.rx_thread = furi_thread_alloc(); - if(!fhttp.rx_thread) { - FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread."); - furi_stream_buffer_free(fhttp.flipper_http_stream); - return false; - } - - furi_thread_set_name(fhttp.rx_thread, "FlipperHTTP_RxThread"); - furi_thread_set_stack_size(fhttp.rx_thread, 1024); - furi_thread_set_context(fhttp.rx_thread, &fhttp); - furi_thread_set_callback(fhttp.rx_thread, flipper_http_worker); - - fhttp.handle_rx_line_cb = callback; - fhttp.callback_context = context; - - furi_thread_start(fhttp.rx_thread); - fhttp.rx_thread_id = furi_thread_get_id(fhttp.rx_thread); - - // handle when the UART control is busy to avoid furi_check failed - if(furi_hal_serial_control_is_busy(UART_CH)) { - FURI_LOG_E(HTTP_TAG, "UART control is busy."); - return false; - } - - fhttp.serial_handle = furi_hal_serial_control_acquire(UART_CH); - if(!fhttp.serial_handle) { - FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL"); - // Cleanup resources - furi_thread_free(fhttp.rx_thread); - furi_stream_buffer_free(fhttp.flipper_http_stream); - return false; - } - - // Initialize UART with acquired handle - furi_hal_serial_init(fhttp.serial_handle, BAUDRATE); - - // Enable RX direction - furi_hal_serial_enable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); - - // Start asynchronous RX with the callback - furi_hal_serial_async_rx_start(fhttp.serial_handle, _flipper_http_rx_callback, &fhttp, false); - - // Wait for the TX to complete to ensure UART is ready - furi_hal_serial_tx_wait_complete(fhttp.serial_handle); - - // Allocate the timer for handling timeouts - fhttp.get_timeout_timer = furi_timer_alloc( - get_timeout_timer_callback, // Callback function - FuriTimerTypeOnce, // One-shot timer - &fhttp // Context passed to callback - ); - - if(!fhttp.get_timeout_timer) { - FURI_LOG_E(HTTP_TAG, "Failed to allocate HTTP request timeout timer."); - // Cleanup resources - furi_hal_serial_async_rx_stop(fhttp.serial_handle); - furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); - furi_hal_serial_control_release(fhttp.serial_handle); - furi_hal_serial_deinit(fhttp.serial_handle); - furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop); - furi_thread_join(fhttp.rx_thread); - furi_thread_free(fhttp.rx_thread); - furi_stream_buffer_free(fhttp.flipper_http_stream); - return false; - } - - // Set the timer thread priority if needed - furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated); - - FURI_LOG_I(HTTP_TAG, "UART initialized successfully."); - return true; -} - -// Deinitialize UART -/** - * @brief Deinitialize UART. - * @return void - * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. - */ -void flipper_http_deinit() { - if(fhttp.serial_handle == NULL) { - FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?"); - return; - } - // Stop asynchronous RX - furi_hal_serial_async_rx_stop(fhttp.serial_handle); - - // Release and deinitialize the serial handle - furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); - furi_hal_serial_control_release(fhttp.serial_handle); - furi_hal_serial_deinit(fhttp.serial_handle); - - // Signal the worker thread to stop - furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop); - // Wait for the thread to finish - furi_thread_join(fhttp.rx_thread); - // Free the thread resources - furi_thread_free(fhttp.rx_thread); - - // Free the stream buffer - furi_stream_buffer_free(fhttp.flipper_http_stream); - - // Free the timer - if(fhttp.get_timeout_timer) { - furi_timer_free(fhttp.get_timeout_timer); - fhttp.get_timeout_timer = NULL; - } - - // Free received data if any - if(fhttp.received_data) { - free(fhttp.received_data); - fhttp.received_data = NULL; - } - - // Free the last response - if(fhttp.last_response) { - free(fhttp.last_response); - fhttp.last_response = NULL; - } - - FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully."); -} - -// Function to send data over UART with newline termination -/** - * @brief Send data over UART with newline termination. - * @return true if the data was sent successfully, false otherwise. - * @param data The data to send over UART. - * @note The data will be sent over UART with a newline character appended. - */ -bool flipper_http_send_data(const char* data) { - size_t data_length = strlen(data); - if(data_length == 0) { - FURI_LOG_E("FlipperHTTP", "Attempted to send empty data."); - return false; - } - - // Create a buffer with data + '\n' - size_t send_length = data_length + 1; // +1 for '\n' - if(send_length > 512) { // Ensure buffer size is sufficient - FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP."); - return false; - } - - char send_buffer[513]; // 512 + 1 for safety - strncpy(send_buffer, data, 512); - send_buffer[data_length] = '\n'; // Append newline - send_buffer[data_length + 1] = '\0'; // Null-terminate - - if(fhttp.state == INACTIVE && ((strstr(send_buffer, "[PING]") == NULL) && - (strstr(send_buffer, "[WIFI/CONNECT]") == NULL))) { - FURI_LOG_E("FlipperHTTP", "Cannot send data while INACTIVE."); - fhttp.last_response = "Cannot send data while INACTIVE."; - return false; - } - - fhttp.state = SENDING; - furi_hal_serial_tx(fhttp.serial_handle, (const uint8_t*)send_buffer, send_length); - - // Uncomment below line to log the data sent over UART - // FURI_LOG_I("FlipperHTTP", "Sent data over UART: %s", send_buffer); - fhttp.state = IDLE; - return true; -} - -// Function to send a PING request -/** - * @brief Send a PING request to check if the Wifi Dev Board is connected. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - * @note This is best used to check if the Wifi Dev Board is connected. - * @note The state will remain INACTIVE until a PONG is received. - */ -bool flipper_http_ping() { - const char* command = "[PING]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send PING command."); - return false; - } - // set state as INACTIVE to be made IDLE if PONG is received - fhttp.state = INACTIVE; - // The response will be handled asynchronously via the callback - return true; -} - -// Function to list available commands -/** - * @brief Send a command to list available commands. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_list_commands() { - const char* command = "[LIST]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send LIST command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to turn on the LED -/** - * @brief Send a command to turn on the LED. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_led_on() { - const char* command = "[LED/ON]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send LED ON command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to turn off the LED -/** - * @brief Send a command to turn off the LED. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_led_off() { - const char* command = "[LED/OFF]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send LED OFF command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to parse JSON data -/** - * @brief Parse JSON data. - * @return true if the JSON data was parsed successfully, false otherwise. - * @param key The key to parse from the JSON data. - * @param json_data The JSON data to parse. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_parse_json(const char* key, const char* json_data) { - if(!key || !json_data) { - FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json."); - return false; - } - - char buffer[256]; - int ret = - snprintf(buffer, sizeof(buffer), "[PARSE]{\"key\":\"%s\",\"json\":%s}", key, json_data); - if(ret < 0 || ret >= (int)sizeof(buffer)) { - FURI_LOG_E("FlipperHTTP", "Failed to format JSON parse command."); - return false; - } - - if(!flipper_http_send_data(buffer)) { - FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to parse JSON array data -/** - * @brief Parse JSON array data. - * @return true if the JSON array data was parsed successfully, false otherwise. - * @param key The key to parse from the JSON array data. - * @param index The index to parse from the JSON array data. - * @param json_data The JSON array data to parse. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_parse_json_array(const char* key, int index, const char* json_data) { - if(!key || !json_data) { - FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json_array."); - return false; - } - - char buffer[256]; - int ret = snprintf( - buffer, - sizeof(buffer), - "[PARSE/ARRAY]{\"key\":\"%s\",\"index\":%d,\"json\":%s}", - key, - index, - json_data); - if(ret < 0 || ret >= (int)sizeof(buffer)) { - FURI_LOG_E("FlipperHTTP", "Failed to format JSON parse array command."); - return false; - } - - if(!flipper_http_send_data(buffer)) { - FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse array command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to scan for WiFi networks -/** - * @brief Send a command to scan for WiFi networks. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_scan_wifi() { - const char* command = "[WIFI/SCAN]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send WiFi scan command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to save WiFi settings (returns true if successful) -/** - * @brief Send a command to save WiFi settings. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_save_wifi(const char* ssid, const char* password) { - if(!ssid || !password) { - FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_save_wifi."); - return false; - } - char buffer[256]; - int ret = snprintf( - buffer, sizeof(buffer), "[WIFI/SAVE]{\"ssid\":\"%s\",\"password\":\"%s\"}", ssid, password); - if(ret < 0 || ret >= (int)sizeof(buffer)) { - FURI_LOG_E("FlipperHTTP", "Failed to format WiFi save command."); - return false; - } - - if(!flipper_http_send_data(buffer)) { - FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to get IP address -/** - * @brief Send a command to get the IP address. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_ip_address() { - const char* command = "[IP/ADDRESS]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send IP address command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to disconnect from WiFi (returns true if successful) -/** - * @brief Send a command to disconnect from WiFi. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_disconnect_wifi() { - const char* command = "[WIFI/DISCONNECT]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to connect to WiFi (returns true if successful) -/** - * @brief Send a command to connect to WiFi. - * @return true if the request was successful, false otherwise. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_connect_wifi() { - const char* command = "[WIFI/CONNECT]"; - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} - -// Function to send a GET request -/** - * @brief Send a GET request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the GET request to. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_get_request(const char* url) { - if(!url) { - FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request."); - return false; - } - - // Prepare GET request command - char command[256]; - int ret = snprintf(command, sizeof(command), "[GET]%s", url); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to format GET request command."); - return false; - } - - // Send GET request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send GET request command."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to send a GET request with headers -/** - * @brief Send a GET request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the GET request to. - * @param headers The headers to send with the GET request. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_get_request_with_headers(const char* url, const char* headers) { - if(!url || !headers) { - FURI_LOG_E( - "FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_with_headers."); - return false; - } - - // Prepare GET request command with headers - char command[512]; // increase for weather url - int ret = snprintf( - command, sizeof(command), "[GET/HTTP]{\"url\":\"%s\",\"headers\":%s}", url, headers); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers."); - return false; - } - - // Send GET request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to send a POST request with headers -/** - * @brief Send a POST request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the POST request to. - * @param headers The headers to send with the POST request. - * @param data The data to send with the POST request. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_post_request_with_headers( - const char* url, - const char* headers, - const char* payload) { - if(!url || !headers || !payload) { - FURI_LOG_E( - "FlipperHTTP", - "Invalid arguments provided to flipper_http_post_request_with_headers."); - return false; - } - - // Prepare POST request command with headers and data - char command[256]; - int ret = snprintf( - command, - sizeof(command), - "[POST/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", - url, - headers, - payload); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data."); - return false; - } - - // Send POST request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to send a PUT request with headers -/** - * @brief Send a PUT request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the PUT request to. - * @param headers The headers to send with the PUT request. - * @param data The data to send with the PUT request. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_put_request_with_headers( - const char* url, - const char* headers, - const char* payload) { - if(!url || !headers || !payload) { - FURI_LOG_E( - "FlipperHTTP", "Invalid arguments provided to flipper_http_put_request_with_headers."); - return false; - } - - // Prepare PUT request command with headers and data - char command[256]; - int ret = snprintf( - command, - sizeof(command), - "[PUT/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", - url, - headers, - payload); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to format PUT request command with headers and data."); - return false; - } - - // Send PUT request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to send a DELETE request with headers -/** - * @brief Send a DELETE request to the specified URL. - * @return true if the request was successful, false otherwise. - * @param url The URL to send the DELETE request to. - * @param headers The headers to send with the DELETE request. - * @param data The data to send with the DELETE request. - * @note The received data will be handled asynchronously via the callback. - */ -bool flipper_http_delete_request_with_headers( - const char* url, - const char* headers, - const char* payload) { - if(!url || !headers || !payload) { - FURI_LOG_E( - "FlipperHTTP", - "Invalid arguments provided to flipper_http_delete_request_with_headers."); - return false; - } - - // Prepare DELETE request command with headers and data - char command[256]; - int ret = snprintf( - command, - sizeof(command), - "[DELETE/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", - url, - headers, - payload); - if(ret < 0 || ret >= (int)sizeof(command)) { - FURI_LOG_E( - "FlipperHTTP", "Failed to format DELETE request command with headers and data."); - return false; - } - - // Send DELETE request via UART - if(!flipper_http_send_data(command)) { - FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data."); - return false; - } - - // The response will be handled asynchronously via the callback - return true; -} -// Function to handle received data asynchronously -/** - * @brief Callback function to handle received data asynchronously. - * @return void - * @param line The received line. - * @param context The context passed to the callback. - * @note The received data will be handled asynchronously via the callback and handles the state of the UART. - */ -void flipper_http_rx_callback(const char* line, void* context) { - if(!line || !context) { - FURI_LOG_E(HTTP_TAG, "Invalid arguments provided to flipper_http_rx_callback."); - return; - } - - // Trim the received line to check if it's empty - char* trimmed_line = trim(line); - if(trimmed_line != NULL && trimmed_line[0] != '\0') { - fhttp.last_response = (char*)line; - } - free(trimmed_line); // Free the allocated memory for trimmed_line - - if(fhttp.state != INACTIVE && fhttp.state != ISSUE) { - fhttp.state = RECEIVING; - } - - // Uncomment below line to log the data received over UART - FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line); - - // Check if we've started receiving data from a GET request - if(fhttp.started_receiving_get) { - // Restart the timeout timer each time new data is received - furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - - if(strstr(line, "[GET/END]") != NULL) { - FURI_LOG_I(HTTP_TAG, "GET request completed."); - // Stop the timer since we've completed the GET request - furi_timer_stop(fhttp.get_timeout_timer); - - if(fhttp.received_data) { - // uncomment if you want to save the received data to the external storage - // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data); - fhttp.started_receiving_get = false; - fhttp.just_started_get = false; - fhttp.state = IDLE; - return; - } else { - FURI_LOG_E(HTTP_TAG, "No data received."); - fhttp.started_receiving_get = false; - fhttp.just_started_get = false; - fhttp.state = IDLE; - return; - } - } - - // Append the new line to the existing data - if(fhttp.received_data == NULL) { - fhttp.received_data = - (char*)malloc(strlen(line) + 2); // +2 for newline and null terminator - if(fhttp.received_data) { - strcpy(fhttp.received_data, line); - fhttp.received_data[strlen(line)] = '\n'; // Add newline - fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator - } - } else { - size_t current_len = strlen(fhttp.received_data); - size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator - fhttp.received_data = (char*)realloc(fhttp.received_data, new_size); - if(fhttp.received_data) { - memcpy( - fhttp.received_data + current_len, - line, - strlen(line)); // Copy line at the end of the current data - fhttp.received_data[current_len + strlen(line)] = '\n'; // Add newline - fhttp.received_data[current_len + strlen(line) + 1] = '\0'; // Null terminator - } - } - - if(!fhttp.just_started_get) { - fhttp.just_started_get = true; - } - return; - } - - // Check if we've started receiving data from a POST request - else if(fhttp.started_receiving_post) { - // Restart the timeout timer each time new data is received - furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - - if(strstr(line, "[POST/END]") != NULL) { - FURI_LOG_I(HTTP_TAG, "POST request completed."); - // Stop the timer since we've completed the POST request - furi_timer_stop(fhttp.get_timeout_timer); - - if(fhttp.received_data) { - // uncomment if you want to save the received data to the external storage - // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data); - fhttp.started_receiving_post = false; - fhttp.just_started_post = false; - fhttp.state = IDLE; - return; - } else { - FURI_LOG_E(HTTP_TAG, "No data received."); - fhttp.started_receiving_post = false; - fhttp.just_started_post = false; - fhttp.state = IDLE; - return; - } - } - - // Append the new line to the existing data - if(fhttp.received_data == NULL) { - fhttp.received_data = - (char*)malloc(strlen(line) + 2); // +2 for newline and null terminator - if(fhttp.received_data) { - strcpy(fhttp.received_data, line); - fhttp.received_data[strlen(line)] = '\n'; // Add newline - fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator - } - } else { - size_t current_len = strlen(fhttp.received_data); - size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator - fhttp.received_data = (char*)realloc(fhttp.received_data, new_size); - if(fhttp.received_data) { - memcpy( - fhttp.received_data + current_len, - line, - strlen(line)); // Copy line at the end of the current data - fhttp.received_data[current_len + strlen(line)] = '\n'; // Add newline - fhttp.received_data[current_len + strlen(line) + 1] = '\0'; // Null terminator - } - } - - if(!fhttp.just_started_post) { - fhttp.just_started_post = true; - } - return; - } - - // Check if we've started receiving data from a PUT request - else if(fhttp.started_receiving_put) { - // Restart the timeout timer each time new data is received - furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - - if(strstr(line, "[PUT/END]") != NULL) { - FURI_LOG_I(HTTP_TAG, "PUT request completed."); - // Stop the timer since we've completed the PUT request - furi_timer_stop(fhttp.get_timeout_timer); - - if(fhttp.received_data) { - // uncomment if you want to save the received data to the external storage - // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data); - fhttp.started_receiving_put = false; - fhttp.just_started_put = false; - fhttp.state = IDLE; - return; - } else { - FURI_LOG_E(HTTP_TAG, "No data received."); - fhttp.started_receiving_put = false; - fhttp.just_started_put = false; - fhttp.state = IDLE; - return; - } - } - - // Append the new line to the existing data - if(fhttp.received_data == NULL) { - fhttp.received_data = - (char*)malloc(strlen(line) + 2); // +2 for newline and null terminator - if(fhttp.received_data) { - strcpy(fhttp.received_data, line); - fhttp.received_data[strlen(line)] = '\n'; // Add newline - fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator - } - } else { - size_t current_len = strlen(fhttp.received_data); - size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator - fhttp.received_data = (char*)realloc(fhttp.received_data, new_size); - if(fhttp.received_data) { - memcpy( - fhttp.received_data + current_len, - line, - strlen(line)); // Copy line at the end of the current data - fhttp.received_data[current_len + strlen(line)] = '\n'; // Add newline - fhttp.received_data[current_len + strlen(line) + 1] = '\0'; // Null terminator - } - } - - if(!fhttp.just_started_put) { - fhttp.just_started_put = true; - } - return; - } - - // Check if we've started receiving data from a DELETE request - else if(fhttp.started_receiving_delete) { - // Restart the timeout timer each time new data is received - furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - - if(strstr(line, "[DELETE/END]") != NULL) { - FURI_LOG_I(HTTP_TAG, "DELETE request completed."); - // Stop the timer since we've completed the DELETE request - furi_timer_stop(fhttp.get_timeout_timer); - - if(fhttp.received_data) { - // uncomment if you want to save the received data to the external storage - // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data); - fhttp.started_receiving_delete = false; - fhttp.just_started_delete = false; - fhttp.state = IDLE; - return; - } else { - FURI_LOG_E(HTTP_TAG, "No data received."); - fhttp.started_receiving_delete = false; - fhttp.just_started_delete = false; - fhttp.state = IDLE; - return; - } - } - - // Append the new line to the existing data - if(fhttp.received_data == NULL) { - fhttp.received_data = - (char*)malloc(strlen(line) + 2); // +2 for newline and null terminator - if(fhttp.received_data) { - strcpy(fhttp.received_data, line); - fhttp.received_data[strlen(line)] = '\n'; // Add newline - fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator - } - } else { - size_t current_len = strlen(fhttp.received_data); - size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator - fhttp.received_data = (char*)realloc(fhttp.received_data, new_size); - if(fhttp.received_data) { - memcpy( - fhttp.received_data + current_len, - line, - strlen(line)); // Copy line at the end of the current data - fhttp.received_data[current_len + strlen(line)] = '\n'; // Add newline - fhttp.received_data[current_len + strlen(line) + 1] = '\0'; // Null terminator - } - } - - if(!fhttp.just_started_delete) { - fhttp.just_started_delete = true; - } - return; - } - - // Handle different types of responses - if(strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL) { - FURI_LOG_I(HTTP_TAG, "Operation succeeded."); - } else if(strstr(line, "[INFO]") != NULL) { - FURI_LOG_I(HTTP_TAG, "Received info: %s", line); - - if(fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL) { - fhttp.state = IDLE; - } - } else if(strstr(line, "[GET/SUCCESS]") != NULL) { - FURI_LOG_I(HTTP_TAG, "GET request succeeded."); - fhttp.started_receiving_get = true; - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - fhttp.received_data = NULL; - return; - } else if(strstr(line, "[POST/SUCCESS]") != NULL) { - FURI_LOG_I(HTTP_TAG, "POST request succeeded."); - fhttp.started_receiving_post = true; - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - fhttp.received_data = NULL; - return; - } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { - FURI_LOG_I(HTTP_TAG, "PUT request succeeded."); - fhttp.started_receiving_put = true; - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - fhttp.received_data = NULL; - return; - } else if(strstr(line, "[DELETE/SUCCESS]") != NULL) { - FURI_LOG_I(HTTP_TAG, "DELETE request succeeded."); - fhttp.started_receiving_delete = true; - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - fhttp.received_data = NULL; - return; - } else if(strstr(line, "[DISCONNECTED]") != NULL) { - FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully."); - } else if(strstr(line, "[ERROR]") != NULL) { - FURI_LOG_E(HTTP_TAG, "Received error: %s", line); - fhttp.state = ISSUE; - return; - } else if(strstr(line, "[PONG]") != NULL) { - FURI_LOG_I(HTTP_TAG, "Received PONG response: Wifi Dev Board is still alive."); - - // send command to connect to WiFi - if(fhttp.state == INACTIVE) { - fhttp.state = IDLE; - return; - } - } - - if(fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) { - fhttp.state = IDLE; - } else if(fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) { - fhttp.state = INACTIVE; - } else { - fhttp.state = IDLE; - } -} -// Function to save received data to a file -/** - * @brief Save the received data to a file. - * @return true if the data was saved successfully, false otherwise. - * @param bytes_received The number of bytes received. - * @param line_buffer The buffer containing the received data. - * @note The data will be saved to a file in the STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/received_data.txt" directory. - */ -bool flipper_http_save_received_data(size_t bytes_received, const char line_buffer[]) { - const char* output_file_path = STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag - "/received_data.txt"; - - // Ensure the directory exists - char directory_path[128]; - snprintf( - directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag); - - Storage* _storage = NULL; - File* _file = NULL; - // Open the storage if not opened already - // Initialize storage and create the directory if it doesn't exist - _storage = furi_record_open(RECORD_STORAGE); - storage_common_mkdir(_storage, directory_path); // Create directory if it doesn't exist - _file = storage_file_alloc(_storage); - - // Open file for writing and append data line by line - if(!storage_file_open(_file, output_file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { - FURI_LOG_E(HTTP_TAG, "Failed to open output file for writing."); - storage_file_free(_file); - furi_record_close(RECORD_STORAGE); - return false; - } - - // Write each line received from the UART to the file - if(bytes_received > 0 && _file) { - storage_file_write(_file, line_buffer, bytes_received); - storage_file_write(_file, "\n", 1); // Add a newline after each line - } else { - FURI_LOG_E(HTTP_TAG, "No data received."); - return false; - } - - if(_file) { - storage_file_close(_file); - storage_file_free(_file); - _file = NULL; - } - if(_storage) { - furi_record_close(RECORD_STORAGE); - _storage = NULL; - } - - return true; -} -// Function to trim leading and trailing spaces and newlines from a constant string -char* trim(const char* str) { - const char* end; - char* trimmed_str; - size_t len; - - // Trim leading space - while(isspace((unsigned char)*str)) - str++; - - // All spaces? - if(*str == 0) return strdup(""); // Return an empty string if all spaces - - // Trim trailing space - end = str + strlen(str) - 1; - while(end > str && isspace((unsigned char)*end)) - end--; - - // Set length for the trimmed string - len = end - str + 1; - - // Allocate space for the trimmed string and null terminator - trimmed_str = (char*)malloc(len + 1); - if(trimmed_str == NULL) { - return NULL; // Handle memory allocation failure - } - - // Copy the trimmed part of the string into trimmed_str - strncpy(trimmed_str, str, len); - trimmed_str[len] = '\0'; // Null terminate the string - - return trimmed_str; -} - -#endif // FLIPPER_HTTP_H diff --git a/flip_library/flipper_http.h b/flip_trader/flipper_http/flipper_http.c similarity index 89% rename from flip_library/flipper_http.h rename to flip_trader/flipper_http/flipper_http.c index 44b2c5ccb..ed3b216de 100644 --- a/flip_library/flipper_http.h +++ b/flip_trader/flipper_http/flipper_http.c @@ -1,137 +1,7 @@ -// flipper_http.h -#ifndef FLIPPER_HTTP_H -#define FLIPPER_HTTP_H - -#include -#include -#include -#include -#include - -// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext - -#define HTTP_TAG "FlipLibrary" // change this to your app name -#define http_tag "flip_library" // change this to your app id -#define UART_CH (FuriHalSerialIdUsart) // UART channel -#define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds -#define BAUDRATE (115200) // UART baudrate -#define RX_BUF_SIZE 1024 // UART RX buffer size -#define RX_LINE_BUFFER_SIZE 4096 // UART RX line buffer size (increase for large responses) -#define MAX_FILE_SHOW 4096 // Maximum data from file to show - -// Forward declaration for callback -typedef void (*FlipperHTTP_Callback)(const char* line, void* context); - -// Functions -bool flipper_http_init(FlipperHTTP_Callback callback, void* context); -void flipper_http_deinit(); -//--- -void flipper_http_rx_callback(const char* line, void* context); -bool flipper_http_send_data(const char* data); -//--- -bool flipper_http_connect_wifi(); -bool flipper_http_disconnect_wifi(); -bool flipper_http_ping(); -bool flipper_http_scan_wifi(); -bool flipper_http_save_wifi(const char* ssid, const char* password); -bool flipper_http_ip_wifi(); -bool flipper_http_ip_address(); -//--- -bool flipper_http_list_commands(); -bool flipper_http_led_on(); -bool flipper_http_led_off(); -bool flipper_http_parse_json(const char* key, const char* json_data); -bool flipper_http_parse_json_array(const char* key, int index, const char* json_data); -//--- -bool flipper_http_get_request(const char* url); -bool flipper_http_get_request_with_headers(const char* url, const char* headers); -bool flipper_http_post_request_with_headers( - const char* url, - const char* headers, - const char* payload); -bool flipper_http_put_request_with_headers( - const char* url, - const char* headers, - const char* payload); -bool flipper_http_delete_request_with_headers( - const char* url, - const char* headers, - const char* payload); -//--- -bool flipper_http_get_request_bytes(const char* url, const char* headers); -bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload); -// -bool flipper_http_append_to_file( - const void* data, - size_t data_size, - bool start_new_file, - char* file_path); - -FuriString* flipper_http_load_from_file(char* file_path); -static char* trim(const char* str); -// -bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)); - -// State variable to track the UART state -typedef enum { - INACTIVE, // Inactive state - IDLE, // Default state - RECEIVING, // Receiving data - SENDING, // Sending data - ISSUE, // Issue with connection -} SerialState; - -// Event Flags for UART Worker Thread -typedef enum { - WorkerEvtStop = (1 << 0), - WorkerEvtRxDone = (1 << 1), -} WorkerEvtFlags; - -// FlipperHTTP Structure -typedef struct { - FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication - FuriHalSerialHandle* serial_handle; // Serial handle for UART communication - FuriThread* rx_thread; // Worker thread for UART - FuriThreadId rx_thread_id; // Worker thread ID - FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines - void* callback_context; // Context for the callback - SerialState state; // State of the UART - - // variable to store the last received data from the UART - char* last_response; - char file_path[256]; // Path to save the received data - - // Timer-related members - FuriTimer* get_timeout_timer; // Timer for HTTP request timeout - - bool started_receiving_get; // Indicates if a GET request has started - bool just_started_get; // Indicates if GET data reception has just started - - bool started_receiving_post; // Indicates if a POST request has started - bool just_started_post; // Indicates if POST data reception has just started - - bool started_receiving_put; // Indicates if a PUT request has started - bool just_started_put; // Indicates if PUT data reception has just started - - bool started_receiving_delete; // Indicates if a DELETE request has started - bool just_started_delete; // Indicates if DELETE data reception has just started - - // Buffer to hold the raw bytes received from the UART - uint8_t* received_bytes; - size_t received_bytes_len; // Length of the received bytes - bool is_bytes_request; // Flag to indicate if the request is for bytes - bool save_bytes; // Flag to save the received data to a file - bool save_received_data; // Flag to save the received data to a file -} FlipperHTTP; - -static FlipperHTTP fhttp; -// Global static array for the line buffer -static char rx_line_buffer[RX_LINE_BUFFER_SIZE]; -#define FILE_BUFFER_SIZE 512 -static uint8_t file_buffer[FILE_BUFFER_SIZE]; - -// fhttp.last_response holds the last received data from the UART - +#include +FlipperHTTP fhttp; +char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +uint8_t file_buffer[FILE_BUFFER_SIZE]; // Function to append received data to file // make sure to initialize the file path before calling this function bool flipper_http_append_to_file( @@ -143,6 +13,15 @@ bool flipper_http_append_to_file( File* file = storage_file_alloc(storage); if(start_new_file) { + // Delete the file if it already exists + if(storage_file_exists(storage, file_path)) { + if(!storage_simply_remove_recursive(storage, file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to delete file: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + } // Open the file in write mode if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path); @@ -258,7 +137,7 @@ FuriString* flipper_http_load_from_file(char* file_path) { * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -static int32_t flipper_http_worker(void* context) { +int32_t flipper_http_worker(void* context) { UNUSED(context); size_t rx_line_pos = 0; static size_t file_buffer_len = 0; @@ -286,10 +165,14 @@ static int32_t flipper_http_worker(void* context) { // Write to file if buffer is full if(file_buffer_len >= FILE_BUFFER_SIZE) { if(!flipper_http_append_to_file( - file_buffer, file_buffer_len, false, fhttp.file_path)) { + file_buffer, + file_buffer_len, + fhttp.just_started_bytes, + fhttp.file_path)) { FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); } file_buffer_len = 0; + fhttp.just_started_bytes = false; } } @@ -302,8 +185,6 @@ static int32_t flipper_http_worker(void* context) { // Invoke the callback with the complete line fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context); - // save the received data - // Reset the line buffer position rx_line_pos = 0; } else { @@ -376,7 +257,7 @@ void get_timeout_timer_callback(void* context) { * @param context The context to pass to the callback. * @note This function will handle received data asynchronously via the callback. */ -static void _flipper_http_rx_callback( +void _flipper_http_rx_callback( FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* context) { @@ -550,13 +431,13 @@ bool flipper_http_send_data(const char* data) { // Create a buffer with data + '\n' size_t send_length = data_length + 1; // +1 for '\n' - if(send_length > 256) { // Ensure buffer size is sufficient + if(send_length > 512) { // Ensure buffer size is sufficient FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP."); return false; } - char send_buffer[257]; // 256 + 1 for safety - strncpy(send_buffer, data, 256); + char send_buffer[513]; // 512 + 1 for safety + strncpy(send_buffer, data, 512); send_buffer[data_length] = '\n'; // Append newline send_buffer[data_length + 1] = '\0'; // Null-terminate @@ -874,7 +755,7 @@ bool flipper_http_get_request_with_headers(const char* url, const char* headers) } // Prepare GET request command with headers - char command[256]; + char command[512]; int ret = snprintf( command, sizeof(command), "[GET/HTTP]{\"url\":\"%s\",\"headers\":%s}", url, headers); if(ret < 0 || ret >= (int)sizeof(command)) { @@ -1281,6 +1162,7 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.state = RECEIVING; // for GET request, save data only if it's a bytes request fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; return; } else if(strstr(line, "[POST/SUCCESS]") != NULL) { FURI_LOG_I(HTTP_TAG, "POST request succeeded."); @@ -1289,6 +1171,7 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.state = RECEIVING; // for POST request, save data only if it's a bytes request fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; return; } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { FURI_LOG_I(HTTP_TAG, "PUT request succeeded."); @@ -1388,5 +1271,3 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars } return true; } - -#endif // FLIPPER_HTTP_H diff --git a/flip_trader/flipper_http/flipper_http.h b/flip_trader/flipper_http/flipper_http.h new file mode 100644 index 000000000..6b8f9e068 --- /dev/null +++ b/flip_trader/flipper_http/flipper_http.h @@ -0,0 +1,363 @@ +// flipper_http.h +#ifndef FLIPPER_HTTP_H +#define FLIPPER_HTTP_H + +#include +#include +#include +#include +#include +#include + +// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext + +#define HTTP_TAG "FlipTrader" // change this to your app name +#define http_tag "flip_trader" // change this to your app id +#define UART_CH (momentum_settings.uart_esp_channel) // UART channel +#define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds +#define BAUDRATE (115200) // UART baudrate +#define RX_BUF_SIZE 1024 // UART RX buffer size +#define RX_LINE_BUFFER_SIZE 4096 // UART RX line buffer size (increase for large responses) +#define MAX_FILE_SHOW 4096 // Maximum data from file to show +#define FILE_BUFFER_SIZE 512 // File buffer size + +// Forward declaration for callback +typedef void (*FlipperHTTP_Callback)(const char* line, void* context); + +// State variable to track the UART state +typedef enum { + INACTIVE, // Inactive state + IDLE, // Default state + RECEIVING, // Receiving data + SENDING, // Sending data + ISSUE, // Issue with connection +} SerialState; + +// Event Flags for UART Worker Thread +typedef enum { + WorkerEvtStop = (1 << 0), + WorkerEvtRxDone = (1 << 1), +} WorkerEvtFlags; + +// FlipperHTTP Structure +typedef struct { + FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication + FuriHalSerialHandle* serial_handle; // Serial handle for UART communication + FuriThread* rx_thread; // Worker thread for UART + FuriThreadId rx_thread_id; // Worker thread ID + FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines + void* callback_context; // Context for the callback + SerialState state; // State of the UART + + // variable to store the last received data from the UART + char* last_response; + char file_path[256]; // Path to save the received data + + // Timer-related members + FuriTimer* get_timeout_timer; // Timer for HTTP request timeout + + bool started_receiving_get; // Indicates if a GET request has started + bool just_started_get; // Indicates if GET data reception has just started + + bool started_receiving_post; // Indicates if a POST request has started + bool just_started_post; // Indicates if POST data reception has just started + + bool started_receiving_put; // Indicates if a PUT request has started + bool just_started_put; // Indicates if PUT data reception has just started + + bool started_receiving_delete; // Indicates if a DELETE request has started + bool just_started_delete; // Indicates if DELETE data reception has just started + + // Buffer to hold the raw bytes received from the UART + uint8_t* received_bytes; + size_t received_bytes_len; // Length of the received bytes + bool is_bytes_request; // Flag to indicate if the request is for bytes + bool save_bytes; // Flag to save the received data to a file + bool save_received_data; // Flag to save the received data to a file + + bool just_started_bytes; // Indicates if bytes data reception has just started +} FlipperHTTP; + +extern FlipperHTTP fhttp; +// Global static array for the line buffer +extern char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +extern uint8_t file_buffer[FILE_BUFFER_SIZE]; + +// fhttp.last_response holds the last received data from the UART + +// Function to append received data to file +// make sure to initialize the file path before calling this function +bool flipper_http_append_to_file( + const void* data, + size_t data_size, + bool start_new_file, + char* file_path); + +FuriString* flipper_http_load_from_file(char* file_path); + +// UART worker thread +/** + * @brief Worker thread to handle UART data asynchronously. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +// UART worker thread +int32_t flipper_http_worker(void* context); + +// Timer callback function +/** + * @brief Callback function for the GET timeout timer. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will be called when the GET request times out. + */ +void get_timeout_timer_callback(void* context); + +// UART RX Handler Callback (Interrupt Context) +/** + * @brief A private callback function to handle received data asynchronously. + * @return void + * @param handle The UART handle. + * @param event The event type. + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +void _flipper_http_rx_callback( + FuriHalSerialHandle* handle, + FuriHalSerialRxEvent event, + void* context); + +// UART initialization function +/** + * @brief Initialize UART. + * @return true if the UART was initialized successfully, false otherwise. + * @param callback The callback function to handle received data (ex. flipper_http_rx_callback). + * @param context The context to pass to the callback. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_init(FlipperHTTP_Callback callback, void* context); + +// Deinitialize UART +/** + * @brief Deinitialize UART. + * @return void + * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. + */ +void flipper_http_deinit(); + +// Function to send data over UART with newline termination +/** + * @brief Send data over UART with newline termination. + * @return true if the data was sent successfully, false otherwise. + * @param data The data to send over UART. + * @note The data will be sent over UART with a newline character appended. + */ +bool flipper_http_send_data(const char* data); + +// Function to send a PING request +/** + * @brief Send a PING request to check if the Wifi Dev Board is connected. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + * @note This is best used to check if the Wifi Dev Board is connected. + * @note The state will remain INACTIVE until a PONG is received. + */ +bool flipper_http_ping(); + +// Function to list available commands +/** + * @brief Send a command to list available commands. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_list_commands(); + +// Function to turn on the LED +/** + * @brief Allow the LED to display while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_on(); + +// Function to turn off the LED +/** + * @brief Disable the LED from displaying while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_off(); + +// Function to parse JSON data +/** + * @brief Parse JSON data. + * @return true if the JSON data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON data. + * @param json_data The JSON data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json(const char* key, const char* json_data); + +// Function to parse JSON array data +/** + * @brief Parse JSON array data. + * @return true if the JSON array data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON array data. + * @param index The index to parse from the JSON array data. + * @param json_data The JSON array data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json_array(const char* key, int index, const char* json_data); + +// Function to scan for WiFi networks +/** + * @brief Send a command to scan for WiFi networks. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_scan_wifi(); + +// Function to save WiFi settings (returns true if successful) +/** + * @brief Send a command to save WiFi settings. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_save_wifi(const char* ssid, const char* password); + +// Function to get IP address of WiFi Devboard +/** + * @brief Send a command to get the IP address of the WiFi Devboard + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_address(); + +// Function to get IP address of the connected WiFi network +/** + * @brief Send a command to get the IP address of the connected WiFi network. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_wifi(); + +// Function to disconnect from WiFi (returns true if successful) +/** + * @brief Send a command to disconnect from WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_disconnect_wifi(); + +// Function to connect to WiFi (returns true if successful) +/** + * @brief Send a command to connect to WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_connect_wifi(); + +// Function to send a GET request +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request(const char* url); + +// Function to send a GET request with headers +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_with_headers(const char* url, const char* headers); + +// Function to send a GET request with headers and return bytes +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_bytes(const char* url, const char* headers); + +// Function to send a POST request with headers +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param data The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to send a POST request with headers and return bytes +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param payload The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload); + +// Function to send a PUT request with headers +/** + * @brief Send a PUT request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the PUT request to. + * @param headers The headers to send with the PUT request. + * @param data The data to send with the PUT request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_put_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to send a DELETE request with headers +/** + * @brief Send a DELETE request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the DELETE request to. + * @param headers The headers to send with the DELETE request. + * @param data The data to send with the DELETE request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_delete_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to handle received data asynchronously +/** + * @brief Callback function to handle received data asynchronously. + * @return void + * @param line The received line. + * @param context The context passed to the callback. + * @note The received data will be handled asynchronously via the callback and handles the state of the UART. + */ +void flipper_http_rx_callback(const char* line, void* context); + +// Function to trim leading and trailing spaces and newlines from a constant string +char* trim(const char* str); +/** + * @brief Process requests and parse JSON data asynchronously + * @param http_request The function to send the request + * @param parse_json The function to parse the JSON + * @return true if successful, false otherwise + */ +bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)); + +#endif // FLIPPER_HTTP_H diff --git a/flip_library/jsmn.h b/flip_trader/jsmn/jsmn.c similarity index 83% rename from flip_library/jsmn.h rename to flip_trader/jsmn/jsmn.c index b4e3098e5..eb33b3cc7 100644 --- a/flip_library/jsmn.h +++ b/flip_trader/jsmn/jsmn.c @@ -3,113 +3,20 @@ * * Copyright (c) 2010 Serge Zaitsev * - * 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. + * [License text continues...] */ -#ifndef JSMN_H -#define JSMN_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef JSMN_STATIC -#define JSMN_API static -#else -#define JSMN_API extern -#endif - -/** - * JSON type identifier. Basic types are: - * o Object - * o Array - * o String - * o Other primitive: number, boolean (true/false) or null - */ -typedef enum { - JSMN_UNDEFINED = 0, - JSMN_OBJECT = 1 << 0, - JSMN_ARRAY = 1 << 1, - JSMN_STRING = 1 << 2, - JSMN_PRIMITIVE = 1 << 3 -} jsmntype_t; - -enum jsmnerr { - /* Not enough tokens were provided */ - JSMN_ERROR_NOMEM = -1, - /* Invalid character inside JSON string */ - JSMN_ERROR_INVAL = -2, - /* The string is not a full JSON packet, more bytes expected */ - JSMN_ERROR_PART = -3 -}; -/** - * JSON token description. - * type type (object, array, string etc.) - * start start position in JSON data string - * end end position in JSON data string - */ -typedef struct jsmntok { - jsmntype_t type; - int start; - int end; - int size; -#ifdef JSMN_PARENT_LINKS - int parent; -#endif -} jsmntok_t; - -/** - * JSON parser. Contains an array of token blocks available. Also stores - * the string being parsed now and current position in that string. - */ -typedef struct jsmn_parser { - unsigned int pos; /* offset in the JSON string */ - unsigned int toknext; /* next token to allocate */ - int toksuper; /* superior token node, e.g. parent object or array */ -} jsmn_parser; - -/** - * Create JSON parser over an array of tokens - */ -JSMN_API void jsmn_init(jsmn_parser* parser); - -/** - * Run JSON parser. It parses a JSON data string into and array of tokens, each - * describing - * a single JSON object. - */ -JSMN_API int jsmn_parse( - jsmn_parser* parser, - const char* js, - const size_t len, - jsmntok_t* tokens, - const unsigned int num_tokens); +#include +#include +#include -#ifndef JSMN_HEADER /** - * Allocates a fresh unused token from the token pool. - */ + * Allocates a fresh unused token from the token pool. + */ static jsmntok_t* jsmn_alloc_token(jsmn_parser* parser, jsmntok_t* tokens, const size_t num_tokens) { jsmntok_t* tok; + if(parser->toknext >= num_tokens) { return NULL; } @@ -123,8 +30,8 @@ static jsmntok_t* } /** - * Fills token type and boundaries. - */ + * Fills token type and boundaries. + */ static void jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { token->type = type; @@ -134,8 +41,8 @@ static void } /** - * Fills next available token with JSON primitive. - */ + * Fills next available token with JSON primitive. + */ static int jsmn_parse_primitive( jsmn_parser* parser, const char* js, @@ -195,8 +102,8 @@ static int jsmn_parse_primitive( } /** - * Fills next token with JSON string. - */ + * Fills next token with JSON string. + */ static int jsmn_parse_string( jsmn_parser* parser, const char* js, @@ -272,9 +179,18 @@ static int jsmn_parse_string( } /** - * Parse JSON string and fill tokens. - */ -JSMN_API int jsmn_parse( + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser* parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +/** + * Parse JSON string and fill tokens. + */ +int jsmn_parse( jsmn_parser* parser, const char* js, const size_t len, @@ -464,33 +380,16 @@ JSMN_API int jsmn_parse( return count; } -/** - * Creates a new parser based over a given buffer with an array of tokens - * available. - */ -JSMN_API void jsmn_init(jsmn_parser* parser) { - parser->pos = 0; - parser->toknext = 0; - parser->toksuper = -1; -} - -#endif /* JSMN_HEADER */ - -#ifdef __cplusplus +// Helper function to create a JSON object +char* jsmn(const char* key, const char* value) { + int length = strlen(key) + strlen(value) + 8; // Calculate required length + char* result = (char*)malloc(length * sizeof(char)); // Allocate memory + if(result == NULL) { + return NULL; // Handle memory allocation failure + } + snprintf(result, length, "{\"%s\":\"%s\"}", key, value); + return result; // Caller is responsible for freeing this memory } -#endif - -#endif /* JSMN_H */ - -#ifndef JB_JSMN_EDIT -#define JB_JSMN_EDIT -/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ - -#include -#include -#include -#include -#include // Helper function to compare JSON keys int jsoneq(const char* json, jsmntok_t* tok, const char* s) { @@ -501,7 +400,7 @@ int jsoneq(const char* json, jsmntok_t* tok, const char* s) { return -1; } -// return the value of the key in the JSON data +// Return the value of the key in the JSON data char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { // Parse the JSON feed if(json_data != NULL) { @@ -765,5 +664,3 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in free(array_str); return values; } - -#endif /* JB_JSMN_EDIT */ diff --git a/flip_trader/jsmn/jsmn.h b/flip_trader/jsmn/jsmn.h new file mode 100644 index 000000000..cd95a0e58 --- /dev/null +++ b/flip_trader/jsmn/jsmn.h @@ -0,0 +1,131 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * 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: + * + * [License text continues...] + */ + +#ifndef JSMN_H +#define JSMN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser* parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing a single JSON object. + */ +JSMN_API int jsmn_parse( + jsmn_parser* parser, + const char* js, + const size_t len, + jsmntok_t* tokens, + const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/* Implementation has been moved to jsmn.c */ +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */ + +/* Custom Helper Functions */ +#ifndef JB_JSMN_EDIT +#define JB_JSMN_EDIT +/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ + +#include +#include +#include +#include +#include + +// Helper function to create a JSON object +char* jsmn(const char* key, const char* value); +// Helper function to compare JSON keys +int jsoneq(const char* json, jsmntok_t* tok, const char* s); + +// Return the value of the key in the JSON data +char* get_json_value(char* key, char* json_data, uint32_t max_tokens); + +// Revised get_json_array_value function +char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t max_tokens); + +// Revised get_json_array_values function with correct token skipping +char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, int* num_values); +#endif /* JB_JSMN_EDIT */ diff --git a/flip_weather/.DS_Store b/flip_weather/.DS_Store deleted file mode 100644 index 41af2b6c1..000000000 Binary files a/flip_weather/.DS_Store and /dev/null differ diff --git a/flip_weather/CHANGELOG.md b/flip_weather/CHANGELOG.md index 3691e805e..4070ec96c 100644 --- a/flip_weather/CHANGELOG.md +++ b/flip_weather/CHANGELOG.md @@ -1,3 +1,8 @@ +## v1.2 +Updates from Derek Jamison: +- Improved progress display. +- Added connectivity check on startup. + ## 1.1 - Improved memory allocation. - Updated WiFi configuration. diff --git a/flip_weather/README.md b/flip_weather/README.md index f6ac20620..99da00b3c 100644 --- a/flip_weather/README.md +++ b/flip_weather/README.md @@ -39,5 +39,5 @@ FlipWeather automatically allocates necessary resources and initializes settings ## Known Issues -1. **GPS Screen Delay**: Occasionally, the GPS screen may get stuck on "Loading Data" or take up to 30 seconds to display information. - - **Solution**: Restart your Flipper Zero if this occurs. \ No newline at end of file +1. **GPS Screen Delay**: Occasionally, the GPS or Weather screen may get stuck on "Loading Data". + - If it takes longer than 10 seconds, restart your Flipper Zero. \ No newline at end of file diff --git a/flip_weather/flip_weather_i.h b/flip_weather/alloc/flip_weather_alloc.c similarity index 89% rename from flip_weather/flip_weather_i.h rename to flip_weather/alloc/flip_weather_alloc.c index 67b6cc3c8..8cebbc983 100644 --- a/flip_weather/flip_weather_i.h +++ b/flip_weather/alloc/flip_weather_alloc.c @@ -1,8 +1,7 @@ -#ifndef FLIP_WEATHER_I_H -#define FLIP_WEATHER_I_H +#include // Function to allocate resources for the FlipWeatherApp -static FlipWeatherApp* flip_weather_app_alloc() { +FlipWeatherApp* flip_weather_app_alloc() { FlipWeatherApp* app = (FlipWeatherApp*)malloc(sizeof(FlipWeatherApp)); Gui* gui = furi_record_open(RECORD_GUI); @@ -38,34 +37,34 @@ static FlipWeatherApp* flip_weather_app_alloc() { if(!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) { return NULL; } - + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, flip_weather_custom_event_callback); // Main view if(!easy_flipper_set_view( - &app->view_weather, - FlipWeatherViewWeather, - flip_weather_view_draw_callback_weather, - NULL, - callback_to_submenu, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_view( - &app->view_gps, - FlipWeatherViewGPS, - flip_weather_view_draw_callback_gps, + &app->view_loader, + FlipWeatherViewLoader, + flip_weather_loader_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app)) { return NULL; } + flip_weather_loader_init(app->view_loader); // Widget if(!easy_flipper_set_widget( &app->widget, FlipWeatherViewAbout, - "FlipWeather v1.1\n-----\nUse WiFi to get GPS and \nWeather information.\n-----\nwww.github.com/jblanked", + "FlipWeather v1.2\n-----\nUse WiFi to get GPS and \nWeather information.\n-----\nwww.github.com/jblanked", + callback_to_submenu, + &app->view_dispatcher)) { + return NULL; + } + if(!easy_flipper_set_widget( + &app->widget_result, + FlipWeatherViewWidgetResult, + "Error, try again.", callback_to_submenu, &app->view_dispatcher)) { return NULL; @@ -118,7 +117,7 @@ static FlipWeatherApp* flip_weather_app_alloc() { if(!easy_flipper_set_submenu( &app->submenu, FlipWeatherViewSubmenu, - "FlipWeather v1.1", + "FlipWeather v1.2", callback_exit_app, &app->view_dispatcher)) { return NULL; @@ -168,5 +167,3 @@ static FlipWeatherApp* flip_weather_app_alloc() { return app; } - -#endif diff --git a/flip_weather/alloc/flip_weather_alloc.h b/flip_weather/alloc/flip_weather_alloc.h new file mode 100644 index 000000000..3fe0b5f66 --- /dev/null +++ b/flip_weather/alloc/flip_weather_alloc.h @@ -0,0 +1,8 @@ +#ifndef FLIP_WEATHER_I_H +#define FLIP_WEATHER_I_H +#include +#include +#include +FlipWeatherApp* flip_weather_app_alloc(); + +#endif diff --git a/flip_weather/app.c b/flip_weather/app.c index 0b02e13dc..f827e3173 100644 --- a/flip_weather/app.c +++ b/flip_weather/app.c @@ -1,9 +1,5 @@ -#include -#include -#include "flip_weather_parse.h" -#include -#include -#include +#include +#include // Entry point for the FlipWeather application int32_t flip_weather_app(void* p) { @@ -11,8 +7,8 @@ int32_t flip_weather_app(void* p) { UNUSED(p); // Initialize the FlipWeather application - FlipWeatherApp* app = flip_weather_app_alloc(); - if(!app) { + app_instance = flip_weather_app_alloc(); + if(!app_instance) { FURI_LOG_E(TAG, "Failed to allocate FlipWeatherApp"); return -1; } @@ -22,11 +18,39 @@ int32_t flip_weather_app(void* p) { return -1; } + // Thanks to Derek Jamison for the following edits + if(app_instance->uart_text_input_buffer_ssid != NULL && + app_instance->uart_text_input_buffer_password != NULL) { + // Try to wait for pong response. + uint8_t counter = 10; + while(fhttp.state == INACTIVE && --counter > 0) { + FURI_LOG_D(TAG, "Waiting for PONG"); + furi_delay_ms(100); + } + + if(counter == 0) { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header( + message, "[FlipperHTTP Error]", 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + message, + "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.", + 0, + 63, + AlignLeft, + AlignBottom); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); + } + } + // Run the view dispatcher - view_dispatcher_run(app->view_dispatcher); + view_dispatcher_run(app_instance->view_dispatcher); // Free the resources used by the FlipWeather application - flip_weather_app_free(app); + flip_weather_app_free(app_instance); // Return 0 to indicate success return 0; diff --git a/flip_weather/application.fam b/flip_weather/application.fam index 6e7dd82e7..d61bf2533 100644 --- a/flip_weather/application.fam +++ b/flip_weather/application.fam @@ -10,5 +10,5 @@ App( fap_description="Use WiFi to get GPS and Weather information on your Flipper Zero.", fap_author="JBlanked", fap_weburl="https://github.com/jblanked/FlipWeather", - fap_version="1.1", + fap_version="1.2", ) diff --git a/flip_weather/assets/01-home.png b/flip_weather/assets/01-home.png index d4ed0a5aa..409e3cdc5 100644 Binary files a/flip_weather/assets/01-home.png and b/flip_weather/assets/01-home.png differ diff --git a/flip_weather/assets/02-weather.png b/flip_weather/assets/02-weather.png index 80a89bf22..bab7c7c67 100644 Binary files a/flip_weather/assets/02-weather.png and b/flip_weather/assets/02-weather.png differ diff --git a/flip_weather/callback/flip_weather_callback.c b/flip_weather/callback/flip_weather_callback.c new file mode 100644 index 000000000..77252ad5e --- /dev/null +++ b/flip_weather/callback/flip_weather_callback.c @@ -0,0 +1,661 @@ +#include "callback/flip_weather_callback.h" + +// Below added by Derek Jamison +// FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port. +#ifdef DEVELOPMENT +#define FURI_LOG_DEV(tag, format, ...) \ + furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__) +#define DEV_CRASH() furi_crash() +#else +#define FURI_LOG_DEV(tag, format, ...) +#define DEV_CRASH() +#endif + +bool weather_request_success = false; +bool sent_weather_request = false; +bool got_weather_data = false; + +static void flip_weather_request_error_draw(Canvas* canvas) { + if(canvas == NULL) { + FURI_LOG_E(TAG, "flip_weather_request_error_draw - canvas is NULL"); + DEV_CRASH(); + return; + } + if(fhttp.last_response != NULL) { + if(strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != + NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } else if( + strstr(fhttp.last_response, "[ERROR] GET request failed or returned empty data.") != + NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] WiFi error."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } else if(strstr(fhttp.last_response, "[PONG]") != NULL) { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[STATUS]Connecting to AP..."); + } else { + canvas_clear(canvas); + FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response); + canvas_draw_str(canvas, 0, 10, "[ERROR] Unusual error..."); + canvas_draw_str(canvas, 0, 60, "Press BACK and retry."); + } + } else { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } +} +static void flip_weather_gps_switch_to_view(FlipWeatherApp* app) { + flip_weather_generic_switch_to_view( + app, + "Fetching GPS data..", + send_geo_location_request, + process_geo_location, + 1, + callback_to_submenu, + FlipWeatherViewLoader); +} + +static void flip_weather_weather_switch_to_view(FlipWeatherApp* app) { + flip_weather_generic_switch_to_view( + app, + "Fetching Weather data..", + send_geo_weather_request, + process_weather, + 1, + callback_to_submenu, + FlipWeatherViewLoader); +} + +void callback_submenu_choices(void* context, uint32_t index) { + FlipWeatherApp* app = (FlipWeatherApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); + return; + } + switch(index) { + case FlipWeatherSubmenuIndexWeather: + flipper_http_loading_task( + send_geo_location_request, + process_geo_location_2, + FlipWeatherViewSubmenu, + FlipWeatherViewSubmenu, + &app->view_dispatcher); + flip_weather_weather_switch_to_view(app); + break; + case FlipWeatherSubmenuIndexGPS: + flip_weather_gps_switch_to_view(app); + break; + case FlipWeatherSubmenuIndexAbout: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewAbout); + break; + case FlipWeatherSubmenuIndexSettings: + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSettings); + break; + default: + break; + } +} + +void text_updated_ssid(void* context) { + FlipWeatherApp* app = (FlipWeatherApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); + return; + } + + // store the entered text + strncpy( + app->uart_text_input_buffer_ssid, + app->uart_text_input_temp_buffer_ssid, + app->uart_text_input_buffer_size_ssid); + + // Ensure null-termination + app->uart_text_input_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; + + // update the variable item text + if(app->variable_item_ssid) { + variable_item_set_current_value_text( + app->variable_item_ssid, app->uart_text_input_buffer_ssid); + } + + // save settings + save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); + + // save wifi settings to devboard + if(strlen(app->uart_text_input_buffer_ssid) > 0 && + strlen(app->uart_text_input_buffer_password) > 0) { + if(!flipper_http_save_wifi( + app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { + FURI_LOG_E(TAG, "Failed to save wifi settings"); + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSettings); +} + +void text_updated_password(void* context) { + FlipWeatherApp* app = (FlipWeatherApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); + return; + } + + // store the entered text + strncpy( + app->uart_text_input_buffer_password, + app->uart_text_input_temp_buffer_password, + app->uart_text_input_buffer_size_password); + + // Ensure null-termination + app->uart_text_input_buffer_password[app->uart_text_input_buffer_size_password - 1] = '\0'; + + // update the variable item text + if(app->variable_item_password) { + variable_item_set_current_value_text( + app->variable_item_password, app->uart_text_input_buffer_password); + } + + // save settings + save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); + + // save wifi settings to devboard + if(strlen(app->uart_text_input_buffer_ssid) > 0 && + strlen(app->uart_text_input_buffer_password) > 0) { + if(!flipper_http_save_wifi( + app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { + FURI_LOG_E(TAG, "Failed to save wifi settings"); + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSettings); +} + +uint32_t callback_to_submenu(void* context) { + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + sent_get_request = false; + get_request_success = false; + got_ip_address = false; + got_weather_data = false; + geo_information_processed = false; + weather_information_processed = false; + sent_weather_request = false; + weather_request_success = false; + if(weather_data != NULL) { + free(weather_data); + weather_data = NULL; + } + if(total_data != NULL) { + free(total_data); + total_data = NULL; + } + return FlipWeatherViewSubmenu; +} + +void settings_item_selected(void* context, uint32_t index) { + FlipWeatherApp* app = (FlipWeatherApp*)context; + if(!app) { + FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); + return; + } + switch(index) { + case 0: // Input SSID + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewTextInputSSID); + break; + case 1: // Input Password + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewTextInputPassword); + break; + default: + FURI_LOG_E(TAG, "Unknown configuration item index"); + break; + } +} + +/** + * @brief Navigation callback for exiting the application + * @param context The context - unused + * @return next view id (VIEW_NONE to exit the app) + */ +uint32_t callback_exit_app(void* context) { + // Exit the application + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + return VIEW_NONE; // Return VIEW_NONE to exit the app +} + +uint32_t callback_to_wifi_settings(void* context) { + if(!context) { + FURI_LOG_E(TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + return FlipWeatherViewSettings; +} + +static void flip_weather_widget_set_text(char* message, Widget** widget) { + if(widget == NULL) { + FURI_LOG_E(TAG, "flip_weather_set_widget_text - widget is NULL"); + DEV_CRASH(); + return; + } + if(message == NULL) { + FURI_LOG_E(TAG, "flip_weather_set_widget_text - message is NULL"); + DEV_CRASH(); + return; + } + widget_reset(*widget); + + uint32_t message_length = strlen(message); // Length of the message + uint32_t i = 0; // Index tracker + uint32_t formatted_index = 0; // Tracker for where we are in the formatted message + char* formatted_message; // Buffer to hold the final formatted message + + // Allocate buffer with double the message length plus one for safety + if(!easy_flipper_set_buffer(&formatted_message, message_length * 2 + 1)) { + return; + } + + while(i < message_length) { + uint32_t max_line_length = 31; // Maximum characters per line + uint32_t remaining_length = message_length - i; // Remaining characters + uint32_t line_length = (remaining_length < max_line_length) ? remaining_length : + max_line_length; + + // Check for newline character within the current segment + uint32_t newline_pos = i; + bool found_newline = false; + for(; newline_pos < i + line_length && newline_pos < message_length; newline_pos++) { + if(message[newline_pos] == '\n') { + found_newline = true; + break; + } + } + + if(found_newline) { + // If newline found, set line_length up to the newline + line_length = newline_pos - i; + } + + // Temporary buffer to hold the current line + char line[32]; + strncpy(line, message + i, line_length); + line[line_length] = '\0'; + + // If newline was found, skip it for the next iteration + if(found_newline) { + i += line_length + 1; // +1 to skip the '\n' character + } else { + // Check if the line ends in the middle of a word and adjust accordingly + if(line_length == max_line_length && message[i + line_length] != '\0' && + message[i + line_length] != ' ') { + // Find the last space within the current line to avoid breaking a word + char* last_space = strrchr(line, ' '); + if(last_space != NULL) { + // Adjust the line_length to avoid cutting the word + line_length = last_space - line; + line[line_length] = '\0'; // Null-terminate at the space + } + } + + // Move the index forward by the determined line_length + i += line_length; + + // Skip any spaces at the beginning of the next line + while(i < message_length && message[i] == ' ') { + i++; + } + } + + // Manually copy the fixed line into the formatted_message buffer + for(uint32_t j = 0; j < line_length; j++) { + formatted_message[formatted_index++] = line[j]; + } + + // Add a newline character for line spacing + formatted_message[formatted_index++] = '\n'; + } + + // Null-terminate the formatted_message + formatted_message[formatted_index] = '\0'; + + // Add the formatted message to the widget + widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message); +} + +void flip_weather_loader_draw_callback(Canvas* canvas, void* model) { + if(!canvas || !model) { + FURI_LOG_E(TAG, "flip_weather_loader_draw_callback - canvas or model is NULL"); + return; + } + + SerialState http_state = fhttp.state; + DataLoaderModel* data_loader_model = (DataLoaderModel*)model; + DataState data_state = data_loader_model->data_state; + char* title = data_loader_model->title; + + canvas_set_font(canvas, FontSecondary); + + if(http_state == INACTIVE) { + canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); + canvas_draw_str(canvas, 0, 17, "Please connect to the board."); + canvas_draw_str(canvas, 0, 32, "If your board is connected,"); + canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); + canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); + canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); + return; + } + + if(data_state == DataStateError || data_state == DataStateParseError) { + flip_weather_request_error_draw(canvas); + return; + } + + canvas_draw_str(canvas, 0, 7, title); + canvas_draw_str(canvas, 0, 17, "Loading..."); + + if(data_state == DataStateInitial) { + return; + } + + if(http_state == SENDING) { + canvas_draw_str(canvas, 0, 27, "Sending..."); + return; + } + + if(http_state == RECEIVING || data_state == DataStateRequested) { + canvas_draw_str(canvas, 0, 27, "Receiving..."); + return; + } + + if(http_state == IDLE && data_state == DataStateReceived) { + canvas_draw_str(canvas, 0, 27, "Processing..."); + return; + } + + if(http_state == IDLE && data_state == DataStateParsed) { + canvas_draw_str(canvas, 0, 27, "Processed..."); + return; + } +} + +static void flip_weather_loader_process_callback(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_weather_loader_process_callback - context is NULL"); + DEV_CRASH(); + return; + } + + FlipWeatherApp* app = (FlipWeatherApp*)context; + View* view = app->view_loader; + + DataState current_data_state; + with_view_model( + view, DataLoaderModel * model, { current_data_state = model->data_state; }, false); + + if(current_data_state == DataStateInitial) { + with_view_model( + view, + DataLoaderModel * model, + { + model->data_state = DataStateRequested; + DataLoaderFetch fetch = model->fetcher; + if(fetch == NULL) { + FURI_LOG_E(TAG, "Model doesn't have Fetch function assigned."); + model->data_state = DataStateError; + return; + } + + // Clear any previous responses + strncpy(fhttp.last_response, "", 1); + bool request_status = fetch(model); + if(!request_status) { + model->data_state = DataStateError; + } + }, + true); + } else if(current_data_state == DataStateRequested || current_data_state == DataStateError) { + if(fhttp.state == IDLE && fhttp.last_response != NULL) { + if(strstr(fhttp.last_response, "[PONG]") != NULL) { + FURI_LOG_DEV(TAG, "PONG received."); + } else if(strncmp(fhttp.last_response, "[SUCCESS]", 9) == 0) { + FURI_LOG_DEV( + TAG, + "SUCCESS received. %s", + fhttp.last_response ? fhttp.last_response : "NULL"); + } else if(strncmp(fhttp.last_response, "[ERROR]", 9) == 0) { + FURI_LOG_DEV( + TAG, "ERROR received. %s", fhttp.last_response ? fhttp.last_response : "NULL"); + } else if(strlen(fhttp.last_response) == 0) { + // Still waiting on response + } else { + with_view_model( + view, + DataLoaderModel * model, + { model->data_state = DataStateReceived; }, + true); + } + } else if(fhttp.state == SENDING || fhttp.state == RECEIVING) { + // continue waiting + } else if(fhttp.state == INACTIVE) { + // inactive. try again + } else if(fhttp.state == ISSUE) { + with_view_model( + view, DataLoaderModel * model, { model->data_state = DataStateError; }, true); + } else { + FURI_LOG_DEV( + TAG, + "Unexpected state: %d lastresp: %s", + fhttp.state, + fhttp.last_response ? fhttp.last_response : "NULL"); + DEV_CRASH(); + } + } else if(current_data_state == DataStateReceived) { + with_view_model( + view, + DataLoaderModel * model, + { + char* data_text; + if(model->parser == NULL) { + data_text = NULL; + FURI_LOG_DEV(TAG, "Parser is NULL"); + DEV_CRASH(); + } else { + data_text = model->parser(model); + } + FURI_LOG_DEV( + TAG, + "Parsed data: %s\r\ntext: %s", + fhttp.last_response ? fhttp.last_response : "NULL", + data_text ? data_text : "NULL"); + model->data_text = data_text; + if(data_text == NULL) { + model->data_state = DataStateParseError; + } else { + model->data_state = DataStateParsed; + } + }, + true); + } else if(current_data_state == DataStateParsed) { + with_view_model( + view, + DataLoaderModel * model, + { + if(++model->request_index < model->request_count) { + model->data_state = DataStateInitial; + } else { + flip_weather_widget_set_text( + model->data_text != NULL ? model->data_text : "Empty result", + &app_instance->widget_result); + if(model->data_text != NULL) { + free(model->data_text); + model->data_text = NULL; + } + view_set_previous_callback( + widget_get_view(app_instance->widget_result), model->back_callback); + view_dispatcher_switch_to_view( + app_instance->view_dispatcher, FlipWeatherViewWidgetResult); + } + }, + true); + } +} + +static void flip_weather_loader_timer_callback(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_weather_loader_timer_callback - context is NULL"); + DEV_CRASH(); + return; + } + FlipWeatherApp* app = (FlipWeatherApp*)context; + view_dispatcher_send_custom_event(app->view_dispatcher, FlipWeatherCustomEventProcess); +} + +static void flip_weather_loader_on_enter(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_weather_loader_on_enter - context is NULL"); + DEV_CRASH(); + return; + } + FlipWeatherApp* app = (FlipWeatherApp*)context; + View* view = app->view_loader; + with_view_model( + view, + DataLoaderModel * model, + { + view_set_previous_callback(view, model->back_callback); + if(model->timer == NULL) { + model->timer = furi_timer_alloc( + flip_weather_loader_timer_callback, FuriTimerTypePeriodic, app); + } + furi_timer_start(model->timer, 250); + }, + true); +} + +static void flip_weather_loader_on_exit(void* context) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_weather_loader_on_exit - context is NULL"); + DEV_CRASH(); + return; + } + FlipWeatherApp* app = (FlipWeatherApp*)context; + View* view = app->view_loader; + with_view_model( + view, + DataLoaderModel * model, + { + if(model->timer) { + furi_timer_stop(model->timer); + } + }, + false); +} + +void flip_weather_loader_init(View* view) { + if(view == NULL) { + FURI_LOG_E(TAG, "flip_weather_loader_init - view is NULL"); + DEV_CRASH(); + return; + } + view_allocate_model(view, ViewModelTypeLocking, sizeof(DataLoaderModel)); + view_set_enter_callback(view, flip_weather_loader_on_enter); + view_set_exit_callback(view, flip_weather_loader_on_exit); +} + +void flip_weather_loader_free_model(View* view) { + if(view == NULL) { + FURI_LOG_E(TAG, "flip_weather_loader_free_model - view is NULL"); + DEV_CRASH(); + return; + } + with_view_model( + view, + DataLoaderModel * model, + { + if(model->timer) { + furi_timer_free(model->timer); + model->timer = NULL; + } + if(model->parser_context) { + free(model->parser_context); + model->parser_context = NULL; + } + }, + false); +} + +bool flip_weather_custom_event_callback(void* context, uint32_t index) { + if(context == NULL) { + FURI_LOG_E(TAG, "flip_weather_custom_event_callback - context is NULL"); + DEV_CRASH(); + return false; + } + + switch(index) { + case FlipWeatherCustomEventProcess: + flip_weather_loader_process_callback(context); + return true; + default: + FURI_LOG_DEV(TAG, "flip_weather_custom_event_callback. Unknown index: %ld", index); + return false; + } +} + +void flip_weather_generic_switch_to_view( + FlipWeatherApp* app, + char* title, + DataLoaderFetch fetcher, + DataLoaderParser parser, + size_t request_count, + ViewNavigationCallback back, + uint32_t view_id) { + if(app == NULL) { + FURI_LOG_E(TAG, "flip_weather_generic_switch_to_view - app is NULL"); + DEV_CRASH(); + return; + } + + View* view = app->view_loader; + if(view == NULL) { + FURI_LOG_E(TAG, "flip_weather_generic_switch_to_view - view is NULL"); + DEV_CRASH(); + return; + } + + with_view_model( + view, + DataLoaderModel * model, + { + model->title = title; + model->fetcher = fetcher; + model->parser = parser; + model->request_index = 0; + model->request_count = request_count; + model->back_callback = back; + model->data_state = DataStateInitial; + model->data_text = NULL; + }, + true); + + view_dispatcher_switch_to_view(app->view_dispatcher, view_id); +} diff --git a/flip_weather/callback/flip_weather_callback.h b/flip_weather/callback/flip_weather_callback.h new file mode 100644 index 000000000..174c43b3f --- /dev/null +++ b/flip_weather/callback/flip_weather_callback.h @@ -0,0 +1,45 @@ +#ifndef FLIP_WEATHER_CALLBACK_H +#define FLIP_WEATHER_CALLBACK_H + +#include "flip_weather.h" +#include +#include + +extern bool weather_request_success; +extern bool sent_weather_request; +extern bool got_weather_data; + +void flip_weather_view_draw_callback_weather(Canvas* canvas, void* model); +void flip_weather_view_draw_callback_gps(Canvas* canvas, void* model); +void callback_submenu_choices(void* context, uint32_t index); +void text_updated_ssid(void* context); +void text_updated_password(void* context); +uint32_t callback_to_submenu(void* context); +void settings_item_selected(void* context, uint32_t index); + +/** + * @brief Navigation callback for exiting the application + * @param context The context - unused + * @return next view id (VIEW_NONE to exit the app) + */ +uint32_t callback_exit_app(void* context); +uint32_t callback_to_wifi_settings(void* context); + +// Add edits by Derek Jamison +void flip_weather_generic_switch_to_view( + FlipWeatherApp* app, + char* title, + DataLoaderFetch fetcher, + DataLoaderParser parser, + size_t request_count, + ViewNavigationCallback back, + uint32_t view_id); + +void flip_weather_loader_draw_callback(Canvas* canvas, void* model); + +void flip_weather_loader_init(View* view); + +void flip_weather_loader_free_model(View* view); + +bool flip_weather_custom_event_callback(void* context, uint32_t index); +#endif diff --git a/flip_weather/easy_flipper.h b/flip_weather/easy_flipper/easy_flipper.c similarity index 96% rename from flip_weather/easy_flipper.h rename to flip_weather/easy_flipper/easy_flipper.c index e8f0ad796..8b98e1a1b 100644 --- a/flip_weather/easy_flipper.h +++ b/flip_weather/easy_flipper/easy_flipper.c @@ -1,24 +1,4 @@ -#ifndef EASY_FLIPPER_H -#define EASY_FLIPPER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define EASY_TAG "EasyFlipper" +#include /** * @brief Navigation callback for exiting the application @@ -530,5 +510,3 @@ bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer furi_string_set_str(*furi_string, buffer); return true; } - -#endif // EASY_FLIPPER_H diff --git a/flip_weather/easy_flipper/easy_flipper.h b/flip_weather/easy_flipper/easy_flipper.h new file mode 100644 index 000000000..219e42c74 --- /dev/null +++ b/flip_weather/easy_flipper/easy_flipper.h @@ -0,0 +1,263 @@ +#ifndef EASY_FLIPPER_H +#define EASY_FLIPPER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EASY_TAG "EasyFlipper" + +/** + * @brief Navigation callback for exiting the application + * @param context The context - unused + * @return next view id (VIEW_NONE to exit the app) + */ +uint32_t easy_flipper_callback_exit_app(void* context); +/** + * @brief Initialize a buffer + * @param buffer The buffer to initialize + * @param buffer_size The size of the buffer + * @return true if successful, false otherwise + */ +bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size); +/** + * @brief Initialize a View object + * @param view The View object to initialize + * @param view_id The ID/Index of the view + * @param draw_callback The draw callback function (set to NULL if not needed) + * @param input_callback The input callback function (set to NULL if not needed) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view( + View** view, + int32_t view_id, + void draw_callback(Canvas*, void*), + bool input_callback(InputEvent*, void*), + uint32_t (*previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a ViewDispatcher object + * @param view_dispatcher The ViewDispatcher object to initialize + * @param gui The GUI object + * @param context The context to pass to the event callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui, void* context); + +/** + * @brief Initialize a Submenu object + * @note This does not set the items in the submenu + * @param submenu The Submenu object to initialize + * @param view_id The ID/Index of the view + * @param title The title/header of the submenu + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_submenu( + Submenu** submenu, + int32_t view_id, + char* title, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a Menu object + * @note This does not set the items in the menu + * @param menu The Menu object to initialize + * @param view_id The ID/Index of the view + * @param item_callback The item callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_menu( + Menu** menu, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a Widget object + * @param widget The Widget object to initialize + * @param view_id The ID/Index of the view + * @param text The text to display in the widget + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_widget( + Widget** widget, + int32_t view_id, + char* text, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a VariableItemList object + * @note This does not set the items in the VariableItemList + * @param variable_item_list The VariableItemList object to initialize + * @param view_id The ID/Index of the view + * @param enter_callback The enter callback function (can be set to NULL) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the enter callback (usually the app) + * @return true if successful, false otherwise + */ +bool easy_flipper_set_variable_item_list( + VariableItemList** variable_item_list, + int32_t view_id, + void (*enter_callback)(void*, uint32_t), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a TextInput object + * @param text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_text_input( + TextInput** text_input, + int32_t view_id, + char* header_text, + char* text_input_temp_buffer, + uint32_t text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a TextInput object with extra symbols + * @param uart_text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_uart_text_input( + TextInput** uart_text_input, + int32_t view_id, + char* header_text, + char* uart_text_input_temp_buffer, + uint32_t uart_text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a DialogEx object + * @param dialog_ex The DialogEx object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param left_button_text The text of the left button + * @param right_button_text The text of the right button + * @param center_button_text The text of the center button + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_dialog_ex( + DialogEx** dialog_ex, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + char* left_button_text, + char* right_button_text, + char* center_button_text, + void (*result_callback)(DialogExResult, void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a Popup object + * @param popup The Popup object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_popup( + Popup** popup, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a Loading object + * @param loading The Loading object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_loading( + Loading** loading, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Set a char butter to a FuriString + * @param furi_string The FuriString object + * @param buffer The buffer to copy the string to + * @return true if successful, false otherwise + */ +bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer); + +#endif diff --git a/flip_weather/flip_weather_storage.h b/flip_weather/flip_storage/flip_weather_storage.c similarity index 88% rename from flip_weather/flip_weather_storage.h rename to flip_weather/flip_storage/flip_weather_storage.c index c6786f9c7..db50eb39a 100644 --- a/flip_weather/flip_weather_storage.h +++ b/flip_weather/flip_storage/flip_weather_storage.c @@ -1,12 +1,7 @@ -#ifndef FLIP_WEATHER_STORAGE_H -#define FLIP_WEATHER_STORAGE_H -#include -#include +#include "flip_storage/flip_weather_storage.h" -#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_weather/settings.bin" - -static void save_settings(const char* ssid, const char* password) { +void save_settings(const char* ssid, const char* password) { // Create the directory for saving settings char directory_path[256]; snprintf( @@ -44,7 +39,7 @@ static void save_settings(const char* ssid, const char* password) { furi_record_close(RECORD_STORAGE); } -static bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size) { +bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size) { Storage* storage = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(storage); @@ -86,5 +81,3 @@ static bool load_settings(char* ssid, size_t ssid_size, char* password, size_t p return true; } - -#endif // FLIP_WEATHER_STORAGE_H diff --git a/flip_weather/flip_storage/flip_weather_storage.h b/flip_weather/flip_storage/flip_weather_storage.h new file mode 100644 index 000000000..1cd6ddcb5 --- /dev/null +++ b/flip_weather/flip_storage/flip_weather_storage.h @@ -0,0 +1,13 @@ +#ifndef FLIP_WEATHER_STORAGE_H +#define FLIP_WEATHER_STORAGE_H + +#include +#include +#include + +#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_weather/settings.bin" + +void save_settings(const char* ssid, const char* password); + +bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size); +#endif // FLIP_WEATHER_STORAGE_H diff --git a/flip_weather/flip_weather_free.h b/flip_weather/flip_weather.c similarity index 78% rename from flip_weather/flip_weather_free.h rename to flip_weather/flip_weather.c index 8ad302228..b29d09bfd 100644 --- a/flip_weather/flip_weather_free.h +++ b/flip_weather/flip_weather.c @@ -1,21 +1,26 @@ -#ifndef FLIP_WEATHER_FREE_H -#define FLIP_WEATHER_FREE_H +#include "flip_weather.h" + +char lat_data[32]; +char lon_data[32]; + +char* total_data = NULL; +char* weather_data = NULL; + +FlipWeatherApp* app_instance = NULL; +void flip_weather_loader_free_model(View* view); // Function to free the resources used by FlipWeatherApp -static void flip_weather_app_free(FlipWeatherApp* app) { +void flip_weather_app_free(FlipWeatherApp* app) { if(!app) { FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); return; } // Free View(s) - if(app->view_weather) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewWeather); - view_free(app->view_weather); - } - if(app->view_gps) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewGPS); - view_free(app->view_gps); + if(app->view_loader) { + view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewLoader); + flip_weather_loader_free_model(app->view_loader); + view_free(app->view_loader); } // Free Submenu(s) @@ -29,6 +34,10 @@ static void flip_weather_app_free(FlipWeatherApp* app) { view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewAbout); widget_free(app->widget); } + if(app->widget_result) { + view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewWidgetResult); + widget_free(app->widget_result); + } // Free Variable Item List(s) if(app->variable_item_list) { @@ -61,8 +70,8 @@ static void flip_weather_app_free(FlipWeatherApp* app) { // close the gui furi_record_close(RECORD_GUI); + if(total_data) free(total_data); + // free the app if(app) free(app); } - -#endif // FLIP_WEATHER_FREE_H diff --git a/flip_weather/flip_weather_e.h b/flip_weather/flip_weather.h similarity index 70% rename from flip_weather/flip_weather_e.h rename to flip_weather/flip_weather.h index f220b3541..656393681 100644 --- a/flip_weather/flip_weather_e.h +++ b/flip_weather/flip_weather.h @@ -1,11 +1,12 @@ #ifndef FLIP_WEATHER_E_H #define FLIP_WEATHER_E_H -#include -#include -#include +#include +#include +#include -#define TAG "FlipWeather" +#define TAG "FlipWeather" +#define MAX_TOKENS 64 // Adjust based on expected JSON size (50) // Define the submenu items for our FlipWeather application typedef enum { @@ -17,22 +18,25 @@ typedef enum { // Define a single view for our FlipWeather application typedef enum { - FlipWeatherViewWeather, // The weather screen - FlipWeatherViewGPS, // The GPS screen FlipWeatherViewSubmenu, // The main submenu FlipWeatherViewAbout, // The about screen FlipWeatherViewSettings, // The wifi settings screen FlipWeatherViewTextInputSSID, // The text input screen for SSID FlipWeatherViewTextInputPassword, // The text input screen for password + // + FlipWeatherViewPopupError, // The error popup screen + FlipWeatherViewWidgetResult, // The text box that displays the random fact + FlipWeatherViewLoader, // The loader screen retrieves data from the internet } FlipWeatherView; // Each screen will have its own view typedef struct { ViewDispatcher* view_dispatcher; // Switches between our views - View* view_weather; // The weather view - View* view_gps; // The GPS view + View* view_loader; // The screen that loads data from internet Submenu* submenu; // The main submenu Widget* widget; // The widget (about) + Widget* widget_result; // The widget that displays the result + Popup* popup_error; // The error popup VariableItemList* variable_item_list; // The variable item list (settngs) VariableItem* variable_item_ssid; // The variable item VariableItem* variable_item_password; // The variable item @@ -48,20 +52,14 @@ typedef struct { uint32_t uart_text_input_buffer_size_password; // Size of the text input buffer } FlipWeatherApp; -static char city_data[48]; -static char region_data[48]; -static char country_data[48]; -static char lat_data[32]; -static char lon_data[32]; -static char ip_data[32]; -static char temperature_data[32]; -static char precipitation_data[32]; -static char rain_data[32]; -static char showers_data[32]; -static char snowfall_data[32]; -static char time_data[32]; -static char ip_address[16]; +extern char lat_data[32]; +extern char lon_data[32]; -#define MAX_TOKENS 64 // Adjust based on expected JSON size (50) +extern char* total_data; +extern char* weather_data; + +// Function to free the resources used by FlipWeatherApp +void flip_weather_app_free(FlipWeatherApp* app); +extern FlipWeatherApp* app_instance; #endif diff --git a/flip_weather/flip_weather_callback.h b/flip_weather/flip_weather_callback.h deleted file mode 100644 index ec33cc377..000000000 --- a/flip_weather/flip_weather_callback.h +++ /dev/null @@ -1,337 +0,0 @@ -#ifndef FLIP_WEATHER_CALLBACK_H -#define FLIP_WEATHER_CALLBACK_H - -static bool weather_request_success = false; -static bool sent_weather_request = false; -static bool got_weather_data = false; - -void flip_weather_request_error(Canvas* canvas) { - if(fhttp.last_response == NULL) { - if(fhttp.last_response != NULL) { - if(strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != - NULL) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } else { - canvas_clear(canvas); - FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response); - canvas_draw_str(canvas, 0, 10, "[ERROR] Unusual error..."); - canvas_draw_str(canvas, 0, 60, "Press BACK and retry."); - } - } else { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - } else { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Failed to receive data."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } -} - -static void flip_weather_handle_gps_draw(Canvas* canvas, bool show_gps_data) { - if(sent_get_request) { - if(fhttp.state == RECEIVING) { - if(show_gps_data) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Loading GPS..."); - canvas_draw_str(canvas, 0, 22, "Receiving..."); - } - } - // check status - else if(fhttp.state == ISSUE || !get_request_success || fhttp.last_response == NULL) { - flip_weather_request_error(canvas); - } else if(fhttp.state == IDLE && fhttp.last_response != NULL) { - // success, draw GPS - process_geo_location(); - - if(show_gps_data) { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, city_data); - canvas_draw_str(canvas, 0, 20, region_data); - canvas_draw_str(canvas, 0, 30, country_data); - canvas_draw_str(canvas, 0, 40, lat_data); - canvas_draw_str(canvas, 0, 50, lon_data); - canvas_draw_str(canvas, 0, 60, ip_data); - } - } - } -} - -// Callback for drawing the weather screen -static void flip_weather_view_draw_callback_weather(Canvas* canvas, void* model) { - if(!canvas) { - return; - } - UNUSED(model); - - canvas_set_font(canvas, FontSecondary); - - if(fhttp.state == INACTIVE) { - canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); - canvas_draw_str(canvas, 0, 17, "Please connect to the board."); - canvas_draw_str(canvas, 0, 32, "If your board is connected,"); - canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); - canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); - canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); - return; - } - - canvas_draw_str(canvas, 0, 10, "Loading Weather..."); - // handle geo location until it's processed and then handle weather - - // start the process - if(!send_geo_location_request()) { - flip_weather_request_error(canvas); - } - // wait until geo location is processed - if(!sent_get_request || !get_request_success || fhttp.state == RECEIVING) { - return; - } - // get/set geo lcoation once - if(!geo_information_processed) { - flip_weather_handle_gps_draw(canvas, false); - } - // start the weather process - if(!sent_weather_request && fhttp.state == IDLE) { - sent_weather_request = true; - char url[512]; - char* lattitude = lat_data + 10; - char* longitude = lon_data + 11; - snprintf( - url, - 512, - "https://api.open-meteo.com/v1/forecast?latitude=%s&longitude=%s¤t=temperature_2m,precipitation,rain,showers,snowfall&temperature_unit=celsius&wind_speed_unit=mph&precipitation_unit=inch&forecast_days=1", - lattitude, - longitude); - weather_request_success = - flipper_http_get_request_with_headers(url, "{\"Content-Type\": \"application/json\"}"); - if(!weather_request_success) { - FURI_LOG_E(TAG, "Failed to send GET request"); - flip_weather_request_error(canvas); - } - fhttp.state = RECEIVING; - } else { - if(fhttp.state == RECEIVING) { - canvas_draw_str(canvas, 0, 10, "Loading Weather..."); - canvas_draw_str(canvas, 0, 22, "Receiving..."); - return; - } - // check status - else if(fhttp.state == ISSUE || !weather_request_success || fhttp.last_response == NULL) { - flip_weather_request_error(canvas); - } else { - // success, draw weather - process_weather(); - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, temperature_data); - canvas_draw_str(canvas, 0, 20, precipitation_data); - canvas_draw_str(canvas, 0, 30, rain_data); - canvas_draw_str(canvas, 0, 40, showers_data); - canvas_draw_str(canvas, 0, 50, snowfall_data); - canvas_draw_str(canvas, 0, 60, time_data); - } - } -} - -// Callback for drawing the GPS screen -static void flip_weather_view_draw_callback_gps(Canvas* canvas, void* model) { - if(!canvas) { - return; - } - UNUSED(model); - - if(fhttp.state == INACTIVE) { - canvas_set_font(canvas, FontSecondary); - canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); - canvas_draw_str(canvas, 0, 17, "Please connect to the board."); - canvas_draw_str(canvas, 0, 32, "If your board is connected,"); - canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); - canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); - canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); - return; - } - - if(!send_geo_location_request()) { - flip_weather_request_error(canvas); - } - - flip_weather_handle_gps_draw(canvas, true); -} - -static void callback_submenu_choices(void* context, uint32_t index) { - FlipWeatherApp* app = (FlipWeatherApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); - return; - } - switch(index) { - case FlipWeatherSubmenuIndexWeather: - if(!flip_weather_handle_ip_address()) { - return; - } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewWeather); - break; - case FlipWeatherSubmenuIndexGPS: - if(!flip_weather_handle_ip_address()) { - return; - } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewGPS); - break; - case FlipWeatherSubmenuIndexAbout: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewAbout); - break; - case FlipWeatherSubmenuIndexSettings: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSettings); - break; - default: - break; - } -} - -static void text_updated_ssid(void* context) { - FlipWeatherApp* app = (FlipWeatherApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); - return; - } - - // store the entered text - strncpy( - app->uart_text_input_buffer_ssid, - app->uart_text_input_temp_buffer_ssid, - app->uart_text_input_buffer_size_ssid); - - // Ensure null-termination - app->uart_text_input_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; - - // update the variable item text - if(app->variable_item_ssid) { - variable_item_set_current_value_text( - app->variable_item_ssid, app->uart_text_input_buffer_ssid); - } - - // save settings - save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); - - // save wifi settings to devboard - if(strlen(app->uart_text_input_buffer_ssid) > 0 && - strlen(app->uart_text_input_buffer_password) > 0) { - if(!flipper_http_save_wifi( - app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { - FURI_LOG_E(TAG, "Failed to save wifi settings"); - } - } - - // switch to the settings view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSettings); -} - -static void text_updated_password(void* context) { - FlipWeatherApp* app = (FlipWeatherApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); - return; - } - - // store the entered text - strncpy( - app->uart_text_input_buffer_password, - app->uart_text_input_temp_buffer_password, - app->uart_text_input_buffer_size_password); - - // Ensure null-termination - app->uart_text_input_buffer_password[app->uart_text_input_buffer_size_password - 1] = '\0'; - - // update the variable item text - if(app->variable_item_password) { - variable_item_set_current_value_text( - app->variable_item_password, app->uart_text_input_buffer_password); - } - - // save settings - save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password); - - // save wifi settings to devboard - if(strlen(app->uart_text_input_buffer_ssid) > 0 && - strlen(app->uart_text_input_buffer_password) > 0) { - if(!flipper_http_save_wifi( - app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_password)) { - FURI_LOG_E(TAG, "Failed to save wifi settings"); - } - } - - // switch to the settings view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSettings); -} - -static uint32_t callback_to_submenu(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - sent_get_request = false; - get_request_success = false; - got_ip_address = false; - got_weather_data = false; - geo_information_processed = false; - weather_information_processed = false; - sent_weather_request = false; - weather_request_success = false; - return FlipWeatherViewSubmenu; -} - -static void settings_item_selected(void* context, uint32_t index) { - FlipWeatherApp* app = (FlipWeatherApp*)context; - if(!app) { - FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); - return; - } - switch(index) { - case 0: // Input SSID - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewTextInputSSID); - break; - case 1: // Input Password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewTextInputPassword); - break; - default: - FURI_LOG_E(TAG, "Unknown configuration item index"); - break; - } -} - -/** - * @brief Navigation callback for exiting the application - * @param context The context - unused - * @return next view id (VIEW_NONE to exit the app) - */ -static uint32_t callback_exit_app(void* context) { - // Exit the application - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - return VIEW_NONE; // Return VIEW_NONE to exit the app -} - -static uint32_t callback_to_wifi_settings(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - return FlipWeatherViewSettings; -} - -#endif diff --git a/flip_weather/flip_weather_parse.h b/flip_weather/flip_weather_parse.h deleted file mode 100644 index 0300c5c4a..000000000 --- a/flip_weather/flip_weather_parse.h +++ /dev/null @@ -1,137 +0,0 @@ -#pragma once - -static bool sent_get_request = false; -static bool get_request_success = false; -static bool got_ip_address = false; -static bool geo_information_processed = false; -static bool weather_information_processed = false; - -static bool flip_weather_parse_ip_address() { - // load the received data from the saved file - FuriString* returned_data = flipper_http_load_from_file(fhttp.file_path); - if(returned_data == NULL) { - FURI_LOG_E(TAG, "Failed to load received data from file."); - return false; - } - const char* data_cstr = furi_string_get_cstr(returned_data); - if(data_cstr == NULL) { - FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); - furi_string_free(returned_data); - return false; - } - char* ip = get_json_value("origin", (char*)data_cstr, MAX_TOKENS); - if(ip == NULL) { - FURI_LOG_E(TAG, "Failed to get IP address"); - sent_get_request = true; - get_request_success = false; - fhttp.state = ISSUE; - free(ip); - furi_string_free(returned_data); - return false; - } - snprintf(ip_address, 16, "%s", ip); - ip_address[15] = '\0'; - free(ip); - furi_string_free(returned_data); - return true; -} - -// handle the async-to-sync process to get and set the IP address -static bool flip_weather_handle_ip_address() { - if(!got_ip_address) { - got_ip_address = true; - snprintf( - fhttp.file_path, - sizeof(fhttp.file_path), - STORAGE_EXT_PATH_PREFIX "/apps_data/flip_weather/ip.txt"); - - fhttp.save_received_data = true; - if(!flipper_http_get_request("https://httpbin.org/get")) { - FURI_LOG_E(TAG, "Failed to get IP address"); - return false; - } else { - fhttp.state = RECEIVING; - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - } - while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { - // Wait for the feed to be received - furi_delay_ms(100); - } - furi_timer_stop(fhttp.get_timeout_timer); - if(!flip_weather_parse_ip_address()) { - FURI_LOG_E(TAG, "Failed to get IP address"); - sent_get_request = true; - get_request_success = false; - fhttp.state = ISSUE; - return false; - } - } - return true; -} - -static bool send_geo_location_request() { - if(!sent_get_request && fhttp.state == IDLE) { - sent_get_request = true; - get_request_success = flipper_http_get_request_with_headers( - "https://ipwhois.app/json/", jsmn("Content-Type", "application/json")); - if(!get_request_success) { - FURI_LOG_E(TAG, "Failed to send GET request"); - return false; - } - fhttp.state = RECEIVING; - } - return true; -} - -static void process_geo_location() { - if(!geo_information_processed && fhttp.last_response != NULL) { - geo_information_processed = true; - char* city = get_json_value("city", fhttp.last_response, MAX_TOKENS); - char* region = get_json_value("region", fhttp.last_response, MAX_TOKENS); - char* country = get_json_value("country", fhttp.last_response, MAX_TOKENS); - char* latitude = get_json_value("latitude", fhttp.last_response, MAX_TOKENS); - char* longitude = get_json_value("longitude", fhttp.last_response, MAX_TOKENS); - - snprintf(city_data, 64, "City: %s", city); - snprintf(region_data, 64, "Region: %s", region); - snprintf(country_data, 64, "Country: %s", country); - snprintf(lat_data, 64, "Latitude: %s", latitude); - snprintf(lon_data, 64, "Longitude: %s", longitude); - snprintf(ip_data, 64, "IP Address: %s", ip_address); - - fhttp.state = IDLE; - } -} - -static void process_weather() { - if(!weather_information_processed && fhttp.last_response != NULL) { - weather_information_processed = true; - char* current_data = get_json_value("current", fhttp.last_response, MAX_TOKENS); - char* temperature = get_json_value("temperature_2m", current_data, MAX_TOKENS); - char* precipitation = get_json_value("precipitation", current_data, MAX_TOKENS); - char* rain = get_json_value("rain", current_data, MAX_TOKENS); - char* showers = get_json_value("showers", current_data, MAX_TOKENS); - char* snowfall = get_json_value("snowfall", current_data, MAX_TOKENS); - char* time = get_json_value("time", current_data, MAX_TOKENS); - - // replace the "T" in time with a space - char* ptr = strstr(time, "T"); - if(ptr != NULL) { - *ptr = ' '; - } - - snprintf(temperature_data, 64, "Temperature (C): %s", temperature); - snprintf(precipitation_data, 64, "Precipitation: %s", precipitation); - snprintf(rain_data, 64, "Rain: %s", rain); - snprintf(showers_data, 64, "Showers: %s", showers); - snprintf(snowfall_data, 64, "Snowfall: %s", snowfall); - snprintf(time_data, 64, "Time: %s", time); - - fhttp.state = IDLE; - } else if(!weather_information_processed && fhttp.last_response == NULL) { - FURI_LOG_E(TAG, "Failed to process weather data"); - // store error message - snprintf(temperature_data, 64, "Failed. Update WiFi settings."); - fhttp.state = ISSUE; - } -} diff --git a/flip_weather/flipper_http/flipper_http.c b/flip_weather/flipper_http/flipper_http.c new file mode 100644 index 000000000..e7aafd857 --- /dev/null +++ b/flip_weather/flipper_http/flipper_http.c @@ -0,0 +1,1349 @@ +#include +FlipperHTTP fhttp; +char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +uint8_t file_buffer[FILE_BUFFER_SIZE]; +size_t file_buffer_len = 0; +// Function to append received data to file +// make sure to initialize the file path before calling this function +bool flipper_http_append_to_file( + const void* data, + size_t data_size, + bool start_new_file, + char* file_path) { + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + + if(start_new_file) { + // Delete the file if it already exists + if(storage_file_exists(storage, file_path)) { + if(!storage_simply_remove_recursive(storage, file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to delete file: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + } + // Open the file in write mode + if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + } else { + // Open the file in append mode + if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_OPEN_APPEND)) { + FURI_LOG_E(HTTP_TAG, "Failed to open file for appending: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + } + + // Write the data to the file + if(storage_file_write(file, data, data_size) != data_size) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return true; +} + +FuriString* flipper_http_load_from_file(char* file_path) { + // Open the storage record + Storage* storage = furi_record_open(RECORD_STORAGE); + if(!storage) { + FURI_LOG_E(HTTP_TAG, "Failed to open storage record"); + return NULL; + } + + // Allocate a file handle + File* file = storage_file_alloc(storage); + if(!file) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate storage file"); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Open the file for reading + if(!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; // Return false if the file does not exist + } + + // Allocate a FuriString to hold the received data + FuriString* str_result = furi_string_alloc(); + if(!str_result) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Reset the FuriString to ensure it's empty before reading + furi_string_reset(str_result); + + // Define a buffer to hold the read data + uint8_t* buffer = (uint8_t*)malloc(MAX_FILE_SHOW); + if(!buffer) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer"); + furi_string_free(str_result); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Read data into the buffer + size_t read_count = storage_file_read(file, buffer, MAX_FILE_SHOW); + if(storage_file_get_error(file) != FSE_OK) { + FURI_LOG_E(HTTP_TAG, "Error reading from file."); + furi_string_free(str_result); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Append each byte to the FuriString + for(size_t i = 0; i < read_count; i++) { + furi_string_push_back(str_result, buffer[i]); + } + + // Check if there is more data beyond the maximum size + char extra_byte; + storage_file_read(file, &extra_byte, 1); + + // Clean up + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + free(buffer); + return str_result; +} + +// UART worker thread +/** + * @brief Worker thread to handle UART data asynchronously. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +// UART worker thread +int32_t flipper_http_worker(void* context) { + UNUSED(context); + size_t rx_line_pos = 0; + + while(1) { + uint32_t events = furi_thread_flags_wait( + WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever); + if(events & WorkerEvtStop) { + break; + } + if(events & WorkerEvtRxDone) { + // Continuously read from the stream buffer until it's empty + while(!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) { + // Read one byte at a time + char c = 0; + size_t received = furi_stream_buffer_receive(fhttp.flipper_http_stream, &c, 1, 0); + + if(received == 0) { + // No more data to read + break; + } + + // Append the received byte to the file if saving is enabled + if(fhttp.save_bytes) { + // Add byte to the buffer + file_buffer[file_buffer_len++] = c; + // Write to file if buffer is full + if(file_buffer_len >= FILE_BUFFER_SIZE) { + if(!flipper_http_append_to_file( + file_buffer, + file_buffer_len, + fhttp.just_started_bytes, + fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); + } + file_buffer_len = 0; + fhttp.just_started_bytes = false; + } + } + + // Handle line buffering only if callback is set (text data) + if(fhttp.handle_rx_line_cb) { + // Handle line buffering + if(c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1) { + rx_line_buffer[rx_line_pos] = '\0'; // Null-terminate the line + + // Invoke the callback with the complete line + fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context); + + // Reset the line buffer position + rx_line_pos = 0; + } else { + rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer + } + } + } + } + } + + return 0; +} +// Timer callback function +/** + * @brief Callback function for the GET timeout timer. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will be called when the GET request times out. + */ +void get_timeout_timer_callback(void* context) { + UNUSED(context); + FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving the end."); + + // Reset the state + fhttp.started_receiving_get = false; + fhttp.started_receiving_post = false; + fhttp.started_receiving_put = false; + fhttp.started_receiving_delete = false; + + // Update UART state + fhttp.state = ISSUE; +} + +// UART RX Handler Callback (Interrupt Context) +/** + * @brief A private callback function to handle received data asynchronously. + * @return void + * @param handle The UART handle. + * @param event The event type. + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +void _flipper_http_rx_callback( + FuriHalSerialHandle* handle, + FuriHalSerialRxEvent event, + void* context) { + UNUSED(context); + if(event == FuriHalSerialRxEventData) { + uint8_t data = furi_hal_serial_async_rx(handle); + furi_stream_buffer_send(fhttp.flipper_http_stream, &data, 1, 0); + furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtRxDone); + } +} + +// UART initialization function +/** + * @brief Initialize UART. + * @return true if the UART was initialized successfully, false otherwise. + * @param callback The callback function to handle received data (ex. flipper_http_rx_callback). + * @param context The context to pass to the callback. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { + if(!context) { + FURI_LOG_E(HTTP_TAG, "Invalid context provided to flipper_http_init."); + return false; + } + if(!callback) { + FURI_LOG_E(HTTP_TAG, "Invalid callback provided to flipper_http_init."); + return false; + } + fhttp.flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1); + if(!fhttp.flipper_http_stream) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate UART stream buffer."); + return false; + } + + fhttp.rx_thread = furi_thread_alloc(); + if(!fhttp.rx_thread) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread."); + furi_stream_buffer_free(fhttp.flipper_http_stream); + return false; + } + + furi_thread_set_name(fhttp.rx_thread, "FlipperHTTP_RxThread"); + furi_thread_set_stack_size(fhttp.rx_thread, 1024); + furi_thread_set_context(fhttp.rx_thread, &fhttp); + furi_thread_set_callback(fhttp.rx_thread, flipper_http_worker); + + fhttp.handle_rx_line_cb = callback; + fhttp.callback_context = context; + + furi_thread_start(fhttp.rx_thread); + fhttp.rx_thread_id = furi_thread_get_id(fhttp.rx_thread); + + // handle when the UART control is busy to avoid furi_check failed + if(furi_hal_serial_control_is_busy(UART_CH)) { + FURI_LOG_E(HTTP_TAG, "UART control is busy."); + return false; + } + + fhttp.serial_handle = furi_hal_serial_control_acquire(UART_CH); + if(!fhttp.serial_handle) { + FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL"); + // Cleanup resources + furi_thread_free(fhttp.rx_thread); + furi_stream_buffer_free(fhttp.flipper_http_stream); + return false; + } + + // Initialize UART with acquired handle + furi_hal_serial_init(fhttp.serial_handle, BAUDRATE); + + // Enable RX direction + furi_hal_serial_enable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); + + // Start asynchronous RX with the callback + furi_hal_serial_async_rx_start(fhttp.serial_handle, _flipper_http_rx_callback, &fhttp, false); + + // Wait for the TX to complete to ensure UART is ready + furi_hal_serial_tx_wait_complete(fhttp.serial_handle); + + // Allocate the timer for handling timeouts + fhttp.get_timeout_timer = furi_timer_alloc( + get_timeout_timer_callback, // Callback function + FuriTimerTypeOnce, // One-shot timer + &fhttp // Context passed to callback + ); + + if(!fhttp.get_timeout_timer) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate HTTP request timeout timer."); + // Cleanup resources + furi_hal_serial_async_rx_stop(fhttp.serial_handle); + furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); + furi_hal_serial_control_release(fhttp.serial_handle); + furi_hal_serial_deinit(fhttp.serial_handle); + furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop); + furi_thread_join(fhttp.rx_thread); + furi_thread_free(fhttp.rx_thread); + furi_stream_buffer_free(fhttp.flipper_http_stream); + return false; + } + + // Set the timer thread priority if needed + furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated); + + fhttp.last_response = (char*)malloc(RX_BUF_SIZE); + if(!fhttp.last_response) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate memory for last_response."); + return false; + } + + // FURI_LOG_I(HTTP_TAG, "UART initialized successfully."); + return true; +} + +// Deinitialize UART +/** + * @brief Deinitialize UART. + * @return void + * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. + */ +void flipper_http_deinit() { + if(fhttp.serial_handle == NULL) { + FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?"); + return; + } + // Stop asynchronous RX + furi_hal_serial_async_rx_stop(fhttp.serial_handle); + + // Release and deinitialize the serial handle + furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); + furi_hal_serial_control_release(fhttp.serial_handle); + furi_hal_serial_deinit(fhttp.serial_handle); + + // Signal the worker thread to stop + furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop); + // Wait for the thread to finish + furi_thread_join(fhttp.rx_thread); + // Free the thread resources + furi_thread_free(fhttp.rx_thread); + + // Free the stream buffer + furi_stream_buffer_free(fhttp.flipper_http_stream); + + // Free the timer + if(fhttp.get_timeout_timer) { + furi_timer_free(fhttp.get_timeout_timer); + fhttp.get_timeout_timer = NULL; + } + + // Free the last response + if(fhttp.last_response) { + free(fhttp.last_response); + fhttp.last_response = NULL; + } + + // FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully."); +} + +// Function to send data over UART with newline termination +/** + * @brief Send data over UART with newline termination. + * @return true if the data was sent successfully, false otherwise. + * @param data The data to send over UART. + * @note The data will be sent over UART with a newline character appended. + */ +bool flipper_http_send_data(const char* data) { + size_t data_length = strlen(data); + if(data_length == 0) { + FURI_LOG_E("FlipperHTTP", "Attempted to send empty data."); + fhttp.state = ISSUE; + return false; + } + + // Create a buffer with data + '\n' + size_t send_length = data_length + 1; // +1 for '\n' + if(send_length > 512) { // Ensure buffer size is sufficient + FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP."); + return false; + } + + char send_buffer[513]; // 512 + 1 for safety + strncpy(send_buffer, data, 512); + send_buffer[data_length] = '\n'; // Append newline + send_buffer[data_length + 1] = '\0'; // Null-terminate + + if(fhttp.state == INACTIVE && ((strstr(send_buffer, "[PING]") == NULL) && + (strstr(send_buffer, "[WIFI/CONNECT]") == NULL))) { + FURI_LOG_E("FlipperHTTP", "Cannot send data while INACTIVE."); + fhttp.last_response = "Cannot send data while INACTIVE."; + return false; + } + + fhttp.state = SENDING; + furi_hal_serial_tx(fhttp.serial_handle, (const uint8_t*)send_buffer, send_length); + + // Uncomment below line to log the data sent over UART + // FURI_LOG_I("FlipperHTTP", "Sent data over UART: %s", send_buffer); + // fhttp.state = IDLE; + return true; +} + +// Function to send a PING request +/** + * @brief Send a PING request to check if the Wifi Dev Board is connected. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + * @note This is best used to check if the Wifi Dev Board is connected. + * @note The state will remain INACTIVE until a PONG is received. + */ +bool flipper_http_ping() { + const char* command = "[PING]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send PING command."); + return false; + } + // set state as INACTIVE to be made IDLE if PONG is received + fhttp.state = INACTIVE; + // The response will be handled asynchronously via the callback + return true; +} + +// Function to list available commands +/** + * @brief Send a command to list available commands. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_list_commands() { + const char* command = "[LIST]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send LIST command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to turn on the LED +/** + * @brief Allow the LED to display while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_on() { + const char* command = "[LED/ON]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send LED ON command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to turn off the LED +/** + * @brief Disable the LED from displaying while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_off() { + const char* command = "[LED/OFF]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send LED OFF command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to parse JSON data +/** + * @brief Parse JSON data. + * @return true if the JSON data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON data. + * @param json_data The JSON data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json(const char* key, const char* json_data) { + if(!key || !json_data) { + FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json."); + return false; + } + + char buffer[256]; + int ret = + snprintf(buffer, sizeof(buffer), "[PARSE]{\"key\":\"%s\",\"json\":%s}", key, json_data); + if(ret < 0 || ret >= (int)sizeof(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to format JSON parse command."); + return false; + } + + if(!flipper_http_send_data(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to parse JSON array data +/** + * @brief Parse JSON array data. + * @return true if the JSON array data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON array data. + * @param index The index to parse from the JSON array data. + * @param json_data The JSON array data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json_array(const char* key, int index, const char* json_data) { + if(!key || !json_data) { + FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json_array."); + return false; + } + + char buffer[256]; + int ret = snprintf( + buffer, + sizeof(buffer), + "[PARSE/ARRAY]{\"key\":\"%s\",\"index\":%d,\"json\":%s}", + key, + index, + json_data); + if(ret < 0 || ret >= (int)sizeof(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to format JSON parse array command."); + return false; + } + + if(!flipper_http_send_data(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse array command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to scan for WiFi networks +/** + * @brief Send a command to scan for WiFi networks. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_scan_wifi() { + const char* command = "[WIFI/SCAN]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi scan command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to save WiFi settings (returns true if successful) +/** + * @brief Send a command to save WiFi settings. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_save_wifi(const char* ssid, const char* password) { + if(!ssid || !password) { + FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_save_wifi."); + return false; + } + char buffer[256]; + int ret = snprintf( + buffer, sizeof(buffer), "[WIFI/SAVE]{\"ssid\":\"%s\",\"password\":\"%s\"}", ssid, password); + if(ret < 0 || ret >= (int)sizeof(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to format WiFi save command."); + return false; + } + + if(!flipper_http_send_data(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to get IP address of WiFi Devboard +/** + * @brief Send a command to get the IP address of the WiFi Devboard + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_address() { + const char* command = "[IP/ADDRESS]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send IP address command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to get IP address of the connected WiFi network +/** + * @brief Send a command to get the IP address of the connected WiFi network. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_wifi() { + const char* command = "[WIFI/IP]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi IP command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to disconnect from WiFi (returns true if successful) +/** + * @brief Send a command to disconnect from WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_disconnect_wifi() { + const char* command = "[WIFI/DISCONNECT]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to connect to WiFi (returns true if successful) +/** + * @brief Send a command to connect to WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_connect_wifi() { + const char* command = "[WIFI/CONNECT]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to send a GET request +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request(const char* url) { + if(!url) { + FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request."); + return false; + } + + // Prepare GET request command + char command[512]; + int ret = snprintf(command, sizeof(command), "[GET]%s", url); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format GET request command."); + return false; + } + + // Send GET request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send GET request command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a GET request with headers +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_with_headers(const char* url, const char* headers) { + if(!url || !headers) { + FURI_LOG_E( + "FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_with_headers."); + return false; + } + + // Prepare GET request command with headers + char command[512]; + int ret = snprintf( + command, sizeof(command), "[GET/HTTP]{\"url\":\"%s\",\"headers\":%s}", url, headers); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers."); + return false; + } + + // Send GET request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a GET request with headers and return bytes +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_bytes(const char* url, const char* headers) { + if(!url || !headers) { + FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_bytes."); + return false; + } + + // Prepare GET request command with headers + char command[256]; + int ret = snprintf( + command, sizeof(command), "[GET/BYTES]{\"url\":\"%s\",\"headers\":%s}", url, headers); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers."); + return false; + } + + // Send GET request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a POST request with headers +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param data The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_with_headers( + const char* url, + const char* headers, + const char* payload) { + if(!url || !headers || !payload) { + FURI_LOG_E( + "FlipperHTTP", + "Invalid arguments provided to flipper_http_post_request_with_headers."); + return false; + } + + // Prepare POST request command with headers and data + char command[256]; + int ret = snprintf( + command, + sizeof(command), + "[POST/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", + url, + headers, + payload); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data."); + return false; + } + + // Send POST request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a POST request with headers and return bytes +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param payload The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload) { + if(!url || !headers || !payload) { + FURI_LOG_E( + "FlipperHTTP", "Invalid arguments provided to flipper_http_post_request_bytes."); + return false; + } + + // Prepare POST request command with headers and data + char command[256]; + int ret = snprintf( + command, + sizeof(command), + "[POST/BYTES]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", + url, + headers, + payload); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data."); + return false; + } + + // Send POST request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a PUT request with headers +/** + * @brief Send a PUT request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the PUT request to. + * @param headers The headers to send with the PUT request. + * @param data The data to send with the PUT request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_put_request_with_headers( + const char* url, + const char* headers, + const char* payload) { + if(!url || !headers || !payload) { + FURI_LOG_E( + "FlipperHTTP", "Invalid arguments provided to flipper_http_put_request_with_headers."); + return false; + } + + // Prepare PUT request command with headers and data + char command[256]; + int ret = snprintf( + command, + sizeof(command), + "[PUT/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", + url, + headers, + payload); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format PUT request command with headers and data."); + return false; + } + + // Send PUT request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a DELETE request with headers +/** + * @brief Send a DELETE request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the DELETE request to. + * @param headers The headers to send with the DELETE request. + * @param data The data to send with the DELETE request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_delete_request_with_headers( + const char* url, + const char* headers, + const char* payload) { + if(!url || !headers || !payload) { + FURI_LOG_E( + "FlipperHTTP", + "Invalid arguments provided to flipper_http_delete_request_with_headers."); + return false; + } + + // Prepare DELETE request command with headers and data + char command[256]; + int ret = snprintf( + command, + sizeof(command), + "[DELETE/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", + url, + headers, + payload); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E( + "FlipperHTTP", "Failed to format DELETE request command with headers and data."); + return false; + } + + // Send DELETE request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to handle received data asynchronously +/** + * @brief Callback function to handle received data asynchronously. + * @return void + * @param line The received line. + * @param context The context passed to the callback. + * @note The received data will be handled asynchronously via the callback and handles the state of the UART. + */ +void flipper_http_rx_callback(const char* line, void* context) { + if(!line || !context) { + FURI_LOG_E(HTTP_TAG, "Invalid arguments provided to flipper_http_rx_callback."); + return; + } + + // Trim the received line to check if it's empty + char* trimmed_line = trim(line); + if(trimmed_line != NULL && trimmed_line[0] != '\0') { + // if the line is not [GET/END] or [POST/END] or [PUT/END] or [DELETE/END] + if(strstr(trimmed_line, "[GET/END]") == NULL && + strstr(trimmed_line, "[POST/END]") == NULL && + strstr(trimmed_line, "[PUT/END]") == NULL && + strstr(trimmed_line, "[DELETE/END]") == NULL) { + strncpy(fhttp.last_response, trimmed_line, RX_BUF_SIZE); + } + } + free(trimmed_line); // Free the allocated memory for trimmed_line + + if(fhttp.state != INACTIVE && fhttp.state != ISSUE) { + fhttp.state = RECEIVING; + } + + // Uncomment below line to log the data received over UART + // FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line); + + // Check if we've started receiving data from a GET request + if(fhttp.started_receiving_get) { + // Restart the timeout timer each time new data is received + furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + + if(strstr(line, "[GET/END]") != NULL) { + FURI_LOG_I(HTTP_TAG, "GET request completed."); + // Stop the timer since we've completed the GET request + furi_timer_stop(fhttp.get_timeout_timer); + fhttp.started_receiving_get = false; + fhttp.just_started_get = false; + fhttp.state = IDLE; + fhttp.save_bytes = false; + fhttp.save_received_data = false; + + if(fhttp.is_bytes_request) { + // Search for the binary marker `[GET/END]` in the file buffer + const char marker[] = "[GET/END]"; + const size_t marker_len = sizeof(marker) - 1; // Exclude null terminator + + for(size_t i = 0; i <= file_buffer_len - marker_len; i++) { + // Check if the marker is found + if(memcmp(&file_buffer[i], marker, marker_len) == 0) { + // Remove the marker by shifting the remaining data left + size_t remaining_len = file_buffer_len - (i + marker_len); + memmove(&file_buffer[i], &file_buffer[i + marker_len], remaining_len); + file_buffer_len -= marker_len; + break; + } + } + + // If there is data left in the buffer, append it to the file + if(file_buffer_len > 0) { + if(!flipper_http_append_to_file( + file_buffer, file_buffer_len, false, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + } + file_buffer_len = 0; + } + } + + fhttp.is_bytes_request = false; + return; + } + + // Append the new line to the existing data + if(fhttp.save_received_data && + !flipper_http_append_to_file( + line, strlen(line), !fhttp.just_started_get, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + fhttp.started_receiving_get = false; + fhttp.just_started_get = false; + fhttp.state = IDLE; + return; + } + + if(!fhttp.just_started_get) { + fhttp.just_started_get = true; + } + return; + } + + // Check if we've started receiving data from a POST request + else if(fhttp.started_receiving_post) { + // Restart the timeout timer each time new data is received + furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + + if(strstr(line, "[POST/END]") != NULL) { + FURI_LOG_I(HTTP_TAG, "POST request completed."); + // Stop the timer since we've completed the POST request + furi_timer_stop(fhttp.get_timeout_timer); + fhttp.started_receiving_post = false; + fhttp.just_started_post = false; + fhttp.state = IDLE; + fhttp.save_bytes = false; + fhttp.save_received_data = false; + + if(fhttp.is_bytes_request) { + // Search for the binary marker `[POST/END]` in the file buffer + const char marker[] = "[POST/END]"; + const size_t marker_len = sizeof(marker) - 1; // Exclude null terminator + + for(size_t i = 0; i <= file_buffer_len - marker_len; i++) { + // Check if the marker is found + if(memcmp(&file_buffer[i], marker, marker_len) == 0) { + // Remove the marker by shifting the remaining data left + size_t remaining_len = file_buffer_len - (i + marker_len); + memmove(&file_buffer[i], &file_buffer[i + marker_len], remaining_len); + file_buffer_len -= marker_len; + break; + } + } + + // If there is data left in the buffer, append it to the file + if(file_buffer_len > 0) { + if(!flipper_http_append_to_file( + file_buffer, file_buffer_len, false, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + } + file_buffer_len = 0; + } + } + + fhttp.is_bytes_request = false; + return; + } + + // Append the new line to the existing data + if(fhttp.save_received_data && + !flipper_http_append_to_file( + line, strlen(line), !fhttp.just_started_post, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + fhttp.started_receiving_post = false; + fhttp.just_started_post = false; + fhttp.state = IDLE; + return; + } + + if(!fhttp.just_started_post) { + fhttp.just_started_post = true; + } + return; + } + + // Check if we've started receiving data from a PUT request + else if(fhttp.started_receiving_put) { + // Restart the timeout timer each time new data is received + furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + + if(strstr(line, "[PUT/END]") != NULL) { + FURI_LOG_I(HTTP_TAG, "PUT request completed."); + // Stop the timer since we've completed the PUT request + furi_timer_stop(fhttp.get_timeout_timer); + fhttp.started_receiving_put = false; + fhttp.just_started_put = false; + fhttp.state = IDLE; + fhttp.save_bytes = false; + fhttp.is_bytes_request = false; + fhttp.save_received_data = false; + return; + } + + // Append the new line to the existing data + if(fhttp.save_received_data && + !flipper_http_append_to_file( + line, strlen(line), !fhttp.just_started_put, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + fhttp.started_receiving_put = false; + fhttp.just_started_put = false; + fhttp.state = IDLE; + return; + } + + if(!fhttp.just_started_put) { + fhttp.just_started_put = true; + } + return; + } + + // Check if we've started receiving data from a DELETE request + else if(fhttp.started_receiving_delete) { + // Restart the timeout timer each time new data is received + furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + + if(strstr(line, "[DELETE/END]") != NULL) { + FURI_LOG_I(HTTP_TAG, "DELETE request completed."); + // Stop the timer since we've completed the DELETE request + furi_timer_stop(fhttp.get_timeout_timer); + fhttp.started_receiving_delete = false; + fhttp.just_started_delete = false; + fhttp.state = IDLE; + fhttp.save_bytes = false; + fhttp.is_bytes_request = false; + fhttp.save_received_data = false; + return; + } + + // Append the new line to the existing data + if(fhttp.save_received_data && + !flipper_http_append_to_file( + line, strlen(line), !fhttp.just_started_delete, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + fhttp.started_receiving_delete = false; + fhttp.just_started_delete = false; + fhttp.state = IDLE; + return; + } + + if(!fhttp.just_started_delete) { + fhttp.just_started_delete = true; + } + return; + } + + // Handle different types of responses + if(strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL) { + FURI_LOG_I(HTTP_TAG, "Operation succeeded."); + } else if(strstr(line, "[INFO]") != NULL) { + FURI_LOG_I(HTTP_TAG, "Received info: %s", line); + + if(fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL) { + fhttp.state = IDLE; + } + } else if(strstr(line, "[GET/SUCCESS]") != NULL) { + FURI_LOG_I(HTTP_TAG, "GET request succeeded."); + fhttp.started_receiving_get = true; + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + // for GET request, save data only if it's a bytes request + fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; + file_buffer_len = 0; + return; + } else if(strstr(line, "[POST/SUCCESS]") != NULL) { + FURI_LOG_I(HTTP_TAG, "POST request succeeded."); + fhttp.started_receiving_post = true; + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + // for POST request, save data only if it's a bytes request + fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; + file_buffer_len = 0; + return; + } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { + FURI_LOG_I(HTTP_TAG, "PUT request succeeded."); + fhttp.started_receiving_put = true; + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + return; + } else if(strstr(line, "[DELETE/SUCCESS]") != NULL) { + FURI_LOG_I(HTTP_TAG, "DELETE request succeeded."); + fhttp.started_receiving_delete = true; + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + return; + } else if(strstr(line, "[DISCONNECTED]") != NULL) { + FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully."); + } else if(strstr(line, "[ERROR]") != NULL) { + FURI_LOG_E(HTTP_TAG, "Received error: %s", line); + fhttp.state = ISSUE; + return; + } else if(strstr(line, "[PONG]") != NULL) { + FURI_LOG_I(HTTP_TAG, "Received PONG response: Wifi Dev Board is still alive."); + + // send command to connect to WiFi + if(fhttp.state == INACTIVE) { + fhttp.state = IDLE; + return; + } + } + + if(fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) { + fhttp.state = IDLE; + } else if(fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) { + fhttp.state = INACTIVE; + } else { + fhttp.state = IDLE; + } +} + +// Function to trim leading and trailing spaces and newlines from a constant string +char* trim(const char* str) { + const char* end; + char* trimmed_str; + size_t len; + + // Trim leading space + while(isspace((unsigned char)*str)) + str++; + + // All spaces? + if(*str == 0) return strdup(""); // Return an empty string if all spaces + + // Trim trailing space + end = str + strlen(str) - 1; + while(end > str && isspace((unsigned char)*end)) + end--; + + // Set length for the trimmed string + len = end - str + 1; + + // Allocate space for the trimmed string and null terminator + trimmed_str = (char*)malloc(len + 1); + if(trimmed_str == NULL) { + return NULL; // Handle memory allocation failure + } + + // Copy the trimmed part of the string into trimmed_str + strncpy(trimmed_str, str, len); + trimmed_str[len] = '\0'; // Null terminate the string + + return trimmed_str; +} + +/** + * @brief Process requests and parse JSON data asynchronously + * @param http_request The function to send the request + * @param parse_json The function to parse the JSON + * @return true if successful, false otherwise + */ +bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)) { + if(http_request()) // start the async request + { + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + } else { + FURI_LOG_E(HTTP_TAG, "Failed to send request"); + return false; + } + while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { + // Wait for the request to be received + furi_delay_ms(100); + } + furi_timer_stop(fhttp.get_timeout_timer); + if(!parse_json()) // parse the JSON before switching to the view (synchonous) + { + FURI_LOG_E(HTTP_TAG, "Failed to parse the JSON..."); + return false; + } + return true; +} + +/** + * @brief Perform a task while displaying a loading screen + * @param http_request The function to send the request + * @param parse_response The function to parse the response + * @param success_view_id The view ID to switch to on success + * @param failure_view_id The view ID to switch to on failure + * @param view_dispatcher The view dispatcher to use + * @return + */ +void flipper_http_loading_task( + bool (*http_request)(void), + bool (*parse_response)(void), + uint32_t success_view_id, + uint32_t failure_view_id, + ViewDispatcher** view_dispatcher) { + Loading* loading; + int32_t loading_view_id = 987654321; // Random ID + + loading = loading_alloc(); + if(!loading) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate loading"); + view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); + + return; + } + + view_dispatcher_add_view(*view_dispatcher, loading_view_id, loading_get_view(loading)); + + // Switch to the loading view + view_dispatcher_switch_to_view(*view_dispatcher, loading_view_id); + + // Make the request + if(!flipper_http_process_response_async(http_request, parse_response)) { + FURI_LOG_E(HTTP_TAG, "Failed to make request"); + view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); + view_dispatcher_remove_view(*view_dispatcher, loading_view_id); + loading_free(loading); + + return; + } + + // Switch to the success view + view_dispatcher_switch_to_view(*view_dispatcher, success_view_id); + view_dispatcher_remove_view(*view_dispatcher, loading_view_id); + loading_free(loading); +} diff --git a/flip_weather/flipper_http/flipper_http.h b/flip_weather/flipper_http/flipper_http.h new file mode 100644 index 000000000..1eb1e1e70 --- /dev/null +++ b/flip_weather/flipper_http/flipper_http.h @@ -0,0 +1,384 @@ +// flipper_http.h +#ifndef FLIPPER_HTTP_H +#define FLIPPER_HTTP_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext + +#define HTTP_TAG "FlipWeather" // change this to your app name +#define http_tag "flip_weather" // change this to your app id +#define UART_CH (momentum_settings.uart_esp_channel) // UART channel +#define TIMEOUT_DURATION_TICKS (6 * 1000) // 6 seconds +#define BAUDRATE (115200) // UART baudrate +#define RX_BUF_SIZE 1024 // UART RX buffer size +#define RX_LINE_BUFFER_SIZE 4096 // UART RX line buffer size (increase for large responses) +#define MAX_FILE_SHOW 4096 // Maximum data from file to show +#define FILE_BUFFER_SIZE 512 // File buffer size + +// Forward declaration for callback +typedef void (*FlipperHTTP_Callback)(const char* line, void* context); + +// State variable to track the UART state +typedef enum { + INACTIVE, // Inactive state + IDLE, // Default state + RECEIVING, // Receiving data + SENDING, // Sending data + ISSUE, // Issue with connection +} SerialState; + +// Event Flags for UART Worker Thread +typedef enum { + WorkerEvtStop = (1 << 0), + WorkerEvtRxDone = (1 << 1), +} WorkerEvtFlags; + +// FlipperHTTP Structure +typedef struct { + FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication + FuriHalSerialHandle* serial_handle; // Serial handle for UART communication + FuriThread* rx_thread; // Worker thread for UART + FuriThreadId rx_thread_id; // Worker thread ID + FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines + void* callback_context; // Context for the callback + SerialState state; // State of the UART + + // variable to store the last received data from the UART + char* last_response; + char file_path[256]; // Path to save the received data + + // Timer-related members + FuriTimer* get_timeout_timer; // Timer for HTTP request timeout + + bool started_receiving_get; // Indicates if a GET request has started + bool just_started_get; // Indicates if GET data reception has just started + + bool started_receiving_post; // Indicates if a POST request has started + bool just_started_post; // Indicates if POST data reception has just started + + bool started_receiving_put; // Indicates if a PUT request has started + bool just_started_put; // Indicates if PUT data reception has just started + + bool started_receiving_delete; // Indicates if a DELETE request has started + bool just_started_delete; // Indicates if DELETE data reception has just started + + // Buffer to hold the raw bytes received from the UART + uint8_t* received_bytes; + size_t received_bytes_len; // Length of the received bytes + bool is_bytes_request; // Flag to indicate if the request is for bytes + bool save_bytes; // Flag to save the received data to a file + bool save_received_data; // Flag to save the received data to a file + + bool just_started_bytes; // Indicates if bytes data reception has just started +} FlipperHTTP; + +extern FlipperHTTP fhttp; +// Global static array for the line buffer +extern char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +extern uint8_t file_buffer[FILE_BUFFER_SIZE]; +extern size_t file_buffer_len; + +// fhttp.last_response holds the last received data from the UART + +// Function to append received data to file +// make sure to initialize the file path before calling this function +bool flipper_http_append_to_file( + const void* data, + size_t data_size, + bool start_new_file, + char* file_path); + +FuriString* flipper_http_load_from_file(char* file_path); + +// UART worker thread +/** + * @brief Worker thread to handle UART data asynchronously. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +// UART worker thread +int32_t flipper_http_worker(void* context); + +// Timer callback function +/** + * @brief Callback function for the GET timeout timer. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will be called when the GET request times out. + */ +void get_timeout_timer_callback(void* context); + +// UART RX Handler Callback (Interrupt Context) +/** + * @brief A private callback function to handle received data asynchronously. + * @return void + * @param handle The UART handle. + * @param event The event type. + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +void _flipper_http_rx_callback( + FuriHalSerialHandle* handle, + FuriHalSerialRxEvent event, + void* context); + +// UART initialization function +/** + * @brief Initialize UART. + * @return true if the UART was initialized successfully, false otherwise. + * @param callback The callback function to handle received data (ex. flipper_http_rx_callback). + * @param context The context to pass to the callback. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_init(FlipperHTTP_Callback callback, void* context); + +// Deinitialize UART +/** + * @brief Deinitialize UART. + * @return void + * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. + */ +void flipper_http_deinit(); + +// Function to send data over UART with newline termination +/** + * @brief Send data over UART with newline termination. + * @return true if the data was sent successfully, false otherwise. + * @param data The data to send over UART. + * @note The data will be sent over UART with a newline character appended. + */ +bool flipper_http_send_data(const char* data); + +// Function to send a PING request +/** + * @brief Send a PING request to check if the Wifi Dev Board is connected. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + * @note This is best used to check if the Wifi Dev Board is connected. + * @note The state will remain INACTIVE until a PONG is received. + */ +bool flipper_http_ping(); + +// Function to list available commands +/** + * @brief Send a command to list available commands. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_list_commands(); + +// Function to turn on the LED +/** + * @brief Allow the LED to display while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_on(); + +// Function to turn off the LED +/** + * @brief Disable the LED from displaying while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_off(); + +// Function to parse JSON data +/** + * @brief Parse JSON data. + * @return true if the JSON data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON data. + * @param json_data The JSON data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json(const char* key, const char* json_data); + +// Function to parse JSON array data +/** + * @brief Parse JSON array data. + * @return true if the JSON array data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON array data. + * @param index The index to parse from the JSON array data. + * @param json_data The JSON array data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json_array(const char* key, int index, const char* json_data); + +// Function to scan for WiFi networks +/** + * @brief Send a command to scan for WiFi networks. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_scan_wifi(); + +// Function to save WiFi settings (returns true if successful) +/** + * @brief Send a command to save WiFi settings. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_save_wifi(const char* ssid, const char* password); + +// Function to get IP address of WiFi Devboard +/** + * @brief Send a command to get the IP address of the WiFi Devboard + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_address(); + +// Function to get IP address of the connected WiFi network +/** + * @brief Send a command to get the IP address of the connected WiFi network. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_wifi(); + +// Function to disconnect from WiFi (returns true if successful) +/** + * @brief Send a command to disconnect from WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_disconnect_wifi(); + +// Function to connect to WiFi (returns true if successful) +/** + * @brief Send a command to connect to WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_connect_wifi(); + +// Function to send a GET request +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request(const char* url); + +// Function to send a GET request with headers +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_with_headers(const char* url, const char* headers); + +// Function to send a GET request with headers and return bytes +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_bytes(const char* url, const char* headers); + +// Function to send a POST request with headers +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param data The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to send a POST request with headers and return bytes +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param payload The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload); + +// Function to send a PUT request with headers +/** + * @brief Send a PUT request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the PUT request to. + * @param headers The headers to send with the PUT request. + * @param data The data to send with the PUT request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_put_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to send a DELETE request with headers +/** + * @brief Send a DELETE request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the DELETE request to. + * @param headers The headers to send with the DELETE request. + * @param data The data to send with the DELETE request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_delete_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to handle received data asynchronously +/** + * @brief Callback function to handle received data asynchronously. + * @return void + * @param line The received line. + * @param context The context passed to the callback. + * @note The received data will be handled asynchronously via the callback and handles the state of the UART. + */ +void flipper_http_rx_callback(const char* line, void* context); + +// Function to trim leading and trailing spaces and newlines from a constant string +char* trim(const char* str); +/** + * @brief Process requests and parse JSON data asynchronously + * @param http_request The function to send the request + * @param parse_json The function to parse the JSON + * @return true if successful, false otherwise + */ +bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)); + +/** + * @brief Perform a task while displaying a loading screen + * @param http_request The function to send the request + * @param parse_response The function to parse the response + * @param success_view_id The view ID to switch to on success + * @param failure_view_id The view ID to switch to on failure + * @param view_dispatcher The view dispatcher to use + * @return + */ +void flipper_http_loading_task( + bool (*http_request)(void), + bool (*parse_response)(void), + uint32_t success_view_id, + uint32_t failure_view_id, + ViewDispatcher** view_dispatcher); + +#endif // FLIPPER_HTTP_H diff --git a/flip_trader/jsmn.h b/flip_weather/jsmn/jsmn.c similarity index 83% rename from flip_trader/jsmn.h rename to flip_weather/jsmn/jsmn.c index b4e3098e5..eb33b3cc7 100644 --- a/flip_trader/jsmn.h +++ b/flip_weather/jsmn/jsmn.c @@ -3,113 +3,20 @@ * * Copyright (c) 2010 Serge Zaitsev * - * 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. + * [License text continues...] */ -#ifndef JSMN_H -#define JSMN_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef JSMN_STATIC -#define JSMN_API static -#else -#define JSMN_API extern -#endif - -/** - * JSON type identifier. Basic types are: - * o Object - * o Array - * o String - * o Other primitive: number, boolean (true/false) or null - */ -typedef enum { - JSMN_UNDEFINED = 0, - JSMN_OBJECT = 1 << 0, - JSMN_ARRAY = 1 << 1, - JSMN_STRING = 1 << 2, - JSMN_PRIMITIVE = 1 << 3 -} jsmntype_t; - -enum jsmnerr { - /* Not enough tokens were provided */ - JSMN_ERROR_NOMEM = -1, - /* Invalid character inside JSON string */ - JSMN_ERROR_INVAL = -2, - /* The string is not a full JSON packet, more bytes expected */ - JSMN_ERROR_PART = -3 -}; -/** - * JSON token description. - * type type (object, array, string etc.) - * start start position in JSON data string - * end end position in JSON data string - */ -typedef struct jsmntok { - jsmntype_t type; - int start; - int end; - int size; -#ifdef JSMN_PARENT_LINKS - int parent; -#endif -} jsmntok_t; - -/** - * JSON parser. Contains an array of token blocks available. Also stores - * the string being parsed now and current position in that string. - */ -typedef struct jsmn_parser { - unsigned int pos; /* offset in the JSON string */ - unsigned int toknext; /* next token to allocate */ - int toksuper; /* superior token node, e.g. parent object or array */ -} jsmn_parser; - -/** - * Create JSON parser over an array of tokens - */ -JSMN_API void jsmn_init(jsmn_parser* parser); - -/** - * Run JSON parser. It parses a JSON data string into and array of tokens, each - * describing - * a single JSON object. - */ -JSMN_API int jsmn_parse( - jsmn_parser* parser, - const char* js, - const size_t len, - jsmntok_t* tokens, - const unsigned int num_tokens); +#include +#include +#include -#ifndef JSMN_HEADER /** - * Allocates a fresh unused token from the token pool. - */ + * Allocates a fresh unused token from the token pool. + */ static jsmntok_t* jsmn_alloc_token(jsmn_parser* parser, jsmntok_t* tokens, const size_t num_tokens) { jsmntok_t* tok; + if(parser->toknext >= num_tokens) { return NULL; } @@ -123,8 +30,8 @@ static jsmntok_t* } /** - * Fills token type and boundaries. - */ + * Fills token type and boundaries. + */ static void jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { token->type = type; @@ -134,8 +41,8 @@ static void } /** - * Fills next available token with JSON primitive. - */ + * Fills next available token with JSON primitive. + */ static int jsmn_parse_primitive( jsmn_parser* parser, const char* js, @@ -195,8 +102,8 @@ static int jsmn_parse_primitive( } /** - * Fills next token with JSON string. - */ + * Fills next token with JSON string. + */ static int jsmn_parse_string( jsmn_parser* parser, const char* js, @@ -272,9 +179,18 @@ static int jsmn_parse_string( } /** - * Parse JSON string and fill tokens. - */ -JSMN_API int jsmn_parse( + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser* parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +/** + * Parse JSON string and fill tokens. + */ +int jsmn_parse( jsmn_parser* parser, const char* js, const size_t len, @@ -464,33 +380,16 @@ JSMN_API int jsmn_parse( return count; } -/** - * Creates a new parser based over a given buffer with an array of tokens - * available. - */ -JSMN_API void jsmn_init(jsmn_parser* parser) { - parser->pos = 0; - parser->toknext = 0; - parser->toksuper = -1; -} - -#endif /* JSMN_HEADER */ - -#ifdef __cplusplus +// Helper function to create a JSON object +char* jsmn(const char* key, const char* value) { + int length = strlen(key) + strlen(value) + 8; // Calculate required length + char* result = (char*)malloc(length * sizeof(char)); // Allocate memory + if(result == NULL) { + return NULL; // Handle memory allocation failure + } + snprintf(result, length, "{\"%s\":\"%s\"}", key, value); + return result; // Caller is responsible for freeing this memory } -#endif - -#endif /* JSMN_H */ - -#ifndef JB_JSMN_EDIT -#define JB_JSMN_EDIT -/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ - -#include -#include -#include -#include -#include // Helper function to compare JSON keys int jsoneq(const char* json, jsmntok_t* tok, const char* s) { @@ -501,7 +400,7 @@ int jsoneq(const char* json, jsmntok_t* tok, const char* s) { return -1; } -// return the value of the key in the JSON data +// Return the value of the key in the JSON data char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { // Parse the JSON feed if(json_data != NULL) { @@ -765,5 +664,3 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in free(array_str); return values; } - -#endif /* JB_JSMN_EDIT */ diff --git a/flip_weather/jsmn/jsmn.h b/flip_weather/jsmn/jsmn.h new file mode 100644 index 000000000..cd95a0e58 --- /dev/null +++ b/flip_weather/jsmn/jsmn.h @@ -0,0 +1,131 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * 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: + * + * [License text continues...] + */ + +#ifndef JSMN_H +#define JSMN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser* parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing a single JSON object. + */ +JSMN_API int jsmn_parse( + jsmn_parser* parser, + const char* js, + const size_t len, + jsmntok_t* tokens, + const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/* Implementation has been moved to jsmn.c */ +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */ + +/* Custom Helper Functions */ +#ifndef JB_JSMN_EDIT +#define JB_JSMN_EDIT +/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ + +#include +#include +#include +#include +#include + +// Helper function to create a JSON object +char* jsmn(const char* key, const char* value); +// Helper function to compare JSON keys +int jsoneq(const char* json, jsmntok_t* tok, const char* s); + +// Return the value of the key in the JSON data +char* get_json_value(char* key, char* json_data, uint32_t max_tokens); + +// Revised get_json_array_value function +char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t max_tokens); + +// Revised get_json_array_values function with correct token skipping +char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, int* num_values); +#endif /* JB_JSMN_EDIT */ diff --git a/flip_weather/parse/flip_weather_parse.c b/flip_weather/parse/flip_weather_parse.c new file mode 100644 index 000000000..26876fcca --- /dev/null +++ b/flip_weather/parse/flip_weather_parse.c @@ -0,0 +1,174 @@ +#include "parse/flip_weather_parse.h" + +bool sent_get_request = false; +bool get_request_success = false; +bool got_ip_address = false; +bool geo_information_processed = false; +bool weather_information_processed = false; + +bool send_geo_location_request() { + if(fhttp.state == INACTIVE) { + FURI_LOG_E(TAG, "Board is INACTIVE"); + flipper_http_ping(); // ping the device + fhttp.state = ISSUE; + return false; + } + if(!flipper_http_get_request_with_headers( + "https://ipwhois.app/json/", "{\"Content-Type\": \"application/json\"}")) { + FURI_LOG_E(TAG, "Failed to send GET request"); + fhttp.state = ISSUE; + return false; + } + fhttp.state = RECEIVING; + return true; +} + +bool send_geo_weather_request(DataLoaderModel* model) { + UNUSED(model); + char url[512]; + char* lattitude = lat_data + 10; + char* longitude = lon_data + 11; + snprintf( + url, + 512, + "https://api.open-meteo.com/v1/forecast?latitude=%s&longitude=%s¤t=temperature_2m,precipitation,rain,showers,snowfall&temperature_unit=celsius&wind_speed_unit=mph&precipitation_unit=inch&forecast_days=1", + lattitude, + longitude); + if(!flipper_http_get_request_with_headers(url, "{\"Content-Type\": \"application/json\"}")) { + FURI_LOG_E(TAG, "Failed to send GET request"); + fhttp.state = ISSUE; + return false; + } + fhttp.state = RECEIVING; + return true; +} +char* process_geo_location(DataLoaderModel* model) { + UNUSED(model); + if(fhttp.last_response != NULL) { + char* city = get_json_value("city", fhttp.last_response, MAX_TOKENS); + char* region = get_json_value("region", fhttp.last_response, MAX_TOKENS); + char* country = get_json_value("country", fhttp.last_response, MAX_TOKENS); + char* latitude = get_json_value("latitude", fhttp.last_response, MAX_TOKENS); + char* longitude = get_json_value("longitude", fhttp.last_response, MAX_TOKENS); + + if(city == NULL || region == NULL || country == NULL || latitude == NULL || + longitude == NULL) { + FURI_LOG_E(TAG, "Failed to get geo location data"); + fhttp.state = ISSUE; + return NULL; + } + + snprintf(lat_data, sizeof(lat_data), "Latitude: %s", latitude); + snprintf(lon_data, sizeof(lon_data), "Longitude: %s", longitude); + + if(!total_data) { + total_data = (char*)malloc(512); + if(!total_data) { + FURI_LOG_E(TAG, "Failed to allocate memory for total_data"); + fhttp.state = ISSUE; + return NULL; + } + } + snprintf( + total_data, + 512, + "You are in %s, %s, %s. \nLatitude: %s, Longitude: %s", + city, + region, + country, + latitude, + longitude); + + fhttp.state = IDLE; + free(city); + free(region); + free(country); + free(latitude); + free(longitude); + } + return total_data; +} + +bool process_geo_location_2() { + if(fhttp.last_response != NULL) { + char* city = get_json_value("city", fhttp.last_response, MAX_TOKENS); + char* region = get_json_value("region", fhttp.last_response, MAX_TOKENS); + char* country = get_json_value("country", fhttp.last_response, MAX_TOKENS); + char* latitude = get_json_value("latitude", fhttp.last_response, MAX_TOKENS); + char* longitude = get_json_value("longitude", fhttp.last_response, MAX_TOKENS); + + if(city == NULL || region == NULL || country == NULL || latitude == NULL || + longitude == NULL) { + FURI_LOG_E(TAG, "Failed to get geo location data"); + fhttp.state = ISSUE; + return false; + } + + snprintf(lat_data, sizeof(lat_data), "Latitude: %s", latitude); + snprintf(lon_data, sizeof(lon_data), "Longitude: %s", longitude); + + fhttp.state = IDLE; + free(city); + free(region); + free(country); + free(latitude); + free(longitude); + return true; + } + return false; +} + +char* process_weather(DataLoaderModel* model) { + UNUSED(model); + if(fhttp.last_response != NULL) { + char* current_data = get_json_value("current", fhttp.last_response, MAX_TOKENS); + char* temperature = get_json_value("temperature_2m", current_data, MAX_TOKENS); + char* precipitation = get_json_value("precipitation", current_data, MAX_TOKENS); + char* rain = get_json_value("rain", current_data, MAX_TOKENS); + char* showers = get_json_value("showers", current_data, MAX_TOKENS); + char* snowfall = get_json_value("snowfall", current_data, MAX_TOKENS); + char* time = get_json_value("time", current_data, MAX_TOKENS); + + if(current_data == NULL || temperature == NULL || precipitation == NULL || rain == NULL || + showers == NULL || snowfall == NULL || time == NULL) { + FURI_LOG_E(TAG, "Failed to get weather data"); + fhttp.state = ISSUE; + return NULL; + } + + // replace the "T" in time with a space + char* ptr = strstr(time, "T"); + if(ptr != NULL) { + *ptr = ' '; + } + + if(!weather_data) { + weather_data = (char*)malloc(512); + if(!weather_data) { + FURI_LOG_E(TAG, "Failed to allocate memory for weather_data"); + fhttp.state = ISSUE; + return NULL; + } + } + snprintf( + weather_data, + 512, + "Temperature: %s C\nPrecipitation: %s\nRain: %s\nShowers: %s\nSnowfall: %s\nTime: %s", + temperature, + precipitation, + rain, + showers, + snowfall, + time); + + fhttp.state = IDLE; + free(current_data); + free(temperature); + free(precipitation); + free(rain); + free(showers); + free(snowfall); + free(time); + } + return weather_data; +} diff --git a/flip_weather/parse/flip_weather_parse.h b/flip_weather/parse/flip_weather_parse.h new file mode 100644 index 000000000..68fcce62e --- /dev/null +++ b/flip_weather/parse/flip_weather_parse.h @@ -0,0 +1,48 @@ +#ifndef FLIP_WEATHER_PARSE_H +#define FLIP_WEATHER_PARSE_H +#include +extern bool sent_get_request; +extern bool get_request_success; +extern bool got_ip_address; +extern bool geo_information_processed; +extern bool weather_information_processed; + +// Add edits by Derek Jamison +typedef enum DataState DataState; +enum DataState { + DataStateInitial, + DataStateRequested, + DataStateReceived, + DataStateParsed, + DataStateParseError, + DataStateError, +}; + +typedef enum FlipWeatherCustomEvent FlipWeatherCustomEvent; +enum FlipWeatherCustomEvent { + FlipWeatherCustomEventProcess, +}; + +typedef struct DataLoaderModel DataLoaderModel; +typedef bool (*DataLoaderFetch)(DataLoaderModel* model); +typedef char* (*DataLoaderParser)(DataLoaderModel* model); +struct DataLoaderModel { + char* title; + char* data_text; + DataState data_state; + DataLoaderFetch fetcher; + DataLoaderParser parser; + void* parser_context; + size_t request_index; + size_t request_count; + ViewNavigationCallback back_callback; + FuriTimer* timer; +}; + +bool send_geo_location_request(); +char* process_geo_location(DataLoaderModel* model); +bool process_geo_location_2(); +char* process_weather(DataLoaderModel* model); +bool send_geo_weather_request(DataLoaderModel* model); + +#endif diff --git a/flip_wifi/.DS_Store b/flip_wifi/.DS_Store deleted file mode 100644 index b23076802..000000000 Binary files a/flip_wifi/.DS_Store and /dev/null differ diff --git a/flip_wifi/CHANGELOG.md b/flip_wifi/CHANGELOG.md index 7db40410f..37718d1cb 100644 --- a/flip_wifi/CHANGELOG.md +++ b/flip_wifi/CHANGELOG.md @@ -1,2 +1,10 @@ +## v1.2 +- Updated scan loading and parsing. +- Added connectivity check on startup. (thanks to Derek Jamison) + +## v1.1 +- Fixed a freeze issue when configurations did not exist (thanks to WillyJL). +- Improved memory allocation. + ## v1.0 -- Initial Release \ No newline at end of file +- Initial release. \ No newline at end of file diff --git a/flip_wifi/README.md b/flip_wifi/README.md index fe4ca87f6..1a342affd 100644 --- a/flip_wifi/README.md +++ b/flip_wifi/README.md @@ -7,7 +7,6 @@ FlipWiFi is the companion app for the popular FlipperHTTP flash, originally intr - WiFi Dev Board or Raspberry Pi Pico W for Flipper Zero with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP - WiFi Access Point - ## Features - **Scan**: Discover nearby WiFi networks and add them to your list. @@ -17,7 +16,7 @@ FlipWiFi is the companion app for the popular FlipperHTTP flash, originally intr FlipWiFi automatically allocates the necessary resources and initializes settings upon launch. If WiFi settings have been previously configured, they are loaded automatically for easy access. You can also edit the list of WiFi settings by downloading and modifying the "wifi_list.txt" file located in the "/SD/apps_data/flip_wifi/" directory. To use the app: -1. **Flash the WiFi Dev Board**: Follow the instructions to flash the WiFi Dev Board with FlipperHTTP: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP +1. **Flash the WiFi Dev Board**: Follow the instructions to flash the WiFi Dev Board with FlipperHTTP: https://github.com/jblanked/FlipperHTTP 2. **Install the App**: Download FlipWiFi from the App Store. 3. **Launch FlipWiFi**: Open the app on your Flipper Zero. 4. Connect, review, and save WiFi networks. \ No newline at end of file diff --git a/flip_wifi/flip_wifi_i.h b/flip_wifi/alloc/flip_wifi_alloc.c similarity index 91% rename from flip_wifi/flip_wifi_i.h rename to flip_wifi/alloc/flip_wifi_alloc.c index 79b9ee4a9..7378c8ec6 100644 --- a/flip_wifi/flip_wifi_i.h +++ b/flip_wifi/alloc/flip_wifi_alloc.c @@ -1,8 +1,7 @@ -#ifndef FLIP_WIFI_I_H -#define FLIP_WIFI_I_H +#include // Function to allocate resources for the FlipWiFiApp -static FlipWiFiApp* flip_wifi_app_alloc() { +FlipWiFiApp* flip_wifi_app_alloc() { FlipWiFiApp* app = (FlipWiFiApp*)malloc(sizeof(FlipWiFiApp)); Gui* gui = furi_record_open(RECORD_GUI); @@ -89,7 +88,7 @@ static FlipWiFiApp* flip_wifi_app_alloc() { if(!easy_flipper_set_widget( &app->widget_info, FlipWiFiViewAbout, - "FlipWiFi v1.0\n-----\nFlipperHTTP companion app.\nScan and save WiFi networks.\n-----\nwww.github.com/jblanked", + "FlipWiFi v1.2\n-----\nFlipperHTTP companion app.\nScan and save WiFi networks.\n-----\nwww.github.com/jblanked", callback_to_submenu_main, &app->view_dispatcher)) { return NULL; @@ -149,7 +148,7 @@ static FlipWiFiApp* flip_wifi_app_alloc() { if(!easy_flipper_set_submenu( &app->submenu_main, FlipWiFiViewSubmenuMain, - "FlipWiFi v1.0", + "FlipWiFi v1.2", easy_flipper_callback_exit_app, &app->view_dispatcher)) { return NULL; @@ -181,26 +180,19 @@ static FlipWiFiApp* flip_wifi_app_alloc() { submenu_add_item( app->submenu_main, "Info", FlipWiFiSubmenuIndexAbout, callback_submenu_choices, app); - // Popup - if(!easy_flipper_set_popup( - &app->popup, - FlipWiFiViewPopup, - "Success", - 0, - 0, - "The WiFi setting has been set.", - 0, - 10, - popup_callback_saved, - callback_to_submenu_saved, - &app->view_dispatcher, - app)) { - return NULL; - } - // Load the playlist from storage if(!load_playlist(&app->wifi_playlist)) { FURI_LOG_E(TAG, "Failed to load playlist"); + + // playlist is empty? + submenu_reset(app->submenu_wifi_saved); + submenu_set_header(app->submenu_wifi_saved, "Saved APs"); + submenu_add_item( + app->submenu_wifi_saved, + "[Add Network]", + FlipWiFiSubmenuIndexWiFiSavedAddSSID, + callback_submenu_choices, + app); } else { // Update the submenu flip_wifi_redraw_submenu_saved(app); @@ -209,9 +201,5 @@ static FlipWiFiApp* flip_wifi_app_alloc() { // Switch to the main view view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuMain); - app_instance = app; - return app; } - -#endif // FLIP_WIFI_I_H diff --git a/flip_wifi/alloc/flip_wifi_alloc.h b/flip_wifi/alloc/flip_wifi_alloc.h new file mode 100644 index 000000000..630514e40 --- /dev/null +++ b/flip_wifi/alloc/flip_wifi_alloc.h @@ -0,0 +1,11 @@ +#ifndef FLIP_WIFI_I_H +#define FLIP_WIFI_I_H + +#include +#include +#include + +// Function to allocate resources for the FlipWiFiApp +FlipWiFiApp* flip_wifi_app_alloc(); + +#endif // FLIP_WIFI_I_H diff --git a/flip_wifi/app.c b/flip_wifi/app.c index e5ce24daa..96abe35a5 100644 --- a/flip_wifi/app.c +++ b/flip_wifi/app.c @@ -1,8 +1,5 @@ -#include -#include -#include -#include -#include +#include +#include // Entry point for the FlipWiFi application int32_t flip_wifi_main(void* p) { @@ -10,8 +7,8 @@ int32_t flip_wifi_main(void* p) { UNUSED(p); // Initialize the FlipWiFi application - FlipWiFiApp* app = flip_wifi_app_alloc(); - if(!app) { + app_instance = flip_wifi_app_alloc(); + if(!app_instance) { FURI_LOG_E(TAG, "Failed to allocate FlipWiFiApp"); return -1; } @@ -21,11 +18,39 @@ int32_t flip_wifi_main(void* p) { return -1; } + // Thanks to Derek Jamison for the following code snippet: + if(app_instance->uart_text_input_buffer_add_ssid != NULL && + app_instance->uart_text_input_buffer_add_password != NULL) { + // Try to wait for pong response. + uint8_t counter = 10; + while(fhttp.state == INACTIVE && --counter > 0) { + FURI_LOG_D(TAG, "Waiting for PONG"); + furi_delay_ms(100); + } + + if(counter == 0) { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header( + message, "[FlipperHTTP Error]", 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + message, + "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.", + 0, + 63, + AlignLeft, + AlignBottom); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); + } + } + // Run the view dispatcher - view_dispatcher_run(app->view_dispatcher); + view_dispatcher_run(app_instance->view_dispatcher); // Free the resources used by the FlipWiFi application - flip_wifi_app_free(app); + flip_wifi_app_free(app_instance); // Return 0 to indicate success return 0; diff --git a/flip_wifi/application.fam b/flip_wifi/application.fam index c5e8b2d13..9522d322b 100644 --- a/flip_wifi/application.fam +++ b/flip_wifi/application.fam @@ -9,6 +9,6 @@ App( fap_icon_assets="assets", fap_author="JBlanked", fap_weburl="https://github.com/jblanked/FlipWiFi", - fap_version="1.0", + fap_version="1.2", fap_description="FlipperHTTP companion app.", ) diff --git a/flip_wifi/assets/01-home.png b/flip_wifi/assets/01-home.png index 8998a0af7..192cbe04a 100644 Binary files a/flip_wifi/assets/01-home.png and b/flip_wifi/assets/01-home.png differ diff --git a/flip_wifi/flip_wifi_callback.h b/flip_wifi/callback/flip_wifi_callback.c similarity index 82% rename from flip_wifi/flip_wifi_callback.h rename to flip_wifi/callback/flip_wifi_callback.c index b939a475d..b9f049392 100644 --- a/flip_wifi/flip_wifi_callback.h +++ b/flip_wifi/callback/flip_wifi_callback.c @@ -1,15 +1,9 @@ -#ifndef FLIP_WIFI_CALLBACK_H -#define FLIP_WIFI_CALLBACK_H -#include "flip_wifi_icons.h" +#include -FlipWiFiApp* app_instance; - -static void callback_submenu_choices(void* context, uint32_t index); -// array to store each SSID char* ssid_list[64]; uint32_t ssid_index = 0; -static void flip_wifi_redraw_submenu_saved(FlipWiFiApp* app) { +void flip_wifi_redraw_submenu_saved(FlipWiFiApp* app) { // re draw the saved submenu submenu_reset(app->submenu_wifi_saved); submenu_set_header(app->submenu_wifi_saved, "Saved APs"); @@ -29,7 +23,7 @@ static void flip_wifi_redraw_submenu_saved(FlipWiFiApp* app) { } } -static uint32_t callback_to_submenu_main(void* context) { +uint32_t callback_to_submenu_main(void* context) { if(!context) { FURI_LOG_E(TAG, "Context is NULL"); return VIEW_NONE; @@ -38,7 +32,7 @@ static uint32_t callback_to_submenu_main(void* context) { ssid_index = 0; return FlipWiFiViewSubmenuMain; } -static uint32_t callback_to_submenu_scan(void* context) { +uint32_t callback_to_submenu_scan(void* context) { if(!context) { FURI_LOG_E(TAG, "Context is NULL"); return VIEW_NONE; @@ -47,7 +41,7 @@ static uint32_t callback_to_submenu_scan(void* context) { ssid_index = 0; return FlipWiFiViewSubmenuScan; } -static uint32_t callback_to_submenu_saved(void* context) { +uint32_t callback_to_submenu_saved(void* context) { if(!context) { FURI_LOG_E(TAG, "Context is NULL"); return VIEW_NONE; @@ -56,7 +50,7 @@ static uint32_t callback_to_submenu_saved(void* context) { ssid_index = 0; return FlipWiFiViewSubmenuSaved; } -void popup_callback_saved(void* context) { +static void popup_callback_saved(void* context) { FlipWiFiApp* app = (FlipWiFiApp*)context; if(!app) { FURI_LOG_E(TAG, "HelloWorldApp is NULL"); @@ -64,7 +58,7 @@ void popup_callback_saved(void* context) { } view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuSaved); } -void popup_callback_main(void* context) { +static void popup_callback_main(void* context) { FlipWiFiApp* app = (FlipWiFiApp*)context; if(!app) { FURI_LOG_E(TAG, "HelloWorldApp is NULL"); @@ -74,7 +68,7 @@ void popup_callback_main(void* context) { } // Callback for drawing the main screen -static void flip_wifi_view_draw_callback_scan(Canvas* canvas, void* model) { +void flip_wifi_view_draw_callback_scan(Canvas* canvas, void* model) { UNUSED(model); canvas_clear(canvas); canvas_set_font(canvas, FontPrimary); @@ -84,7 +78,7 @@ static void flip_wifi_view_draw_callback_scan(Canvas* canvas, void* model) { canvas_draw_icon(canvas, 96, 53, &I_ButtonRight_4x7); canvas_draw_str_aligned(canvas, 103, 54, AlignLeft, AlignTop, "Add"); } -static void flip_wifi_view_draw_callback_saved(Canvas* canvas, void* model) { +void flip_wifi_view_draw_callback_saved(Canvas* canvas, void* model) { UNUSED(model); canvas_clear(canvas); canvas_set_font(canvas, FontPrimary); @@ -184,7 +178,7 @@ bool flip_wifi_view_input_callback_saved(InputEvent* event, void* context) { // Function to trim leading and trailing whitespace // Returns the trimmed start pointer and updates the length -char* trim_whitespace(char* start, size_t* length) { +static char* trim_whitespace(char* start, size_t* length) { // Trim leading whitespace while(*length > 0 && isspace((unsigned char)*start)) { start++; @@ -199,15 +193,30 @@ char* trim_whitespace(char* start, size_t* length) { return start; } -bool flip_wifi_handle_scan(FlipWiFiApp* app) { - if(fhttp.last_response == NULL || fhttp.last_response[0] == '\0') { - FURI_LOG_E(TAG, "Failed to receive WiFi scan"); +static bool flip_wifi_handle_scan() { + if(!app_instance) { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return false; + } + // load the received data from the saved file + FuriString* scan_data = flipper_http_load_from_file(fhttp.file_path); + if(scan_data == NULL) { + FURI_LOG_E(TAG, "Failed to load received data from file."); + fhttp.state = ISSUE; + return false; + } + char* data_cstr = (char*)furi_string_get_cstr(scan_data); + if(data_cstr == NULL) { + FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); + furi_string_free(scan_data); + fhttp.state = ISSUE; + free(data_cstr); return false; } uint32_t ssid_count = 0; - char* current_position = fhttp.last_response; + char* current_position = data_cstr; char* next_comma = NULL; // Manually split the string on commas @@ -229,6 +238,8 @@ bool flip_wifi_handle_scan(FlipWiFiApp* app) { ssid_list[ssid_count] = malloc(trimmed_length + 1); if(ssid_list[ssid_count] == NULL) { FURI_LOG_E(TAG, "Memory allocation failed"); + free(data_cstr); + furi_string_free(scan_data); return false; } strncpy(ssid_list[ssid_count], trim_start, trimmed_length); @@ -265,7 +276,8 @@ bool flip_wifi_handle_scan(FlipWiFiApp* app) { } // Add each SSID as a submenu item - submenu_set_header(app->submenu_wifi_scan, "WiFi Nearby"); + submenu_reset(app_instance->submenu_wifi_scan); + submenu_set_header(app_instance->submenu_wifi_scan, "WiFi Nearby"); for(uint32_t i = 0; i < ssid_count; i++) { char* ssid_item = ssid_list[i]; if(ssid_item == NULL) { @@ -275,16 +287,17 @@ bool flip_wifi_handle_scan(FlipWiFiApp* app) { char ssid[64]; snprintf(ssid, sizeof(ssid), "%s", ssid_item); submenu_add_item( - app->submenu_wifi_scan, + app_instance->submenu_wifi_scan, ssid, FlipWiFiSubmenuIndexWiFiScanStart + i, callback_submenu_choices, - app); + app_instance); } - + free(data_cstr); + furi_string_free(scan_data); return true; } -static void callback_submenu_choices(void* context, uint32_t index) { +void callback_submenu_choices(void* context, uint32_t index) { FlipWiFiApp* app = (FlipWiFiApp*)context; if(!app) { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); @@ -292,8 +305,29 @@ static void callback_submenu_choices(void* context, uint32_t index) { } switch(index) { case FlipWiFiSubmenuIndexWiFiScan: + // Popup + if(!app->popup) { + if(!easy_flipper_set_popup( + &app->popup, + FlipWiFiViewPopup, + "Success", + 0, + 0, + "The WiFi setting has been set.", + 0, + 10, + popup_callback_saved, + callback_to_submenu_saved, + &app->view_dispatcher, + app)) { + return; + } + } + popup_set_header(app->popup, "[ERROR]", 0, 0, AlignLeft, AlignTop); + view_set_previous_callback(popup_get_view(app->popup), callback_to_submenu_main); + popup_set_callback(app->popup, popup_callback_main); + if(fhttp.state == INACTIVE) { - popup_set_header(app->popup, "[ERROR]", 0, 0, AlignLeft, AlignTop); popup_set_text( app->popup, "WiFi Devboard Disconnected.\nPlease reconnect the board.", @@ -301,40 +335,26 @@ static void callback_submenu_choices(void* context, uint32_t index) { 40, AlignLeft, AlignTop); - view_set_previous_callback(popup_get_view(app->popup), callback_to_submenu_main); - popup_set_callback(app->popup, popup_callback_main); - // switch to the popup view view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewPopup); - } - // scan for wifi - if(!flipper_http_scan_wifi()) { - FURI_LOG_E(TAG, "Failed to scan for WiFi"); return; - } else // start the async feed request - { - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - } - while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { - // Wait for the feed to be received - furi_delay_ms(100); } - furi_timer_stop(fhttp.get_timeout_timer); - // set each SSID as a submenu item - if(fhttp.state != IDLE || fhttp.last_response == NULL) { - FURI_LOG_E(TAG, "Failed to receive WiFi scan"); - return; - } else { - submenu_reset(app->submenu_wifi_scan); - submenu_set_header(app->submenu_wifi_scan, "WiFi Nearby"); - if(flip_wifi_handle_scan(app)) { - // switch to the submenu - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuScan); - } - FURI_LOG_E(TAG, "Failed to handle WiFi scan"); - return; - } + // update the text in case the loading task fails + popup_set_text( + app->popup, + "Failed to scan...\nTry reconnecting the board!", + 0, + 40, + AlignLeft, + AlignTop); + + // scan for wifi ad parse the results + flipper_http_loading_task( + flipper_http_scan_wifi, + flip_wifi_handle_scan, + FlipWiFiViewSubmenuScan, + FlipWiFiViewPopup, + &app->view_dispatcher); break; case FlipWiFiSubmenuIndexWiFiSaved: view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuSaved); @@ -358,7 +378,7 @@ static void callback_submenu_choices(void* context, uint32_t index) { } } -static void flip_wifi_text_updated_password_scan(void* context) { +void flip_wifi_text_updated_password_scan(void* context) { FlipWiFiApp* app = (FlipWiFiApp*)context; if(!app) { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); @@ -389,7 +409,7 @@ static void flip_wifi_text_updated_password_scan(void* context) { // switch to back to the scan view view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuScan); } -static void flip_wifi_text_updated_password_saved(void* context) { +void flip_wifi_text_updated_password_saved(void* context) { FlipWiFiApp* app = (FlipWiFiApp*)context; if(!app) { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); @@ -416,7 +436,7 @@ static void flip_wifi_text_updated_password_saved(void* context) { view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuSaved); } -static void flip_wifi_text_updated_add_ssid(void* context) { +void flip_wifi_text_updated_add_ssid(void* context) { FlipWiFiApp* app = (FlipWiFiApp*)context; if(!app) { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); @@ -435,7 +455,7 @@ static void flip_wifi_text_updated_add_ssid(void* context) { // do nothing for now, go to the next text input to set the password view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInputSavedAddPassword); } -static void flip_wifi_text_updated_add_password(void* context) { +void flip_wifi_text_updated_add_password(void* context) { FlipWiFiApp* app = (FlipWiFiApp*)context; if(!app) { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); @@ -467,5 +487,3 @@ static void flip_wifi_text_updated_add_password(void* context) { // switch to back to the saved view view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuSaved); } - -#endif // FLIP_WIFI_CALLBACK_H diff --git a/flip_wifi/callback/flip_wifi_callback.h b/flip_wifi/callback/flip_wifi_callback.h new file mode 100644 index 000000000..39c266223 --- /dev/null +++ b/flip_wifi/callback/flip_wifi_callback.h @@ -0,0 +1,41 @@ +#ifndef FLIP_WIFI_CALLBACK_H +#define FLIP_WIFI_CALLBACK_H + +#include +#include +#include + +// array to store each SSID +extern char* ssid_list[64]; +extern uint32_t ssid_index; + +void flip_wifi_redraw_submenu_saved(FlipWiFiApp* app); + +uint32_t callback_to_submenu_main(void* context); + +uint32_t callback_to_submenu_scan(void* context); + +uint32_t callback_to_submenu_saved(void* context); + +// Callback for drawing the main screen +void flip_wifi_view_draw_callback_scan(Canvas* canvas, void* model); + +void flip_wifi_view_draw_callback_saved(Canvas* canvas, void* model); + +// Input callback for the view (async input handling) +bool flip_wifi_view_input_callback_scan(InputEvent* event, void* context); + +// Input callback for the view (async input handling) +bool flip_wifi_view_input_callback_saved(InputEvent* event, void* context); + +void callback_submenu_choices(void* context, uint32_t index); + +void flip_wifi_text_updated_password_scan(void* context); + +void flip_wifi_text_updated_password_saved(void* context); + +void flip_wifi_text_updated_add_ssid(void* context); + +void flip_wifi_text_updated_add_password(void* context); + +#endif // FLIP_WIFI_CALLBACK_H diff --git a/flip_wifi/easy_flipper.h b/flip_wifi/easy_flipper.h deleted file mode 100644 index e8f0ad796..000000000 --- a/flip_wifi/easy_flipper.h +++ /dev/null @@ -1,534 +0,0 @@ -#ifndef EASY_FLIPPER_H -#define EASY_FLIPPER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define EASY_TAG "EasyFlipper" - -/** - * @brief Navigation callback for exiting the application - * @param context The context - unused - * @return next view id (VIEW_NONE to exit the app) - */ -uint32_t easy_flipper_callback_exit_app(void* context) { - // Exit the application - if(!context) { - FURI_LOG_E(EASY_TAG, "Context is NULL"); - return VIEW_NONE; - } - UNUSED(context); - return VIEW_NONE; // Return VIEW_NONE to exit the app -} - -/** - * @brief Initialize a buffer - * @param buffer The buffer to initialize - * @param buffer_size The size of the buffer - * @return true if successful, false otherwise - */ -bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size) { - if(!buffer) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_buffer"); - return false; - } - *buffer = (char*)malloc(buffer_size); - if(!*buffer) { - FURI_LOG_E(EASY_TAG, "Failed to allocate buffer"); - return false; - } - *buffer[0] = '\0'; - return true; -} - -/** - * @brief Initialize a View object - * @param view The View object to initialize - * @param view_id The ID/Index of the view - * @param draw_callback The draw callback function (set to NULL if not needed) - * @param input_callback The input callback function (set to NULL if not needed) - * @param previous_callback The previous callback function (can be set to NULL) - * @param view_dispatcher The ViewDispatcher object - * @return true if successful, false otherwise - */ -bool easy_flipper_set_view( - View** view, - int32_t view_id, - void draw_callback(Canvas*, void*), - bool input_callback(InputEvent*, void*), - uint32_t (*previous_callback)(void*), - ViewDispatcher** view_dispatcher, - void* context) { - if(!view || !view_dispatcher) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_view"); - return false; - } - *view = view_alloc(); - if(!*view) { - FURI_LOG_E(EASY_TAG, "Failed to allocate View"); - return false; - } - if(draw_callback) { - view_set_draw_callback(*view, draw_callback); - } - if(input_callback) { - view_set_input_callback(*view, input_callback); - } - if(context) { - view_set_context(*view, context); - } - if(previous_callback) { - view_set_previous_callback(*view, previous_callback); - } - view_dispatcher_add_view(*view_dispatcher, view_id, *view); - return true; -} - -/** - * @brief Initialize a ViewDispatcher object - * @param view_dispatcher The ViewDispatcher object to initialize - * @param gui The GUI object - * @param context The context to pass to the event callback - * @return true if successful, false otherwise - */ -bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui, void* context) { - if(!view_dispatcher) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_view_dispatcher"); - return false; - } - *view_dispatcher = view_dispatcher_alloc(); - if(!*view_dispatcher) { - FURI_LOG_E(EASY_TAG, "Failed to allocate ViewDispatcher"); - return false; - } - view_dispatcher_attach_to_gui(*view_dispatcher, gui, ViewDispatcherTypeFullscreen); - if(context) { - view_dispatcher_set_event_callback_context(*view_dispatcher, context); - } - return true; -} - -/** - * @brief Initialize a Submenu object - * @note This does not set the items in the submenu - * @param submenu The Submenu object to initialize - * @param view_id The ID/Index of the view - * @param title The title/header of the submenu - * @param previous_callback The previous callback function (can be set to NULL) - * @param view_dispatcher The ViewDispatcher object - * @return true if successful, false otherwise - */ -bool easy_flipper_set_submenu( - Submenu** submenu, - int32_t view_id, - char* title, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!submenu) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_submenu"); - return false; - } - *submenu = submenu_alloc(); - if(!*submenu) { - FURI_LOG_E(EASY_TAG, "Failed to allocate Submenu"); - return false; - } - if(title) { - submenu_set_header(*submenu, title); - } - if(previous_callback) { - view_set_previous_callback(submenu_get_view(*submenu), previous_callback); - } - view_dispatcher_add_view(*view_dispatcher, view_id, submenu_get_view(*submenu)); - return true; -} -/** - * @brief Initialize a Menu object - * @note This does not set the items in the menu - * @param menu The Menu object to initialize - * @param view_id The ID/Index of the view - * @param item_callback The item callback function - * @param previous_callback The previous callback function (can be set to NULL) - * @param view_dispatcher The ViewDispatcher object - * @return true if successful, false otherwise - */ -bool easy_flipper_set_menu( - Menu** menu, - int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!menu) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_menu"); - return false; - } - *menu = menu_alloc(); - if(!*menu) { - FURI_LOG_E(EASY_TAG, "Failed to allocate Menu"); - return false; - } - if(previous_callback) { - view_set_previous_callback(menu_get_view(*menu), previous_callback); - } - view_dispatcher_add_view(*view_dispatcher, view_id, menu_get_view(*menu)); - return true; -} - -/** - * @brief Initialize a Widget object - * @param widget The Widget object to initialize - * @param view_id The ID/Index of the view - * @param text The text to display in the widget - * @param previous_callback The previous callback function (can be set to NULL) - * @param view_dispatcher The ViewDispatcher object - * @return true if successful, false otherwise - */ -bool easy_flipper_set_widget( - Widget** widget, - int32_t view_id, - char* text, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!widget) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_widget"); - return false; - } - *widget = widget_alloc(); - if(!*widget) { - FURI_LOG_E(EASY_TAG, "Failed to allocate Widget"); - return false; - } - if(text) { - widget_add_text_scroll_element(*widget, 0, 0, 128, 64, text); - } - if(previous_callback) { - view_set_previous_callback(widget_get_view(*widget), previous_callback); - } - view_dispatcher_add_view(*view_dispatcher, view_id, widget_get_view(*widget)); - return true; -} - -/** - * @brief Initialize a VariableItemList object - * @note This does not set the items in the VariableItemList - * @param variable_item_list The VariableItemList object to initialize - * @param view_id The ID/Index of the view - * @param enter_callback The enter callback function (can be set to NULL) - * @param previous_callback The previous callback function (can be set to NULL) - * @param view_dispatcher The ViewDispatcher object - * @param context The context to pass to the enter callback (usually the app) - * @return true if successful, false otherwise - */ -bool easy_flipper_set_variable_item_list( - VariableItemList** variable_item_list, - int32_t view_id, - void (*enter_callback)(void*, uint32_t), - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher, - void* context) { - if(!variable_item_list) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_variable_item_list"); - return false; - } - *variable_item_list = variable_item_list_alloc(); - if(!*variable_item_list) { - FURI_LOG_E(EASY_TAG, "Failed to allocate VariableItemList"); - return false; - } - if(enter_callback) { - variable_item_list_set_enter_callback(*variable_item_list, enter_callback, context); - } - if(previous_callback) { - view_set_previous_callback( - variable_item_list_get_view(*variable_item_list), previous_callback); - } - view_dispatcher_add_view( - *view_dispatcher, view_id, variable_item_list_get_view(*variable_item_list)); - return true; -} - -/** - * @brief Initialize a TextInput object - * @param text_input The TextInput object to initialize - * @param view_id The ID/Index of the view - * @param previous_callback The previous callback function (can be set to NULL) - * @param view_dispatcher The ViewDispatcher object - * @return true if successful, false otherwise - */ -bool easy_flipper_set_text_input( - TextInput** text_input, - int32_t view_id, - char* header_text, - char* text_input_temp_buffer, - uint32_t text_input_buffer_size, - void (*result_callback)(void*), - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher, - void* context) { - if(!text_input) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_text_input"); - return false; - } - *text_input = text_input_alloc(); - if(!*text_input) { - FURI_LOG_E(EASY_TAG, "Failed to allocate TextInput"); - return false; - } - if(previous_callback) { - view_set_previous_callback(text_input_get_view(*text_input), previous_callback); - } - if(header_text) { - text_input_set_header_text(*text_input, header_text); - } - if(text_input_temp_buffer && text_input_buffer_size && result_callback) { - text_input_set_result_callback( - *text_input, - result_callback, - context, - text_input_temp_buffer, - text_input_buffer_size, - false); - } - view_dispatcher_add_view(*view_dispatcher, view_id, text_input_get_view(*text_input)); - return true; -} - -/** - * @brief Initialize a TextInput object with extra symbols - * @param uart_text_input The TextInput object to initialize - * @param view_id The ID/Index of the view - * @param previous_callback The previous callback function (can be set to NULL) - * @param view_dispatcher The ViewDispatcher object - * @return true if successful, false otherwise - */ -bool easy_flipper_set_uart_text_input( - TextInput** uart_text_input, - int32_t view_id, - char* header_text, - char* uart_text_input_temp_buffer, - uint32_t uart_text_input_buffer_size, - void (*result_callback)(void*), - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher, - void* context) { - if(!uart_text_input) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_uart_text_input"); - return false; - } - *uart_text_input = text_input_alloc(); - if(!*uart_text_input) { - FURI_LOG_E(EASY_TAG, "Failed to allocate UART_TextInput"); - return false; - } - if(previous_callback) { - view_set_previous_callback(text_input_get_view(*uart_text_input), previous_callback); - } - if(header_text) { - text_input_set_header_text(*uart_text_input, header_text); - } - if(uart_text_input_temp_buffer && uart_text_input_buffer_size && result_callback) { - text_input_set_result_callback( - *uart_text_input, - result_callback, - context, - uart_text_input_temp_buffer, - uart_text_input_buffer_size, - false); - } - text_input_show_illegal_symbols(*uart_text_input, true); - view_dispatcher_add_view(*view_dispatcher, view_id, text_input_get_view(*uart_text_input)); - return true; -} - -/** - * @brief Initialize a DialogEx object - * @param dialog_ex The DialogEx object to initialize - * @param view_id The ID/Index of the view - * @param header The header of the dialog - * @param header_x The x coordinate of the header - * @param header_y The y coordinate of the header - * @param text The text of the dialog - * @param text_x The x coordinate of the dialog - * @param text_y The y coordinate of the dialog - * @param left_button_text The text of the left button - * @param right_button_text The text of the right button - * @param center_button_text The text of the center button - * @param result_callback The result callback function - * @param previous_callback The previous callback function (can be set to NULL) - * @param view_dispatcher The ViewDispatcher object - * @param context The context to pass to the result callback - * @return true if successful, false otherwise - */ -bool easy_flipper_set_dialog_ex( - DialogEx** dialog_ex, - int32_t view_id, - char* header, - uint16_t header_x, - uint16_t header_y, - char* text, - uint16_t text_x, - uint16_t text_y, - char* left_button_text, - char* right_button_text, - char* center_button_text, - void (*result_callback)(DialogExResult, void*), - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher, - void* context) { - if(!dialog_ex) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_dialog_ex"); - return false; - } - *dialog_ex = dialog_ex_alloc(); - if(!*dialog_ex) { - FURI_LOG_E(EASY_TAG, "Failed to allocate DialogEx"); - return false; - } - if(header) { - dialog_ex_set_header(*dialog_ex, header, header_x, header_y, AlignLeft, AlignTop); - } - if(text) { - dialog_ex_set_text(*dialog_ex, text, text_x, text_y, AlignLeft, AlignTop); - } - if(left_button_text) { - dialog_ex_set_left_button_text(*dialog_ex, left_button_text); - } - if(right_button_text) { - dialog_ex_set_right_button_text(*dialog_ex, right_button_text); - } - if(center_button_text) { - dialog_ex_set_center_button_text(*dialog_ex, center_button_text); - } - if(result_callback) { - dialog_ex_set_result_callback(*dialog_ex, result_callback); - } - if(previous_callback) { - view_set_previous_callback(dialog_ex_get_view(*dialog_ex), previous_callback); - } - if(context) { - dialog_ex_set_context(*dialog_ex, context); - } - view_dispatcher_add_view(*view_dispatcher, view_id, dialog_ex_get_view(*dialog_ex)); - return true; -} - -/** - * @brief Initialize a Popup object - * @param popup The Popup object to initialize - * @param view_id The ID/Index of the view - * @param header The header of the dialog - * @param header_x The x coordinate of the header - * @param header_y The y coordinate of the header - * @param text The text of the dialog - * @param text_x The x coordinate of the dialog - * @param text_y The y coordinate of the dialog - * @param result_callback The result callback function - * @param previous_callback The previous callback function (can be set to NULL) - * @param view_dispatcher The ViewDispatcher object - * @param context The context to pass to the result callback - * @return true if successful, false otherwise - */ -bool easy_flipper_set_popup( - Popup** popup, - int32_t view_id, - char* header, - uint16_t header_x, - uint16_t header_y, - char* text, - uint16_t text_x, - uint16_t text_y, - void (*result_callback)(void*), - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher, - void* context) { - if(!popup) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_popup"); - return false; - } - *popup = popup_alloc(); - if(!*popup) { - FURI_LOG_E(EASY_TAG, "Failed to allocate Popup"); - return false; - } - if(header) { - popup_set_header(*popup, header, header_x, header_y, AlignLeft, AlignTop); - } - if(text) { - popup_set_text(*popup, text, text_x, text_y, AlignLeft, AlignTop); - } - if(result_callback) { - popup_set_callback(*popup, result_callback); - } - if(previous_callback) { - view_set_previous_callback(popup_get_view(*popup), previous_callback); - } - if(context) { - popup_set_context(*popup, context); - } - view_dispatcher_add_view(*view_dispatcher, view_id, popup_get_view(*popup)); - return true; -} - -/** - * @brief Initialize a Loading object - * @param loading The Loading object to initialize - * @param view_id The ID/Index of the view - * @param previous_callback The previous callback function (can be set to NULL) - * @param view_dispatcher The ViewDispatcher object - * @return true if successful, false otherwise - */ -bool easy_flipper_set_loading( - Loading** loading, - int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!loading) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_loading"); - return false; - } - *loading = loading_alloc(); - if(!*loading) { - FURI_LOG_E(EASY_TAG, "Failed to allocate Loading"); - return false; - } - if(previous_callback) { - view_set_previous_callback(loading_get_view(*loading), previous_callback); - } - view_dispatcher_add_view(*view_dispatcher, view_id, loading_get_view(*loading)); - return true; -} - -/** - * @brief Set a char butter to a FuriString - * @param furi_string The FuriString object - * @param buffer The buffer to copy the string to - * @return true if successful, false otherwise - */ -bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer) { - if(!furi_string) { - FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_buffer_to_furi_string"); - return false; - } - *furi_string = furi_string_alloc(); - if(!furi_string) { - FURI_LOG_E(EASY_TAG, "Failed to allocate FuriString"); - return false; - } - furi_string_set_str(*furi_string, buffer); - return true; -} - -#endif // EASY_FLIPPER_H diff --git a/flip_wifi/easy_flipper/easy_flipper.c b/flip_wifi/easy_flipper/easy_flipper.c new file mode 100644 index 000000000..8b98e1a1b --- /dev/null +++ b/flip_wifi/easy_flipper/easy_flipper.c @@ -0,0 +1,512 @@ +#include + +/** + * @brief Navigation callback for exiting the application + * @param context The context - unused + * @return next view id (VIEW_NONE to exit the app) + */ +uint32_t easy_flipper_callback_exit_app(void* context) { + // Exit the application + if(!context) { + FURI_LOG_E(EASY_TAG, "Context is NULL"); + return VIEW_NONE; + } + UNUSED(context); + return VIEW_NONE; // Return VIEW_NONE to exit the app +} + +/** + * @brief Initialize a buffer + * @param buffer The buffer to initialize + * @param buffer_size The size of the buffer + * @return true if successful, false otherwise + */ +bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size) { + if(!buffer) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_buffer"); + return false; + } + *buffer = (char*)malloc(buffer_size); + if(!*buffer) { + FURI_LOG_E(EASY_TAG, "Failed to allocate buffer"); + return false; + } + *buffer[0] = '\0'; + return true; +} + +/** + * @brief Initialize a View object + * @param view The View object to initialize + * @param view_id The ID/Index of the view + * @param draw_callback The draw callback function (set to NULL if not needed) + * @param input_callback The input callback function (set to NULL if not needed) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view( + View** view, + int32_t view_id, + void draw_callback(Canvas*, void*), + bool input_callback(InputEvent*, void*), + uint32_t (*previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context) { + if(!view || !view_dispatcher) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_view"); + return false; + } + *view = view_alloc(); + if(!*view) { + FURI_LOG_E(EASY_TAG, "Failed to allocate View"); + return false; + } + if(draw_callback) { + view_set_draw_callback(*view, draw_callback); + } + if(input_callback) { + view_set_input_callback(*view, input_callback); + } + if(context) { + view_set_context(*view, context); + } + if(previous_callback) { + view_set_previous_callback(*view, previous_callback); + } + view_dispatcher_add_view(*view_dispatcher, view_id, *view); + return true; +} + +/** + * @brief Initialize a ViewDispatcher object + * @param view_dispatcher The ViewDispatcher object to initialize + * @param gui The GUI object + * @param context The context to pass to the event callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui, void* context) { + if(!view_dispatcher) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_view_dispatcher"); + return false; + } + *view_dispatcher = view_dispatcher_alloc(); + if(!*view_dispatcher) { + FURI_LOG_E(EASY_TAG, "Failed to allocate ViewDispatcher"); + return false; + } + view_dispatcher_attach_to_gui(*view_dispatcher, gui, ViewDispatcherTypeFullscreen); + if(context) { + view_dispatcher_set_event_callback_context(*view_dispatcher, context); + } + return true; +} + +/** + * @brief Initialize a Submenu object + * @note This does not set the items in the submenu + * @param submenu The Submenu object to initialize + * @param view_id The ID/Index of the view + * @param title The title/header of the submenu + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_submenu( + Submenu** submenu, + int32_t view_id, + char* title, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher) { + if(!submenu) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_submenu"); + return false; + } + *submenu = submenu_alloc(); + if(!*submenu) { + FURI_LOG_E(EASY_TAG, "Failed to allocate Submenu"); + return false; + } + if(title) { + submenu_set_header(*submenu, title); + } + if(previous_callback) { + view_set_previous_callback(submenu_get_view(*submenu), previous_callback); + } + view_dispatcher_add_view(*view_dispatcher, view_id, submenu_get_view(*submenu)); + return true; +} +/** + * @brief Initialize a Menu object + * @note This does not set the items in the menu + * @param menu The Menu object to initialize + * @param view_id The ID/Index of the view + * @param item_callback The item callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_menu( + Menu** menu, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher) { + if(!menu) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_menu"); + return false; + } + *menu = menu_alloc(); + if(!*menu) { + FURI_LOG_E(EASY_TAG, "Failed to allocate Menu"); + return false; + } + if(previous_callback) { + view_set_previous_callback(menu_get_view(*menu), previous_callback); + } + view_dispatcher_add_view(*view_dispatcher, view_id, menu_get_view(*menu)); + return true; +} + +/** + * @brief Initialize a Widget object + * @param widget The Widget object to initialize + * @param view_id The ID/Index of the view + * @param text The text to display in the widget + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_widget( + Widget** widget, + int32_t view_id, + char* text, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher) { + if(!widget) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_widget"); + return false; + } + *widget = widget_alloc(); + if(!*widget) { + FURI_LOG_E(EASY_TAG, "Failed to allocate Widget"); + return false; + } + if(text) { + widget_add_text_scroll_element(*widget, 0, 0, 128, 64, text); + } + if(previous_callback) { + view_set_previous_callback(widget_get_view(*widget), previous_callback); + } + view_dispatcher_add_view(*view_dispatcher, view_id, widget_get_view(*widget)); + return true; +} + +/** + * @brief Initialize a VariableItemList object + * @note This does not set the items in the VariableItemList + * @param variable_item_list The VariableItemList object to initialize + * @param view_id The ID/Index of the view + * @param enter_callback The enter callback function (can be set to NULL) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the enter callback (usually the app) + * @return true if successful, false otherwise + */ +bool easy_flipper_set_variable_item_list( + VariableItemList** variable_item_list, + int32_t view_id, + void (*enter_callback)(void*, uint32_t), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context) { + if(!variable_item_list) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_variable_item_list"); + return false; + } + *variable_item_list = variable_item_list_alloc(); + if(!*variable_item_list) { + FURI_LOG_E(EASY_TAG, "Failed to allocate VariableItemList"); + return false; + } + if(enter_callback) { + variable_item_list_set_enter_callback(*variable_item_list, enter_callback, context); + } + if(previous_callback) { + view_set_previous_callback( + variable_item_list_get_view(*variable_item_list), previous_callback); + } + view_dispatcher_add_view( + *view_dispatcher, view_id, variable_item_list_get_view(*variable_item_list)); + return true; +} + +/** + * @brief Initialize a TextInput object + * @param text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_text_input( + TextInput** text_input, + int32_t view_id, + char* header_text, + char* text_input_temp_buffer, + uint32_t text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context) { + if(!text_input) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_text_input"); + return false; + } + *text_input = text_input_alloc(); + if(!*text_input) { + FURI_LOG_E(EASY_TAG, "Failed to allocate TextInput"); + return false; + } + if(previous_callback) { + view_set_previous_callback(text_input_get_view(*text_input), previous_callback); + } + if(header_text) { + text_input_set_header_text(*text_input, header_text); + } + if(text_input_temp_buffer && text_input_buffer_size && result_callback) { + text_input_set_result_callback( + *text_input, + result_callback, + context, + text_input_temp_buffer, + text_input_buffer_size, + false); + } + view_dispatcher_add_view(*view_dispatcher, view_id, text_input_get_view(*text_input)); + return true; +} + +/** + * @brief Initialize a TextInput object with extra symbols + * @param uart_text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_uart_text_input( + TextInput** uart_text_input, + int32_t view_id, + char* header_text, + char* uart_text_input_temp_buffer, + uint32_t uart_text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context) { + if(!uart_text_input) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_uart_text_input"); + return false; + } + *uart_text_input = text_input_alloc(); + if(!*uart_text_input) { + FURI_LOG_E(EASY_TAG, "Failed to allocate UART_TextInput"); + return false; + } + if(previous_callback) { + view_set_previous_callback(text_input_get_view(*uart_text_input), previous_callback); + } + if(header_text) { + text_input_set_header_text(*uart_text_input, header_text); + } + if(uart_text_input_temp_buffer && uart_text_input_buffer_size && result_callback) { + text_input_set_result_callback( + *uart_text_input, + result_callback, + context, + uart_text_input_temp_buffer, + uart_text_input_buffer_size, + false); + } + text_input_show_illegal_symbols(*uart_text_input, true); + view_dispatcher_add_view(*view_dispatcher, view_id, text_input_get_view(*uart_text_input)); + return true; +} + +/** + * @brief Initialize a DialogEx object + * @param dialog_ex The DialogEx object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param left_button_text The text of the left button + * @param right_button_text The text of the right button + * @param center_button_text The text of the center button + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_dialog_ex( + DialogEx** dialog_ex, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + char* left_button_text, + char* right_button_text, + char* center_button_text, + void (*result_callback)(DialogExResult, void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context) { + if(!dialog_ex) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_dialog_ex"); + return false; + } + *dialog_ex = dialog_ex_alloc(); + if(!*dialog_ex) { + FURI_LOG_E(EASY_TAG, "Failed to allocate DialogEx"); + return false; + } + if(header) { + dialog_ex_set_header(*dialog_ex, header, header_x, header_y, AlignLeft, AlignTop); + } + if(text) { + dialog_ex_set_text(*dialog_ex, text, text_x, text_y, AlignLeft, AlignTop); + } + if(left_button_text) { + dialog_ex_set_left_button_text(*dialog_ex, left_button_text); + } + if(right_button_text) { + dialog_ex_set_right_button_text(*dialog_ex, right_button_text); + } + if(center_button_text) { + dialog_ex_set_center_button_text(*dialog_ex, center_button_text); + } + if(result_callback) { + dialog_ex_set_result_callback(*dialog_ex, result_callback); + } + if(previous_callback) { + view_set_previous_callback(dialog_ex_get_view(*dialog_ex), previous_callback); + } + if(context) { + dialog_ex_set_context(*dialog_ex, context); + } + view_dispatcher_add_view(*view_dispatcher, view_id, dialog_ex_get_view(*dialog_ex)); + return true; +} + +/** + * @brief Initialize a Popup object + * @param popup The Popup object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_popup( + Popup** popup, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context) { + if(!popup) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_popup"); + return false; + } + *popup = popup_alloc(); + if(!*popup) { + FURI_LOG_E(EASY_TAG, "Failed to allocate Popup"); + return false; + } + if(header) { + popup_set_header(*popup, header, header_x, header_y, AlignLeft, AlignTop); + } + if(text) { + popup_set_text(*popup, text, text_x, text_y, AlignLeft, AlignTop); + } + if(result_callback) { + popup_set_callback(*popup, result_callback); + } + if(previous_callback) { + view_set_previous_callback(popup_get_view(*popup), previous_callback); + } + if(context) { + popup_set_context(*popup, context); + } + view_dispatcher_add_view(*view_dispatcher, view_id, popup_get_view(*popup)); + return true; +} + +/** + * @brief Initialize a Loading object + * @param loading The Loading object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_loading( + Loading** loading, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher) { + if(!loading) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_loading"); + return false; + } + *loading = loading_alloc(); + if(!*loading) { + FURI_LOG_E(EASY_TAG, "Failed to allocate Loading"); + return false; + } + if(previous_callback) { + view_set_previous_callback(loading_get_view(*loading), previous_callback); + } + view_dispatcher_add_view(*view_dispatcher, view_id, loading_get_view(*loading)); + return true; +} + +/** + * @brief Set a char butter to a FuriString + * @param furi_string The FuriString object + * @param buffer The buffer to copy the string to + * @return true if successful, false otherwise + */ +bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer) { + if(!furi_string) { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_buffer_to_furi_string"); + return false; + } + *furi_string = furi_string_alloc(); + if(!furi_string) { + FURI_LOG_E(EASY_TAG, "Failed to allocate FuriString"); + return false; + } + furi_string_set_str(*furi_string, buffer); + return true; +} diff --git a/flip_wifi/easy_flipper/easy_flipper.h b/flip_wifi/easy_flipper/easy_flipper.h new file mode 100644 index 000000000..219e42c74 --- /dev/null +++ b/flip_wifi/easy_flipper/easy_flipper.h @@ -0,0 +1,263 @@ +#ifndef EASY_FLIPPER_H +#define EASY_FLIPPER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EASY_TAG "EasyFlipper" + +/** + * @brief Navigation callback for exiting the application + * @param context The context - unused + * @return next view id (VIEW_NONE to exit the app) + */ +uint32_t easy_flipper_callback_exit_app(void* context); +/** + * @brief Initialize a buffer + * @param buffer The buffer to initialize + * @param buffer_size The size of the buffer + * @return true if successful, false otherwise + */ +bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size); +/** + * @brief Initialize a View object + * @param view The View object to initialize + * @param view_id The ID/Index of the view + * @param draw_callback The draw callback function (set to NULL if not needed) + * @param input_callback The input callback function (set to NULL if not needed) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view( + View** view, + int32_t view_id, + void draw_callback(Canvas*, void*), + bool input_callback(InputEvent*, void*), + uint32_t (*previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a ViewDispatcher object + * @param view_dispatcher The ViewDispatcher object to initialize + * @param gui The GUI object + * @param context The context to pass to the event callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui, void* context); + +/** + * @brief Initialize a Submenu object + * @note This does not set the items in the submenu + * @param submenu The Submenu object to initialize + * @param view_id The ID/Index of the view + * @param title The title/header of the submenu + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_submenu( + Submenu** submenu, + int32_t view_id, + char* title, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a Menu object + * @note This does not set the items in the menu + * @param menu The Menu object to initialize + * @param view_id The ID/Index of the view + * @param item_callback The item callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_menu( + Menu** menu, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a Widget object + * @param widget The Widget object to initialize + * @param view_id The ID/Index of the view + * @param text The text to display in the widget + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_widget( + Widget** widget, + int32_t view_id, + char* text, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Initialize a VariableItemList object + * @note This does not set the items in the VariableItemList + * @param variable_item_list The VariableItemList object to initialize + * @param view_id The ID/Index of the view + * @param enter_callback The enter callback function (can be set to NULL) + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the enter callback (usually the app) + * @return true if successful, false otherwise + */ +bool easy_flipper_set_variable_item_list( + VariableItemList** variable_item_list, + int32_t view_id, + void (*enter_callback)(void*, uint32_t), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a TextInput object + * @param text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_text_input( + TextInput** text_input, + int32_t view_id, + char* header_text, + char* text_input_temp_buffer, + uint32_t text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a TextInput object with extra symbols + * @param uart_text_input The TextInput object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_uart_text_input( + TextInput** uart_text_input, + int32_t view_id, + char* header_text, + char* uart_text_input_temp_buffer, + uint32_t uart_text_input_buffer_size, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a DialogEx object + * @param dialog_ex The DialogEx object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param left_button_text The text of the left button + * @param right_button_text The text of the right button + * @param center_button_text The text of the center button + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_dialog_ex( + DialogEx** dialog_ex, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + char* left_button_text, + char* right_button_text, + char* center_button_text, + void (*result_callback)(DialogExResult, void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a Popup object + * @param popup The Popup object to initialize + * @param view_id The ID/Index of the view + * @param header The header of the dialog + * @param header_x The x coordinate of the header + * @param header_y The y coordinate of the header + * @param text The text of the dialog + * @param text_x The x coordinate of the dialog + * @param text_y The y coordinate of the dialog + * @param result_callback The result callback function + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @param context The context to pass to the result callback + * @return true if successful, false otherwise + */ +bool easy_flipper_set_popup( + Popup** popup, + int32_t view_id, + char* header, + uint16_t header_x, + uint16_t header_y, + char* text, + uint16_t text_x, + uint16_t text_y, + void (*result_callback)(void*), + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher, + void* context); + +/** + * @brief Initialize a Loading object + * @param loading The Loading object to initialize + * @param view_id The ID/Index of the view + * @param previous_callback The previous callback function (can be set to NULL) + * @param view_dispatcher The ViewDispatcher object + * @return true if successful, false otherwise + */ +bool easy_flipper_set_loading( + Loading** loading, + int32_t view_id, + uint32_t(previous_callback)(void*), + ViewDispatcher** view_dispatcher); + +/** + * @brief Set a char butter to a FuriString + * @param furi_string The FuriString object + * @param buffer The buffer to copy the string to + * @return true if successful, false otherwise + */ +bool easy_flipper_set_char_to_furi_string(FuriString** furi_string, char* buffer); + +#endif diff --git a/flip_wifi/flip_wifi_storage.h b/flip_wifi/flip_storage/flip_wifi_storage.c similarity index 97% rename from flip_wifi/flip_wifi_storage.h rename to flip_wifi/flip_storage/flip_wifi_storage.c index 0c0faac7d..0622b1210 100644 --- a/flip_wifi/flip_wifi_storage.h +++ b/flip_wifi/flip_storage/flip_wifi_storage.c @@ -1,11 +1,17 @@ -#ifndef FLIP_WIFI_STORAGE_H -#define FLIP_WIFI_STORAGE_H +#include -// define the paths for all of the FlipperHTTP apps -#define WIFI_SSID_LIST_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/wifi_list.txt" +char* app_ids[8] = { + "flip_wifi", + "flip_store", + "flip_social", + "flip_trader", + "flip_weather", + "flip_library", + "web_crawler", + "flip_rss"}; // Function to save the playlist -void save_playlist(const WiFiPlaylist* playlist) { +void save_playlist(WiFiPlaylist* playlist) { if(!playlist) { FURI_LOG_E(TAG, "Playlist is NULL"); return; @@ -192,20 +198,11 @@ bool load_playlist(WiFiPlaylist* playlist) { return true; } -char* app_ids[7] = { - "flip_wifi", - "flip_store", - "flip_social", - "flip_trader", - "flip_weather", - "flip_library", - "web_crawler"}; - void save_settings(const char* ssid, const char* password) { char edited_directory_path[128]; char edited_file_path[128]; - for(size_t i = 0; i < 7; i++) { + for(size_t i = 0; i < 8; i++) { // Construct the directory and file paths for the current app snprintf( edited_directory_path, @@ -270,6 +267,7 @@ void save_settings(const char* ssid, const char* password) { file_size = 0; buffer = NULL; } + storage_file_free(file); // Prepare new SSID and Password @@ -389,5 +387,3 @@ void save_settings(const char* ssid, const char* password) { furi_record_close(RECORD_STORAGE); } } - -#endif diff --git a/flip_wifi/flip_storage/flip_wifi_storage.h b/flip_wifi/flip_storage/flip_wifi_storage.h new file mode 100644 index 000000000..1948e8608 --- /dev/null +++ b/flip_wifi/flip_storage/flip_wifi_storage.h @@ -0,0 +1,18 @@ +#ifndef FLIP_WIFI_STORAGE_H +#define FLIP_WIFI_STORAGE_H + +#include + +// define the paths for all of the FlipperHTTP apps +#define WIFI_SSID_LIST_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/wifi_list.txt" + +extern char* app_ids[8]; + +// Function to save the playlist +void save_playlist(WiFiPlaylist* playlist); + +// Function to load the playlist +bool load_playlist(WiFiPlaylist* playlist); + +void save_settings(const char* ssid, const char* password); +#endif diff --git a/flip_wifi/flip_wifi_free.h b/flip_wifi/flip_wifi.c similarity index 95% rename from flip_wifi/flip_wifi_free.h rename to flip_wifi/flip_wifi.c index 8f4e32cff..af54dcc69 100644 --- a/flip_wifi/flip_wifi_free.h +++ b/flip_wifi/flip_wifi.c @@ -1,8 +1,9 @@ -#ifndef FLIP_WIFI_FREE_H -#define FLIP_WIFI_FREE_H +#include "flip_wifi.h" + +FlipWiFiApp* app_instance = NULL; // Function to free the resources used by FlipWiFiApp -static void flip_wifi_app_free(FlipWiFiApp* app) { +void flip_wifi_app_free(FlipWiFiApp* app) { if(!app) { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); return; @@ -80,5 +81,3 @@ static void flip_wifi_app_free(FlipWiFiApp* app) { // free the app if(app) free(app); } - -#endif // FLIP_WIFI_FREE_H diff --git a/flip_wifi/flip_wifi_e.h b/flip_wifi/flip_wifi.h similarity index 94% rename from flip_wifi/flip_wifi_e.h rename to flip_wifi/flip_wifi.h index 13ed1aa05..93b87256e 100644 --- a/flip_wifi/flip_wifi_e.h +++ b/flip_wifi/flip_wifi.h @@ -1,8 +1,8 @@ #ifndef FLIP_WIFI_E_H #define FLIP_WIFI_E_H -#include -#include +#include +#include #include #define TAG "FlipWiFi" @@ -82,4 +82,9 @@ typedef struct { WiFiPlaylist wifi_playlist; // The playlist of wifi networks } FlipWiFiApp; +extern FlipWiFiApp* app_instance; + +// Function to free the resources used by FlipWiFiApp +void flip_wifi_app_free(FlipWiFiApp* app); + #endif // FLIP_WIFI_E_H diff --git a/flip_wifi/flipper_http/flipper_http.c b/flip_wifi/flipper_http/flipper_http.c new file mode 100644 index 000000000..060d7f58c --- /dev/null +++ b/flip_wifi/flipper_http/flipper_http.c @@ -0,0 +1,1366 @@ +#include +FlipperHTTP fhttp; +char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +uint8_t file_buffer[FILE_BUFFER_SIZE]; +size_t file_buffer_len = 0; +// Function to append received data to file +// make sure to initialize the file path before calling this function +bool flipper_http_append_to_file( + const void* data, + size_t data_size, + bool start_new_file, + char* file_path) { + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + + if(start_new_file) { + // Delete the file if it already exists + if(storage_file_exists(storage, file_path)) { + if(!storage_simply_remove_recursive(storage, file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to delete file: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + } + // Open the file in write mode + if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + } else { + // Open the file in append mode + if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_OPEN_APPEND)) { + FURI_LOG_E(HTTP_TAG, "Failed to open file for appending: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + } + + // Write the data to the file + if(storage_file_write(file, data, data_size) != data_size) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return true; +} + +FuriString* flipper_http_load_from_file(char* file_path) { + // Open the storage record + Storage* storage = furi_record_open(RECORD_STORAGE); + if(!storage) { + FURI_LOG_E(HTTP_TAG, "Failed to open storage record"); + return NULL; + } + + // Allocate a file handle + File* file = storage_file_alloc(storage); + if(!file) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate storage file"); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Open the file for reading + if(!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; // Return false if the file does not exist + } + + // Allocate a FuriString to hold the received data + FuriString* str_result = furi_string_alloc(); + if(!str_result) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Reset the FuriString to ensure it's empty before reading + furi_string_reset(str_result); + + // Define a buffer to hold the read data + uint8_t* buffer = (uint8_t*)malloc(MAX_FILE_SHOW); + if(!buffer) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer"); + furi_string_free(str_result); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Read data into the buffer + size_t read_count = storage_file_read(file, buffer, MAX_FILE_SHOW); + if(storage_file_get_error(file) != FSE_OK) { + FURI_LOG_E(HTTP_TAG, "Error reading from file."); + furi_string_free(str_result); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Append each byte to the FuriString + for(size_t i = 0; i < read_count; i++) { + furi_string_push_back(str_result, buffer[i]); + } + + // Check if there is more data beyond the maximum size + char extra_byte; + storage_file_read(file, &extra_byte, 1); + + // Clean up + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + free(buffer); + return str_result; +} + +// UART worker thread +/** + * @brief Worker thread to handle UART data asynchronously. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +// UART worker thread +int32_t flipper_http_worker(void* context) { + UNUSED(context); + size_t rx_line_pos = 0; + + while(1) { + uint32_t events = furi_thread_flags_wait( + WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever); + if(events & WorkerEvtStop) { + break; + } + if(events & WorkerEvtRxDone) { + // Continuously read from the stream buffer until it's empty + while(!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) { + // Read one byte at a time + char c = 0; + size_t received = furi_stream_buffer_receive(fhttp.flipper_http_stream, &c, 1, 0); + + if(received == 0) { + // No more data to read + break; + } + + // Append the received byte to the file if saving is enabled + if(fhttp.save_bytes) { + // Add byte to the buffer + file_buffer[file_buffer_len++] = c; + // Write to file if buffer is full + if(file_buffer_len >= FILE_BUFFER_SIZE) { + if(!flipper_http_append_to_file( + file_buffer, + file_buffer_len, + fhttp.just_started_bytes, + fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); + } + file_buffer_len = 0; + fhttp.just_started_bytes = false; + } + } + + // Handle line buffering only if callback is set (text data) + if(fhttp.handle_rx_line_cb) { + // Handle line buffering + if(c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1) { + rx_line_buffer[rx_line_pos] = '\0'; // Null-terminate the line + + // Invoke the callback with the complete line + fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context); + + // Reset the line buffer position + rx_line_pos = 0; + } else { + rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer + } + } + } + } + } + + return 0; +} +// Timer callback function +/** + * @brief Callback function for the GET timeout timer. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will be called when the GET request times out. + */ +void get_timeout_timer_callback(void* context) { + UNUSED(context); + FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving the end."); + + // Reset the state + fhttp.started_receiving_get = false; + fhttp.started_receiving_post = false; + fhttp.started_receiving_put = false; + fhttp.started_receiving_delete = false; + + // Update UART state + fhttp.state = ISSUE; +} + +// UART RX Handler Callback (Interrupt Context) +/** + * @brief A private callback function to handle received data asynchronously. + * @return void + * @param handle The UART handle. + * @param event The event type. + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +void _flipper_http_rx_callback( + FuriHalSerialHandle* handle, + FuriHalSerialRxEvent event, + void* context) { + UNUSED(context); + if(event == FuriHalSerialRxEventData) { + uint8_t data = furi_hal_serial_async_rx(handle); + furi_stream_buffer_send(fhttp.flipper_http_stream, &data, 1, 0); + furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtRxDone); + } +} + +// UART initialization function +/** + * @brief Initialize UART. + * @return true if the UART was initialized successfully, false otherwise. + * @param callback The callback function to handle received data (ex. flipper_http_rx_callback). + * @param context The context to pass to the callback. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { + if(!context) { + FURI_LOG_E(HTTP_TAG, "Invalid context provided to flipper_http_init."); + return false; + } + if(!callback) { + FURI_LOG_E(HTTP_TAG, "Invalid callback provided to flipper_http_init."); + return false; + } + fhttp.flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1); + if(!fhttp.flipper_http_stream) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate UART stream buffer."); + return false; + } + + fhttp.rx_thread = furi_thread_alloc(); + if(!fhttp.rx_thread) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread."); + furi_stream_buffer_free(fhttp.flipper_http_stream); + return false; + } + + furi_thread_set_name(fhttp.rx_thread, "FlipperHTTP_RxThread"); + furi_thread_set_stack_size(fhttp.rx_thread, 1024); + furi_thread_set_context(fhttp.rx_thread, &fhttp); + furi_thread_set_callback(fhttp.rx_thread, flipper_http_worker); + + fhttp.handle_rx_line_cb = callback; + fhttp.callback_context = context; + + furi_thread_start(fhttp.rx_thread); + fhttp.rx_thread_id = furi_thread_get_id(fhttp.rx_thread); + + // handle when the UART control is busy to avoid furi_check failed + if(furi_hal_serial_control_is_busy(UART_CH)) { + FURI_LOG_E(HTTP_TAG, "UART control is busy."); + return false; + } + + fhttp.serial_handle = furi_hal_serial_control_acquire(UART_CH); + if(!fhttp.serial_handle) { + FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL"); + // Cleanup resources + furi_thread_free(fhttp.rx_thread); + furi_stream_buffer_free(fhttp.flipper_http_stream); + return false; + } + + // Initialize UART with acquired handle + furi_hal_serial_init(fhttp.serial_handle, BAUDRATE); + + // Enable RX direction + furi_hal_serial_enable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); + + // Start asynchronous RX with the callback + furi_hal_serial_async_rx_start(fhttp.serial_handle, _flipper_http_rx_callback, &fhttp, false); + + // Wait for the TX to complete to ensure UART is ready + furi_hal_serial_tx_wait_complete(fhttp.serial_handle); + + // Allocate the timer for handling timeouts + fhttp.get_timeout_timer = furi_timer_alloc( + get_timeout_timer_callback, // Callback function + FuriTimerTypeOnce, // One-shot timer + &fhttp // Context passed to callback + ); + + if(!fhttp.get_timeout_timer) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate HTTP request timeout timer."); + // Cleanup resources + furi_hal_serial_async_rx_stop(fhttp.serial_handle); + furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); + furi_hal_serial_control_release(fhttp.serial_handle); + furi_hal_serial_deinit(fhttp.serial_handle); + furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop); + furi_thread_join(fhttp.rx_thread); + furi_thread_free(fhttp.rx_thread); + furi_stream_buffer_free(fhttp.flipper_http_stream); + return false; + } + + // Set the timer thread priority if needed + furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated); + + fhttp.last_response = (char*)malloc(RX_BUF_SIZE); + if(!fhttp.last_response) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate memory for last_response."); + return false; + } + + // FURI_LOG_I(HTTP_TAG, "UART initialized successfully."); + return true; +} + +// Deinitialize UART +/** + * @brief Deinitialize UART. + * @return void + * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. + */ +void flipper_http_deinit() { + if(fhttp.serial_handle == NULL) { + FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?"); + return; + } + // Stop asynchronous RX + furi_hal_serial_async_rx_stop(fhttp.serial_handle); + + // Release and deinitialize the serial handle + furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); + furi_hal_serial_control_release(fhttp.serial_handle); + furi_hal_serial_deinit(fhttp.serial_handle); + + // Signal the worker thread to stop + furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop); + // Wait for the thread to finish + furi_thread_join(fhttp.rx_thread); + // Free the thread resources + furi_thread_free(fhttp.rx_thread); + + // Free the stream buffer + furi_stream_buffer_free(fhttp.flipper_http_stream); + + // Free the timer + if(fhttp.get_timeout_timer) { + furi_timer_free(fhttp.get_timeout_timer); + fhttp.get_timeout_timer = NULL; + } + + // Free the last response + if(fhttp.last_response) { + free(fhttp.last_response); + fhttp.last_response = NULL; + } + + // FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully."); +} + +// Function to send data over UART with newline termination +/** + * @brief Send data over UART with newline termination. + * @return true if the data was sent successfully, false otherwise. + * @param data The data to send over UART. + * @note The data will be sent over UART with a newline character appended. + */ +bool flipper_http_send_data(const char* data) { + size_t data_length = strlen(data); + if(data_length == 0) { + FURI_LOG_E("FlipperHTTP", "Attempted to send empty data."); + return false; + } + + // Create a buffer with data + '\n' + size_t send_length = data_length + 1; // +1 for '\n' + if(send_length > 256) { // Ensure buffer size is sufficient + FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP."); + return false; + } + + char send_buffer[257]; // 256 + 1 for safety + strncpy(send_buffer, data, 256); + send_buffer[data_length] = '\n'; // Append newline + send_buffer[data_length + 1] = '\0'; // Null-terminate + + if(fhttp.state == INACTIVE && ((strstr(send_buffer, "[PING]") == NULL) && + (strstr(send_buffer, "[WIFI/CONNECT]") == NULL))) { + FURI_LOG_E("FlipperHTTP", "Cannot send data while INACTIVE."); + fhttp.last_response = "Cannot send data while INACTIVE."; + return false; + } + + fhttp.state = SENDING; + furi_hal_serial_tx(fhttp.serial_handle, (const uint8_t*)send_buffer, send_length); + + // Uncomment below line to log the data sent over UART + // FURI_LOG_I("FlipperHTTP", "Sent data over UART: %s", send_buffer); + // fhttp.state = IDLE; + return true; +} + +// Function to send a PING request +/** + * @brief Send a PING request to check if the Wifi Dev Board is connected. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + * @note This is best used to check if the Wifi Dev Board is connected. + * @note The state will remain INACTIVE until a PONG is received. + */ +bool flipper_http_ping() { + const char* command = "[PING]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send PING command."); + return false; + } + // set state as INACTIVE to be made IDLE if PONG is received + fhttp.state = INACTIVE; + // The response will be handled asynchronously via the callback + return true; +} + +// Function to list available commands +/** + * @brief Send a command to list available commands. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_list_commands() { + const char* command = "[LIST]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send LIST command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to turn on the LED +/** + * @brief Allow the LED to display while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_on() { + const char* command = "[LED/ON]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send LED ON command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to turn off the LED +/** + * @brief Disable the LED from displaying while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_off() { + const char* command = "[LED/OFF]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send LED OFF command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to parse JSON data +/** + * @brief Parse JSON data. + * @return true if the JSON data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON data. + * @param json_data The JSON data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json(const char* key, const char* json_data) { + if(!key || !json_data) { + FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json."); + return false; + } + + char buffer[256]; + int ret = + snprintf(buffer, sizeof(buffer), "[PARSE]{\"key\":\"%s\",\"json\":%s}", key, json_data); + if(ret < 0 || ret >= (int)sizeof(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to format JSON parse command."); + return false; + } + + if(!flipper_http_send_data(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to parse JSON array data +/** + * @brief Parse JSON array data. + * @return true if the JSON array data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON array data. + * @param index The index to parse from the JSON array data. + * @param json_data The JSON array data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json_array(const char* key, int index, const char* json_data) { + if(!key || !json_data) { + FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json_array."); + return false; + } + + char buffer[256]; + int ret = snprintf( + buffer, + sizeof(buffer), + "[PARSE/ARRAY]{\"key\":\"%s\",\"index\":%d,\"json\":%s}", + key, + index, + json_data); + if(ret < 0 || ret >= (int)sizeof(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to format JSON parse array command."); + return false; + } + + if(!flipper_http_send_data(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse array command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to scan for WiFi networks +/** + * @brief Send a command to scan for WiFi networks. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_scan_wifi() { + if(!flipper_http_send_data("[WIFI/SCAN]")) { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi scan command."); + return false; + } + // custom for FlipWiFi app + fhttp.just_started_get = true; + + snprintf( + fhttp.file_path, + sizeof(fhttp.file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/scan.txt"); + + fhttp.save_received_data = true; + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to save WiFi settings (returns true if successful) +/** + * @brief Send a command to save WiFi settings. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_save_wifi(const char* ssid, const char* password) { + if(!ssid || !password) { + FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_save_wifi."); + return false; + } + char buffer[256]; + int ret = snprintf( + buffer, sizeof(buffer), "[WIFI/SAVE]{\"ssid\":\"%s\",\"password\":\"%s\"}", ssid, password); + if(ret < 0 || ret >= (int)sizeof(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to format WiFi save command."); + return false; + } + + if(!flipper_http_send_data(buffer)) { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to get IP address of WiFi Devboard +/** + * @brief Send a command to get the IP address of the WiFi Devboard + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_address() { + if(!flipper_http_send_data("[IP/ADDRESS]")) { + FURI_LOG_E("FlipperHTTP", "Failed to send IP address command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to get IP address of the connected WiFi network +/** + * @brief Send a command to get the IP address of the connected WiFi network. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_wifi() { + const char* command = "[WIFI/IP]"; + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi IP command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to disconnect from WiFi (returns true if successful) +/** + * @brief Send a command to disconnect from WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_disconnect_wifi() { + if(!flipper_http_send_data("[WIFI/DISCONNECT]")) { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to connect to WiFi (returns true if successful) +/** + * @brief Send a command to connect to WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_connect_wifi() { + if(!flipper_http_send_data("[WIFI/CONNECT]")) { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} + +// Function to send a GET request +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request(const char* url) { + if(!url) { + FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request."); + return false; + } + + // Prepare GET request command + char command[256]; + int ret = snprintf(command, sizeof(command), "[GET]%s", url); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format GET request command."); + return false; + } + + // Send GET request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send GET request command."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a GET request with headers +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_with_headers(const char* url, const char* headers) { + if(!url || !headers) { + FURI_LOG_E( + "FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_with_headers."); + return false; + } + + // Prepare GET request command with headers + char command[256]; + int ret = snprintf( + command, sizeof(command), "[GET/HTTP]{\"url\":\"%s\",\"headers\":%s}", url, headers); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers."); + return false; + } + + // Send GET request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a GET request with headers and return bytes +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_bytes(const char* url, const char* headers) { + if(!url || !headers) { + FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_bytes."); + return false; + } + + // Prepare GET request command with headers + char command[256]; + int ret = snprintf( + command, sizeof(command), "[GET/BYTES]{\"url\":\"%s\",\"headers\":%s}", url, headers); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers."); + return false; + } + + // Send GET request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a POST request with headers +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param data The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_with_headers( + const char* url, + const char* headers, + const char* payload) { + if(!url || !headers || !payload) { + FURI_LOG_E( + "FlipperHTTP", + "Invalid arguments provided to flipper_http_post_request_with_headers."); + return false; + } + + // Prepare POST request command with headers and data + char command[256]; + int ret = snprintf( + command, + sizeof(command), + "[POST/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", + url, + headers, + payload); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data."); + return false; + } + + // Send POST request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a POST request with headers and return bytes +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param payload The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload) { + if(!url || !headers || !payload) { + FURI_LOG_E( + "FlipperHTTP", "Invalid arguments provided to flipper_http_post_request_bytes."); + return false; + } + + // Prepare POST request command with headers and data + char command[256]; + int ret = snprintf( + command, + sizeof(command), + "[POST/BYTES]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", + url, + headers, + payload); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data."); + return false; + } + + // Send POST request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a PUT request with headers +/** + * @brief Send a PUT request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the PUT request to. + * @param headers The headers to send with the PUT request. + * @param data The data to send with the PUT request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_put_request_with_headers( + const char* url, + const char* headers, + const char* payload) { + if(!url || !headers || !payload) { + FURI_LOG_E( + "FlipperHTTP", "Invalid arguments provided to flipper_http_put_request_with_headers."); + return false; + } + + // Prepare PUT request command with headers and data + char command[256]; + int ret = snprintf( + command, + sizeof(command), + "[PUT/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", + url, + headers, + payload); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to format PUT request command with headers and data."); + return false; + } + + // Send PUT request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to send a DELETE request with headers +/** + * @brief Send a DELETE request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the DELETE request to. + * @param headers The headers to send with the DELETE request. + * @param data The data to send with the DELETE request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_delete_request_with_headers( + const char* url, + const char* headers, + const char* payload) { + if(!url || !headers || !payload) { + FURI_LOG_E( + "FlipperHTTP", + "Invalid arguments provided to flipper_http_delete_request_with_headers."); + return false; + } + + // Prepare DELETE request command with headers and data + char command[256]; + int ret = snprintf( + command, + sizeof(command), + "[DELETE/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", + url, + headers, + payload); + if(ret < 0 || ret >= (int)sizeof(command)) { + FURI_LOG_E( + "FlipperHTTP", "Failed to format DELETE request command with headers and data."); + return false; + } + + // Send DELETE request via UART + if(!flipper_http_send_data(command)) { + FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data."); + return false; + } + + // The response will be handled asynchronously via the callback + return true; +} +// Function to handle received data asynchronously +/** + * @brief Callback function to handle received data asynchronously. + * @return void + * @param line The received line. + * @param context The context passed to the callback. + * @note The received data will be handled asynchronously via the callback and handles the state of the UART. + */ +void flipper_http_rx_callback(const char* line, void* context) { + if(!line || !context) { + FURI_LOG_E(HTTP_TAG, "Invalid arguments provided to flipper_http_rx_callback."); + return; + } + + // Trim the received line to check if it's empty + char* trimmed_line = trim(line); + if(trimmed_line != NULL && trimmed_line[0] != '\0') { + // if the line is not [GET/END] or [POST/END] or [PUT/END] or [DELETE/END] + if(strstr(trimmed_line, "[GET/END]") == NULL && + strstr(trimmed_line, "[POST/END]") == NULL && + strstr(trimmed_line, "[PUT/END]") == NULL && + strstr(trimmed_line, "[DELETE/END]") == NULL) { + strncpy(fhttp.last_response, trimmed_line, RX_BUF_SIZE); + } + } + free(trimmed_line); // Free the allocated memory for trimmed_line + + if(fhttp.state != INACTIVE && fhttp.state != ISSUE) { + fhttp.state = RECEIVING; + } + + // Uncomment below line to log the data received over UART + // FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line); + + // custom function to FlipWiFi + if(fhttp.save_received_data) { + if(!flipper_http_append_to_file( + line, strlen(line), fhttp.just_started_get, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + fhttp.state = ISSUE; + return; + } + if(fhttp.just_started_get) { + fhttp.just_started_get = false; + } + } + + // Check if we've started receiving data from a GET request + if(fhttp.started_receiving_get) { + // Restart the timeout timer each time new data is received + furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + + if(strstr(line, "[GET/END]") != NULL) { + FURI_LOG_I(HTTP_TAG, "GET request completed."); + // Stop the timer since we've completed the GET request + furi_timer_stop(fhttp.get_timeout_timer); + fhttp.started_receiving_get = false; + fhttp.just_started_get = false; + fhttp.state = IDLE; + fhttp.save_bytes = false; + fhttp.save_received_data = false; + + if(fhttp.is_bytes_request) { + // Search for the binary marker `[GET/END]` in the file buffer + const char marker[] = "[GET/END]"; + const size_t marker_len = sizeof(marker) - 1; // Exclude null terminator + + for(size_t i = 0; i <= file_buffer_len - marker_len; i++) { + // Check if the marker is found + if(memcmp(&file_buffer[i], marker, marker_len) == 0) { + // Remove the marker by shifting the remaining data left + size_t remaining_len = file_buffer_len - (i + marker_len); + memmove(&file_buffer[i], &file_buffer[i + marker_len], remaining_len); + file_buffer_len -= marker_len; + break; + } + } + + // If there is data left in the buffer, append it to the file + if(file_buffer_len > 0) { + if(!flipper_http_append_to_file( + file_buffer, file_buffer_len, false, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + } + file_buffer_len = 0; + } + } + + fhttp.is_bytes_request = false; + return; + } + + // Append the new line to the existing data + if(fhttp.save_received_data && + !flipper_http_append_to_file( + line, strlen(line), !fhttp.just_started_get, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + fhttp.started_receiving_get = false; + fhttp.just_started_get = false; + fhttp.state = IDLE; + return; + } + + if(!fhttp.just_started_get) { + fhttp.just_started_get = true; + } + return; + } + + // Check if we've started receiving data from a POST request + else if(fhttp.started_receiving_post) { + // Restart the timeout timer each time new data is received + furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + + if(strstr(line, "[POST/END]") != NULL) { + FURI_LOG_I(HTTP_TAG, "POST request completed."); + // Stop the timer since we've completed the POST request + furi_timer_stop(fhttp.get_timeout_timer); + fhttp.started_receiving_post = false; + fhttp.just_started_post = false; + fhttp.state = IDLE; + fhttp.save_bytes = false; + fhttp.save_received_data = false; + + if(fhttp.is_bytes_request) { + // Search for the binary marker `[POST/END]` in the file buffer + const char marker[] = "[POST/END]"; + const size_t marker_len = sizeof(marker) - 1; // Exclude null terminator + + for(size_t i = 0; i <= file_buffer_len - marker_len; i++) { + // Check if the marker is found + if(memcmp(&file_buffer[i], marker, marker_len) == 0) { + // Remove the marker by shifting the remaining data left + size_t remaining_len = file_buffer_len - (i + marker_len); + memmove(&file_buffer[i], &file_buffer[i + marker_len], remaining_len); + file_buffer_len -= marker_len; + break; + } + } + + // If there is data left in the buffer, append it to the file + if(file_buffer_len > 0) { + if(!flipper_http_append_to_file( + file_buffer, file_buffer_len, false, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + } + file_buffer_len = 0; + } + } + + fhttp.is_bytes_request = false; + return; + } + + // Append the new line to the existing data + if(fhttp.save_received_data && + !flipper_http_append_to_file( + line, strlen(line), !fhttp.just_started_post, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + fhttp.started_receiving_post = false; + fhttp.just_started_post = false; + fhttp.state = IDLE; + return; + } + + if(!fhttp.just_started_post) { + fhttp.just_started_post = true; + } + return; + } + + // Check if we've started receiving data from a PUT request + else if(fhttp.started_receiving_put) { + // Restart the timeout timer each time new data is received + furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + + if(strstr(line, "[PUT/END]") != NULL) { + FURI_LOG_I(HTTP_TAG, "PUT request completed."); + // Stop the timer since we've completed the PUT request + furi_timer_stop(fhttp.get_timeout_timer); + fhttp.started_receiving_put = false; + fhttp.just_started_put = false; + fhttp.state = IDLE; + fhttp.save_bytes = false; + fhttp.is_bytes_request = false; + fhttp.save_received_data = false; + return; + } + + // Append the new line to the existing data + if(fhttp.save_received_data && + !flipper_http_append_to_file( + line, strlen(line), !fhttp.just_started_put, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + fhttp.started_receiving_put = false; + fhttp.just_started_put = false; + fhttp.state = IDLE; + return; + } + + if(!fhttp.just_started_put) { + fhttp.just_started_put = true; + } + return; + } + + // Check if we've started receiving data from a DELETE request + else if(fhttp.started_receiving_delete) { + // Restart the timeout timer each time new data is received + furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + + if(strstr(line, "[DELETE/END]") != NULL) { + FURI_LOG_I(HTTP_TAG, "DELETE request completed."); + // Stop the timer since we've completed the DELETE request + furi_timer_stop(fhttp.get_timeout_timer); + fhttp.started_receiving_delete = false; + fhttp.just_started_delete = false; + fhttp.state = IDLE; + fhttp.save_bytes = false; + fhttp.is_bytes_request = false; + fhttp.save_received_data = false; + return; + } + + // Append the new line to the existing data + if(fhttp.save_received_data && + !flipper_http_append_to_file( + line, strlen(line), !fhttp.just_started_delete, fhttp.file_path)) { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + fhttp.started_receiving_delete = false; + fhttp.just_started_delete = false; + fhttp.state = IDLE; + return; + } + + if(!fhttp.just_started_delete) { + fhttp.just_started_delete = true; + } + return; + } + + // Handle different types of responses + if(strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL) { + FURI_LOG_I(HTTP_TAG, "Operation succeeded."); + } else if(strstr(line, "[INFO]") != NULL) { + FURI_LOG_I(HTTP_TAG, "Received info: %s", line); + + if(fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL) { + fhttp.state = IDLE; + } + } else if(strstr(line, "[GET/SUCCESS]") != NULL) { + FURI_LOG_I(HTTP_TAG, "GET request succeeded."); + fhttp.started_receiving_get = true; + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + // for GET request, save data only if it's a bytes request + fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; + file_buffer_len = 0; + return; + } else if(strstr(line, "[POST/SUCCESS]") != NULL) { + FURI_LOG_I(HTTP_TAG, "POST request succeeded."); + fhttp.started_receiving_post = true; + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + // for POST request, save data only if it's a bytes request + fhttp.save_bytes = fhttp.is_bytes_request; + fhttp.just_started_bytes = true; + file_buffer_len = 0; + return; + } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { + FURI_LOG_I(HTTP_TAG, "PUT request succeeded."); + fhttp.started_receiving_put = true; + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + return; + } else if(strstr(line, "[DELETE/SUCCESS]") != NULL) { + FURI_LOG_I(HTTP_TAG, "DELETE request succeeded."); + fhttp.started_receiving_delete = true; + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + return; + } else if(strstr(line, "[DISCONNECTED]") != NULL) { + FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully."); + } else if(strstr(line, "[ERROR]") != NULL) { + FURI_LOG_E(HTTP_TAG, "Received error: %s", line); + fhttp.state = ISSUE; + return; + } else if(strstr(line, "[PONG]") != NULL) { + FURI_LOG_I(HTTP_TAG, "Received PONG response: Wifi Dev Board is still alive."); + + // send command to connect to WiFi + if(fhttp.state == INACTIVE) { + fhttp.state = IDLE; + return; + } + } + + if(fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) { + fhttp.state = IDLE; + } else if(fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) { + fhttp.state = INACTIVE; + } else { + fhttp.state = IDLE; + } +} + +// Function to trim leading and trailing spaces and newlines from a constant string +char* trim(const char* str) { + const char* end; + char* trimmed_str; + size_t len; + + // Trim leading space + while(isspace((unsigned char)*str)) + str++; + + // All spaces? + if(*str == 0) return strdup(""); // Return an empty string if all spaces + + // Trim trailing space + end = str + strlen(str) - 1; + while(end > str && isspace((unsigned char)*end)) + end--; + + // Set length for the trimmed string + len = end - str + 1; + + // Allocate space for the trimmed string and null terminator + trimmed_str = (char*)malloc(len + 1); + if(trimmed_str == NULL) { + return NULL; // Handle memory allocation failure + } + + // Copy the trimmed part of the string into trimmed_str + strncpy(trimmed_str, str, len); + trimmed_str[len] = '\0'; // Null terminate the string + + return trimmed_str; +} + +/** + * @brief Process requests and parse JSON data asynchronously + * @param http_request The function to send the request + * @param parse_json The function to parse the JSON + * @return true if successful, false otherwise + */ +bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)) { + if(http_request()) // start the async request + { + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + } else { + FURI_LOG_E(HTTP_TAG, "Failed to send request"); + return false; + } + while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { + // Wait for the request to be received + furi_delay_ms(100); + } + furi_timer_stop(fhttp.get_timeout_timer); + if(!parse_json()) // parse the JSON before switching to the view (synchonous) + { + FURI_LOG_E(HTTP_TAG, "Failed to parse the JSON..."); + return false; + } + return true; +} + +/** + * @brief Perform a task while displaying a loading screen + * @param http_request The function to send the request + * @param parse_response The function to parse the response + * @param success_view_id The view ID to switch to on success + * @param failure_view_id The view ID to switch to on failure + * @param view_dispatcher The view dispatcher to use + * @return + */ +void flipper_http_loading_task( + bool (*http_request)(void), + bool (*parse_response)(void), + uint32_t success_view_id, + uint32_t failure_view_id, + ViewDispatcher** view_dispatcher) { + Loading* loading; + int32_t loading_view_id = 987654321; // Random ID + + loading = loading_alloc(); + if(!loading) { + FURI_LOG_E(HTTP_TAG, "Failed to allocate loading"); + view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); + + return; + } + + view_dispatcher_add_view(*view_dispatcher, loading_view_id, loading_get_view(loading)); + + // Switch to the loading view + view_dispatcher_switch_to_view(*view_dispatcher, loading_view_id); + + // Make the request + if(!flipper_http_process_response_async(http_request, parse_response)) { + FURI_LOG_E(HTTP_TAG, "Failed to make request"); + view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); + view_dispatcher_remove_view(*view_dispatcher, loading_view_id); + loading_free(loading); + + return; + } + + // Switch to the success view + view_dispatcher_switch_to_view(*view_dispatcher, success_view_id); + view_dispatcher_remove_view(*view_dispatcher, loading_view_id); + loading_free(loading); +} diff --git a/flip_wifi/flipper_http/flipper_http.h b/flip_wifi/flipper_http/flipper_http.h new file mode 100644 index 000000000..c072967e0 --- /dev/null +++ b/flip_wifi/flipper_http/flipper_http.h @@ -0,0 +1,384 @@ +// flipper_http.h +#ifndef FLIPPER_HTTP_H +#define FLIPPER_HTTP_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext + +#define HTTP_TAG "FlipWiFi" // change this to your app name +#define http_tag "flip_wifi" // change this to your app id +#define UART_CH (momentum_settings.uart_esp_channel) // UART channel +#define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds +#define BAUDRATE (115200) // UART baudrate +#define RX_BUF_SIZE 1024 // UART RX buffer size +#define RX_LINE_BUFFER_SIZE 4096 // UART RX line buffer size (increase for large responses) +#define MAX_FILE_SHOW 4096 // Maximum data from file to show +#define FILE_BUFFER_SIZE 512 // File buffer size + +// Forward declaration for callback +typedef void (*FlipperHTTP_Callback)(const char* line, void* context); + +// State variable to track the UART state +typedef enum { + INACTIVE, // Inactive state + IDLE, // Default state + RECEIVING, // Receiving data + SENDING, // Sending data + ISSUE, // Issue with connection +} SerialState; + +// Event Flags for UART Worker Thread +typedef enum { + WorkerEvtStop = (1 << 0), + WorkerEvtRxDone = (1 << 1), +} WorkerEvtFlags; + +// FlipperHTTP Structure +typedef struct { + FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication + FuriHalSerialHandle* serial_handle; // Serial handle for UART communication + FuriThread* rx_thread; // Worker thread for UART + FuriThreadId rx_thread_id; // Worker thread ID + FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines + void* callback_context; // Context for the callback + SerialState state; // State of the UART + + // variable to store the last received data from the UART + char* last_response; + char file_path[256]; // Path to save the received data + + // Timer-related members + FuriTimer* get_timeout_timer; // Timer for HTTP request timeout + + bool started_receiving_get; // Indicates if a GET request has started + bool just_started_get; // Indicates if GET data reception has just started + + bool started_receiving_post; // Indicates if a POST request has started + bool just_started_post; // Indicates if POST data reception has just started + + bool started_receiving_put; // Indicates if a PUT request has started + bool just_started_put; // Indicates if PUT data reception has just started + + bool started_receiving_delete; // Indicates if a DELETE request has started + bool just_started_delete; // Indicates if DELETE data reception has just started + + // Buffer to hold the raw bytes received from the UART + uint8_t* received_bytes; + size_t received_bytes_len; // Length of the received bytes + bool is_bytes_request; // Flag to indicate if the request is for bytes + bool save_bytes; // Flag to save the received data to a file + bool save_received_data; // Flag to save the received data to a file + + bool just_started_bytes; // Indicates if bytes data reception has just started +} FlipperHTTP; + +extern FlipperHTTP fhttp; +// Global static array for the line buffer +extern char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +extern uint8_t file_buffer[FILE_BUFFER_SIZE]; +extern size_t file_buffer_len; + +// fhttp.last_response holds the last received data from the UART + +// Function to append received data to file +// make sure to initialize the file path before calling this function +bool flipper_http_append_to_file( + const void* data, + size_t data_size, + bool start_new_file, + char* file_path); + +FuriString* flipper_http_load_from_file(char* file_path); + +// UART worker thread +/** + * @brief Worker thread to handle UART data asynchronously. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +// UART worker thread +int32_t flipper_http_worker(void* context); + +// Timer callback function +/** + * @brief Callback function for the GET timeout timer. + * @return 0 + * @param context The context to pass to the callback. + * @note This function will be called when the GET request times out. + */ +void get_timeout_timer_callback(void* context); + +// UART RX Handler Callback (Interrupt Context) +/** + * @brief A private callback function to handle received data asynchronously. + * @return void + * @param handle The UART handle. + * @param event The event type. + * @param context The context to pass to the callback. + * @note This function will handle received data asynchronously via the callback. + */ +void _flipper_http_rx_callback( + FuriHalSerialHandle* handle, + FuriHalSerialRxEvent event, + void* context); + +// UART initialization function +/** + * @brief Initialize UART. + * @return true if the UART was initialized successfully, false otherwise. + * @param callback The callback function to handle received data (ex. flipper_http_rx_callback). + * @param context The context to pass to the callback. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_init(FlipperHTTP_Callback callback, void* context); + +// Deinitialize UART +/** + * @brief Deinitialize UART. + * @return void + * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. + */ +void flipper_http_deinit(); + +// Function to send data over UART with newline termination +/** + * @brief Send data over UART with newline termination. + * @return true if the data was sent successfully, false otherwise. + * @param data The data to send over UART. + * @note The data will be sent over UART with a newline character appended. + */ +bool flipper_http_send_data(const char* data); + +// Function to send a PING request +/** + * @brief Send a PING request to check if the Wifi Dev Board is connected. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + * @note This is best used to check if the Wifi Dev Board is connected. + * @note The state will remain INACTIVE until a PONG is received. + */ +bool flipper_http_ping(); + +// Function to list available commands +/** + * @brief Send a command to list available commands. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_list_commands(); + +// Function to turn on the LED +/** + * @brief Allow the LED to display while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_on(); + +// Function to turn off the LED +/** + * @brief Disable the LED from displaying while processing. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_off(); + +// Function to parse JSON data +/** + * @brief Parse JSON data. + * @return true if the JSON data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON data. + * @param json_data The JSON data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json(const char* key, const char* json_data); + +// Function to parse JSON array data +/** + * @brief Parse JSON array data. + * @return true if the JSON array data was parsed successfully, false otherwise. + * @param key The key to parse from the JSON array data. + * @param index The index to parse from the JSON array data. + * @param json_data The JSON array data to parse. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_parse_json_array(const char* key, int index, const char* json_data); + +// Function to scan for WiFi networks +/** + * @brief Send a command to scan for WiFi networks. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_scan_wifi(); + +// Function to save WiFi settings (returns true if successful) +/** + * @brief Send a command to save WiFi settings. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_save_wifi(const char* ssid, const char* password); + +// Function to get IP address of WiFi Devboard +/** + * @brief Send a command to get the IP address of the WiFi Devboard + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_address(); + +// Function to get IP address of the connected WiFi network +/** + * @brief Send a command to get the IP address of the connected WiFi network. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_wifi(); + +// Function to disconnect from WiFi (returns true if successful) +/** + * @brief Send a command to disconnect from WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_disconnect_wifi(); + +// Function to connect to WiFi (returns true if successful) +/** + * @brief Send a command to connect to WiFi. + * @return true if the request was successful, false otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_connect_wifi(); + +// Function to send a GET request +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request(const char* url); + +// Function to send a GET request with headers +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_with_headers(const char* url, const char* headers); + +// Function to send a GET request with headers and return bytes +/** + * @brief Send a GET request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the GET request to. + * @param headers The headers to send with the GET request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_get_request_bytes(const char* url, const char* headers); + +// Function to send a POST request with headers +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param data The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to send a POST request with headers and return bytes +/** + * @brief Send a POST request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the POST request to. + * @param headers The headers to send with the POST request. + * @param payload The data to send with the POST request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_post_request_bytes(const char* url, const char* headers, const char* payload); + +// Function to send a PUT request with headers +/** + * @brief Send a PUT request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the PUT request to. + * @param headers The headers to send with the PUT request. + * @param data The data to send with the PUT request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_put_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to send a DELETE request with headers +/** + * @brief Send a DELETE request to the specified URL. + * @return true if the request was successful, false otherwise. + * @param url The URL to send the DELETE request to. + * @param headers The headers to send with the DELETE request. + * @param data The data to send with the DELETE request. + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_delete_request_with_headers( + const char* url, + const char* headers, + const char* payload); + +// Function to handle received data asynchronously +/** + * @brief Callback function to handle received data asynchronously. + * @return void + * @param line The received line. + * @param context The context passed to the callback. + * @note The received data will be handled asynchronously via the callback and handles the state of the UART. + */ +void flipper_http_rx_callback(const char* line, void* context); + +// Function to trim leading and trailing spaces and newlines from a constant string +char* trim(const char* str); +/** + * @brief Process requests and parse JSON data asynchronously + * @param http_request The function to send the request + * @param parse_json The function to parse the JSON + * @return true if successful, false otherwise + */ +bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)); + +/** + * @brief Perform a task while displaying a loading screen + * @param http_request The function to send the request + * @param parse_response The function to parse the response + * @param success_view_id The view ID to switch to on success + * @param failure_view_id The view ID to switch to on failure + * @param view_dispatcher The view dispatcher to use + * @return + */ +void flipper_http_loading_task( + bool (*http_request)(void), + bool (*parse_response)(void), + uint32_t success_view_id, + uint32_t failure_view_id, + ViewDispatcher** view_dispatcher); + +#endif // FLIPPER_HTTP_H diff --git a/flip_wifi/jsmn.h b/flip_wifi/jsmn.h deleted file mode 100644 index b4e3098e5..000000000 --- a/flip_wifi/jsmn.h +++ /dev/null @@ -1,769 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2010 Serge Zaitsev - * - * 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 JSMN_H -#define JSMN_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef JSMN_STATIC -#define JSMN_API static -#else -#define JSMN_API extern -#endif - -/** - * JSON type identifier. Basic types are: - * o Object - * o Array - * o String - * o Other primitive: number, boolean (true/false) or null - */ -typedef enum { - JSMN_UNDEFINED = 0, - JSMN_OBJECT = 1 << 0, - JSMN_ARRAY = 1 << 1, - JSMN_STRING = 1 << 2, - JSMN_PRIMITIVE = 1 << 3 -} jsmntype_t; - -enum jsmnerr { - /* Not enough tokens were provided */ - JSMN_ERROR_NOMEM = -1, - /* Invalid character inside JSON string */ - JSMN_ERROR_INVAL = -2, - /* The string is not a full JSON packet, more bytes expected */ - JSMN_ERROR_PART = -3 -}; - -/** - * JSON token description. - * type type (object, array, string etc.) - * start start position in JSON data string - * end end position in JSON data string - */ -typedef struct jsmntok { - jsmntype_t type; - int start; - int end; - int size; -#ifdef JSMN_PARENT_LINKS - int parent; -#endif -} jsmntok_t; - -/** - * JSON parser. Contains an array of token blocks available. Also stores - * the string being parsed now and current position in that string. - */ -typedef struct jsmn_parser { - unsigned int pos; /* offset in the JSON string */ - unsigned int toknext; /* next token to allocate */ - int toksuper; /* superior token node, e.g. parent object or array */ -} jsmn_parser; - -/** - * Create JSON parser over an array of tokens - */ -JSMN_API void jsmn_init(jsmn_parser* parser); - -/** - * Run JSON parser. It parses a JSON data string into and array of tokens, each - * describing - * a single JSON object. - */ -JSMN_API int jsmn_parse( - jsmn_parser* parser, - const char* js, - const size_t len, - jsmntok_t* tokens, - const unsigned int num_tokens); - -#ifndef JSMN_HEADER -/** - * Allocates a fresh unused token from the token pool. - */ -static jsmntok_t* - jsmn_alloc_token(jsmn_parser* parser, jsmntok_t* tokens, const size_t num_tokens) { - jsmntok_t* tok; - if(parser->toknext >= num_tokens) { - return NULL; - } - tok = &tokens[parser->toknext++]; - tok->start = tok->end = -1; - tok->size = 0; -#ifdef JSMN_PARENT_LINKS - tok->parent = -1; -#endif - return tok; -} - -/** - * Fills token type and boundaries. - */ -static void - jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { - token->type = type; - token->start = start; - token->end = end; - token->size = 0; -} - -/** - * Fills next available token with JSON primitive. - */ -static int jsmn_parse_primitive( - jsmn_parser* parser, - const char* js, - const size_t len, - jsmntok_t* tokens, - const size_t num_tokens) { - jsmntok_t* token; - int start; - - start = parser->pos; - - for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - switch(js[parser->pos]) { -#ifndef JSMN_STRICT - /* In strict mode primitive must be followed by "," or "}" or "]" */ - case ':': -#endif - case '\t': - case '\r': - case '\n': - case ' ': - case ',': - case ']': - case '}': - goto found; - default: - /* to quiet a warning from gcc*/ - break; - } - if(js[parser->pos] < 32 || js[parser->pos] >= 127) { - parser->pos = start; - return JSMN_ERROR_INVAL; - } - } -#ifdef JSMN_STRICT - /* In strict mode primitive must be followed by a comma/object/array */ - parser->pos = start; - return JSMN_ERROR_PART; -#endif - -found: - if(tokens == NULL) { - parser->pos--; - return 0; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { - parser->pos = start; - return JSMN_ERROR_NOMEM; - } - jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - parser->pos--; - return 0; -} - -/** - * Fills next token with JSON string. - */ -static int jsmn_parse_string( - jsmn_parser* parser, - const char* js, - const size_t len, - jsmntok_t* tokens, - const size_t num_tokens) { - jsmntok_t* token; - - int start = parser->pos; - - /* Skip starting quote */ - parser->pos++; - - for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c = js[parser->pos]; - - /* Quote: end of string */ - if(c == '\"') { - if(tokens == NULL) { - return 0; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { - parser->pos = start; - return JSMN_ERROR_NOMEM; - } - jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - return 0; - } - - /* Backslash: Quoted symbol expected */ - if(c == '\\' && parser->pos + 1 < len) { - int i; - parser->pos++; - switch(js[parser->pos]) { - /* Allowed escaped symbols */ - case '\"': - case '/': - case '\\': - case 'b': - case 'f': - case 'r': - case 'n': - case 't': - break; - /* Allows escaped symbol \uXXXX */ - case 'u': - parser->pos++; - for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { - /* If it isn't a hex character we have an error */ - if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ - (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ - (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ - parser->pos = start; - return JSMN_ERROR_INVAL; - } - parser->pos++; - } - parser->pos--; - break; - /* Unexpected symbol */ - default: - parser->pos = start; - return JSMN_ERROR_INVAL; - } - } - } - parser->pos = start; - return JSMN_ERROR_PART; -} - -/** - * Parse JSON string and fill tokens. - */ -JSMN_API int jsmn_parse( - jsmn_parser* parser, - const char* js, - const size_t len, - jsmntok_t* tokens, - const unsigned int num_tokens) { - int r; - int i; - jsmntok_t* token; - int count = parser->toknext; - - for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c; - jsmntype_t type; - - c = js[parser->pos]; - switch(c) { - case '{': - case '[': - count++; - if(tokens == NULL) { - break; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { - return JSMN_ERROR_NOMEM; - } - if(parser->toksuper != -1) { - jsmntok_t* t = &tokens[parser->toksuper]; -#ifdef JSMN_STRICT - /* In strict mode an object or array can't become a key */ - if(t->type == JSMN_OBJECT) { - return JSMN_ERROR_INVAL; - } -#endif - t->size++; -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - } - token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); - token->start = parser->pos; - parser->toksuper = parser->toknext - 1; - break; - case '}': - case ']': - if(tokens == NULL) { - break; - } - type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); -#ifdef JSMN_PARENT_LINKS - if(parser->toknext < 1) { - return JSMN_ERROR_INVAL; - } - token = &tokens[parser->toknext - 1]; - for(;;) { - if(token->start != -1 && token->end == -1) { - if(token->type != type) { - return JSMN_ERROR_INVAL; - } - token->end = parser->pos + 1; - parser->toksuper = token->parent; - break; - } - if(token->parent == -1) { - if(token->type != type || parser->toksuper == -1) { - return JSMN_ERROR_INVAL; - } - break; - } - token = &tokens[token->parent]; - } -#else - for(i = parser->toknext - 1; i >= 0; i--) { - token = &tokens[i]; - if(token->start != -1 && token->end == -1) { - if(token->type != type) { - return JSMN_ERROR_INVAL; - } - parser->toksuper = -1; - token->end = parser->pos + 1; - break; - } - } - /* Error if unmatched closing bracket */ - if(i == -1) { - return JSMN_ERROR_INVAL; - } - for(; i >= 0; i--) { - token = &tokens[i]; - if(token->start != -1 && token->end == -1) { - parser->toksuper = i; - break; - } - } -#endif - break; - case '\"': - r = jsmn_parse_string(parser, js, len, tokens, num_tokens); - if(r < 0) { - return r; - } - count++; - if(parser->toksuper != -1 && tokens != NULL) { - tokens[parser->toksuper].size++; - } - break; - case '\t': - case '\r': - case '\n': - case ' ': - break; - case ':': - parser->toksuper = parser->toknext - 1; - break; - case ',': - if(tokens != NULL && parser->toksuper != -1 && - tokens[parser->toksuper].type != JSMN_ARRAY && - tokens[parser->toksuper].type != JSMN_OBJECT) { -#ifdef JSMN_PARENT_LINKS - parser->toksuper = tokens[parser->toksuper].parent; -#else - for(i = parser->toknext - 1; i >= 0; i--) { - if(tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { - if(tokens[i].start != -1 && tokens[i].end == -1) { - parser->toksuper = i; - break; - } - } - } -#endif - } - break; -#ifdef JSMN_STRICT - /* In strict mode primitives are: numbers and booleans */ - case '-': - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case 't': - case 'f': - case 'n': - /* And they must not be keys of the object */ - if(tokens != NULL && parser->toksuper != -1) { - const jsmntok_t* t = &tokens[parser->toksuper]; - if(t->type == JSMN_OBJECT || (t->type == JSMN_STRING && t->size != 0)) { - return JSMN_ERROR_INVAL; - } - } -#else - /* In non-strict mode every unquoted value is a primitive */ - default: -#endif - r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); - if(r < 0) { - return r; - } - count++; - if(parser->toksuper != -1 && tokens != NULL) { - tokens[parser->toksuper].size++; - } - break; - -#ifdef JSMN_STRICT - /* Unexpected char in strict mode */ - default: - return JSMN_ERROR_INVAL; -#endif - } - } - - if(tokens != NULL) { - for(i = parser->toknext - 1; i >= 0; i--) { - /* Unmatched opened object or array */ - if(tokens[i].start != -1 && tokens[i].end == -1) { - return JSMN_ERROR_PART; - } - } - } - - return count; -} - -/** - * Creates a new parser based over a given buffer with an array of tokens - * available. - */ -JSMN_API void jsmn_init(jsmn_parser* parser) { - parser->pos = 0; - parser->toknext = 0; - parser->toksuper = -1; -} - -#endif /* JSMN_HEADER */ - -#ifdef __cplusplus -} -#endif - -#endif /* JSMN_H */ - -#ifndef JB_JSMN_EDIT -#define JB_JSMN_EDIT -/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ - -#include -#include -#include -#include -#include - -// Helper function to compare JSON keys -int jsoneq(const char* json, jsmntok_t* tok, const char* s) { - if(tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && - strncmp(json + tok->start, s, tok->end - tok->start) == 0) { - return 0; - } - return -1; -} - -// return the value of the key in the JSON data -char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { - // Parse the JSON feed - if(json_data != NULL) { - jsmn_parser parser; - jsmn_init(&parser); - - // Allocate tokens array on the heap - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); - if(tokens == NULL) { - FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); - return NULL; - } - - int ret = jsmn_parse(&parser, json_data, strlen(json_data), tokens, max_tokens); - if(ret < 0) { - // Handle parsing errors - FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); - free(tokens); - return NULL; - } - - // Ensure that the root element is an object - if(ret < 1 || tokens[0].type != JSMN_OBJECT) { - FURI_LOG_E("JSMM.H", "Root element is not an object."); - free(tokens); - return NULL; - } - - // Loop through the tokens to find the key - for(int i = 1; i < ret; i++) { - if(jsoneq(json_data, &tokens[i], key) == 0) { - // We found the key. Now, return the associated value. - int length = tokens[i + 1].end - tokens[i + 1].start; - char* value = malloc(length + 1); - if(value == NULL) { - FURI_LOG_E("JSMM.H", "Failed to allocate memory for value."); - free(tokens); - return NULL; - } - strncpy(value, json_data + tokens[i + 1].start, length); - value[length] = '\0'; // Null-terminate the string - - free(tokens); // Free the token array - return value; // Return the extracted value - } - } - - // Free the token array if key was not found - free(tokens); - } else { - FURI_LOG_E("JSMM.H", "JSON data is NULL"); - } - FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON."); - return NULL; // Return NULL if something goes wrong -} - -// Revised get_json_array_value function -char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t max_tokens) { - // Retrieve the array string for the given key - char* array_str = get_json_value(key, json_data, max_tokens); - if(array_str == NULL) { - FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); - return NULL; - } - - // Initialize the JSON parser - jsmn_parser parser; - jsmn_init(&parser); - - // Allocate memory for JSON tokens - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); - if(tokens == NULL) { - FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); - free(array_str); - return NULL; - } - - // Parse the JSON array - int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); - if(ret < 0) { - FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); - free(tokens); - free(array_str); - return NULL; - } - - // Ensure the root element is an array - if(ret < 1 || tokens[0].type != JSMN_ARRAY) { - FURI_LOG_E("JSMM.H", "Value for key '%s' is not an array.", key); - free(tokens); - free(array_str); - return NULL; - } - - // Check if the index is within bounds - if(index >= (uint32_t)tokens[0].size) { - FURI_LOG_E( - "JSMM.H", - "Index %lu out of bounds for array with size %d.", - (unsigned long)index, - tokens[0].size); - free(tokens); - free(array_str); - return NULL; - } - - // Locate the token corresponding to the desired array element - int current_token = 1; // Start after the array token - for(uint32_t i = 0; i < index; i++) { - if(tokens[current_token].type == JSMN_OBJECT) { - // For objects, skip all key-value pairs - current_token += 1 + 2 * tokens[current_token].size; - } else if(tokens[current_token].type == JSMN_ARRAY) { - // For nested arrays, skip all elements - current_token += 1 + tokens[current_token].size; - } else { - // For primitive types, simply move to the next token - current_token += 1; - } - - // Safety check to prevent out-of-bounds - if(current_token >= ret) { - FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array."); - free(tokens); - free(array_str); - return NULL; - } - } - - // Extract the array element - jsmntok_t element = tokens[current_token]; - int length = element.end - element.start; - char* value = malloc(length + 1); - if(value == NULL) { - FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element."); - free(tokens); - free(array_str); - return NULL; - } - - // Copy the element value to a new string - strncpy(value, array_str + element.start, length); - value[length] = '\0'; // Null-terminate the string - - // Clean up - free(tokens); - free(array_str); - - return value; -} - -// Revised get_json_array_values function with correct token skipping -char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, int* num_values) { - // Retrieve the array string for the given key - char* array_str = get_json_value(key, json_data, max_tokens); - if(array_str == NULL) { - FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); - return NULL; - } - - // Initialize the JSON parser - jsmn_parser parser; - jsmn_init(&parser); - - // Allocate memory for JSON tokens - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); // Allocate on the heap - if(tokens == NULL) { - FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); - free(array_str); - return NULL; - } - - // Parse the JSON array - int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); - if(ret < 0) { - FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); - free(tokens); - free(array_str); - return NULL; - } - - // Ensure the root element is an array - if(tokens[0].type != JSMN_ARRAY) { - FURI_LOG_E("JSMM.H", "Value for key '%s' is not an array.", key); - free(tokens); - free(array_str); - return NULL; - } - - // Allocate memory for the array of values (maximum possible) - int array_size = tokens[0].size; - char** values = malloc(array_size * sizeof(char*)); - if(values == NULL) { - FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values."); - free(tokens); - free(array_str); - return NULL; - } - - int actual_num_values = 0; - - // Traverse the array and extract all object values - int current_token = 1; // Start after the array token - for(int i = 0; i < array_size; i++) { - if(current_token >= ret) { - FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array."); - break; - } - - jsmntok_t element = tokens[current_token]; - - if(element.type != JSMN_OBJECT) { - FURI_LOG_E("JSMM.H", "Array element %d is not an object, skipping.", i); - // Skip this element - current_token += 1; - continue; - } - - int length = element.end - element.start; - - // Allocate a new string for the value and copy the data - char* value = malloc(length + 1); - if(value == NULL) { - FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element."); - for(int j = 0; j < actual_num_values; j++) { - free(values[j]); - } - free(values); - free(tokens); - free(array_str); - return NULL; - } - - strncpy(value, array_str + element.start, length); - value[length] = '\0'; // Null-terminate the string - - values[actual_num_values] = value; - actual_num_values++; - - // Skip all tokens related to this object to avoid misparsing - current_token += 1 + (2 * element.size); // Each key-value pair consumes two tokens - } - - *num_values = actual_num_values; - - // Reallocate the values array to actual_num_values if necessary - if(actual_num_values < array_size) { - char** reduced_values = realloc(values, actual_num_values * sizeof(char*)); - if(reduced_values != NULL) { - values = reduced_values; - } - - // Free the remaining values - for(int i = actual_num_values; i < array_size; i++) { - free(values[i]); - } - } - - // Clean up - free(tokens); - free(array_str); - return values; -} - -#endif /* JB_JSMN_EDIT */ diff --git a/flip_wifi/jsmn/jsmn.c b/flip_wifi/jsmn/jsmn.c new file mode 100644 index 000000000..eb33b3cc7 --- /dev/null +++ b/flip_wifi/jsmn/jsmn.c @@ -0,0 +1,666 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * [License text continues...] + */ + +#include +#include +#include + +/** + * Allocates a fresh unused token from the token pool. + */ +static jsmntok_t* + jsmn_alloc_token(jsmn_parser* parser, jsmntok_t* tokens, const size_t num_tokens) { + jsmntok_t* tok; + + if(parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void + jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive( + jsmn_parser* parser, + const char* js, + const size_t len, + jsmntok_t* tokens, + const size_t num_tokens) { + jsmntok_t* token; + int start; + + start = parser->pos; + + for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch(js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + /* to quiet a warning from gcc*/ + break; + } + if(js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if(tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if(token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string( + jsmn_parser* parser, + const char* js, + const size_t len, + jsmntok_t* tokens, + const size_t num_tokens) { + jsmntok_t* token; + + int start = parser->pos; + + /* Skip starting quote */ + parser->pos++; + + for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if(c == '\"') { + if(tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if(token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if(c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch(js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + /* If it isn't a hex character we have an error */ + if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser* parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +/** + * Parse JSON string and fill tokens. + */ +int jsmn_parse( + jsmn_parser* parser, + const char* js, + const size_t len, + jsmntok_t* tokens, + const unsigned int num_tokens) { + int r; + int i; + jsmntok_t* token; + int count = parser->toknext; + + for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch(c) { + case '{': + case '[': + count++; + if(tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if(token == NULL) { + return JSMN_ERROR_NOMEM; + } + if(parser->toksuper != -1) { + jsmntok_t* t = &tokens[parser->toksuper]; +#ifdef JSMN_STRICT + /* In strict mode an object or array can't become a key */ + if(t->type == JSMN_OBJECT) { + return JSMN_ERROR_INVAL; + } +#endif + t->size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': + case ']': + if(tokens == NULL) { + break; + } + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if(parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for(;;) { + if(token->start != -1 && token->end == -1) { + if(token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if(token->parent == -1) { + if(token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for(i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if(token->start != -1 && token->end == -1) { + if(token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if(i == -1) { + return JSMN_ERROR_INVAL; + } + for(; i >= 0; i--) { + token = &tokens[i]; + if(token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if(r < 0) { + return r; + } + count++; + if(parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + case '\t': + case '\r': + case '\n': + case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if(tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for(i = parser->toknext - 1; i >= 0; i--) { + if(tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if(tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 't': + case 'f': + case 'n': + /* And they must not be keys of the object */ + if(tokens != NULL && parser->toksuper != -1) { + const jsmntok_t* t = &tokens[parser->toksuper]; + if(t->type == JSMN_OBJECT || (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if(r < 0) { + return r; + } + count++; + if(parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if(tokens != NULL) { + for(i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if(tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +// Helper function to create a JSON object +char* jsmn(const char* key, const char* value) { + int length = strlen(key) + strlen(value) + 8; // Calculate required length + char* result = (char*)malloc(length * sizeof(char)); // Allocate memory + if(result == NULL) { + return NULL; // Handle memory allocation failure + } + snprintf(result, length, "{\"%s\":\"%s\"}", key, value); + return result; // Caller is responsible for freeing this memory +} + +// Helper function to compare JSON keys +int jsoneq(const char* json, jsmntok_t* tok, const char* s) { + if(tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && + strncmp(json + tok->start, s, tok->end - tok->start) == 0) { + return 0; + } + return -1; +} + +// Return the value of the key in the JSON data +char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { + // Parse the JSON feed + if(json_data != NULL) { + jsmn_parser parser; + jsmn_init(&parser); + + // Allocate tokens array on the heap + jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); + if(tokens == NULL) { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + return NULL; + } + + int ret = jsmn_parse(&parser, json_data, strlen(json_data), tokens, max_tokens); + if(ret < 0) { + // Handle parsing errors + FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); + free(tokens); + return NULL; + } + + // Ensure that the root element is an object + if(ret < 1 || tokens[0].type != JSMN_OBJECT) { + FURI_LOG_E("JSMM.H", "Root element is not an object."); + free(tokens); + return NULL; + } + + // Loop through the tokens to find the key + for(int i = 1; i < ret; i++) { + if(jsoneq(json_data, &tokens[i], key) == 0) { + // We found the key. Now, return the associated value. + int length = tokens[i + 1].end - tokens[i + 1].start; + char* value = malloc(length + 1); + if(value == NULL) { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for value."); + free(tokens); + return NULL; + } + strncpy(value, json_data + tokens[i + 1].start, length); + value[length] = '\0'; // Null-terminate the string + + free(tokens); // Free the token array + return value; // Return the extracted value + } + } + + // Free the token array if key was not found + free(tokens); + } else { + FURI_LOG_E("JSMM.H", "JSON data is NULL"); + } + FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON."); + return NULL; // Return NULL if something goes wrong +} + +// Revised get_json_array_value function +char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t max_tokens) { + // Retrieve the array string for the given key + char* array_str = get_json_value(key, json_data, max_tokens); + if(array_str == NULL) { + FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); + return NULL; + } + + // Initialize the JSON parser + jsmn_parser parser; + jsmn_init(&parser); + + // Allocate memory for JSON tokens + jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); + if(tokens == NULL) { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + free(array_str); + return NULL; + } + + // Parse the JSON array + int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); + if(ret < 0) { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + free(array_str); + return NULL; + } + + // Ensure the root element is an array + if(ret < 1 || tokens[0].type != JSMN_ARRAY) { + FURI_LOG_E("JSMM.H", "Value for key '%s' is not an array.", key); + free(tokens); + free(array_str); + return NULL; + } + + // Check if the index is within bounds + if(index >= (uint32_t)tokens[0].size) { + FURI_LOG_E( + "JSMM.H", + "Index %lu out of bounds for array with size %d.", + (unsigned long)index, + tokens[0].size); + free(tokens); + free(array_str); + return NULL; + } + + // Locate the token corresponding to the desired array element + int current_token = 1; // Start after the array token + for(uint32_t i = 0; i < index; i++) { + if(tokens[current_token].type == JSMN_OBJECT) { + // For objects, skip all key-value pairs + current_token += 1 + 2 * tokens[current_token].size; + } else if(tokens[current_token].type == JSMN_ARRAY) { + // For nested arrays, skip all elements + current_token += 1 + tokens[current_token].size; + } else { + // For primitive types, simply move to the next token + current_token += 1; + } + + // Safety check to prevent out-of-bounds + if(current_token >= ret) { + FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array."); + free(tokens); + free(array_str); + return NULL; + } + } + + // Extract the array element + jsmntok_t element = tokens[current_token]; + int length = element.end - element.start; + char* value = malloc(length + 1); + if(value == NULL) { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element."); + free(tokens); + free(array_str); + return NULL; + } + + // Copy the element value to a new string + strncpy(value, array_str + element.start, length); + value[length] = '\0'; // Null-terminate the string + + // Clean up + free(tokens); + free(array_str); + + return value; +} + +// Revised get_json_array_values function with correct token skipping +char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, int* num_values) { + // Retrieve the array string for the given key + char* array_str = get_json_value(key, json_data, max_tokens); + if(array_str == NULL) { + FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); + return NULL; + } + + // Initialize the JSON parser + jsmn_parser parser; + jsmn_init(&parser); + + // Allocate memory for JSON tokens + jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); // Allocate on the heap + if(tokens == NULL) { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + free(array_str); + return NULL; + } + + // Parse the JSON array + int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); + if(ret < 0) { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + free(array_str); + return NULL; + } + + // Ensure the root element is an array + if(tokens[0].type != JSMN_ARRAY) { + FURI_LOG_E("JSMM.H", "Value for key '%s' is not an array.", key); + free(tokens); + free(array_str); + return NULL; + } + + // Allocate memory for the array of values (maximum possible) + int array_size = tokens[0].size; + char** values = malloc(array_size * sizeof(char*)); + if(values == NULL) { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values."); + free(tokens); + free(array_str); + return NULL; + } + + int actual_num_values = 0; + + // Traverse the array and extract all object values + int current_token = 1; // Start after the array token + for(int i = 0; i < array_size; i++) { + if(current_token >= ret) { + FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array."); + break; + } + + jsmntok_t element = tokens[current_token]; + + if(element.type != JSMN_OBJECT) { + FURI_LOG_E("JSMM.H", "Array element %d is not an object, skipping.", i); + // Skip this element + current_token += 1; + continue; + } + + int length = element.end - element.start; + + // Allocate a new string for the value and copy the data + char* value = malloc(length + 1); + if(value == NULL) { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element."); + for(int j = 0; j < actual_num_values; j++) { + free(values[j]); + } + free(values); + free(tokens); + free(array_str); + return NULL; + } + + strncpy(value, array_str + element.start, length); + value[length] = '\0'; // Null-terminate the string + + values[actual_num_values] = value; + actual_num_values++; + + // Skip all tokens related to this object to avoid misparsing + current_token += 1 + (2 * element.size); // Each key-value pair consumes two tokens + } + + *num_values = actual_num_values; + + // Reallocate the values array to actual_num_values if necessary + if(actual_num_values < array_size) { + char** reduced_values = realloc(values, actual_num_values * sizeof(char*)); + if(reduced_values != NULL) { + values = reduced_values; + } + + // Free the remaining values + for(int i = actual_num_values; i < array_size; i++) { + free(values[i]); + } + } + + // Clean up + free(tokens); + free(array_str); + return values; +} diff --git a/flip_wifi/jsmn/jsmn.h b/flip_wifi/jsmn/jsmn.h new file mode 100644 index 000000000..cd95a0e58 --- /dev/null +++ b/flip_wifi/jsmn/jsmn.h @@ -0,0 +1,131 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * 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: + * + * [License text continues...] + */ + +#ifndef JSMN_H +#define JSMN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser* parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing a single JSON object. + */ +JSMN_API int jsmn_parse( + jsmn_parser* parser, + const char* js, + const size_t len, + jsmntok_t* tokens, + const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/* Implementation has been moved to jsmn.c */ +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */ + +/* Custom Helper Functions */ +#ifndef JB_JSMN_EDIT +#define JB_JSMN_EDIT +/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ + +#include +#include +#include +#include +#include + +// Helper function to create a JSON object +char* jsmn(const char* key, const char* value); +// Helper function to compare JSON keys +int jsoneq(const char* json, jsmntok_t* tok, const char* s); + +// Return the value of the key in the JSON data +char* get_json_value(char* key, char* json_data, uint32_t max_tokens); + +// Revised get_json_array_value function +char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t max_tokens); + +// Revised get_json_array_values function with correct token skipping +char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, int* num_values); +#endif /* JB_JSMN_EDIT */ diff --git a/flipbip/application.fam b/flipbip/application.fam index 041093165..4041fa78b 100644 --- a/flipbip/application.fam +++ b/flipbip/application.fam @@ -17,6 +17,6 @@ App( fap_category="Tools", fap_author="Struan Clark (xtruan)", fap_weburl="https://github.com/xtruan/FlipBIP", - fap_version=(1, 17), + fap_version=(1, 18), fap_description="Crypto wallet for Flipper", ) diff --git a/flipbip/catalog/manifest.yml b/flipbip/catalog/manifest.yml index dd52bc180..c2d23c2e0 100644 --- a/flipbip/catalog/manifest.yml +++ b/flipbip/catalog/manifest.yml @@ -2,9 +2,9 @@ sourcecode: type: git location: origin: https://github.com/xtruan/FlipBIP.git - commit_sha: 62ef09fc3eede4494571d55f3d9340695482781a + commit_sha: 9f9bce59bc47ed03eda62380f32b33f09207516a description: "Cryptocurrency wallet with support for BTC, ETH, DOGE, and ZEC (t-addr)" -changelog: "v1.17" +changelog: "v1.18" author: "@xtruan" screenshots: - "./catalog/menu1.png" diff --git a/flipbip/flipbip.c b/flipbip/flipbip.c index 42c468442..c31131517 100644 --- a/flipbip/flipbip.c +++ b/flipbip/flipbip.c @@ -131,7 +131,7 @@ FlipBip* flipbip_app_alloc() { app->passphrase = FlipBipPassphraseOff; // Main menu - app->bip44_coin = FlipBipCoinBTC0; // 0 (BTC) + app->coin_type = CoinTypeBTC0; // 0 (BTC) app->overwrite_saved_seed = 0; app->import_from_mnemonic = 0; app->mnemonic_menu_text = MNEMONIC_MENU_DEFAULT; diff --git a/flipbip/flipbip.h b/flipbip/flipbip.h index 5a0318ec0..53592f9b4 100644 --- a/flipbip/flipbip.h +++ b/flipbip/flipbip.h @@ -14,14 +14,9 @@ #include #include "scenes/flipbip_scene.h" #include "views/flipbip_scene_1.h" +#include "flipbip_coins.h" -#define FLIPBIP_VERSION "v1.17" - -#define COIN_BTC 0 -#define COIN_DOGE 3 -#define COIN_ETH 60 -#define COIN_ZEC 133 - +#define FLIPBIP_VERSION "v1.18" #define TEXT_BUFFER_SIZE 256 typedef struct { @@ -39,7 +34,7 @@ typedef struct { int bip39_strength; int passphrase; // Main menu options - int bip44_coin; + int coin_type; int overwrite_saved_seed; int import_from_mnemonic; // Text input @@ -71,13 +66,6 @@ typedef enum { FlipBipPassphraseOn, } FlipBipPassphraseState; -typedef enum { - FlipBipCoinBTC0, - FlipBipCoinETH60, - FlipBipCoinDOGE3, - FlipBipCoinZEC133, -} FlipBipCoin; - typedef enum { FlipBipTextInputDefault, FlipBipTextInputPassphrase, @@ -92,12 +80,9 @@ typedef enum { FlipBipStatusMnemonicCheckError = 13, } FlipBipStatus; +// There's a scene ID for each coin, then these scenes are after so need to offset the first entry by at least NUM_COINS typedef enum { - SubmenuIndexScene1BTC = 10, - SubmenuIndexScene1ETH, - SubmenuIndexScene1DOGE, - SubmenuIndexScene1ZEC, - SubmenuIndexScene1New, + SubmenuIndexScene1New = NUM_COINS + 1, SubmenuIndexScene1Renew, SubmenuIndexScene1Import, SubmenuIndexSettings, diff --git a/flipbip/flipbip_coins.c b/flipbip/flipbip_coins.c new file mode 100644 index 000000000..004e500dd --- /dev/null +++ b/flipbip/flipbip_coins.c @@ -0,0 +1,16 @@ +#include + +// bip44_coin, xprv_version, xpub_version, addr_version, wif_version, addr_format +const uint32_t COIN_INFO_ARRAY[NUM_COINS][COIN_INFO_SIZE] = { + {0, 0x0488ade4, 0x0488b21e, 0x00, 0x80, CoinTypeBTC0}, + {60, 0x0488ade4, 0x0488b21e, 0x00, 0x80, CoinTypeETH60}, + {3, 0x02fac398, 0x02facafd, 0x1e, 0x9e, CoinTypeBTC0}, + {133, 0x0488ade4, 0x0488b21e, 0x1cb8, 0x80, CoinTypeBTC0}, +}; + +// coin_label, derivation_path, coin_name, static_prefix ("_" for none) +const char* COIN_TEXT_ARRAY[NUM_COINS][COIN_TEXT_SIZE] = { + {"BTC", "m/44'/0'/0'/0", "bitcoin:", "_"}, + {"ETH", "m/44'/60'/0'/0", "ethereum:", "_"}, + {"DOGE", "m/44'/3'/0'/0", "dogecoin:", "_"}, + {"ZEC", "m/44'/133'/0'/0", "zcash:", "t"}}; diff --git a/flipbip/flipbip_coins.h b/flipbip/flipbip_coins.h new file mode 100644 index 000000000..895842436 --- /dev/null +++ b/flipbip/flipbip_coins.h @@ -0,0 +1,31 @@ +#pragma once +#include + +#define NUM_COINS 4 + +typedef enum { + CoinTypeBTC0, + CoinTypeETH60, + CoinTypeDOGE3, + CoinTypeZEC133, +} CoinType; + +#define COIN_INFO_SIZE 6 +#define COIN_INFO_BIP44_COIN 0 +#define COIN_INFO_XPRV_VERS 1 +#define COIN_INFO_XPUB_VERS 2 +#define COIN_INFO_ADDR_VERS 3 +#define COIN_INFO_WIF_VERS 4 +#define COIN_INFO_ADDR_FMT 5 + +// bip44_coin, xprv_version, xpub_version, addr_version, wif_version, addr_format +extern const uint32_t COIN_INFO_ARRAY[NUM_COINS][COIN_INFO_SIZE]; + +#define COIN_TEXT_SIZE 4 +#define COIN_TEXT_LABEL 0 +#define COIN_TEXT_DERIV 1 +#define COIN_TEXT_NAME 2 +#define COIN_TEXT_PREFIX 3 + +// coin_label, derivation_path, coin_name, static_prefix ("_" for none) +extern const char* COIN_TEXT_ARRAY[NUM_COINS][COIN_TEXT_SIZE]; diff --git a/flipbip/scenes/flipbip_scene_menu.c b/flipbip/scenes/flipbip_scene_menu.c index 47955119f..779a30969 100644 --- a/flipbip/scenes/flipbip_scene_menu.c +++ b/flipbip/scenes/flipbip_scene_menu.c @@ -22,30 +22,20 @@ void flipbip_scene_menu_on_enter(void* context) { if(flipbip_has_file(FlipBipFileKey, NULL, false) && flipbip_has_file(FlipBipFileDat, NULL, false)) { - submenu_add_item( - app->submenu, - "View BTC wallet", - SubmenuIndexScene1BTC, - flipbip_scene_menu_submenu_callback, - app); - submenu_add_item( - app->submenu, - "View ETH wallet", - SubmenuIndexScene1ETH, - flipbip_scene_menu_submenu_callback, - app); - submenu_add_item( - app->submenu, - "View DOGE wallet", - SubmenuIndexScene1DOGE, - flipbip_scene_menu_submenu_callback, - app); - submenu_add_item( - app->submenu, - "View ZEC (t-addr) wallet", - SubmenuIndexScene1ZEC, - flipbip_scene_menu_submenu_callback, - app); + for(uint32_t coin_type = 0; coin_type < NUM_COINS; coin_type++) { + char wallet_menu_item[17] = "View wallet"; + strncpy( + wallet_menu_item + 5, + COIN_TEXT_ARRAY[coin_type][COIN_TEXT_LABEL], + strlen(COIN_TEXT_ARRAY[coin_type][COIN_TEXT_LABEL])); + submenu_add_item( + app->submenu, + wallet_menu_item, + coin_type, + flipbip_scene_menu_submenu_callback, + app); + } + submenu_add_item( app->submenu, "Regenerate wallet", @@ -85,36 +75,12 @@ bool flipbip_scene_menu_on_event(void* context, SceneManagerEvent event) { view_dispatcher_stop(app->view_dispatcher); return true; } else if(event.type == SceneManagerEventTypeCustom) { - if(event.event == SubmenuIndexScene1BTC) { - app->overwrite_saved_seed = 0; - app->import_from_mnemonic = 0; - app->bip44_coin = FlipBipCoinBTC0; - scene_manager_set_scene_state( - app->scene_manager, FlipBipSceneMenu, SubmenuIndexScene1BTC); - scene_manager_next_scene(app->scene_manager, FlipBipSceneScene_1); - return true; - } else if(event.event == SubmenuIndexScene1ETH) { - app->overwrite_saved_seed = 0; - app->import_from_mnemonic = 0; - app->bip44_coin = FlipBipCoinETH60; - scene_manager_set_scene_state( - app->scene_manager, FlipBipSceneMenu, SubmenuIndexScene1ETH); - scene_manager_next_scene(app->scene_manager, FlipBipSceneScene_1); - return true; - } else if(event.event == SubmenuIndexScene1DOGE) { - app->overwrite_saved_seed = 0; - app->import_from_mnemonic = 0; - app->bip44_coin = FlipBipCoinDOGE3; - scene_manager_set_scene_state( - app->scene_manager, FlipBipSceneMenu, SubmenuIndexScene1DOGE); - scene_manager_next_scene(app->scene_manager, FlipBipSceneScene_1); - return true; - } else if(event.event == SubmenuIndexScene1ZEC) { + if(event.event < SubmenuIndexScene1New) { app->overwrite_saved_seed = 0; app->import_from_mnemonic = 0; - app->bip44_coin = FlipBipCoinZEC133; + app->coin_type = event.event; // CoinType scene_manager_set_scene_state( - app->scene_manager, FlipBipSceneMenu, SubmenuIndexScene1ZEC); + app->scene_manager, FlipBipSceneMenu, event.event); // CoinType scene_manager_next_scene(app->scene_manager, FlipBipSceneScene_1); return true; } else if(event.event == SubmenuIndexScene1New) { diff --git a/flipbip/views/flipbip_scene_1.c b/flipbip/views/flipbip_scene_1.c index acc8aaf93..793b5e375 100644 --- a/flipbip/views/flipbip_scene_1.c +++ b/flipbip/views/flipbip_scene_1.c @@ -51,21 +51,6 @@ const char* TEXT_INFO = "-Scroll pages with up/down-" // #define TEXT_SAVE_QR "Save QR" #define TEXT_QRFILE_EXT ".qrcode" // 7 chars + 1 null -// bip44_coin, xprv_version, xpub_version, addr_version, wif_version, addr_format -const uint32_t COIN_INFO_ARRAY[4][6] = { - {COIN_BTC, 0x0488ade4, 0x0488b21e, 0x00, 0x80, FlipBipCoinBTC0}, - {COIN_ETH, 0x0488ade4, 0x0488b21e, 0x00, 0x80, FlipBipCoinETH60}, - {COIN_DOGE, 0x02fac398, 0x02facafd, 0x1e, 0x9e, FlipBipCoinBTC0}, - {COIN_ZEC, 0x0488ade4, 0x0488b21e, 0x1cb8, 0x80, FlipBipCoinZEC133}, -}; - -// coin_name, derivation_path -const char* COIN_TEXT_ARRAY[4][3] = { - {"BTC", "m/44'/0'/0'/0", "bitcoin:"}, - {"ETH", "m/44'/60'/0'/0", "ethereum:"}, - {"DOGE", "m/44'/3'/0'/0", "dogecoin:"}, - {"ZEC", "m/44'/133'/0'/0", "zcash:"}}; - struct FlipBipScene1 { View* view; FlipBipScene1Callback callback; @@ -74,7 +59,7 @@ struct FlipBipScene1 { typedef struct { int page; int strength; - uint32_t coin; + uint32_t coin_type; bool overwrite; bool mnemonic_only; CONFIDENTIAL const char* mnemonic; @@ -135,33 +120,29 @@ static void flipbip_scene_1_init_address( hdnode_private_ckd(s_addr_node, addr_index); hdnode_fill_public_key(s_addr_node); - // coin info - // bip44_coin, xprv_version, xpub_version, addr_version, wif_version, addr_format - uint32_t coin_info[6] = {0}; - for(size_t i = 0; i < 6; i++) { - coin_info[i] = COIN_INFO_ARRAY[coin_type][i]; - } - - if(coin_info[5] == FlipBipCoinBTC0) { // BTC / DOGE style address + if(COIN_INFO_ARRAY[coin_type][COIN_INFO_ADDR_FMT] == CoinTypeBTC0) { // BTC / DOGE style address ecdsa_get_address( - s_addr_node->public_key, coin_info[3], HASHER_SHA2_RIPEMD, HASHER_SHA2D, buf, buflen); + s_addr_node->public_key, + COIN_INFO_ARRAY[coin_type][COIN_INFO_ADDR_VERS], + HASHER_SHA2_RIPEMD, + HASHER_SHA2D, + buf, + buflen); + // If prefix is set (not '_') then override beginning of addr_text + if(COIN_TEXT_ARRAY[coin_type][COIN_TEXT_PREFIX][0] != '_') { + addr_text[0] = COIN_TEXT_ARRAY[coin_type][COIN_TEXT_PREFIX][0]; + } strcpy(addr_text, buf); //ecdsa_get_wif(addr_node->private_key, WIF_VERSION, HASHER_SHA2D, buf, buflen); - } else if(coin_info[5] == FlipBipCoinETH60) { // ETH + } else if(COIN_INFO_ARRAY[coin_type][COIN_INFO_ADDR_FMT] == CoinTypeETH60) { // ETH style address hdnode_get_ethereum_pubkeyhash(s_addr_node, (uint8_t*)buf); addr_text[0] = '0'; addr_text[1] = 'x'; // Convert the hash to a hex string flipbip_btox((uint8_t*)buf, 20, addr_text + 2); - - } else if(coin_info[5] == FlipBipCoinZEC133) { // ZEC - ecdsa_get_address( - s_addr_node->public_key, coin_info[3], HASHER_SHA2_RIPEMD, HASHER_SHA2D, buf, buflen); - addr_text[0] = 't'; - strcpy(addr_text, buf); } // Clear the address node @@ -305,7 +286,7 @@ void flipbip_scene_1_draw(Canvas* canvas, FlipBipScene1Model* model) { flipbip_scene_1_draw_generic(model->xpub_extended, 20, false); } else if(model->page >= PAGE_ADDR_BEGIN && model->page <= PAGE_ADDR_END) { size_t line_len = 12; - if(model->coin == FlipBipCoinETH60) { + if(model->coin_type == CoinTypeETH60) { line_len = 14; } flipbip_scene_1_draw_generic( @@ -327,7 +308,7 @@ void flipbip_scene_1_draw(Canvas* canvas, FlipBipScene1Model* model) { // draw address header canvas_set_font(canvas, FontSecondary); // coin_name, derivation_path - const char* receive_text = COIN_TEXT_ARRAY[model->coin][0]; + const char* receive_text = COIN_TEXT_ARRAY[model->coin_type][COIN_TEXT_LABEL]; if(receive_text == NULL) { receive_text = TEXT_DEFAULT_COIN; } @@ -345,7 +326,7 @@ void flipbip_scene_1_draw(Canvas* canvas, FlipBipScene1Model* model) { // draw QR code file path char addr_name_text[14] = {0}; - strcpy(addr_name_text, COIN_TEXT_ARRAY[model->coin][0]); + strcpy(addr_name_text, COIN_TEXT_ARRAY[model->coin_type][COIN_TEXT_LABEL]); flipbip_btox(addr_num, 1, addr_name_text + strlen(addr_name_text)); strcpy(addr_name_text + strlen(addr_name_text), TEXT_QRFILE_EXT); //elements_button_right(canvas, addr_name_text); @@ -371,13 +352,13 @@ void flipbip_scene_1_draw(Canvas* canvas, FlipBipScene1Model* model) { static int flipbip_scene_1_model_init( FlipBipScene1Model* const model, const int strength, - const uint32_t coin, + const uint32_t coin_type, const bool overwrite, const char* passphrase_text) { model->page = PAGE_LOADING; model->mnemonic_only = false; model->strength = strength; - model->coin = coin; + model->coin_type = coin_type; model->overwrite = overwrite; // Allocate memory for mnemonic @@ -435,16 +416,10 @@ static int flipbip_scene_1_model_init( const size_t buflen = 128; char buf[128 + 1] = {0}; - // coin info - // bip44_coin, xprv_version, xpub_version, addr_version, wif_version, addr_format - uint32_t coin_info[6] = {0}; - for(size_t i = 0; i < 6; i++) { - coin_info[i] = COIN_INFO_ARRAY[coin][i]; - } - // root uint32_t fingerprint = 0; - hdnode_serialize_private(root, fingerprint, coin_info[1], buf, buflen); + hdnode_serialize_private( + root, fingerprint, COIN_INFO_ARRAY[coin_type][COIN_INFO_XPRV_VERS], buf, buflen); char* xprv_root = malloc(buflen + 1); strncpy(xprv_root, buf, buflen); model->xprv_root = xprv_root; @@ -457,18 +432,20 @@ static int flipbip_scene_1_model_init( // coin m/44'/0' or m/44'/60' fingerprint = hdnode_fingerprint(node); - hdnode_private_ckd_prime(node, coin_info[0]); // coin + hdnode_private_ckd_prime(node, COIN_INFO_ARRAY[coin_type][COIN_INFO_BIP44_COIN]); // coin // account m/44'/0'/0' or m/44'/60'/0' fingerprint = hdnode_fingerprint(node); hdnode_private_ckd_prime(node, DERIV_ACCOUNT); // account - hdnode_serialize_private(node, fingerprint, coin_info[1], buf, buflen); + hdnode_serialize_private( + node, fingerprint, COIN_INFO_ARRAY[coin_type][COIN_INFO_XPRV_VERS], buf, buflen); char* xprv_acc = malloc(buflen + 1); strncpy(xprv_acc, buf, buflen); model->xprv_account = xprv_acc; - hdnode_serialize_public(node, fingerprint, coin_info[2], buf, buflen); + hdnode_serialize_public( + node, fingerprint, COIN_INFO_ARRAY[coin_type][COIN_INFO_XPUB_VERS], buf, buflen); char* xpub_acc = malloc(buflen + 1); strncpy(xpub_acc, buf, buflen); model->xpub_account = xpub_acc; @@ -477,12 +454,14 @@ static int flipbip_scene_1_model_init( fingerprint = hdnode_fingerprint(node); hdnode_private_ckd(node, DERIV_CHANGE); // external/internal (change) - hdnode_serialize_private(node, fingerprint, coin_info[1], buf, buflen); + hdnode_serialize_private( + node, fingerprint, COIN_INFO_ARRAY[coin_type][COIN_INFO_XPRV_VERS], buf, buflen); char* xprv_ext = malloc(buflen + 1); strncpy(xprv_ext, buf, buflen); model->xprv_extended = xprv_ext; - hdnode_serialize_public(node, fingerprint, coin_info[2], buf, buflen); + hdnode_serialize_public( + node, fingerprint, COIN_INFO_ARRAY[coin_type][COIN_INFO_XPUB_VERS], buf, buflen); char* xpub_ext = malloc(buflen + 1); strncpy(xpub_ext, buf, buflen); model->xpub_extended = xpub_ext; @@ -493,15 +472,16 @@ static int flipbip_scene_1_model_init( for(uint8_t a = 0; a < NUM_ADDRS; a++) { model->recv_addresses[a] = malloc(MAX_ADDR_BUF); memzero(model->recv_addresses[a], MAX_ADDR_BUF); - flipbip_scene_1_init_address(model->recv_addresses[a], node, coin, a); + flipbip_scene_1_init_address(model->recv_addresses[a], node, coin_type, a); // Save QR code file memzero(buf, buflen); - strcpy(buf, COIN_TEXT_ARRAY[coin][0]); + strcpy(buf, COIN_TEXT_ARRAY[coin_type][COIN_TEXT_LABEL]); const unsigned char addr_num[1] = {a}; flipbip_btox(addr_num, 1, buf + strlen(buf)); strcpy(buf + strlen(buf), TEXT_QRFILE_EXT); - flipbip_save_qrfile(COIN_TEXT_ARRAY[coin][2], model->recv_addresses[a], buf); + flipbip_save_qrfile( + COIN_TEXT_ARRAY[coin_type][COIN_TEXT_NAME], model->recv_addresses[a], buf); memzero(buf, buflen); } @@ -597,7 +577,7 @@ void flipbip_scene_1_exit(void* context) { { model->page = PAGE_LOADING; model->strength = FlipBipStrength256; - model->coin = FlipBipCoinBTC0; + model->coin_type = CoinTypeBTC0; memzero(model->seed, 64); // if mnemonic_only is true, then we don't need to free the data here if(!model->mnemonic_only) { @@ -650,9 +630,9 @@ void flipbip_scene_1_enter(void* context) { } // BIP44 Coin setting - const uint32_t coin = app->bip44_coin; + const uint32_t coin_type = app->coin_type; // coin_name, derivation_path - s_derivation_text = COIN_TEXT_ARRAY[coin][1]; + s_derivation_text = COIN_TEXT_ARRAY[coin_type][COIN_TEXT_DERIV]; // Overwrite the saved seed with a new one setting bool overwrite = app->overwrite_saved_seed != 0; @@ -661,7 +641,7 @@ void flipbip_scene_1_enter(void* context) { } // Wait a beat to allow the display time to update to the loading screen - furi_thread_flags_wait(0, FuriFlagWaitAny, 20); + furi_thread_flags_wait(0, FuriFlagWaitAny, 50); //flipbip_play_happy_bump(app); //notification_message(app->notification, &sequence_blink_cyan_100); @@ -674,7 +654,7 @@ void flipbip_scene_1_enter(void* context) { // s_busy = true; const int status = - flipbip_scene_1_model_init(model, strength, coin, overwrite, passphrase_text); + flipbip_scene_1_model_init(model, strength, coin_type, overwrite, passphrase_text); // nonzero status, free the mnemonic if(status != FlipBipStatusSuccess) { diff --git a/gpio_reader_b/.gitsubtree b/gpio_reader_b/.gitsubtree index b72dad72f..12f485f24 100644 --- a/gpio_reader_b/.gitsubtree +++ b/gpio_reader_b/.gitsubtree @@ -1,2 +1,2 @@ -https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/flipperzero_gpioreader 4558d74c9da36abc851edd96a95d18f7d5511a75 +https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/flipperzero_gpioreader 17bec3e26b8250c59acebd4fa52b6b08f68152d7 https://github.com/biotinker/flipperzero-gpioreader main / diff --git a/gpio_reader_b/usb_uart_bridge.c b/gpio_reader_b/usb_uart_bridge.c index e711a5aff..a99da0f8a 100644 --- a/gpio_reader_b/usb_uart_bridge.c +++ b/gpio_reader_b/usb_uart_bridge.c @@ -70,6 +70,7 @@ static const CdcCallbacks cdc_cb = { vcp_state_callback, vcp_on_cdc_control_line, vcp_on_line_config, + NULL, }; /* USB UART worker */ diff --git a/metroflip/.gitsubtree b/metroflip/.gitsubtree new file mode 100644 index 000000000..209421578 --- /dev/null +++ b/metroflip/.gitsubtree @@ -0,0 +1 @@ +https://github.com/luu176/Metroflip main / diff --git a/metroflip/CHANGELOG.md b/metroflip/CHANGELOG.md new file mode 100644 index 000000000..2a4bb3b83 --- /dev/null +++ b/metroflip/CHANGELOG.md @@ -0,0 +1,3 @@ +## v1.0 + +- Initial release by [@luu176](https://github.com/luu176) diff --git a/flip_library/LICENSE b/metroflip/LICENSE similarity index 97% rename from flip_library/LICENSE rename to metroflip/LICENSE index c5e43e291..4bd1b2947 100644 --- a/flip_library/LICENSE +++ b/metroflip/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 jblanked +Copyright (c) 2024 Luu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/metroflip/README.md b/metroflip/README.md new file mode 100644 index 000000000..6d4a35c8a --- /dev/null +++ b/metroflip/README.md @@ -0,0 +1,43 @@ +# Metroflip +Metroflip is a multi-protocol metro card reader app for the Flipper Zero, inspired by the Metrodroid project. It enables the parsing and analysis of metro cards from transit systems around the world, providing a proof-of-concept for exploring transit card data in a portable format. + +# Author +[@luu176](https://github.com/luu176) + +# Metroflip - Card Support TODO List + +This is a list of metro cards and transit systems that need support or have partial support. + +## ✅ Supported Cards +- [x] **Rav-Kav** + - Status: Needs more functionality (currently only able to read balance). +- [x] **Charliecard** + - Status: Fully supported. +- [x] **Metromoney** + - Status: Fully supported. +- [x] **Bip!** + - Status: Fully supported. + +## 🚧 In Progress / Needs More Functionality +- [ ] **Rav-Kav** + - Current functionality: Reads balance only. + - To Do: Parse more data from the card (e.g., transaction history, expiration date, etc.). + +## 📝 To Do (Unimplemented) +- [ ] **Tianjin Railway Transit (TRT)** + - To Do: Add support for reading and analyzing Tianjin Railway Transit cards. +- [ ] **Clipper** + - To Do: Add support for reading and analyzing Clipper cards. + + +--- + +### Credits: +- **App Author**: [@luu176](https://github.com/luu176) +- **Charliecard Parser**: [@zacharyweiss](https://github.com/zacharyweiss) +- **Rav-Kav Parser**: [@luu176](https://github.com/luu176) +- **Metromoney Parser**: [@Leptopt1los](https://github.com/Leptopt1los) +- **Bip! Parser**: [@rbasoalto](https://github.com/rbasoalto) [@gornekich](https://github.com/gornekich) +- **Info Slave**: [@equipter](https://github.com/equipter) + + diff --git a/metroflip/application.fam b/metroflip/application.fam new file mode 100644 index 000000000..1f046afc5 --- /dev/null +++ b/metroflip/application.fam @@ -0,0 +1,13 @@ +App( + appid="metroflip", + name="Metroflip", + apptype=FlipperAppType.EXTERNAL, + entry_point="metroflip", + stack_size=2 * 1024, + fap_category="NFC", + fap_version="0.1", + fap_icon="icon.png", + fap_description="An implementation of metrodroid on the flipper", + fap_author="luu176", + fap_icon_assets="images", # Image assets to compile for this application +) diff --git a/metroflip/icon.png b/metroflip/icon.png new file mode 100644 index 000000000..36e42a809 Binary files /dev/null and b/metroflip/icon.png differ diff --git a/metroflip/images/RFIDDolphinReceive_97x61.png b/metroflip/images/RFIDDolphinReceive_97x61.png new file mode 100644 index 000000000..e1f5f9f80 Binary files /dev/null and b/metroflip/images/RFIDDolphinReceive_97x61.png differ diff --git a/metroflip/manifest.yml b/metroflip/manifest.yml new file mode 100644 index 000000000..114a60234 --- /dev/null +++ b/metroflip/manifest.yml @@ -0,0 +1,20 @@ +author: 'luu176' +category: 'NFC' +changelog: 'CHANGELOG.md' +description: 'README.md' +icon: 'icon.png' +id: 'metroflip' +name: 'Metroflip' +screenshots: + - 'Menu-Top.png' + - 'Menu-Middle.png' + - 'Rav-Kav.png' + - 'App.png' +short_description: 'An implementation of metrodroid on the flipper' +sourcecode: + location: + commit_sha: 500144a8e9576797c085d8d9506700964a34dd73 + origin: https://github.com/luu176/Metroflip + subdir: + type: git +version: 1.0 diff --git a/metroflip/metroflip.c b/metroflip/metroflip.c new file mode 100644 index 000000000..c89143cfa --- /dev/null +++ b/metroflip/metroflip.c @@ -0,0 +1,137 @@ + +#include "metroflip_i.h" + +static bool metroflip_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + Metroflip* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool metroflip_back_event_callback(void* context) { + furi_assert(context); + Metroflip* app = context; + + return scene_manager_handle_back_event(app->scene_manager); +} + +Metroflip* metroflip_alloc() { + Metroflip* app = malloc(sizeof(Metroflip)); + app->gui = furi_record_open(RECORD_GUI); + //nfc device + app->nfc = nfc_alloc(); + app->nfc_device = nfc_device_alloc(); + + // notifs + app->notifications = furi_record_open(RECORD_NOTIFICATION); + // View Dispatcher and Scene Manager + app->view_dispatcher = view_dispatcher_alloc(); + app->scene_manager = scene_manager_alloc(&metroflip_scene_handlers, app); + + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, metroflip_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, metroflip_back_event_callback); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + // Custom Widget + app->widget = widget_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, MetroflipViewWidget, widget_get_view(app->widget)); + + // Gui Modules + app->submenu = submenu_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, MetroflipViewSubmenu, submenu_get_view(app->submenu)); + + app->text_input = text_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, MetroflipViewTextInput, text_input_get_view(app->text_input)); + + app->byte_input = byte_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, MetroflipViewByteInput, byte_input_get_view(app->byte_input)); + + app->popup = popup_alloc(); + view_dispatcher_add_view(app->view_dispatcher, MetroflipViewPopup, popup_get_view(app->popup)); + app->nfc_device = nfc_device_alloc(); + + // TextBox + app->text_box = text_box_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, MetroflipViewTextBox, text_box_get_view(app->text_box)); + app->text_box_store = furi_string_alloc(); + + return app; +} + +void metroflip_free(Metroflip* app) { + furi_assert(app); + + //nfc device + nfc_free(app->nfc); + nfc_device_free(app->nfc_device); + + //notifs + furi_record_close(RECORD_NOTIFICATION); + app->notifications = NULL; + + // Gui modules + view_dispatcher_remove_view(app->view_dispatcher, MetroflipViewSubmenu); + submenu_free(app->submenu); + view_dispatcher_remove_view(app->view_dispatcher, MetroflipViewTextInput); + text_input_free(app->text_input); + view_dispatcher_remove_view(app->view_dispatcher, MetroflipViewByteInput); + byte_input_free(app->byte_input); + view_dispatcher_remove_view(app->view_dispatcher, MetroflipViewPopup); + popup_free(app->popup); + + // Custom Widget + view_dispatcher_remove_view(app->view_dispatcher, MetroflipViewWidget); + widget_free(app->widget); + + // TextBox + view_dispatcher_remove_view(app->view_dispatcher, MetroflipViewTextBox); + text_box_free(app->text_box); + furi_string_free(app->text_box_store); + + // View Dispatcher and Scene Manager + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + // Records + furi_record_close(RECORD_GUI); + free(app); +} + +static const NotificationSequence metroflip_app_sequence_blink_start_blue = { + &message_blink_start_10, + &message_blink_set_color_blue, + &message_do_not_reset, + NULL, +}; + +static const NotificationSequence metroflip_app_sequence_blink_stop = { + &message_blink_stop, + NULL, +}; + +void metroflip_app_blink_start(Metroflip* metroflip) { + notification_message(metroflip->notifications, &metroflip_app_sequence_blink_start_blue); +} + +void metroflip_app_blink_stop(Metroflip* metroflip) { + notification_message(metroflip->notifications, &metroflip_app_sequence_blink_stop); +} + +extern int32_t metroflip(void* p) { + UNUSED(p); + Metroflip* app = metroflip_alloc(); + scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneRavKav); + scene_manager_next_scene(app->scene_manager, MetroflipSceneStart); + view_dispatcher_run(app->view_dispatcher); + metroflip_free(app); + return 0; +} diff --git a/metroflip/metroflip.h b/metroflip/metroflip.h new file mode 100644 index 000000000..013f70233 --- /dev/null +++ b/metroflip/metroflip.h @@ -0,0 +1,3 @@ +#pragma once + +typedef struct Metroflip Metroflip; diff --git a/metroflip/metroflip_i.h b/metroflip/metroflip_i.h new file mode 100644 index 000000000..8c5d5c1ed --- /dev/null +++ b/metroflip/metroflip_i.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#if __has_include() +#include +#else +extern const Icon I_RFIDDolphinReceive_97x61; +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include "scenes/metroflip_scene.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "scenes/metroflip_scene.h" + +typedef struct { + Gui* gui; + SceneManager* scene_manager; + ViewDispatcher* view_dispatcher; + NotificationApp* notifications; + Submenu* submenu; + TextInput* text_input; + TextBox* text_box; + ByteInput* byte_input; + Popup* popup; + uint8_t mac_buf[GAP_MAC_ADDR_SIZE]; + FuriString* text_box_store; + Widget* widget; + + Nfc* nfc; + NfcPoller* poller; + NfcScanner* scanner; + NfcDevice* nfc_device; + + // card details: + uint32_t balance_lari; + uint8_t balance_tetri; + uint32_t card_number; + size_t sec_num; + float value; + char currency[4]; + char card_type[32]; + +} Metroflip; + +enum MetroflipCustomEvent { + // Reserve first 100 events for button types and indexes, starting from 0 + MetroflipCustomEventReserved = 100, + + MetroflipCustomEventViewExit, + MetroflipCustomEventByteInputDone, + MetroflipCustomEventTextInputDone, + MetroflipCustomEventWorkerExit, + + MetroflipCustomEventPollerDetect, + MetroflipCustomEventPollerSuccess, + MetroflipCustomEventPollerFail, + MetroflipCustomEventPollerSelectFailed, + MetroflipCustomEventPollerFileNotFound, + + MetroflipCustomEventCardLost, + MetroflipCustomEventCardDetected, + MetroflipCustomEventWrongCard +}; + +typedef enum { + MetroflipPollerEventTypeStart, + MetroflipPollerEventTypeCardDetect, + + MetroflipPollerEventTypeSuccess, + MetroflipPollerEventTypeFail, +} MetroflipPollerEventType; + +typedef enum { + MetroflipViewSubmenu, + MetroflipViewTextInput, + MetroflipViewByteInput, + MetroflipViewPopup, + MetroflipViewMenu, + MetroflipViewLoading, + MetroflipViewTextBox, + MetroflipViewWidget, + MetroflipViewUart, +} MetroflipView; + +void metroflip_app_blink_start(Metroflip* metroflip); +void metroflip_app_blink_stop(Metroflip* metroflip); + +#ifdef FW_ORIGIN_Official +#define submenu_add_lockable_item( \ + submenu, label, index, callback, callback_context, locked, locked_message) \ + if(!(locked)) submenu_add_item(submenu, label, index, callback, callback_context) +#endif diff --git a/metroflip/scenes/metroflip_scene.c b/metroflip/scenes/metroflip_scene.c new file mode 100644 index 000000000..d20473c75 --- /dev/null +++ b/metroflip/scenes/metroflip_scene.c @@ -0,0 +1,30 @@ +#include "metroflip_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const metroflip_on_enter_handlers[])(void*) = { +#include "metroflip_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 metroflip_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "metroflip_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 metroflip_on_exit_handlers[])(void* context) = { +#include "metroflip_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers metroflip_scene_handlers = { + .on_enter_handlers = metroflip_on_enter_handlers, + .on_event_handlers = metroflip_on_event_handlers, + .on_exit_handlers = metroflip_on_exit_handlers, + .scene_num = MetroflipSceneNum, +}; diff --git a/metroflip/scenes/metroflip_scene.h b/metroflip/scenes/metroflip_scene.h new file mode 100644 index 000000000..bb9cbc1e0 --- /dev/null +++ b/metroflip/scenes/metroflip_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) MetroflipScene##id, +typedef enum { +#include "metroflip_scene_config.h" + MetroflipSceneNum, +} MetroflipScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers metroflip_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "metroflip_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 "metroflip_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 "metroflip_scene_config.h" +#undef ADD_SCENE diff --git a/metroflip/scenes/metroflip_scene_about.c b/metroflip/scenes/metroflip_scene_about.c new file mode 100644 index 000000000..c655a0e4c --- /dev/null +++ b/metroflip/scenes/metroflip_scene_about.c @@ -0,0 +1,57 @@ +#include "../metroflip_i.h" +#include + +#define TAG "Metroflip:Scene:About" + +void metroflip_about_widget_callback(GuiButtonType result, InputType type, void* context) { + Metroflip* app = context; + UNUSED(result); + + if(type == InputTypeShort) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + } +} + +void metroflip_scene_about_on_enter(void* context) { + Metroflip* app = context; + Widget* widget = app->widget; + + dolphin_deed(DolphinDeedNfcReadSuccess); + furi_string_reset(app->text_box_store); + + FuriString* str = furi_string_alloc(); + + furi_string_printf(str, "\e#About:\n\n"); + furi_string_cat_printf( + str, + "Metroflip is a multi-protocol metro card reader app for the Flipper Zero, created by luu176, inspired by the Metrodroid project. It enables the parsing and analysis of metro cards from transit systems around the world, providing a proof-of-concept for exploring transit card data in a portable format."); + + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(str)); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_about_widget_callback, app); + + furi_string_free(str); + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); +} + +bool metroflip_scene_about_on_event(void* context, SceneManagerEvent event) { + Metroflip* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_previous_scene(app->scene_manager); + } + } else if(event.type == SceneManagerEventTypeBack) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + consumed = true; + } + return consumed; +} + +void metroflip_scene_about_on_exit(void* context) { + Metroflip* app = context; + widget_reset(app->widget); + UNUSED(context); +} diff --git a/metroflip/scenes/metroflip_scene_bip.c b/metroflip/scenes/metroflip_scene_bip.c new file mode 100644 index 000000000..e90f42f88 --- /dev/null +++ b/metroflip/scenes/metroflip_scene_bip.c @@ -0,0 +1,399 @@ +/* + * Parser for bip card (Georgia). + * + * Copyright 2023 Leptoptilos + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include "../metroflip_i.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define TAG "Metroflip:Scene:Bip" + +#define BIP_CARD_ID_SECTOR_NUMBER (0) +#define BIP_BALANCE_SECTOR_NUMBER (8) +#define BIP_TRIP_TIME_WINDOW_SECTOR_NUMBER (5) +#define BIP_LAST_TOP_UPS_SECTOR_NUMBER (10) +#define BIP_TRIPS_INFO_SECTOR_NUMBER (11) + +typedef struct { + DateTime datetime; + uint16_t amount; +} BipTransaction; + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +static const MfClassicKeyPair bip_1k_keys[] = { + {.a = 0x3a42f33af429, .b = 0x1fc235ac1309}, + {.a = 0x6338a371c0ed, .b = 0x243f160918d1}, + {.a = 0xf124c2578ad0, .b = 0x9afc42372af1}, + {.a = 0x32ac3b90ac13, .b = 0x682d401abb09}, + {.a = 0x4ad1e273eaf1, .b = 0x067db45454a9}, + {.a = 0xe2c42591368a, .b = 0x15fc4c7613fe}, + {.a = 0x2a3c347a1200, .b = 0x68d30288910a}, + {.a = 0x16f3d5ab1139, .b = 0xf59a36a2546d}, + {.a = 0x937a4fff3011, .b = 0x64e3c10394c2}, + {.a = 0x35c3d2caee88, .b = 0xb736412614af}, + {.a = 0x693143f10368, .b = 0x324f5df65310}, + {.a = 0xa3f97428dd01, .b = 0x643fb6de2217}, + {.a = 0x63f17a449af0, .b = 0x82f435dedf01}, + {.a = 0xc4652c54261c, .b = 0x0263de1278f3}, + {.a = 0xd49e2826664f, .b = 0x51284c3686a6}, + {.a = 0x3df14c8000a1, .b = 0x6a470d54127c}, +}; + +static void bip_parse_datetime(const MfClassicBlock* block, DateTime* parsed_data) { + furi_assert(block); + furi_assert(parsed_data); + + parsed_data->day = (((block->data[1] << 8) + block->data[0]) >> 6) & 0x1f; + parsed_data->month = (((block->data[1] << 8) + block->data[0]) >> 11) & 0xf; + parsed_data->year = 2000 + ((((block->data[2] << 8) + block->data[1]) >> 7) & 0x1f); + parsed_data->hour = (((block->data[3] << 8) + block->data[2]) >> 4) & 0x1f; + parsed_data->minute = (((block->data[3] << 8) + block->data[2]) >> 9) & 0x3f; + parsed_data->second = (((block->data[4] << 8) + block->data[3]) >> 7) & 0x3f; +} + +static void bip_print_datetime(const DateTime* datetime, FuriString* str) { + furi_assert(datetime); + furi_assert(str); + + LocaleDateFormat date_format = locale_get_date_format(); + const char* separator = (date_format == LocaleDateFormatDMY) ? "." : "/"; + + FuriString* date_str = furi_string_alloc(); + locale_format_date(date_str, datetime, date_format, separator); + + FuriString* time_str = furi_string_alloc(); + locale_format_time(time_str, datetime, locale_get_time_format(), true); + + furi_string_cat_printf( + str, "%s %s", furi_string_get_cstr(date_str), furi_string_get_cstr(time_str)); + + furi_string_free(date_str); + furi_string_free(time_str); +} + +static int datetime_cmp(const DateTime* dt_1, const DateTime* dt_2) { + furi_assert(dt_1); + furi_assert(dt_2); + + if(dt_1->year != dt_2->year) { + return dt_1->year - dt_2->year; + } + if(dt_1->month != dt_2->month) { + return dt_1->month - dt_2->month; + } + if(dt_1->day != dt_2->day) { + return dt_1->day - dt_2->day; + } + if(dt_1->hour != dt_2->hour) { + return dt_1->hour - dt_2->hour; + } + if(dt_1->minute != dt_2->minute) { + return dt_1->minute - dt_2->minute; + } + if(dt_1->second != dt_2->second) { + return dt_1->second - dt_2->second; + } + return 0; +} + +static bool is_bip_block_empty(const MfClassicBlock* block) { + furi_assert(block); + // check if all but last byte are zero (last is checksum) + for(size_t i = 0; i < sizeof(block->data) - 1; i++) { + if(block->data[i] != 0) { + return false; + } + } + return true; +} + +static bool + bip_parse(const NfcDevice* device, FuriString* parsed_data, const MfClassicData* data) { + furi_assert(device); + furi_assert(parsed_data); + + struct { + uint32_t card_id; + uint16_t balance; + uint16_t flags; + DateTime trip_time_window; + BipTransaction top_ups[3]; + BipTransaction charges[3]; + } bip_data = {0}; + + bool parsed = false; + + do { + // Get Card ID, little-endian 4 bytes at sector 0 block 1, bytes 4-7 + const uint8_t card_id_start_block_num = + mf_classic_get_first_block_num_of_sector(BIP_CARD_ID_SECTOR_NUMBER); + const uint8_t* block_start_ptr = &data->block[card_id_start_block_num + 1].data[0]; + + bip_data.card_id = bit_lib_bytes_to_num_le(block_start_ptr + 4, 4); + + // Get balance, little-endian 2 bytes at sector 8 block 1, bytes 0-1 + const uint8_t balance_start_block_num = + mf_classic_get_first_block_num_of_sector(BIP_BALANCE_SECTOR_NUMBER); + block_start_ptr = &data->block[balance_start_block_num + 1].data[0]; + + bip_data.balance = bit_lib_bytes_to_num_le(block_start_ptr, 2); + + // Get balance flags (negative balance, etc.), little-endian 2 bytes at sector 8 block 1, bytes 2-3 + bip_data.flags = bit_lib_bytes_to_num_le(block_start_ptr + 2, 2); + + // Get trip time window, proprietary format, at sector 5 block 1, bytes 0-7 + const uint8_t trip_time_window_start_block_num = + mf_classic_get_first_block_num_of_sector(BIP_TRIP_TIME_WINDOW_SECTOR_NUMBER); + const MfClassicBlock* trip_window_block_ptr = + &data->block[trip_time_window_start_block_num + 1]; + + bip_parse_datetime(trip_window_block_ptr, &bip_data.trip_time_window); + + // Last 3 top-ups: sector 10, ring-buffer of 3 blocks, timestamp in bytes 0-7, amount in bytes 9-10 + const uint8_t top_ups_start_block_num = + mf_classic_get_first_block_num_of_sector(BIP_LAST_TOP_UPS_SECTOR_NUMBER); + for(size_t i = 0; i < 3; i++) { + const MfClassicBlock* block = &data->block[top_ups_start_block_num + i]; + + if(is_bip_block_empty(block)) continue; + + BipTransaction* top_up = &bip_data.top_ups[i]; + bip_parse_datetime(block, &top_up->datetime); + + top_up->amount = bit_lib_bytes_to_num_le(&block->data[9], 2) >> 2; + } + + // Last 3 charges (i.e. trips), sector 11, ring-buffer of 3 blocks, timestamp in bytes 0-7, amount in bytes 10-11 + const uint8_t trips_start_block_num = + mf_classic_get_first_block_num_of_sector(BIP_TRIPS_INFO_SECTOR_NUMBER); + for(size_t i = 0; i < 3; i++) { + const MfClassicBlock* block = &data->block[trips_start_block_num + i]; + + if(is_bip_block_empty(block)) continue; + + BipTransaction* charge = &bip_data.charges[i]; + bip_parse_datetime(block, &charge->datetime); + + charge->amount = bit_lib_bytes_to_num_le(&block->data[10], 2) >> 2; + } + + // All data is now parsed and stored in bip_data, now print it + + // Print basic info + furi_string_printf( + parsed_data, + "\e#Tarjeta Bip!\n" + "Card Number: %lu\n" + "Balance: $%hu (flags %hu)\n" + "Current Trip Window Ends:\n @", + bip_data.card_id, + bip_data.balance, + bip_data.flags); + + bip_print_datetime(&bip_data.trip_time_window, parsed_data); + + // Find newest top-up + size_t newest_top_up = 0; + for(size_t i = 1; i < 3; i++) { + const DateTime* newest = &bip_data.top_ups[newest_top_up].datetime; + const DateTime* current = &bip_data.top_ups[i].datetime; + if(datetime_cmp(current, newest) > 0) { + newest_top_up = i; + } + } + + // Print top-ups, newest first + furi_string_cat_printf(parsed_data, "\n\e#Last Top-ups"); + for(size_t i = 0; i < 3; i++) { + const BipTransaction* top_up = &bip_data.top_ups[(3u + newest_top_up - i) % 3]; + furi_string_cat_printf(parsed_data, "\n+$%d\n @", top_up->amount); + bip_print_datetime(&top_up->datetime, parsed_data); + } + + // Find newest charge + size_t newest_charge = 0; + for(size_t i = 1; i < 3; i++) { + const DateTime* newest = &bip_data.charges[newest_charge].datetime; + const DateTime* current = &bip_data.charges[i].datetime; + if(datetime_cmp(current, newest) > 0) { + newest_charge = i; + } + } + + // Print charges + furi_string_cat_printf(parsed_data, "\n\e#Last Charges (Trips)"); + for(size_t i = 0; i < 3; i++) { + const BipTransaction* charge = &bip_data.charges[(3u + newest_charge - i) % 3]; + furi_string_cat_printf(parsed_data, "\n-$%d\n @", charge->amount); + bip_print_datetime(&charge->datetime, parsed_data); + } + + parsed = true; + } while(false); + + return parsed; +} + +void metroflip_bip_widget_callback(GuiButtonType result, InputType type, void* context) { + Metroflip* app = context; + UNUSED(result); + + if(type == InputTypeShort) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + } +} + +static NfcCommand metroflip_scene_bip_poller_callback(NfcGenericEvent event, void* context) { + furi_assert(context); + furi_assert(event.event_data); + furi_assert(event.protocol == NfcProtocolMfClassic); + + NfcCommand command = NfcCommandContinue; + const MfClassicPollerEvent* mfc_event = event.event_data; + Metroflip* app = context; + + if(mfc_event->type == MfClassicPollerEventTypeCardDetected) { + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardDetected); + command = NfcCommandContinue; + } else if(mfc_event->type == MfClassicPollerEventTypeCardLost) { + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardLost); + app->sec_num = 0; + command = NfcCommandStop; + } else if(mfc_event->type == MfClassicPollerEventTypeRequestMode) { + mfc_event->data->poller_mode.mode = MfClassicPollerModeRead; + + } else if(mfc_event->type == MfClassicPollerEventTypeRequestReadSector) { + MfClassicKey key = {0}; + bit_lib_num_to_bytes_be(bip_1k_keys[app->sec_num].a, COUNT_OF(key.data), key.data); + + MfClassicKeyType key_type = MfClassicKeyTypeA; + mfc_event->data->read_sector_request_data.sector_num = app->sec_num; + mfc_event->data->read_sector_request_data.key = key; + mfc_event->data->read_sector_request_data.key_type = key_type; + mfc_event->data->read_sector_request_data.key_provided = true; + if(app->sec_num == 16) { + mfc_event->data->read_sector_request_data.key_provided = false; + app->sec_num = 0; + } + app->sec_num++; + } else if(mfc_event->type == MfClassicPollerEventTypeSuccess) { + nfc_device_set_data( + app->nfc_device, NfcProtocolMfClassic, nfc_poller_get_data(app->poller)); + const MfClassicData* mfc_data = nfc_device_get_data(app->nfc_device, NfcProtocolMfClassic); + FuriString* parsed_data = furi_string_alloc(); + Widget* widget = app->widget; + + dolphin_deed(DolphinDeedNfcReadSuccess); + furi_string_reset(app->text_box_store); + bip_parse(app->nfc_device, parsed_data, mfc_data); + metroflip_app_blink_stop(app); + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_bip_widget_callback, app); + + furi_string_free(parsed_data); + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); + + command = NfcCommandStop; + } else if(mfc_event->type == MfClassicPollerEventTypeFail) { + FURI_LOG_I(TAG, "fail"); + command = NfcCommandStop; + } + + return command; +} + +void metroflip_scene_bip_on_enter(void* context) { + Metroflip* app = context; + dolphin_deed(DolphinDeedNfcRead); + + app->sec_num = 0; + + // Setup view + Popup* popup = app->popup; + popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop); + popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61); + + // Start worker + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup); + nfc_scanner_alloc(app->nfc); + app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic); + nfc_poller_start(app->poller, metroflip_scene_bip_poller_callback, app); + + metroflip_app_blink_start(app); +} + +bool metroflip_scene_bip_on_event(void* context, SceneManagerEvent event) { + Metroflip* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == MetroflipCustomEventCardDetected) { + Popup* popup = app->popup; + popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventCardLost) { + Popup* popup = app->popup; + popup_set_header(popup, "Card \n lost", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventWrongCard) { + Popup* popup = app->popup; + popup_set_header(popup, "WRONG \n CARD", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventPollerFail) { + Popup* popup = app->popup; + popup_set_header(popup, "Failed", 68, 30, AlignLeft, AlignTop); + consumed = true; + } + } else if(event.type == SceneManagerEventTypeBack) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + consumed = true; + } + + return consumed; +} + +void metroflip_scene_bip_on_exit(void* context) { + Metroflip* app = context; + widget_reset(app->widget); + + if(app->poller) { + nfc_poller_stop(app->poller); + nfc_poller_free(app->poller); + } + + // Clear view + popup_reset(app->popup); + + metroflip_app_blink_stop(app); +} diff --git a/metroflip/scenes/metroflip_scene_charliecard.c b/metroflip/scenes/metroflip_scene_charliecard.c new file mode 100644 index 000000000..5ebecaded --- /dev/null +++ b/metroflip/scenes/metroflip_scene_charliecard.c @@ -0,0 +1,1306 @@ +/* + * Parser for MBTA CharlieCard (Boston, MA, USA). + * + * Copyright 2024 Zachary Weiss + * + * Public security research on the MBTA's fare system stretches back to 2008, + * starting with Russel Ryan, Zack Anderson, and Alessandro Chiesa's + * "Anatomy of a Subway Hack", for which they were famously issued a gag order. + * A thorough history of research & researchers deserving of credit is + * detailed by @bobbyrsec in his 2022 blog post (& presentation): + * "Operation Charlie: Hacking the MBTA CharlieCard from 2008 to Present" + * https://medium.com/@bobbyrsec/operation-charlie-hacking-the-mbta-charliecard-from-2008-to-present-24ea9f0aaa38 + * + * Fare gate IDs, card types, and general assistance courtesy of the + * minds behind DEFCON 31's "Boston Infinite Money Glitch" presentation: + * — Matthew Harris; mattyharris.net + * — Zachary Bertocchi; zackbertocchi.com + * — Scott Campbell; josephscottcampbell.com + * — Noah Gibson; + * Talk available at: https://www.youtube.com/watch?v=1JT_lTfK69Q + * + * TODOs: + * — Reverse engineer passes (sectors 4 & 5?), impl. + * — Infer transaction flag meanings + * — Infer remaining unknown bytes in the balance sectors (2 & 3) + * — Improve string output formatting, esp. of transaction log + * — Mapping of buses to garages, and subsequently, route subsets via + * http://roster.transithistory.org/ data + * — Mapping of stations to lines + * — Add'l data fields for side of station fare gates are on? Some stations + * separate inbound & outbound sides, so direction could be inferred + * from gates used. + * — Continually gather data on fare gate ID mappings, update as collected; + * check locations this might be scrapable / inferrable from: + * [X] MBTA GTFS spec (https://www.mbta.com/developers/gtfs) features & IDs + * seem too-coarse-grained & uncorrelated + * [X] MBTA ArcGIS (https://mbta-massdot.opendata.arcgis.com/) & Tableau + * (https://public.tableau.com/app/profile/mbta.office.of.performance.management.and.innovation/vizzes) + * files don't seem to have anything of that resolution (only down to ridership by station) + * [X] (skim of) MBTA public GitHub (https://github.com/mbta) repos make no reference to fare-gate-level data + * [X] (skim of) MBTA public engineering docs (https://www.mbta.com/engineering) unfruitful; + * Closest mention spotted is 2014 "Ridership and Service Statistics" + * (https://cdn.mbta.com/sites/default/files/fmcb-meeting-docs/reports-policies/2014-07-mbta-bluebook-ed14.pdf) + * where on pg.40, "Equipment at Stations" is enumerated, and fare gates counts are given, + * listed as "AFC Gates" (presumably standing for "Automated Fare Collection") + * [X] Josiah Zachery criminal trial public evidence — convicted partially on + * data on his CharlieCard, appeals partially on basis of legality of this search. + * Prev. court case (gag order mentioned in preamble) leaked some data in the files + * entered into evidence. Seemingly did not happen here; fare gate IDs unmentioned, + * only ever the nature of stored/saved data and methods of retrieval. + * Appelate case dockets 2019-P-0401, SJC-12952, SJ-2017-0390 + * (https://www.ma-appellatecourts.org/party) + * Trial court indictment 04/02/2015, Case# 1584CR10265 @Suffolk County Criminal Superior Court + * (https://www.masscourts.org/eservices/home.page.16) + * [ ] FOIA / public records request? + * (https://massachusettsdot.mycusthelp.com/WEBAPP/_rs/(S(tbcygdlm0oojy35p1wv0y2y5))/supporthome.aspx) + * [X] MBTA data blog? (https://www.massdottracker.com/datablog/) + * [ ] MassDOT developers Google group? (https://groups.google.com/g/massdotdevelopers) + * [X] preexisting posts + * [ ] ask directly? + * [ ] Other? + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include "../metroflip_i.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAG "Metroflip:Scene:CharlieCard" + +// starts Wednesday 2003/1/1 @ midnight +#define CHARLIE_EPOCH \ + (DateTime) { \ + 0, 0, 0, 1, 1, 2003, 4 \ + } +// timestep is one minute +#define CHARLIE_TIME_DELTA_SECS 60 +#define CHARLIE_END_VALID_DELTA_SECS 60 * 8 +#define CHARLIE_N_TRANSACTION_HISTORY 10 +#define CHARLIE_N_PASSES 4 + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +// always from the same set of keys (cf. default keys dict for list w/o multiplicity) +// we only care about the data in the first half of the sectors +// second half sectors keys seemingly change position sometimes across cards? +// no data stored there, but might want to impl some custom read function +// accounting for this such that reading is faster (else it seems to fall back on dict +// approach for remaining keys)... + +static const MfClassicKeyPair charliecard_1k_keys[] = { + {.a = 0x3060206F5B0A, .b = 0xF1B9F5669CC8}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89}, + {.a = 0x3A09594C8587, .b = 0x62387B8D250D}, + {.a = 0xF238D78FF48F, .b = 0x9DC282D46217}, + {.a = 0xAFD0BA94D624, .b = 0x92EE4DC87191}, + {.a = 0xB35A0E4ACC09, .b = 0x756EF55E2507}, + {.a = 0x447AB7FD5A6B, .b = 0x932B9CB730EF}, + {.a = 0x1F1A0A111B5B, .b = 0xAD9E0A1CA2F7}, + {.a = 0xD58023BA2BDC, .b = 0x62CED42A6D87}, + {.a = 0x2548A443DF28, .b = 0x2ED3B15E7C0F}, +}; + +typedef struct { + uint16_t dollars; + uint8_t cents; +} Money; + +#define FARE_BUS \ + (Money) { \ + 1, 70 \ + } +#define FARE_SUB \ + (Money) { \ + 2, 40 \ + } + +typedef struct { + DateTime date; + uint16_t gate; + uint8_t g_flag; + Money fare; + uint16_t f_flag; +} Transaction; + +typedef struct { + bool valid; + uint16_t pre; + uint16_t post; + DateTime date; +} Pass; + +typedef struct { + uint16_t n_uses; + uint8_t active_balance_sector; +} CounterSector; + +typedef struct { + Money balance; + uint16_t type; + DateTime issued; + DateTime end_validity; +} BalanceSector; + +// IdMapping approach borrowed from Jeremy Cooper's 'clipper.c' +typedef struct { + uint16_t id; + const char* name; +} IdMapping; + +// this should be a complete accounting of types, (1 and 7 day pass types maybe missing?) +static const IdMapping charliecard_types[] = { + // Regular card types + {.id = 367, .name = "Adult"}, + {.id = 366, .name = "SV Adult"}, + {.id = 418, .name = "Student"}, + {.id = 419, .name = "Senior"}, + {.id = 420, .name = "TAP"}, + {.id = 417, .name = "Blind"}, + {.id = 426, .name = "Child"}, + {.id = 410, .name = "Employee ID Without Passback"}, + {.id = 414, .name = "Employee ID With Passback"}, + {.id = 415, .name = "Retiree"}, + {.id = 416, .name = "Police/Fire"}, + + // Passes + {.id = 135, .name = "30 Day Local Bus Pass"}, + {.id = 136, .name = "30 Day Inner Express Bus Pass"}, + {.id = 137, .name = "30 Day Outer Express Bus Pass"}, + {.id = 138, .name = "30 Day LinkPass"}, + {.id = 139, .name = "30 Day Senior LinkPass"}, + {.id = 148, .name = "30 Day TAP LinkPass"}, + {.id = 150, .name = "Monthly Student LinkPass"}, + {.id = 424, .name = "Monthly TAP LinkPass"}, + {.id = 425, .name = "Monthly Senior LinkPass"}, + {.id = 421, .name = "Senior TAP/Permit"}, + {.id = 422, .name = "Senior TAP/Permit 30 Days"}, + + // Commuter rail passes + {.id = 166, .name = "30 Day Commuter Rail Zone 1A Pass"}, + {.id = 167, .name = "30 Day Commuter Rail Zone 1 Pass"}, + {.id = 168, .name = "30 Day Commuter Rail Zone 2 Pass"}, + {.id = 169, .name = "30 Day Commuter Rail Zone 3 Pass"}, + {.id = 170, .name = "30 Day Commuter Rail Zone 4 Pass"}, + {.id = 171, .name = "30 Day Commuter Rail Zone 5 Pass"}, + {.id = 172, .name = "30 Day Commuter Rail Zone 6 Pass"}, + {.id = 173, .name = "30 Day Commuter Rail Zone 7 Pass"}, + {.id = 174, .name = "30 Day Commuter Rail Zone 8 Pass"}, + {.id = 175, .name = "30 Day Interzone 1 Pass"}, + {.id = 176, .name = "30 Day Interzone 2 Pass"}, + {.id = 177, .name = "30 Day Interzone 3 Pass"}, + {.id = 178, .name = "30 Day Interzone 4 Pass"}, + {.id = 179, .name = "30 Day Interzone 5 Pass"}, + {.id = 180, .name = "30 Day Interzone 6 Pass"}, + {.id = 181, .name = "30 Day Interzone 7 Pass"}, + {.id = 182, .name = "30 Day Interzone 8 Pass"}, + + {.id = 140, .name = "One Way Interzone Adult 1 Zone"}, + {.id = 141, .name = "One Way Interzone Adult 2 Zones"}, + {.id = 142, .name = "One Way Interzone Adult 3 Zones"}, + {.id = 143, .name = "One Way Interzone Adult 4 Zones"}, + {.id = 144, .name = "One Way Interzone Adult 5 Zones"}, + {.id = 145, .name = "One Way Interzone Adult 6 Zones"}, + {.id = 146, .name = "One Way Interzone Adult 7 Zones"}, + {.id = 147, .name = "One Way Interzone Adult 8 Zones"}, + + {.id = 428, .name = "One Way Half Fare Zone 1"}, + {.id = 429, .name = "One Way Half Fare Zone 2"}, + {.id = 430, .name = "One Way Half Fare Zone 3"}, + {.id = 431, .name = "One Way Half Fare Zone 4"}, + {.id = 432, .name = "One Way Half Fare Zone 5"}, + {.id = 433, .name = "One Way Half Fare Zone 6"}, + {.id = 434, .name = "One Way Half Fare Zone 7"}, + {.id = 435, .name = "One Way Half Fare Zone 8"}, + {.id = 436, .name = "One Way Interzone Half Fare 1 Zone"}, + {.id = 437, .name = "One Way Interzone Half Fare 2 Zones"}, + {.id = 438, .name = "One Way Interzone Half Fare 3 Zones"}, + {.id = 439, .name = "One Way Interzone Half Fare 4 Zones"}, + {.id = 440, .name = "One Way Interzone Half Fare 5 Zones"}, + {.id = 441, .name = "One Way Interzone Half Fare 6 Zones"}, + {.id = 442, .name = "One Way Interzone Half Fare 7 Zones"}, + {.id = 443, .name = "One Way Interzone Half Fare 8 Zones"}, + + {.id = 509, .name = "Group Interzone 1 Zones"}, + {.id = 510, .name = "Group Interzone 2 Zones"}, + {.id = 511, .name = "Group Interzone 3 Zones"}, + {.id = 512, .name = "Group Interzone 4 Zones"}, + {.id = 513, .name = "Group Interzone 5 Zones"}, + {.id = 514, .name = "Group Interzone 6 Zones"}, + {.id = 515, .name = "Group Interzone 7 Zones"}, + {.id = 516, .name = "Group Interzone 8 Zones"}, + + {.id = 952, .name = "Zone 1 Student Monthly Pass"}, + {.id = 953, .name = "Zone 2 Student Monthly Pass"}, + {.id = 954, .name = "Zone 3 Student Monthly Pass"}, + {.id = 955, .name = "Zone 4 Student Monthly Pass"}, + {.id = 956, .name = "Zone 5 Student Monthly Pass"}, + {.id = 957, .name = "Zone 6 Student Monthly Pass"}, + {.id = 958, .name = "Zone 7 Student Monthly Pass"}, + {.id = 959, .name = "Zone 8 Student Monthly Pass"}, + {.id = 960, .name = "Zone 9 Student Monthly Pass"}, + {.id = 961, .name = "Zone 10 Student Monthly Pass"}, + + {.id = 963, .name = "Interzone 1 Zone Student Monthly Pass"}, + {.id = 964, .name = "Interzone 2 Zone Student Monthly Pass"}, + {.id = 965, .name = "Interzone 3 Zone Student Monthly Pass"}, + {.id = 966, .name = "Interzone 4 Zone Student Monthly Pass"}, + {.id = 967, .name = "Interzone 5 Zone Student Monthly Pass"}, + {.id = 968, .name = "Interzone 6 Zone Student Monthly Pass"}, + {.id = 969, .name = "Interzone 7 Zone Student Monthly Pass"}, + {.id = 970, .name = "Interzone 8 Zone Student Monthly Pass"}, + {.id = 971, .name = "Interzone 9 Zone Student Monthly Pass"}, + {.id = 972, .name = "Interzone 10 Zone Student Monthly Pass"}, +}; +static const size_t kNumTypes = COUNT_OF(charliecard_types); + +// Incomplete, and subject to change +// Only covers Orange & Blue line stations +// Gathered manually, and provided courtesy of, DEFCON31 researchers +// as cited above. +static const IdMapping charliecard_fare_gate_ids[] = { + // Davis + {.id = 6766, .name = "Davis"}, + {.id = 6767, .name = "Davis"}, + {.id = 6768, .name = "Davis"}, + {.id = 6769, .name = "Davis"}, + {.id = 6770, .name = "Davis"}, + {.id = 6771, .name = "Davis"}, + {.id = 6772, .name = "Davis"}, + {.id = 2167, .name = "Davis"}, + {.id = 7020, .name = "Davis"}, + // Porter + {.id = 6781, .name = "Porter"}, + {.id = 6780, .name = "Porter"}, + {.id = 6779, .name = "Porter"}, + {.id = 6778, .name = "Porter"}, + {.id = 6777, .name = "Porter"}, + {.id = 6776, .name = "Porter"}, + {.id = 6775, .name = "Porter"}, + {.id = 2168, .name = "Porter"}, + {.id = 7021, .name = "Porter"}, + {.id = 6782, .name = "Porter"}, + // Oak Grove + {.id = 6640, .name = "Oak Grove"}, + {.id = 6641, .name = "Oak Grove"}, + {.id = 6639, .name = "Oak Grove"}, + {.id = 2036, .name = "Oak Grove"}, + {.id = 6642, .name = "Oak Grove"}, + {.id = 6979, .name = "Oak Grove"}, + // Downtown Crossing + {.id = 2091, .name = "Downtown Crossing"}, + {.id = 6995, .name = "Downtown Crossing"}, + {.id = 6699, .name = "Downtown Crossing"}, + {.id = 6700, .name = "Downtown Crossing"}, + {.id = 1926, .name = "Downtown Crossing"}, + {.id = 2084, .name = "Downtown Crossing"}, + {.id = 6994, .name = "Downtown Crossing"}, + {.id = 6695, .name = "Downtown Crossing"}, + {.id = 6694, .name = "Downtown Crossing"}, + {.id = 6696, .name = "Downtown Crossing"}, + {.id = 2336, .name = "Downtown Crossing"}, + {.id = 1056, .name = "Downtown Crossing"}, + {.id = 6814, .name = "Downtown Crossing"}, + {.id = 6813, .name = "Downtown Crossing"}, + {.id = 2212, .name = "Downtown Crossing"}, + {.id = 7038, .name = "Downtown Crossing"}, + // State + {.id = 7092, .name = "State"}, + {.id = 1844, .name = "State"}, + {.id = 6689, .name = "State"}, + {.id = 6988, .name = "State"}, + {.id = 6991, .name = "State"}, + {.id = 2083, .name = "State"}, + {.id = 6688, .name = "State"}, + {.id = 6687, .name = "State"}, + {.id = 6686, .name = "State"}, + {.id = 2078, .name = "State"}, + {.id = 6987, .name = "State"}, + {.id = 7090, .name = "State"}, + {.id = 1842, .name = "State"}, + // Haymarket + {.id = 6684, .name = "Haymarket"}, + {.id = 6683, .name = "Haymarket"}, + {.id = 6682, .name = "Haymarket"}, + {.id = 6681, .name = "Haymarket"}, + {.id = 2073, .name = "Haymarket"}, + {.id = 7074, .name = "Haymarket"}, + {.id = 6883, .name = "Haymarket"}, + {.id = 6884, .name = "Haymarket"}, + {.id = 6885, .name = "Haymarket"}, + {.id = 6886, .name = "Haymarket"}, + {.id = 2303, .name = "Haymarket"}, + {.id = 6986, .name = "Haymarket"}, + // North Station + {.id = 6985, .name = "North Station"}, + {.id = 2063, .name = "North Station"}, + {.id = 6671, .name = "North Station"}, + {.id = 6672, .name = "North Station"}, + {.id = 6673, .name = "North Station"}, + {.id = 6674, .name = "North Station"}, + {.id = 6675, .name = "North Station"}, + {.id = 6676, .name = "North Station"}, + {.id = 6677, .name = "North Station"}, + {.id = 6678, .name = "North Station"}, + {.id = 6984, .name = "North Station"}, + {.id = 2062, .name = "North Station"}, + {.id = 6668, .name = "North Station"}, + {.id = 6667, .name = "North Station"}, + {.id = 6666, .name = "North Station"}, + {.id = 6665, .name = "North Station"}, + {.id = 6664, .name = "North Station"}, + // Sullivan Square + {.id = 6654, .name = "Sullivan Square"}, + {.id = 6655, .name = "Sullivan Square"}, + {.id = 6656, .name = "Sullivan Square"}, + {.id = 6657, .name = "Sullivan Square"}, + {.id = 6658, .name = "Sullivan Square"}, + {.id = 6659, .name = "Sullivan Square"}, + {.id = 2053, .name = "Sullivan Square"}, + {.id = 6982, .name = "Sullivan Square"}, + // Community College + {.id = 6661, .name = "Community College"}, + {.id = 6662, .name = "Community College"}, + {.id = 2056, .name = "Community College"}, + {.id = 6983, .name = "Community College"}, + // Assembly + {.id = 3876, .name = "Assembly"}, + {.id = 3875, .name = "Assembly"}, + {.id = 6957, .name = "Assembly"}, + {.id = 6956, .name = "Assembly"}, + {.id = 6955, .name = "Assembly"}, + {.id = 6954, .name = "Assembly"}, + {.id = 6953, .name = "Assembly"}, + {.id = 7101, .name = "Assembly"}, + {.id = 3873, .name = "Assembly"}, + {.id = 3872, .name = "Assembly"}, + // Wellington + {.id = 6981, .name = "Wellington"}, + {.id = 2042, .name = "Wellington"}, + {.id = 6650, .name = "Wellington"}, + {.id = 6651, .name = "Wellington"}, + {.id = 6652, .name = "Wellington"}, + {.id = 6653, .name = "Wellington"}, + // Malden + {.id = 6980, .name = "Malden Center"}, + {.id = 2037, .name = "Malden Center"}, + {.id = 6645, .name = "Malden Center"}, + {.id = 6646, .name = "Malden Center"}, + {.id = 6647, .name = "Malden Center"}, + {.id = 6648, .name = "Malden Center"}, + // Chinatown + {.id = 6704, .name = "Chinatown"}, + {.id = 6705, .name = "Chinatown"}, + {.id = 2099, .name = "Chinatown"}, + {.id = 7003, .name = "Chinatown"}, + {.id = 7002, .name = "Chinatown"}, + {.id = 2096, .name = "Chinatown"}, + {.id = 6702, .name = "Chinatown"}, + {.id = 6701, .name = "Chinatown"}, + // Tufts Medical Center + {.id = 6707, .name = "Tufts Medical Center"}, + {.id = 6708, .name = "Tufts Medical Center"}, + {.id = 6709, .name = "Tufts Medical Center"}, + {.id = 6710, .name = "Tufts Medical Center"}, + {.id = 6711, .name = "Tufts Medical Center"}, + {.id = 2105, .name = "Tufts Medical Center"}, + {.id = 7004, .name = "Tufts Medical Center"}, + {.id = 1941, .name = "Tufts Medical Center"}, + {.id = 7006, .name = "Tufts Medical Center"}, + // Back Bay + {.id = 7007, .name = "Back Bay"}, + {.id = 1480, .name = "Back Bay"}, + {.id = 6714, .name = "Back Bay"}, + {.id = 6715, .name = "Back Bay"}, + {.id = 6716, .name = "Back Bay"}, + {.id = 6717, .name = "Back Bay"}, + {.id = 6718, .name = "Back Bay"}, + {.id = 6719, .name = "Back Bay"}, + {.id = 6720, .name = "Back Bay"}, + {.id = 1801, .name = "Back Bay"}, + {.id = 7009, .name = "Back Bay"}, + // Massachusetts Avenue + {.id = 7010, .name = "Massachusetts Avenue"}, + {.id = 2118, .name = "Massachusetts Avenue"}, + {.id = 6724, .name = "Massachusetts Avenue"}, + {.id = 6723, .name = "Massachusetts Avenue"}, + {.id = 6722, .name = "Massachusetts Avenue"}, + {.id = 6721, .name = "Massachusetts Avenue"}, + // Ruggles + {.id = 6726, .name = "Ruggles"}, + {.id = 6727, .name = "Ruggles"}, + {.id = 6728, .name = "Ruggles"}, + {.id = 2122, .name = "Ruggles"}, + {.id = 2123, .name = "Ruggles"}, + {.id = 2124, .name = "Ruggles"}, + {.id = 1804, .name = "Ruggles"}, + // Roxbury Crossing + {.id = 6737, .name = "Roxbury Crossing"}, + {.id = 6736, .name = "Roxbury Crossing"}, + {.id = 6735, .name = "Roxbury Crossing"}, + {.id = 6734, .name = "Roxbury Crossing"}, + {.id = 6733, .name = "Roxbury Crossing"}, + {.id = 2125, .name = "Roxbury Crossing"}, + {.id = 7012, .name = "Roxbury Crossing"}, + // Jackson Square + {.id = 6741, .name = "Jackson Square"}, + {.id = 6740, .name = "Jackson Square"}, + {.id = 6739, .name = "Jackson Square"}, + {.id = 2131, .name = "Jackson Square"}, + {.id = 7013, .name = "Jackson Square"}, + {.id = 7014, .name = "Jackson Square"}, + {.id = 2135, .name = "Jackson Square"}, + {.id = 6743, .name = "Jackson Square"}, + {.id = 6744, .name = "Jackson Square"}, + {.id = 6745, .name = "Jackson Square"}, + // Green Street + {.id = 6746, .name = "Green Street"}, + {.id = 6747, .name = "Green Street"}, + {.id = 6748, .name = "Green Street"}, + {.id = 2142, .name = "Green Street"}, + {.id = 7015, .name = "Green Street"}, + // Forest Hills + {.id = 6750, .name = "Forest Hills"}, + {.id = 6751, .name = "Forest Hills"}, + {.id = 6752, .name = "Forest Hills"}, + {.id = 6753, .name = "Forest Hills"}, + {.id = 6754, .name = "Forest Hills"}, + {.id = 6755, .name = "Forest Hills"}, + {.id = 2150, .name = "Forest Hills"}, + {.id = 7016, .name = "Forest Hills"}, + {.id = 6950, .name = "Forest Hills"}, + {.id = 6951, .name = "Forest Hills"}, + {.id = 604, .name = "Forest Hills"}, + {.id = 7096, .name = "Forest Hills"}, + // South Station + {.id = 7039, .name = "South Station"}, + {.id = 2215, .name = "South Station"}, + {.id = 6816, .name = "South Station"}, + {.id = 6817, .name = "South Station"}, + {.id = 6818, .name = "South Station"}, + {.id = 6819, .name = "South Station"}, + {.id = 6820, .name = "South Station"}, + {.id = 6821, .name = "South Station"}, + {.id = 6822, .name = "South Station"}, + {.id = 6823, .name = "South Station"}, + {.id = 7040, .name = "South Station"}, + {.id = 2228, .name = "South Station"}, + {.id = 6827, .name = "South Station"}, + {.id = 6826, .name = "South Station"}, + {.id = 6825, .name = "South Station"}, + {.id = 6824, .name = "South Station"}, + // Courthouse + {.id = 6929, .name = "Courthouse"}, + {.id = 2357, .name = "Courthouse"}, + {.id = 7079, .name = "Courthouse"}, + {.id = 6933, .name = "Courthouse"}, + {.id = 6932, .name = "Courthouse"}, + {.id = 2358, .name = "Courthouse"}, + {.id = 6792, .name = "Courthouse"}, + // Bowdoin + {.id = 6937, .name = "Bowdoin"}, + {.id = 2367, .name = "Bowdoin"}, + {.id = 7085, .name = "Bowdoin"}, + // Government Center + {.id = 6963, .name = "Government Center"}, + {.id = 6962, .name = "Government Center"}, + {.id = 6961, .name = "Government Center"}, + {.id = 6960, .name = "Government Center"}, + {.id = 6959, .name = "Government Center"}, + {.id = 6958, .name = "Government Center"}, + {.id = 5298, .name = "Government Center"}, + // Aquarium + {.id = 6609, .name = "Aquarium"}, + {.id = 6608, .name = "Aquarium"}, + {.id = 1877, .name = "Aquarium"}, + {.id = 6965, .name = "Aquarium"}, + {.id = 6610, .name = "Aquarium"}, + {.id = 1880, .name = "Aquarium"}, + {.id = 1871, .name = "Aquarium"}, + {.id = 6966, .name = "Aquarium"}, + // Maverick + {.id = 7088, .name = "Maverick"}, + {.id = 6944, .name = "Maverick"}, + {.id = 4384, .name = "Maverick"}, + {.id = 6946, .name = "Maverick"}, + {.id = 6947, .name = "Maverick"}, + {.id = 6948, .name = "Maverick"}, + {.id = 6949, .name = "Maverick"}, + {.id = 1840, .name = "Maverick"}, + {.id = 7083, .name = "Maverick"}, + // Airport + {.id = 6613, .name = "Airport"}, + {.id = 6612, .name = "Airport"}, + {.id = 6611, .name = "Airport"}, + {.id = 6968, .name = "Airport"}, + {.id = 2009, .name = "Airport"}, + {.id = 6616, .name = "Airport"}, + {.id = 6615, .name = "Airport"}, + {.id = 6614, .name = "Airport"}, + {.id = 6970, .name = "Airport"}, + {.id = 1847, .name = "Airport"}, + // Wood Island + {.id = 6618, .name = "Wood Island"}, + {.id = 6619, .name = "Wood Island"}, + {.id = 2010, .name = "Wood Island"}, + {.id = 6971, .name = "Wood Island"}, + // Orient Heights + {.id = 6621, .name = "Orient Heights"}, + {.id = 6622, .name = "Orient Heights"}, + {.id = 6623, .name = "Orient Heights"}, + {.id = 2014, .name = "Orient Heights"}, + {.id = 6972, .name = "Orient Heights"}, + {.id = 6974, .name = "Orient Heights"}, + {.id = 1868, .name = "Orient Heights"}, + // Suffolk Downs + {.id = 6625, .name = "Suffolk Downs"}, + {.id = 6626, .name = "Suffolk Downs"}, + {.id = 2017, .name = "Suffolk Downs"}, + {.id = 6975, .name = "Suffolk Downs"}, + // Beachmont + {.id = 6628, .name = "Beachmont"}, + {.id = 6629, .name = "Beachmont"}, + {.id = 6630, .name = "Beachmont"}, + {.id = 2021, .name = "Beachmont"}, + {.id = 6976, .name = "Beachmont"}, + // Revere Beach + {.id = 6632, .name = "Revere Beach"}, + {.id = 6633, .name = "Revere Beach"}, + {.id = 2024, .name = "Revere Beach"}, + {.id = 6977, .name = "Revere Beach"}, + // Wonderland + {.id = 6638, .name = "Wonderland"}, + {.id = 6637, .name = "Wonderland"}, + {.id = 6636, .name = "Wonderland"}, + {.id = 2025, .name = "Wonderland"}, + {.id = 6978, .name = "Wonderland"}, +}; +static const size_t kNumFareGateIds = COUNT_OF(charliecard_fare_gate_ids); + +// ********************************************************** +// ********************* MISC HELPERS *********************** +// ********************************************************** + +static const uint8_t* + pos_to_ptr(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + // returns pointer to specified sector/block/byte of MFClassic card data + uint8_t block_offset = mf_classic_get_first_block_num_of_sector(sector_num); + return &data->block[block_offset + block_num].data[byte_num]; +} + +static uint64_t pos_to_num( + const MfClassicData* data, + uint8_t sector_num, + uint8_t block_num, + uint8_t byte_num, + uint8_t byte_len) { + // returns numeric values at specified card location, for given byte length. + // assumes big endian. + return bit_lib_bytes_to_num_be(pos_to_ptr(data, sector_num, block_num, byte_num), byte_len); +} + +static DateTime dt_delta(DateTime dt, uint64_t delta_secs) { + // returns shifted DateTime, from initial DateTime and time offset in seconds + DateTime dt_shifted = {0}; + datetime_timestamp_to_datetime(datetime_datetime_to_timestamp(&dt) + delta_secs, &dt_shifted); + + return dt_shifted; +} + +static bool dt_ge(DateTime dt1, DateTime dt2) { + // compares two DateTimes + return datetime_datetime_to_timestamp(&dt1) >= datetime_datetime_to_timestamp(&dt2); +} + +static bool dt_eq(DateTime dt1, DateTime dt2) { + // compares two DateTimes + return datetime_datetime_to_timestamp(&dt1) == datetime_datetime_to_timestamp(&dt2); +} + +static bool get_map_item(uint16_t id, const IdMapping* map, size_t sz, const char** out) { + // code borrowed from Jeremy Cooper's 'clipper.c'. Used as follows: + // const char* s; if(!get_map_item(_,_,_,&s)) {s="Default str";} + // TODO: change to furistring out? + for(size_t i = 0; i < sz; i++) { + if(map[i].id == id) { + *out = map[i].name; + return true; + } + } + + return false; +} + +uint32_t time_now() { + return furi_hal_rtc_get_timestamp(); +} + +static bool is_debug() { + return furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug); +} + +// ********************************************************** +// ******************** FIELD PARSING *********************** +// ********************************************************** + +static Money money_parse( + const MfClassicData* data, + uint8_t sector_num, + uint8_t block_num, + uint8_t byte_num) { + // CharlieCards store all money values in two bytes as half-cents + // bitmask removes sign/flag, bitshift converts half-cents to cents, div & mod yield dollars & cents + uint16_t amt = (pos_to_num(data, sector_num, block_num, byte_num, 2) & 0x7FFF) >> 1; + return (Money){amt / 100, amt % 100}; +} + +static DateTime + date_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + // Dates are 3 bytes, in minutes since 2003/1/1 ("CHARLIE_EPOCH") + uint32_t ts_charlie = pos_to_num(data, sector_num, block_num, byte_num, 3); + return dt_delta(CHARLIE_EPOCH, ts_charlie * CHARLIE_TIME_DELTA_SECS); +} + +static DateTime end_validity_parse( + const MfClassicData* data, + uint8_t sector_num, + uint8_t block_num, + uint8_t byte_num) { + // End validity field is weird; shares first byte with another variable (the card type field), + // occupying the last 5 bits (and subsequent two bytes), hence bitmask + uint32_t ts_charlie_ev = pos_to_num(data, sector_num, block_num, byte_num, 3) & 0x1FFFFF; + + // additionally, instead of minute deltas, is in 8 minute increments + // relative to CHARLIE_EPOCH (2003/1/1), per DEFCON31 researcher's work + return dt_delta(CHARLIE_EPOCH, ts_charlie_ev * CHARLIE_END_VALID_DELTA_SECS); +} + +static Pass + pass_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + // WIP; testing only. Speculating it may be structured as follows + // Sub-byte field divisions not drawn to scale, see code for exact bit offsets + // + // 0 1 2 3 4 5 + // +----.----.----.----+----.----+ + // | uk1 | date | uk2 | + // +----.----.----.----+----.----+ + // + // "Blank" entries are as follows: + // 0 1 2 3 4 5 + // +----.----.----.----.----.----+ + // | 00 20 00 00 00 00 | + // +----.----.----.----.----.----+ + // + // even when not blank, uk1 LSB seems to always be set to 1... + // the sole bit set to 1 on the blank entry seems to divide + // the uk1 and date fields, and is always set to 1 regardless + // same is true of type & end-validity split found in balance sector + // + // likely fields incl + // — type #, + // — a secondary date field (eg start/end, end validity or normal format) + // — ID of FVM from which the pass was loaded + + // check for empty, if so, return struct filled w/ 0s + // (incl "valid" field: hence, "valid" is false-y) + if(pos_to_num(data, sector_num, block_num, byte_num, 6) == 0x002000000000) { + return (Pass){0}; + } + + // const DateTime start = date_parse(data, sector_num, block_num, byte_num + 1); + + const uint16_t pre = pos_to_num(data, sector_num, block_num, byte_num, 2) >> 6; + const uint16_t post = (pos_to_num(data, sector_num, block_num, byte_num + 4, 2) >> 2) & 0x3ff; + + // these values make sense for a date, but implied position of type + // before end validity, as seen in balance sector, doesn't seem + // to produce sensible values + const DateTime date = end_validity_parse(data, sector_num, block_num, byte_num + 1); + + // DateTime start = date_parse(data, sector_num, block_num, byte_num); + // uint16_t type = 0; // pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 6; + + return (Pass){true, pre, post, date}; +} + +static Transaction + transaction_parse(const MfClassicData* data, uint8_t sector, uint8_t block, uint8_t byte) { + // This function parses individual transactions. Each transaction packs 7 bytes, stored as follows: + // + // 0 1 2 3 4 5 6 + // +----.----.----+----.--+-+----.----+ + // | date | loc |f| amt | + // +----.----.----+----.--+-+----.----+ + // + // Where date is in the typical format, loc represents the fare gate tapped, and amt is the fare amount. + // Amount appears to contain some flag bits, however, it is unclear what precisely their function is. + // + // Gate ID ("loc") is only the first 13 bits of 0x3:0x5, the final three bits appear to be flags ("f"). + // Least significant flag bit seems to indicate: + // — When f & 1 == 1, fare (the amount by which balance is decremented) + // — When f & 1 == 0, refill (the amount by which balance is incremented) + // MSB (sign bit) of amt seems to serve the same role, just inverted, ie + // — When amt & 0x8000 == 0, fare + // — When amt & 0x8000 == 0x8000, refill + // Only contradiction between the two observed is on cards w/ passes; + // MSB of amt seems to be set for every transaction when (remaining bits of) amt is 0 on a card w/ a pass + // Hence, using f's LSB as method for inferring fare v. refill + // + // Remaining unknown bits: + // — f & 0b100; seems to be set on fares where the card has a pass, and amt is 0 + // — f & 0b010 + // — amt & 1; does not seem to correspond with card type, last transaction, first transaction, refill v. fare, etc + + const DateTime date = date_parse(data, sector, block, byte); + const uint16_t gate = pos_to_num(data, sector, block, byte + 3, 2) >> 3; + const uint8_t g_flag = pos_to_num(data, sector, block, byte + 3, 2) & 0b111; + const Money fare = money_parse(data, sector, block, byte + 5); + const uint16_t f_flag = pos_to_num(data, sector, block, byte + 5, 2) & 0x8001; + return (Transaction){date, gate, g_flag, fare, f_flag}; +} + +// ********************************************************** +// ******************* SECTOR PARSING *********************** +// ********************************************************** + +static uint32_t mfg_sector_parse(const MfClassicData* data) { + // Manufacturer data (Sector 0) + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+ + // 0x000 | UID | rc | 88 04 00 C8 | uk | 00 20 00 00 00 | uk | + // +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+ + // 0x010 | 4E 0F 04 10 04 10 04 10 04 10 04 10 04 10 04 10 | + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+ + // 0x020 | ... 00 00 ... | + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+ + // + // rc := "redundancy check" (lrc / bcc) + // uk := "unknown" + + size_t uid_len = 0; + const uint8_t* uid = mf_classic_get_uid(data, &uid_len); + const uint32_t card_number = bit_lib_bytes_to_num_be(uid, 4); + + return card_number; +} + +static CounterSector counter_sector_parse(const MfClassicData* data) { + // Trip/transaction counters (Sector 1) + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+ + // 0x040 | 04 10 23 45 66 77 ... 00 00 ... | + // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ + // 0x050 | uses1 | uk | ... 00 00 ... | + // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ + // 0x060 | uses2 | uk | ... 00 00 ... | + // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ + // + // uk := "unknown"; if nonzero, seems to only occupy the first 4 bits (ie, uk & 0xF0 == uk), + // with the remaining 4 zero + + // Card has two sectors (2 & 3) containing balance data, with two + // corresponding trip counters in 0x50:0x51 & 0x60:0x61 (sector 1, byte 0:1 of blocks 1 & 2). + + // The *lower* of the two values *minus one* is the true use count, + // and corresponds to the active balance sector, + // (0x50 counter lower -> sector 2 active, 0x60 counter lower -> 3 active) + // per DEFCON31 researcher's findings + + const uint16_t n_uses1 = pos_to_num(data, 1, 1, 0, 2); + const uint16_t n_uses2 = pos_to_num(data, 1, 2, 0, 2); + + const bool is_sec2_active = n_uses1 <= n_uses2; + const uint8_t active_sector = is_sec2_active ? 2 : 3; + const uint16_t n_uses = (is_sec2_active ? n_uses1 : n_uses2) - 1; + + return (CounterSector){n_uses, active_sector}; +} + +static BalanceSector balance_sector_parse(const MfClassicData* data, uint8_t active_sector) { + // Balance & misc card info (Sector 2 or 3) + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----+----.----.----+----.----+----.----.----+----.----+----.----+----+----.----+ + // 0x080 | 11 | date last | loc last| date issued | 65 00 | unknown | 00 | crc | 0x0C0 + // +----+----.----.----+----+----+----+----+----+----.----+----.----+----+----.----+ + // 0x090 | type |end validity| uk | balance | 00 | unknown | crc | 0x0D0 + // +----.----.----.----+----+----.----+----+----.----.----.----.----.----+----.----+ + // 0x0A0 | 20 ... 00 00 ... 04 | crc | 0x0E0 + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + // + // "Active" balance sector alternates between 2 and 3 + // Last trip/transaction info in balance sector ("date last" & "loc last") + // is also included in transaction log, hence don't bother to read here + // + // Inactive balance sector represent the transaction N-1 version + // (where active sector represents data from transaction N). + + const DateTime issued = date_parse(data, active_sector, 0, 6); + const DateTime end_validity = end_validity_parse(data, active_sector, 1, 1); + // Card type data stored in the first 10bits of block 1 + // (0x90 or 0xD0 depending on active sector) + // bitshift (2bytes = 16 bits) by 6bits for just first 10bits + const uint16_t type = pos_to_num(data, active_sector, 1, 0, 2) >> 6; + const Money bal = money_parse(data, active_sector, 1, 5); + + return (BalanceSector){bal, type, issued, end_validity}; +} + +static Pass* passes_parse(const MfClassicData* data) { + // Passes, speculative (Sectors 4 &/or 5) + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+ + // 0x100 | pass0/2? | 00 | pass1/3? | 00 | crc | 0x140 + // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+ + // 0x110 | ... 00 00 ... | crc | 0x150 + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + // 0x120 | ... 00 ... 05 | crc | 0x160 + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + // + // WIP. Read in all speculative passes into array + // 4 separate fields? active vs inactive sector for 2 passes? + // something else entirely? + + Pass* passes = malloc(sizeof(Pass) * CHARLIE_N_PASSES); + + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + passes[i] = pass_parse(data, 4 + (i / 2), 0, (i % 2) * 7); + } + + return passes; +} + +static Transaction* transactions_parse(const MfClassicData* data) { + // Transaction history (Sectors 6–7) + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // 0x180 | transaction0 | transaction1 | crc | + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // ... ... ... ... + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // 0x1D0 | transaction8 | transaction9 | crc | + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // 0x1E0 | ... 00 00 ... | crc | + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + // + // Transactions are not sorted, rather, appear to get overwritten + // sequentially. (eg, sorted modulo array rotation) + + Transaction* transactions = malloc(sizeof(Transaction) * CHARLIE_N_TRANSACTION_HISTORY); + + // Parse each transaction field using some modular math magic to get the offsets: + // move from sector 6 -> 7 after the first 6 transactions + // move a block within a given sector every 2 transactions, reset every 3 blocks (as sector has changed) + // alternate between a start byte of 0 and 7 with every iteration + for(size_t i = 0; i < CHARLIE_N_TRANSACTION_HISTORY; i++) { + transactions[i] = transaction_parse(data, 6 + (i / 6), (i / 2) % 3, (i % 2) * 7); + } + + // Iterate through the array to find the maximum (newest) date value + int max_idx = 0; + for(int i = 1; i < CHARLIE_N_TRANSACTION_HISTORY; i++) { + if(dt_ge(transactions[i].date, transactions[max_idx].date)) { + max_idx = i; + } + } + + // Sort by rotating + for(int r = 0; r < (max_idx + 1); r++) { + // Store the first element + Transaction temp = transactions[0]; + // Shift elements to the left + for(int i = 0; i < CHARLIE_N_TRANSACTION_HISTORY - 1; i++) { + transactions[i] = transactions[i + 1]; + } + // Move the first element to the last + transactions[CHARLIE_N_TRANSACTION_HISTORY - 1] = temp; + } + + // Reverse order, such that newest is first, oldest last + for(int i = 0; i < CHARLIE_N_TRANSACTION_HISTORY / 2; i++) { + // Swap elements at index i and size - i - 1 + Transaction temp = transactions[i]; + transactions[i] = transactions[CHARLIE_N_TRANSACTION_HISTORY - i - 1]; + transactions[CHARLIE_N_TRANSACTION_HISTORY - i - 1] = temp; + } + + return transactions; +} + +/* +static DateTime expiry(DateTime iss) { + // Per Metrodroid CharlieCard parser (https://github.com/metrodroid/metrodroid/blob/master/src/commonMain/kotlin/au/id/micolous/metrodroid/transit/charlie/CharlieCardTransitData.kt) + // Expiry not explicitly stored in card data; rather, calculated from date of issue + // Cards were first issued in 2006, expired in 5 years, w/ no printed expiry date + // Cards issued after 2011 expire in 10 years + // + // Per DEFCON31 researcher's work (cited above): + // Student cards last one school year and expire at the end of August the following year + // Pre-2011 issued cards expire in 7 years, not 5 as claimed by Metrodroid + // Post-2011 expire in 10 years, less one day + // Redundant function given the existance of the end validity field? + // Any important distinctions between the two? + + + // perhaps additionally clipping to 2030-12-__ in anticipation of upcoming system migration? + // need to get a new card to confirm. + + // TODO add card type logic for student card expiry + DateTime exp; + if(iss.year < 2011) { + // add 7 years; assumes average year of 8766 hrs (to account for leap years) + // may be off by a few hours as a result + exp = dt_delta(iss, 7 * 8766 * 60 * 60); + } else { + // add 10 years, subtract a day. Same assumption as above + exp = dt_delta(iss, ((10 * 8766) - 24) * 60 * 60); + } + + return exp; +} + +static bool expired(DateTime expiry, DateTime last_transaction) { + // if a card has sat unused for >2 years, expired (verify this claim?) + // else expired if current date > expiry date + + uint32_t ts_exp = datetime_datetime_to_timestamp(&expiry); + uint32_t ts_last = datetime_datetime_to_timestamp(&last_transaction); + uint32_t ts_now = time_now(); + + return (ts_exp <= ts_now) | ((ts_now - ts_last) >= (2 * 365 * 24 * 60 * 60)); +} +*/ + +// ********************************************************** +// ****************** STRING FORMATTING ********************* +// ********************************************************** + +void locale_format_dt_cat(FuriString* out, const DateTime* dt) { + // helper to print datetimes + FuriString* s = furi_string_alloc(); + + LocaleDateFormat date_format = locale_get_date_format(); + const char* separator = (date_format == LocaleDateFormatDMY) ? "." : "/"; + locale_format_date(s, dt, date_format, separator); + furi_string_cat(out, s); + locale_format_time(s, dt, locale_get_time_format(), false); + furi_string_cat_printf(out, " "); + furi_string_cat(out, s); + + furi_string_free(s); +} + +void type_format_cat(FuriString* out, uint16_t type) { + const char* s; + if(!get_map_item(type, charliecard_types, kNumTypes, &s)) { + s = ""; + furi_string_cat_printf(out, "Unknown-%u", type); + } + + furi_string_cat_str(out, s); +} + +void pass_format_cat(FuriString* out, Pass pass) { + furi_string_cat_printf(out, "\n-Pre: %b", pass.pre); + // type_format_cat(out, pass.type); + furi_string_cat_printf(out, "\n-Post: "); + type_format_cat(out, pass.post); + // locale_format_dt_cat(out, &pass.start); + furi_string_cat_printf(out, "\n-Date: "); + locale_format_dt_cat(out, &pass.date); +} + +void passes_format_cat(FuriString* out, Pass* passes) { + // only print passes if DEBUG on + if(!is_debug()) { + return; + } + + // only print if there is at least 1 valid pass to print + bool any_valid = false; + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + any_valid |= passes[i].valid; + } + if(!any_valid) { + return; + } + + furi_string_cat_printf(out, "\nPasses (DEBUG / WIP):"); + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + if(passes[i].valid) { + furi_string_cat_printf(out, "\nPass %u", i + 1); + pass_format_cat(out, passes[i]); + furi_string_cat_printf(out, "\n"); + } + } +} + +void money_format_cat(FuriString* out, Money money) { + furi_string_cat_printf(out, "$%u.%02u", money.dollars, money.cents); +} + +void transaction_format_cat(FuriString* out, Transaction transaction) { + const char* sep = " "; + const char* sta; + + locale_format_dt_cat(out, &transaction.date); + furi_string_cat_printf(out, "\n%s", !!(transaction.g_flag & 0x1) ? "-" : "+"); + money_format_cat(out, transaction.fare); + if(!!(transaction.g_flag & 0x1) && (transaction.fare.dollars == FARE_BUS.dollars) && + (transaction.fare.cents == FARE_BUS.cents)) { + // if not a refill, and the fare amount is equal to bus fare (any better approach? flag bits for modality?) + // format for bus — supposedly some correlation between gate ID & bus #, haven't investigated + furi_string_cat_printf(out, "%s#%u", sep, transaction.gate); + } else if(get_map_item(transaction.gate, charliecard_fare_gate_ids, kNumFareGateIds, &sta)) { + // station found in fare gate ID map, append station name + furi_string_cat_str(out, sep); + furi_string_cat_str(out, sta); + } else { + // no found station in fare gate ID map & not a bus, just print ID w/o add'l info + furi_string_cat_printf(out, "%s#%u", sep, transaction.gate); + } + // print flags for debugging purposes + if(is_debug()) { + furi_string_cat_printf(out, "%s%x%s%x", sep, transaction.g_flag, sep, transaction.f_flag); + } +} + +void transactions_format_cat(FuriString* out, Transaction* transactions) { + furi_string_cat_printf(out, "\nTransactions:"); + for(size_t i = 0; i < CHARLIE_N_TRANSACTION_HISTORY; i++) { + furi_string_cat_printf(out, "\n"); + transaction_format_cat(out, transactions[i]); + furi_string_cat_printf(out, "\n"); + } +} + +// ********************************************************** +// **************** NFC PLUGIN BOILERPLATE ****************** +// ********************************************************** + +static bool charliecard_parse(FuriString* parsed_data, const MfClassicData* data) { + bool parsed = false; + + do { + // parse card data + const uint32_t card_number = mfg_sector_parse(data); + const CounterSector counter_sector = counter_sector_parse(data); + const BalanceSector balance_sector = + balance_sector_parse(data, counter_sector.active_balance_sector); + Pass* passes = passes_parse(data); + Transaction* transactions = transactions_parse(data); + + // print/append card data + furi_string_cat_printf(parsed_data, "\e#CharlieCard"); + furi_string_cat_printf(parsed_data, "\nSerial: 5-%lu", card_number); + + // Type and balance 0 on some (Perq) cards + // (ie no "main" type / balance / end validity, + // essentially only pass & trip info) + // skip/change formatting for that case? + furi_string_cat_printf(parsed_data, "\nBal: "); + money_format_cat(parsed_data, balance_sector.balance); + + furi_string_cat_printf(parsed_data, "\nType: "); + type_format_cat(parsed_data, balance_sector.type); + + furi_string_cat_printf(parsed_data, "\nTrip Count: %u", counter_sector.n_uses); + + furi_string_cat_printf(parsed_data, "\nIssued: "); + locale_format_dt_cat(parsed_data, &balance_sector.issued); + + if(!dt_eq(balance_sector.end_validity, CHARLIE_EPOCH) & + dt_ge(balance_sector.end_validity, balance_sector.issued)) { + // sometimes (seen on Perq cards) end validity field is all 0 + // When this is the case, calc'd end validity is equal to CHARLIE_EPOCH). + // Only print if not 0, & end validity after issuance date + furi_string_cat_printf(parsed_data, "\nExpiry: "); + locale_format_dt_cat(parsed_data, &balance_sector.end_validity); + } + + // const DateTime last = date_parse(data, active_sector, 0, 1); + // furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No"); + + transactions_format_cat(parsed_data, transactions); + free(transactions); + + passes_format_cat(parsed_data, passes); + free(passes); + + parsed = true; + } while(false); + + return parsed; +} + +void metroflip_charliecard_widget_callback(GuiButtonType result, InputType type, void* context) { + Metroflip* app = context; + UNUSED(result); + + if(type == InputTypeShort) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + } +} + +static NfcCommand + metroflip_scene_charlicard_poller_callback(NfcGenericEvent event, void* context) { + furi_assert(context); + furi_assert(event.event_data); + furi_assert(event.protocol == NfcProtocolMfClassic); + + NfcCommand command = NfcCommandContinue; + const MfClassicPollerEvent* mfc_event = event.event_data; + Metroflip* app = context; + + if(mfc_event->type == MfClassicPollerEventTypeCardDetected) { + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardDetected); + command = NfcCommandContinue; + } else if(mfc_event->type == MfClassicPollerEventTypeCardLost) { + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardLost); + app->sec_num = 0; + command = NfcCommandStop; + } else if(mfc_event->type == MfClassicPollerEventTypeRequestMode) { + mfc_event->data->poller_mode.mode = MfClassicPollerModeRead; + + } else if(mfc_event->type == MfClassicPollerEventTypeRequestReadSector) { + MfClassicKey key = {0}; + bit_lib_num_to_bytes_be(charliecard_1k_keys[app->sec_num].a, COUNT_OF(key.data), key.data); + + MfClassicKeyType key_type = MfClassicKeyTypeA; + mfc_event->data->read_sector_request_data.sector_num = app->sec_num; + mfc_event->data->read_sector_request_data.key = key; + mfc_event->data->read_sector_request_data.key_type = key_type; + mfc_event->data->read_sector_request_data.key_provided = true; + if(app->sec_num == 16) { + mfc_event->data->read_sector_request_data.key_provided = false; + app->sec_num = 0; + } + app->sec_num++; + } else if(mfc_event->type == MfClassicPollerEventTypeSuccess) { + nfc_device_set_data( + app->nfc_device, NfcProtocolMfClassic, nfc_poller_get_data(app->poller)); + const MfClassicData* mfc_data = nfc_device_get_data(app->nfc_device, NfcProtocolMfClassic); + FuriString* parsed_data = furi_string_alloc(); + Widget* widget = app->widget; + + dolphin_deed(DolphinDeedNfcReadSuccess); + furi_string_reset(app->text_box_store); + charliecard_parse(parsed_data, mfc_data); + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_charliecard_widget_callback, app); + + furi_string_free(parsed_data); + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); + command = NfcCommandStop; + metroflip_app_blink_stop(app); + } else if(mfc_event->type == MfClassicPollerEventTypeFail) { + FURI_LOG_I(TAG, "fail"); + command = NfcCommandStop; + } + + return command; +} + +void metroflip_scene_charliecard_on_enter(void* context) { + Metroflip* app = context; + dolphin_deed(DolphinDeedNfcRead); + + app->sec_num = 0; + + // Setup view + Popup* popup = app->popup; + popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop); + popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61); + + // Start worker + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup); + nfc_scanner_alloc(app->nfc); + app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic); + nfc_poller_start(app->poller, metroflip_scene_charlicard_poller_callback, app); + + metroflip_app_blink_start(app); +} + +bool metroflip_scene_charliecard_on_event(void* context, SceneManagerEvent event) { + Metroflip* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == MetroflipCustomEventCardDetected) { + Popup* popup = app->popup; + popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventCardLost) { + Popup* popup = app->popup; + popup_set_header(popup, "Card \n lost", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventWrongCard) { + Popup* popup = app->popup; + popup_set_header(popup, "WRONG \n CARD", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventPollerFail) { + Popup* popup = app->popup; + popup_set_header(popup, "Failed", 68, 30, AlignLeft, AlignTop); + consumed = true; + } + } else if(event.type == SceneManagerEventTypeBack) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + consumed = true; + } + + return consumed; +} + +void metroflip_scene_charliecard_on_exit(void* context) { + Metroflip* app = context; + widget_reset(app->widget); + + if(app->poller) { + nfc_poller_stop(app->poller); + nfc_poller_free(app->poller); + } + + // Clear view + popup_reset(app->popup); + + metroflip_app_blink_stop(app); +} diff --git a/metroflip/scenes/metroflip_scene_config.h b/metroflip/scenes/metroflip_scene_config.h new file mode 100644 index 000000000..f304dc9ff --- /dev/null +++ b/metroflip/scenes/metroflip_scene_config.h @@ -0,0 +1,8 @@ +ADD_SCENE(metroflip, start, Start) +ADD_SCENE(metroflip, ravkav, RavKav) +ADD_SCENE(metroflip, charliecard, CharlieCard) +ADD_SCENE(metroflip, metromoney, Metromoney) +ADD_SCENE(metroflip, read_success, ReadSuccess) +ADD_SCENE(metroflip, bip, Bip) +ADD_SCENE(metroflip, about, About) +ADD_SCENE(metroflip, credits, Credits) diff --git a/metroflip/scenes/metroflip_scene_credits.c b/metroflip/scenes/metroflip_scene_credits.c new file mode 100644 index 000000000..41b9f4f95 --- /dev/null +++ b/metroflip/scenes/metroflip_scene_credits.c @@ -0,0 +1,62 @@ +#include "../metroflip_i.h" +#include + +#define TAG "Metroflip:Scene:Credits" + +void metroflip_credits_widget_callback(GuiButtonType result, InputType type, void* context) { + Metroflip* app = context; + UNUSED(result); + + if(type == InputTypeShort) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + } +} + +void metroflip_scene_credits_on_enter(void* context) { + Metroflip* app = context; + Widget* widget = app->widget; + + dolphin_deed(DolphinDeedNfcReadSuccess); + furi_string_reset(app->text_box_store); + + FuriString* str = furi_string_alloc(); + + furi_string_printf(str, "\e#Credits:\n\n"); + furi_string_cat_printf(str, "Created by luu176\n"); + furi_string_cat_printf(str, "Inspired by Metrodroid\n\n"); + furi_string_cat_printf(str, "\e#Parser Credits:\n\n"); + furi_string_cat_printf(str, "Rav-Kav Parser: luu176\n\n"); + furi_string_cat_printf(str, "Metromoney Parser:\n Leptopt1los\n\n"); + furi_string_cat_printf(str, "Bip! Parser:\n rbasoalto, gornekich\n\n"); + furi_string_cat_printf(str, "CharlieCard Parser:\n zacharyweiss\n\n"); + furi_string_cat_printf(str, "Info Slave: equip\n\n"); + + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(str)); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_credits_widget_callback, app); + + furi_string_free(str); + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); +} + +bool metroflip_scene_credits_on_event(void* context, SceneManagerEvent event) { + Metroflip* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_previous_scene(app->scene_manager); + } + } else if(event.type == SceneManagerEventTypeBack) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + consumed = true; + } + return consumed; +} + +void metroflip_scene_credits_on_exit(void* context) { + Metroflip* app = context; + widget_reset(app->widget); + UNUSED(context); +} diff --git a/metroflip/scenes/metroflip_scene_metromoney.c b/metroflip/scenes/metroflip_scene_metromoney.c new file mode 100644 index 000000000..03d1317a8 --- /dev/null +++ b/metroflip/scenes/metroflip_scene_metromoney.c @@ -0,0 +1,217 @@ +/* + * Parser for Metromoney card (Georgia). + * + * Copyright 2023 Leptoptilos + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include "../metroflip_i.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define TAG "Metroflip:Scene:Metromoney" + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +static const MfClassicKeyPair metromoney_1k_keys[] = { + {.a = 0x2803BCB0C7E1, .b = 0x4FA9EB49F75E}, + {.a = 0x9C616585E26D, .b = 0xD1C71E590D16}, + {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C}, + {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C}, + {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C}, + {.a = 0x9C616585E26D, .b = 0xA160FCD5EC4C}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0x112233445566, .b = 0x361A62F35BC9}, + {.a = 0x112233445566, .b = 0x361A62F35BC9}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, +}; + +static bool metromoney_parse(const NfcDevice* device, const MfClassicData* data, Metroflip* app) { + furi_assert(device); + + bool parsed = false; + + do { + // Verify key + const uint8_t ticket_sector_number = 1; + const uint8_t ticket_block_number = 1; + + const MfClassicSectorTrailer* sec_tr = + mf_classic_get_sector_trailer_by_sector(data, ticket_sector_number); + + const uint64_t key = + bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data)); + if(key != metromoney_1k_keys[ticket_sector_number].a) break; + + // Parse data + const uint8_t start_block_num = + mf_classic_get_first_block_num_of_sector(ticket_sector_number); + + const uint8_t* block_start_ptr = + &data->block[start_block_num + ticket_block_number].data[0]; + + uint32_t balance = bit_lib_bytes_to_num_le(block_start_ptr, 4) - 100; + + uint32_t balance_lari = balance / 100; + uint8_t balance_tetri = balance % 100; + + size_t uid_len = 0; + const uint8_t* uid = mf_classic_get_uid(data, &uid_len); + uint32_t card_number = bit_lib_bytes_to_num_le(uid, 4); + strncpy(app->card_type, "Metromoney", sizeof(app->card_type)); + app->balance_lari = balance_lari; + app->balance_tetri = balance_tetri; + app->card_number = card_number; + parsed = true; + } while(false); + + return parsed; +} + +static NfcCommand + metroflip_scene_metromoney_poller_callback(NfcGenericEvent event, void* context) { + furi_assert(context); + furi_assert(event.event_data); + furi_assert(event.protocol == NfcProtocolMfClassic); + + NfcCommand command = NfcCommandContinue; + const MfClassicPollerEvent* mfc_event = event.event_data; + Metroflip* app = context; + + if(mfc_event->type == MfClassicPollerEventTypeCardDetected) { + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardDetected); + command = NfcCommandContinue; + } else if(mfc_event->type == MfClassicPollerEventTypeCardLost) { + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardLost); + app->sec_num = 0; + command = NfcCommandStop; + } else if(mfc_event->type == MfClassicPollerEventTypeRequestMode) { + mfc_event->data->poller_mode.mode = MfClassicPollerModeRead; + + } else if(mfc_event->type == MfClassicPollerEventTypeRequestReadSector) { + MfClassicKey key = {0}; + bit_lib_num_to_bytes_be(metromoney_1k_keys[app->sec_num].a, COUNT_OF(key.data), key.data); + + MfClassicKeyType key_type = MfClassicKeyTypeA; + mfc_event->data->read_sector_request_data.sector_num = app->sec_num; + mfc_event->data->read_sector_request_data.key = key; + mfc_event->data->read_sector_request_data.key_type = key_type; + mfc_event->data->read_sector_request_data.key_provided = true; + if(app->sec_num == 16) { + mfc_event->data->read_sector_request_data.key_provided = false; + app->sec_num = 0; + } + app->sec_num++; + } else if(mfc_event->type == MfClassicPollerEventTypeSuccess) { + nfc_device_set_data( + app->nfc_device, NfcProtocolMfClassic, nfc_poller_get_data(app->poller)); + const MfClassicData* mfc_data = nfc_device_get_data(app->nfc_device, NfcProtocolMfClassic); + metromoney_parse(app->nfc_device, mfc_data, app); + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess); + command = NfcCommandStop; + metroflip_app_blink_stop(app); + } else if(mfc_event->type == MfClassicPollerEventTypeFail) { + FURI_LOG_I(TAG, "fail"); + command = NfcCommandStop; + } + + return command; +} + +void metroflip_scene_metromoney_on_enter(void* context) { + Metroflip* app = context; + dolphin_deed(DolphinDeedNfcRead); + + app->sec_num = 0; + + // Setup view + Popup* popup = app->popup; + popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop); + popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61); + + // Start worker + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup); + nfc_scanner_alloc(app->nfc); + app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic); + nfc_poller_start(app->poller, metroflip_scene_metromoney_poller_callback, app); + + metroflip_app_blink_start(app); +} + +bool metroflip_scene_metromoney_on_event(void* context, SceneManagerEvent event) { + Metroflip* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == MetroflipCustomEventCardDetected) { + Popup* popup = app->popup; + popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventCardLost) { + Popup* popup = app->popup; + popup_set_header(popup, "Card \n lost", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventWrongCard) { + Popup* popup = app->popup; + popup_set_header(popup, "WRONG \n CARD", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventPollerFail) { + Popup* popup = app->popup; + popup_set_header(popup, "Failed", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventPollerSuccess) { + scene_manager_next_scene(app->scene_manager, MetroflipSceneReadSuccess); + consumed = true; + } + } else if(event.type == SceneManagerEventTypeBack) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + consumed = true; + } + + return consumed; +} + +void metroflip_scene_metromoney_on_exit(void* context) { + Metroflip* app = context; + widget_reset(app->widget); + + if(app->poller) { + nfc_poller_stop(app->poller); + nfc_poller_free(app->poller); + } + + // Clear view + popup_reset(app->popup); + + metroflip_app_blink_stop(app); +} diff --git a/metroflip/scenes/metroflip_scene_ravkav.c b/metroflip/scenes/metroflip_scene_ravkav.c new file mode 100644 index 000000000..3e8ea9ffb --- /dev/null +++ b/metroflip/scenes/metroflip_scene_ravkav.c @@ -0,0 +1,191 @@ +#include "../metroflip_i.h" + +#include + +#include + +#define Metroflip_POLLER_MAX_BUFFER_SIZE 1024 + +#define TAG "Metroflip:Scene:RavKav" + +uint8_t apdu_success[] = {0x90, 0x00}; +uint8_t apdu_file_not_found[] = {0x6a, 0x82}; + +// balance +uint8_t select_balance_file[] = {0x94, 0xA4, 0x00, 0x00, 0x02, 0x20, 0x2A, 0x00}; +uint8_t read_balance[] = {0x94, 0xb2, 0x01, 0x04, 0x1D}; + +static NfcCommand metroflip_scene_ravkav_poller_callback(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolIso14443_4b); + NfcCommand next_command = NfcCommandContinue; + MetroflipPollerEventType stage = MetroflipPollerEventTypeStart; + + Metroflip* app = context; + + const Iso14443_4bPollerEvent* iso14443_4b_event = event.event_data; + + Iso14443_4bPoller* iso14443_4b_poller = event.instance; + + BitBuffer* tx_buffer = bit_buffer_alloc(Metroflip_POLLER_MAX_BUFFER_SIZE); + BitBuffer* rx_buffer = bit_buffer_alloc(Metroflip_POLLER_MAX_BUFFER_SIZE); + + if(iso14443_4b_event->type == Iso14443_4bPollerEventTypeReady) { + if(stage == MetroflipPollerEventTypeStart) { + nfc_device_set_data( + app->nfc_device, NfcProtocolIso14443_4b, nfc_poller_get_data(app->poller)); + + Iso14443_4bError error; + size_t response_length = 0; + + do { + // Select file of balance + bit_buffer_append_bytes( + tx_buffer, select_balance_file, sizeof(select_balance_file)); + error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer); + if(error != Iso14443_4bErrorNone) { + FURI_LOG_I(TAG, "Select File: iso14443_4b_poller_send_block error %d", error); + stage = MetroflipPollerEventTypeFail; + view_dispatcher_send_custom_event( + app->view_dispatcher, MetroflipCustomEventPollerFail); + break; + } + + // Check the response after selecting file + response_length = bit_buffer_get_size_bytes(rx_buffer); + if(bit_buffer_get_byte(rx_buffer, response_length - 2) != apdu_success[0] || + bit_buffer_get_byte(rx_buffer, response_length - 1) != apdu_success[1]) { + FURI_LOG_I( + TAG, + "Select file failed: %02x%02x", + bit_buffer_get_byte(rx_buffer, response_length - 2), + bit_buffer_get_byte(rx_buffer, response_length - 1)); + stage = MetroflipPollerEventTypeFail; + view_dispatcher_send_custom_event( + app->view_dispatcher, MetroflipCustomEventPollerFileNotFound); + break; + } + + // Now send the read command + bit_buffer_reset(tx_buffer); + bit_buffer_append_bytes(tx_buffer, read_balance, sizeof(read_balance)); + error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer); + if(error != Iso14443_4bErrorNone) { + FURI_LOG_I(TAG, "Read File: iso14443_4b_poller_send_block error %d", error); + stage = MetroflipPollerEventTypeFail; + view_dispatcher_send_custom_event( + app->view_dispatcher, MetroflipCustomEventPollerFail); + break; + } + + // Check the response after reading the file + response_length = bit_buffer_get_size_bytes(rx_buffer); + if(bit_buffer_get_byte(rx_buffer, response_length - 2) != apdu_success[0] || + bit_buffer_get_byte(rx_buffer, response_length - 1) != apdu_success[1]) { + FURI_LOG_I( + TAG, + "Read file failed: %02x%02x", + bit_buffer_get_byte(rx_buffer, response_length - 2), + bit_buffer_get_byte(rx_buffer, response_length - 1)); + stage = MetroflipPollerEventTypeFail; + view_dispatcher_send_custom_event( + app->view_dispatcher, MetroflipCustomEventPollerFileNotFound); + break; + } + + // Process the response data + if(response_length < 3) { + FURI_LOG_I(TAG, "Response too short: %d bytes", response_length); + stage = MetroflipPollerEventTypeFail; + view_dispatcher_send_custom_event( + app->view_dispatcher, MetroflipCustomEventPollerFail); + break; + } + + uint32_t value = 0; + for(uint8_t i = 0; i < 3; i++) { + value = (value << 8) | bit_buffer_get_byte(rx_buffer, i); + } + + float result = value / 100.0f; + app->value = result; + strcpy(app->currency, "ILS"); + FURI_LOG_I(TAG, "Value: %.2f %s", (double)app->value, app->currency); + + strncpy(app->card_type, "Rav-Kav", sizeof(app->card_type)); + + // Send success event + view_dispatcher_send_custom_event( + app->view_dispatcher, MetroflipCustomEventPollerSuccess); + + stage = MetroflipPollerEventTypeSuccess; + next_command = NfcCommandStop; + } while(false); + + if(stage != MetroflipPollerEventTypeSuccess) { + next_command = NfcCommandStop; + } + } + } + bit_buffer_free(tx_buffer); + bit_buffer_free(rx_buffer); + + return next_command; +} + +void metroflip_scene_ravkav_on_enter(void* context) { + Metroflip* app = context; + dolphin_deed(DolphinDeedNfcRead); + + // Setup view + Popup* popup = app->popup; + popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop); + popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61); + + // Start worker + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup); + nfc_scanner_alloc(app->nfc); + app->poller = nfc_poller_alloc(app->nfc, NfcProtocolIso14443_4b); + nfc_poller_start(app->poller, metroflip_scene_ravkav_poller_callback, app); + + metroflip_app_blink_start(app); +} + +bool metroflip_scene_ravkav_on_event(void* context, SceneManagerEvent event) { + Metroflip* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == MetroflipCustomEventPollerSuccess) { + scene_manager_next_scene(app->scene_manager, MetroflipSceneReadSuccess); + consumed = true; + } else if(event.event == MetroflipPollerEventTypeCardDetect) { + Popup* popup = app->popup; + popup_set_header(popup, "Scanning..", 68, 30, AlignLeft, AlignTop); + consumed = true; + } else if(event.event == MetroflipCustomEventPollerFileNotFound) { + Popup* popup = app->popup; + popup_set_header(popup, "No\nRecord\nFile", 68, 30, AlignLeft, AlignTop); + consumed = true; + } + } else if(event.type == SceneManagerEventTypeBack) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + consumed = true; + } + + return consumed; +} + +void metroflip_scene_ravkav_on_exit(void* context) { + Metroflip* app = context; + + if(app->poller) { + nfc_poller_stop(app->poller); + nfc_poller_free(app->poller); + } + metroflip_app_blink_stop(app); + + // Clear view + popup_reset(app->popup); + + //metroflip_app_blink_stop(app); +} diff --git a/metroflip/scenes/metroflip_scene_read_success.c b/metroflip/scenes/metroflip_scene_read_success.c new file mode 100644 index 000000000..afd7e72c5 --- /dev/null +++ b/metroflip/scenes/metroflip_scene_read_success.c @@ -0,0 +1,70 @@ +#include "../metroflip_i.h" +#include + +#define TAG "Metroflip:Scene:ReadSuccess" + +void metroflip_success_widget_callback(GuiButtonType result, InputType type, void* context) { + Metroflip* app = context; + UNUSED(result); + + if(type == InputTypeShort) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + } +} + +void metroflip_scene_read_success_on_enter(void* context) { + Metroflip* app = context; + Widget* widget = app->widget; + + dolphin_deed(DolphinDeedNfcReadSuccess); + furi_string_reset(app->text_box_store); + + FuriString* str = furi_string_alloc(); + + if(strcmp(app->card_type, "Rav-Kav") == 0) { + FURI_LOG_I(TAG, "Rav Kav card detected"); + furi_string_printf(str, "\e#Rav-Kav:\n"); + furi_string_cat_printf(str, "Balance: %.2f\n", (double)app->value); + furi_string_cat_printf(str, "Currency: %s\n", app->currency); + } else if(strcmp(app->card_type, "Metromoney") == 0) { + FURI_LOG_I(TAG, "Metromoney card detected"); + furi_string_printf( + str, + "\e#Metromoney\nCard number: %lu\nBalance: %lu.%02u GEL", + app->card_number, + app->balance_lari, + app->balance_tetri); + } else { + FURI_LOG_I(TAG, "Unknown card type"); + furi_string_printf(str, "\e#Unknown card\n"); + } + + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(str)); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_success_widget_callback, app); + + furi_string_free(str); + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); +} + +bool metroflip_scene_read_success_on_event(void* context, SceneManagerEvent event) { + Metroflip* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_previous_scene(app->scene_manager); + } + } else if(event.type == SceneManagerEventTypeBack) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + consumed = true; + } + return consumed; +} + +void metroflip_scene_read_success_on_exit(void* context) { + Metroflip* app = context; + widget_reset(app->widget); + UNUSED(context); +} diff --git a/metroflip/scenes/metroflip_scene_start.c b/metroflip/scenes/metroflip_scene_start.c new file mode 100644 index 000000000..d4b127a34 --- /dev/null +++ b/metroflip/scenes/metroflip_scene_start.c @@ -0,0 +1,62 @@ +#include "../metroflip_i.h" + +void metroflip_scene_start_submenu_callback(void* context, uint32_t index) { + Metroflip* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void metroflip_scene_start_on_enter(void* context) { + Metroflip* app = context; + Submenu* submenu = app->submenu; + + submenu_set_header(submenu, "Metroflip"); + + submenu_add_item( + submenu, "Rav-Kav", MetroflipSceneRavKav, metroflip_scene_start_submenu_callback, app); + + submenu_add_item( + submenu, + "CharlieCard", + MetroflipSceneCharlieCard, + metroflip_scene_start_submenu_callback, + app); + + submenu_add_item( + submenu, + "Metromoney", + MetroflipSceneMetromoney, + metroflip_scene_start_submenu_callback, + app); + + submenu_add_item( + submenu, "Bip!", MetroflipSceneBip, metroflip_scene_start_submenu_callback, app); + + submenu_add_item( + submenu, "About", MetroflipSceneAbout, metroflip_scene_start_submenu_callback, app); + + submenu_add_item( + submenu, "Credits", MetroflipSceneCredits, metroflip_scene_start_submenu_callback, app); + + submenu_set_selected_item( + submenu, scene_manager_get_scene_state(app->scene_manager, MetroflipSceneStart)); + + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewSubmenu); +} + +bool metroflip_scene_start_on_event(void* context, SceneManagerEvent event) { + Metroflip* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, event.event); + consumed = true; + scene_manager_next_scene(app->scene_manager, event.event); + } + + return consumed; +} + +void metroflip_scene_start_on_exit(void* context) { + Metroflip* app = context; + submenu_reset(app->submenu); +} diff --git a/metroflip/screenshots/App.png b/metroflip/screenshots/App.png new file mode 100644 index 000000000..ac0973012 Binary files /dev/null and b/metroflip/screenshots/App.png differ diff --git a/metroflip/screenshots/Menu-Middle.png b/metroflip/screenshots/Menu-Middle.png new file mode 100644 index 000000000..a9106392f Binary files /dev/null and b/metroflip/screenshots/Menu-Middle.png differ diff --git a/metroflip/screenshots/Menu-Top.png b/metroflip/screenshots/Menu-Top.png new file mode 100644 index 000000000..5cf2e932b Binary files /dev/null and b/metroflip/screenshots/Menu-Top.png differ diff --git a/metroflip/screenshots/Rav-Kav.png b/metroflip/screenshots/Rav-Kav.png new file mode 100644 index 000000000..c97a0f80b Binary files /dev/null and b/metroflip/screenshots/Rav-Kav.png differ diff --git a/mp_flipper/.gitsubtree b/mp_flipper/.gitsubtree index 72b91eb54..fad379fbc 100644 --- a/mp_flipper/.gitsubtree +++ b/mp_flipper/.gitsubtree @@ -1,2 +1,2 @@ -https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/mp_flipper 73d394b22988de08bc984a161819f87bca90c7c5 +https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/mp_flipper 17bec3e26b8250c59acebd4fa52b6b08f68152d7 https://github.com/ofabel/mp-flipper master / diff --git a/mp_flipper/CHANGELOG.md b/mp_flipper/CHANGELOG.md index 93f350c88..d250fbdc5 100644 --- a/mp_flipper/CHANGELOG.md +++ b/mp_flipper/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.6.0] - 2024-11-17 + +### Added + +* Enabled extra functions for the `random` module. + ## [1.5.0] - 2024-10-06 ### Added @@ -156,7 +162,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Basic build setup * Minimal working example -[Unreleased]: https://github.com/ofabel/mp-flipper/compare/v1.5.0...dev +[Unreleased]: https://github.com/ofabel/mp-flipper/compare/v1.6.0...dev +[1.6.0]: https://github.com/ofabel/mp-flipper/compare/v1.5.0...v1.6.0 [1.5.0]: https://github.com/ofabel/mp-flipper/compare/v1.4.0...v1.5.0 [1.4.0]: https://github.com/ofabel/mp-flipper/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/ofabel/mp-flipper/compare/v1.2.0...v1.3.0 diff --git a/mp_flipper/application.fam b/mp_flipper/application.fam index 77033b79e..0a1b4a4c2 100644 --- a/mp_flipper/application.fam +++ b/mp_flipper/application.fam @@ -5,7 +5,7 @@ App( entry_point="upython", stack_size=4 * 1024, fap_category="Tools", - fap_version="1.5", + fap_version="1.6", fap_description="Compile and execute MicroPython scripts", fap_icon="icon.png", fap_icon_assets="images", diff --git a/mp_flipper/docs/pages/conf.py b/mp_flipper/docs/pages/conf.py index 0a6611904..63a2dba98 100644 --- a/mp_flipper/docs/pages/conf.py +++ b/mp_flipper/docs/pages/conf.py @@ -25,6 +25,11 @@ def copy_dict(source, target): copy_dict(flipperzero.io, io) +import flipperzero.random +import random + +copy_dict(flipperzero.random, random) + now = datetime.datetime.now() project = "uPython" diff --git a/mp_flipper/docs/pages/reference.rst b/mp_flipper/docs/pages/reference.rst index 93ad2aa57..f08753d2c 100644 --- a/mp_flipper/docs/pages/reference.rst +++ b/mp_flipper/docs/pages/reference.rst @@ -452,7 +452,7 @@ Functions I/O --- -Read and write files on the SD card. +Read and write SD card files. Constants ~~~~~~~~~ @@ -475,6 +475,22 @@ Classes .. autoclass:: io.TextFileIO :members: name, read, readline, readlines, readable, writable, write, flush, seek, tell, close, __enter__, __exit__, __del__ +Random +------ + +Generate pseudo-random number for various use cases. + +Functions +~~~~~~~~~ + +.. autofunction:: random.random +.. autofunction:: random.uniform +.. autofunction:: random.randrange +.. autofunction:: random.getrandbits +.. autofunction:: random.randint +.. autofunction:: random.choice +.. autofunction:: random.seed + Built-In -------- diff --git a/mp_flipper/docs/pages/roadmap.md b/mp_flipper/docs/pages/roadmap.md index 7f3efc43f..83eed1f4f 100644 --- a/mp_flipper/docs/pages/roadmap.md +++ b/mp_flipper/docs/pages/roadmap.md @@ -5,16 +5,17 @@ Here you can see what to expect from future releases. ## Next Release -* Improved `print` function. -* UART +* SPI * I2C * Maybe USB HID +_The next release might takes the form of a fork of the official firmware._ +_Check out [this issue](https://github.com/ofabel/mp-flipper/issues/4) for details._ + ## Planned * I2C * NFC -* UART * USB HID * Subghz * Bluetooth diff --git a/mp_flipper/flipperzero/random.py b/mp_flipper/flipperzero/random.py new file mode 100644 index 000000000..19d214905 --- /dev/null +++ b/mp_flipper/flipperzero/random.py @@ -0,0 +1,87 @@ +def random() -> float: + ''' + Get a random float value between 0.0 (inclusive) and 1.0 (exclusive). + + :returns: The random value. + + .. versionadded:: 1.6.0 + ''' + pass + +def randrange(start: int, stop: int, step: int = 1) -> int: + ''' + Get a random integer between ``start`` (inclusive) and ``stop`` + (exclusive) with an optional ``step`` between the values. + + :param start: The start value. + :param stop: The end value. + :param step: The optional step value. + :returns: The random value. + + .. versionadded:: 1.6.0 + + .. hint:: + + This function does only generate integer values. + ''' + pass + +def randint(a: int, b: int) -> int: + ''' + Get a random integer between ``a`` (inclusive) and ``b`` (inclusive). + + :param a: The lower value. + :param b: The upper value. + :returns: The random value. + + .. versionadded:: 1.6.0 + ''' + pass + +def choice[T](seq: list[T]) -> T: + ''' + Get a random element from the provided sequence. + + :param seq: The sequence to use. + :returns: A random element. + + .. versionadded:: 1.6.0 + ''' + pass + +def getrandbits(k: int) -> int: + ''' + Get ``k`` random bits. + + :param k: The number of bits. + :returns: The random bits. + + .. versionadded:: 1.6.0 + ''' + pass + +def uniform(a: float, b: float) -> float: + ''' + Get a random float value between ``a`` (inclusive) and ``b`` (inclusive). + + :param a: The lower value. + :param b: The upper value. + :returns: The random value. + + .. versionadded:: 1.6.0 + ''' + pass + +def seed(a: int) -> None: + ''' + Initialize the random number generator. + + :param a: The initialization value to use. + + .. versionadded:: 1.6.0 + + .. hint:: + + Random generator seeding is done automatically, so there is no need to call this function. + ''' + pass diff --git a/mp_flipper/lib/micropython/.gitsubtree b/mp_flipper/lib/micropython/.gitsubtree index e6093d712..45c3d9f9d 100644 --- a/mp_flipper/lib/micropython/.gitsubtree +++ b/mp_flipper/lib/micropython/.gitsubtree @@ -1 +1 @@ -https://github.com/ofabel/mp-flipper 29091048ae9a613070da21b373062a450f7ecd08 / +https://github.com/ofabel/mp-flipper 242fff05599d97faafab563a827dd86791c0cbee / diff --git a/mp_flipper/lib/micropython/genhdr/mpversion.h b/mp_flipper/lib/micropython/genhdr/mpversion.h index 942420124..c0a76c5d6 100644 --- a/mp_flipper/lib/micropython/genhdr/mpversion.h +++ b/mp_flipper/lib/micropython/genhdr/mpversion.h @@ -1,4 +1,4 @@ // This file was generated by py/makeversionhdr.py #define MICROPY_GIT_TAG "v1.23.0" #define MICROPY_GIT_HASH "a61c446c0" -#define MICROPY_BUILD_DATE "2024-10-06" +#define MICROPY_BUILD_DATE "2024-11-15" diff --git a/mp_flipper/lib/micropython/genhdr/qstrdefs.generated.h b/mp_flipper/lib/micropython/genhdr/qstrdefs.generated.h index 715323321..5601148ed 100644 --- a/mp_flipper/lib/micropython/genhdr/qstrdefs.generated.h +++ b/mp_flipper/lib/micropython/genhdr/qstrdefs.generated.h @@ -389,6 +389,7 @@ QDEF1(MP_QSTR_canvas_text_height, 239, 18, "canvas_text_height") QDEF1(MP_QSTR_canvas_text_width, 86, 17, "canvas_text_width") QDEF1(MP_QSTR_canvas_update, 131, 13, "canvas_update") QDEF1(MP_QSTR_canvas_width, 180, 12, "canvas_width") +QDEF1(MP_QSTR_choice, 46, 6, "choice") QDEF1(MP_QSTR_closure, 116, 7, "closure") QDEF1(MP_QSTR_debug, 212, 5, "debug") QDEF1(MP_QSTR_decode, 169, 6, "decode") @@ -448,7 +449,9 @@ QDEF1(MP_QSTR_on_input, 141, 8, "on_input") QDEF1(MP_QSTR_pwm_is_running, 82, 14, "pwm_is_running") QDEF1(MP_QSTR_pwm_start, 240, 9, "pwm_start") QDEF1(MP_QSTR_pwm_stop, 200, 8, "pwm_stop") +QDEF1(MP_QSTR_randint, 175, 7, "randint") QDEF1(MP_QSTR_random, 190, 6, "random") +QDEF1(MP_QSTR_randrange, 163, 9, "randrange") QDEF1(MP_QSTR_rb, 213, 2, "rb") QDEF1(MP_QSTR_readable, 93, 8, "readable") QDEF1(MP_QSTR_readlines, 106, 9, "readlines") @@ -474,6 +477,7 @@ QDEF1(MP_QSTR_time, 240, 4, "time") QDEF1(MP_QSTR_time_ns, 114, 7, "time_ns") QDEF1(MP_QSTR_trace, 164, 5, "trace") QDEF1(MP_QSTR_uart_open, 220, 9, "uart_open") +QDEF1(MP_QSTR_uniform, 1, 7, "uniform") QDEF1(MP_QSTR_union, 246, 5, "union") QDEF1(MP_QSTR_vibro_set, 216, 9, "vibro_set") QDEF1(MP_QSTR_warn, 175, 4, "warn") diff --git a/mp_flipper/lib/micropython/mpconfigport.h b/mp_flipper/lib/micropython/mpconfigport.h index 30be0a3bf..a8895590b 100644 --- a/mp_flipper/lib/micropython/mpconfigport.h +++ b/mp_flipper/lib/micropython/mpconfigport.h @@ -59,7 +59,7 @@ typedef long mp_off_t; #define MICROPY_PY_TIME_TIME_TIME_NS (1) #define MICROPY_PY_RANDOM (1) -#define MICROPY_PY_RANDOM_EXTRA_FUNCS (0) +#define MICROPY_PY_RANDOM_EXTRA_FUNCS (1) #define MICROPY_PY_RANDOM_SEED_INIT_FUNC (mp_flipper_seed_init()) diff --git a/mp_flipper/pyproject.toml b/mp_flipper/pyproject.toml index 9523ddc54..eb9c3bb3d 100644 --- a/mp_flipper/pyproject.toml +++ b/mp_flipper/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "flipperzero" -version = "1.5.0" +version = "1.6.0" authors = [ { name = "Oliver Fabel" }, ] diff --git a/mtp/src/scenes/mtp/mtp.c b/mtp/src/scenes/mtp/mtp.c index 55481f2ee..ba394e1ca 100644 --- a/mtp/src/scenes/mtp/mtp.c +++ b/mtp/src/scenes/mtp/mtp.c @@ -1099,15 +1099,25 @@ int BuildDeviceInfo(uint8_t* buffer) { void GetStorageIDs(AppMTP* mtp, uint32_t* storage_ids, uint32_t* count) { SDInfo sd_info; FS_Error err = storage_sd_info(mtp->storage, &sd_info); - storage_ids[0] = INTERNAL_STORAGE_ID; + + // Due to filesystem change, we only have external storage. + // storage_ids[0] = INTERNAL_STORAGE_ID; + // // Check if SD card is present + // if(err != FSE_OK) { + // FURI_LOG_E("MTP", "SD Card not found"); + // *count = 1; // We have only one storage + // return; + // } + + // Check if SD card is present if(err != FSE_OK) { FURI_LOG_E("MTP", "SD Card not found"); - *count = 1; // We have only one storage + *count = 0; // No storage. return; } - storage_ids[1] = EXTERNAL_STORAGE_ID; - *count = 2; // We have two storages: internal and external + storage_ids[0] = EXTERNAL_STORAGE_ID; + *count = 1; } int GetStorageInfo(AppMTP* mtp, uint32_t storage_id, uint8_t* buf) { diff --git a/multi_fuzzer/CHANGELOG.md b/multi_fuzzer/CHANGELOG.md index 9d4c8a752..bcbafda1b 100644 --- a/multi_fuzzer/CHANGELOG.md +++ b/multi_fuzzer/CHANGELOG.md @@ -1,3 +1,10 @@ +## v1.6 +New systems in RFID Fuzzer: + - Electra + - Idteck + - Gallagher + - Nexwatch +- Changed with how lists are handled to make adding RFID protocols easier to add ## v1.4 - Fix worker being not in LFRFIDWorkerIdle before next key (limit TD to 0.1) ## v1.3 @@ -36,4 +43,4 @@ | Default Values | + | + | | Load key file | + | + | | Load UIDs from file | + | + | -| BFCustomer ID | - | + | \ No newline at end of file +| BFCustomer ID | - | + | diff --git a/multi_fuzzer/README.md b/multi_fuzzer/README.md index bcdcc9e14..6d1a4cc56 100644 --- a/multi_fuzzer/README.md +++ b/multi_fuzzer/README.md @@ -28,6 +28,10 @@ This is a completely remade app, visual style inspired by [iButton fuzzer](https - Pyramid - Keri - Jablotron +- Electra +- Idteck +- Gallagher +- Nexwatch ## Application Features ### Main screen diff --git a/multi_fuzzer/application.fam b/multi_fuzzer/application.fam index 2263ab5ed..bc110faa2 100644 --- a/multi_fuzzer/application.fam +++ b/multi_fuzzer/application.fam @@ -14,7 +14,7 @@ App( stack_size=2 * 1024, fap_author="gid9798 xMasterX", fap_weburl="https://github.com/DarkFlippers/Multi_Fuzzer", - fap_version="1.5", + fap_version="1.6", targets=["f7"], fap_description="Fuzzer for ibutton readers", fap_icon="ibutt_10px.png", @@ -45,7 +45,7 @@ App( stack_size=2 * 1024, fap_author="gid9798 xMasterX", fap_weburl="https://github.com/DarkFlippers/Multi_Fuzzer", - fap_version="1.5", + fap_version="1.6", targets=["f7"], fap_description="Fuzzer for lfrfid readers", fap_icon="icons/rfid_10px.png", diff --git a/multi_fuzzer/catalog/docs/rfid/Changelog.md b/multi_fuzzer/catalog/docs/rfid/Changelog.md index 532014998..644b4bf3f 100644 --- a/multi_fuzzer/catalog/docs/rfid/Changelog.md +++ b/multi_fuzzer/catalog/docs/rfid/Changelog.md @@ -20,6 +20,10 @@ - Pyramid - Keri - Jablotron +- Electra +- Idteck +- Gallagher +- Nexwatch **Suported attack** - Default Values diff --git a/multi_fuzzer/catalog/docs/rfid/README.md b/multi_fuzzer/catalog/docs/rfid/README.md index eaee060fb..ec24b8448 100644 --- a/multi_fuzzer/catalog/docs/rfid/README.md +++ b/multi_fuzzer/catalog/docs/rfid/README.md @@ -19,6 +19,10 @@ The application will also help to identify the "denial of service" vulnerability - Pyramid - Keri - Jablotron +- Electra +- Idteck +- Gallagher +- Nexwatch # Application Features ## Main screen diff --git a/multi_fuzzer/lib/worker/protocol.c b/multi_fuzzer/lib/worker/protocol.c index 0c8322e5c..4bebd0047 100644 --- a/multi_fuzzer/lib/worker/protocol.c +++ b/multi_fuzzer/lib/worker/protocol.c @@ -27,6 +27,230 @@ const uint8_t uid_list_ds1990[][DS1990_DATA_SIZE] = { {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x14}, // Only FF {0x01, 0x78, 0x00, 0x48, 0xFD, 0xFF, 0xFF, 0xD1}, // StarNew Uni5 {0x01, 0xA9, 0xE4, 0x3C, 0x09, 0x00, 0x00, 0xE6}, // Eltis Uni + {0x01, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66}, + {0x01, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8B}, + {0x01, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0}, + {0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48}, + {0x01, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13}, + {0x01, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE}, + {0x01, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5}, + {0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD7}, + {0x01, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C}, + {0x01, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61}, + {0x01, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A}, + {0x01, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA2}, + {0x01, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF9}, + {0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14}, + {0x01, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x63}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0xD1}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x81}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xFC}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xDF}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x10}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x5C}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xA6}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x02}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x4A}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0xE0}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x67}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xBE}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x8B}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x12}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0xA1}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00, 0xFE}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x43}, + {0x01, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x57}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x1D}, + {0x01, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0xFC}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x9E}, + {0x01, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x86}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0xC0}, + {0x01, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x2D}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x22}, + {0x01, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0xC9}, + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x7C}, + {0x01, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x62}, + {0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x79}, + {0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xE3}, + {0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x8A}, + {0x01, 0x10, 0x01, 0x10, 0x01, 0x00, 0x00, 0x38}, + {0x01, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x19}, + {0x01, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x68}, + {0x01, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x5D}, + {0x01, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, 0xB6}, + {0x01, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x68}, + {0x01, 0x11, 0x11, 0x22, 0x22, 0x00, 0x00, 0x4C}, + {0x01, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x91}, + {0x01, 0x12, 0x12, 0x12, 0x12, 0x00, 0x00, 0xCD}, + {0x01, 0x12, 0x31, 0x23, 0x12, 0x31, 0x23, 0x94}, + {0x01, 0x12, 0x31, 0x23, 0x12, 0x00, 0x00, 0xBC}, + {0x01, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x4E}, + {0x01, 0x12, 0x34, 0x12, 0x34, 0x00, 0x00, 0xEC}, + {0x01, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0xFD}, + {0x01, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x32}, + {0x01, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x9D}, + {0x01, 0x33, 0x33, 0x33, 0x33, 0x00, 0x00, 0xB9}, + {0x01, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0xA4}, + {0x01, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, 0x23}, + {0x01, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xC4}, + {0x01, 0x55, 0x55, 0x55, 0x55, 0x00, 0x00, 0xA8}, + {0x01, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x64}, + {0x01, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x2C}, + {0x01, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x04}, + {0x01, 0x77, 0x77, 0x77, 0x77, 0x00, 0x00, 0xA7}, + {0x01, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x16}, + {0x01, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x01}, + {0x01, 0x87, 0x65, 0x43, 0x21, 0x00, 0x00, 0xC6}, + {0x01, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x76}, + {0x01, 0x99, 0x99, 0x99, 0x99, 0x00, 0x00, 0x8A}, + {0x01, 0x98, 0x76, 0x54, 0x32, 0x10, 0x00, 0x77}, + {0x01, 0x98, 0x76, 0x54, 0x32, 0x00, 0x00, 0x9B}, + {0x01, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xD6}, + {0x01, 0xAA, 0xAA, 0xAA, 0xAA, 0x00, 0x00, 0x0E}, + {0x01, 0xAB, 0xCD, 0xEF, 0x00, 0x00, 0x00, 0x84}, + {0x01, 0xAB, 0xCD, 0xAB, 0xCD, 0xAB, 0xCD, 0xD3}, + {0x01, 0xAB, 0xCD, 0xAB, 0xCD, 0x00, 0x00, 0x29}, + {0x01, 0xAB, 0xCA, 0xBC, 0xAB, 0xCA, 0xBC, 0x70}, + {0x01, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xB6}, + {0x01, 0xBB, 0xBB, 0xBB, 0xBB, 0x00, 0x00, 0x85}, + {0x01, 0xBA, 0xBA, 0xBA, 0xBA, 0xBA, 0xBA, 0xF2}, + {0x01, 0xBA, 0xBA, 0xBA, 0xBA, 0x00, 0x00, 0x5B}, + {0x01, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x8F}, + {0x01, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00, 0x1F}, + {0x01, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xEF}, + {0x01, 0xDD, 0xDD, 0xDD, 0xDD, 0x00, 0x00, 0x94}, + {0x01, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0x4F}, + {0x01, 0xEE, 0xEE, 0xEE, 0xEE, 0x00, 0x00, 0x10}, + {0x01, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0x3D}, + {0x01, 0x12, 0x34, 0x56, 0x78, 0x9A, 0x00, 0xAC}, + {0x01, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x88}, + {0x01, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x6A}, + {0x01, 0x12, 0x34, 0x56, 0x78, 0x9A, 0x12, 0x8D}, + {0x01, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x88}, + {0x01, 0xCB, 0xA9, 0x87, 0x65, 0x43, 0x21, 0xE6}, + {0x01, 0xCB, 0xA9, 0x87, 0x65, 0x00, 0x00, 0x55}, + {0x01, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x02}, + {0x01, 0xFE, 0xDC, 0xBA, 0x98, 0x00, 0x00, 0xA4}, + {0x01, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0x0E}, + {0x01, 0xCA, 0xCA, 0xCA, 0xCA, 0x00, 0x00, 0xE9}, + {0x01, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x6B}, + {0x01, 0xFE, 0xFE, 0xFE, 0xFE, 0x00, 0x00, 0x45}, + {0x01, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0x0A}, + {0x01, 0xCA, 0xFE, 0xCA, 0xFE, 0x00, 0x00, 0x32}, + {0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D}, + {0xFF, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB0}, + {0xFF, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEB}, + {0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73}, + {0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28}, + {0xFF, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC5}, + {0xFF, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9E}, + {0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEC}, + {0xFF, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB7}, + {0xFF, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A}, + {0xFF, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, + {0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99}, + {0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC2}, + {0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2F}, + {0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x58}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0xEA}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xBA}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xC7}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE4}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x2B}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x67}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x9D}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x39}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x71}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0xDB}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x5C}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x85}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0xB0}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xC4}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x29}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x9A}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00, 0xC5}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x78}, + {0xFF, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x6C}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x26}, + {0xFF, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0xC7}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xA5}, + {0xFF, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xBD}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0xFB}, + {0xFF, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x16}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x19}, + {0xFF, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0xF2}, + {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x47}, + {0xFF, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x59}, + {0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x42}, + {0xFF, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xD8}, + {0xFF, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0xB1}, + {0xFF, 0x10, 0x01, 0x10, 0x01, 0x00, 0x00, 0x03}, + {0xFF, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x22}, + {0xFF, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x53}, + {0xFF, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x66}, + {0xFF, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, 0x8D}, + {0xFF, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x53}, + {0xFF, 0x11, 0x11, 0x22, 0x22, 0x00, 0x00, 0x77}, + {0xFF, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0xAA}, + {0xFF, 0x12, 0x12, 0x12, 0x12, 0x00, 0x00, 0xF6}, + {0xFF, 0x12, 0x31, 0x23, 0x12, 0x31, 0x23, 0xAF}, + {0xFF, 0x12, 0x31, 0x23, 0x12, 0x00, 0x00, 0x87}, + {0xFF, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x75}, + {0xFF, 0x12, 0x34, 0x12, 0x34, 0x00, 0x00, 0xD7}, + {0xFF, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0xC6}, + {0xFF, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x09}, + {0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xA6}, + {0xFF, 0x33, 0x33, 0x33, 0x33, 0x00, 0x00, 0x82}, + {0xFF, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x9F}, + {0xFF, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, 0x18}, + {0xFF, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xFF}, + {0xFF, 0x55, 0x55, 0x55, 0x55, 0x00, 0x00, 0x93}, + {0xFF, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x5F}, + {0xFF, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x17}, + {0xFF, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x3F}, + {0xFF, 0x77, 0x77, 0x77, 0x77, 0x00, 0x00, 0x9C}, + {0xFF, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x2D}, + {0xFF, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x3A}, + {0xFF, 0x87, 0x65, 0x43, 0x21, 0x00, 0x00, 0xFD}, + {0xFF, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x4D}, + {0xFF, 0x99, 0x99, 0x99, 0x99, 0x00, 0x00, 0xB1}, + {0xFF, 0x98, 0x76, 0x54, 0x32, 0x10, 0x00, 0x4C}, + {0xFF, 0x98, 0x76, 0x54, 0x32, 0x00, 0x00, 0xA0}, + {0xFF, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xED}, + {0xFF, 0xAA, 0xAA, 0xAA, 0xAA, 0x00, 0x00, 0x35}, + {0xFF, 0xAB, 0xCD, 0xEF, 0x00, 0x00, 0x00, 0xBF}, + {0xFF, 0xAB, 0xCD, 0xAB, 0xCD, 0xAB, 0xCD, 0xE8}, + {0xFF, 0xAB, 0xCD, 0xAB, 0xCD, 0x00, 0x00, 0x12}, + {0xFF, 0xAB, 0xCA, 0xBC, 0xAB, 0xCA, 0xBC, 0x4B}, + {0xFF, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0x8D}, + {0xFF, 0xBB, 0xBB, 0xBB, 0xBB, 0x00, 0x00, 0xBE}, + {0xFF, 0xBA, 0xBA, 0xBA, 0xBA, 0xBA, 0xBA, 0xC9}, + {0xFF, 0xBA, 0xBA, 0xBA, 0xBA, 0x00, 0x00, 0x60}, + {0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xB4}, + {0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00, 0x24}, + {0xFF, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xD4}, + {0xFF, 0xDD, 0xDD, 0xDD, 0xDD, 0x00, 0x00, 0xAF}, + {0xFF, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0x74}, + {0xFF, 0xEE, 0xEE, 0xEE, 0xEE, 0x00, 0x00, 0x2B}, + {0xFF, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0x06}, + {0xFF, 0x12, 0x34, 0x56, 0x78, 0x9A, 0x00, 0x97}, + {0xFF, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0xB3}, + {0xFF, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x51}, + {0xFF, 0x12, 0x34, 0x56, 0x78, 0x9A, 0x12, 0xB6}, + {0xFF, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0xB3}, + {0xFF, 0xCB, 0xA9, 0x87, 0x65, 0x43, 0x21, 0xDD}, + {0xFF, 0xCB, 0xA9, 0x87, 0x65, 0x00, 0x00, 0x6E}, + {0xFF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x39}, + {0xFF, 0xFE, 0xDC, 0xBA, 0x98, 0x00, 0x00, 0x9F}, + {0xFF, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0x35}, + {0xFF, 0xCA, 0xCA, 0xCA, 0xCA, 0x00, 0x00, 0xD2}, + {0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x50}, + {0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0x00, 0x00, 0x7E}, + {0xFF, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0x31}, + {0xFF, 0xCA, 0xFE, 0xCA, 0xFE, 0x00, 0x00, 0x09}, }; const uint8_t uid_list_metakom[][Metakom_DATA_SIZE] = { @@ -71,19 +295,13 @@ const uint8_t uid_list_cyfral[][Cyfral_DATA_SIZE] = { // ########################### // ## Rfid_125khz Protocols ## // ########################### -#define EM4100_DATA_SIZE (5) -#define HIDProx_DATA_SIZE (6) -#define PAC_DATA_SIZE (4) -#define H10301_DATA_SIZE (3) -#define IOPROXXSF_DATA_SIZE (4) -#define PARADOX_DATA_SIZE (6) -#define INDALA26_DATA_SIZE (4) -#define VIKING_DATA_SIZE (4) -#define PYRAMID_DATA_SIZE (4) -#define KERI_DATA_SIZE (4) -#define JABLOTRON_DATA_SIZE (5) +#define THREEBYTE_DATA_SIZE (3) // H10301 +#define FOURBYTE_DATA_SIZE (4) // PAC/Stanley, IoProxXSF, Indala26, Viking, Pyramid, Keri +#define FIVEBYTE_DATA_SIZE (5) // EM4100, Joblotron +#define SIXBYTE_DATA_SIZE (6) // HIDProx, Paradox +#define EIGHTBYTE_DATA_SIZE (8) // Electra, Idteck, Gallagher, Nexwatch -const uint8_t uid_list_em4100[][EM4100_DATA_SIZE] = { +const uint8_t uid_list_5byte[][FIVEBYTE_DATA_SIZE] = { {0x00, 0x00, 0x00, 0x00, 0x00}, // Null bytes {0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // Only FF {0x11, 0x11, 0x11, 0x11, 0x11}, // Only 11 @@ -103,7 +321,7 @@ const uint8_t uid_list_em4100[][EM4100_DATA_SIZE] = { {0xCA, 0xCA, 0xCA, 0xCA, 0xCA}, // From arha }; -const uint8_t uid_list_hid[][HIDProx_DATA_SIZE] = { +const uint8_t uid_list_6byte[][SIXBYTE_DATA_SIZE] = { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // Null bytes {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // Only FF {0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, // Only 11 @@ -120,7 +338,7 @@ const uint8_t uid_list_hid[][HIDProx_DATA_SIZE] = { {0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA}, // From arha }; -const uint8_t uid_list_pac[][PAC_DATA_SIZE] = { +const uint8_t uid_list_4byte[][FOURBYTE_DATA_SIZE] = { {0x00, 0x00, 0x00, 0x00}, // Null bytes {0xFF, 0xFF, 0xFF, 0xFF}, // Only FF {0x11, 0x11, 0x11, 0x11}, // Only 11 @@ -140,7 +358,7 @@ const uint8_t uid_list_pac[][PAC_DATA_SIZE] = { {0xCA, 0xCA, 0xCA, 0xCA}, // From arha }; -const uint8_t uid_list_h10301[][H10301_DATA_SIZE] = { +const uint8_t uid_list_3byte[][THREEBYTE_DATA_SIZE] = { {0x00, 0x00, 0x00}, // Null bytes {0xFF, 0xFF, 0xFF}, // Only FF {0x11, 0x11, 0x11}, // Only 11 @@ -157,126 +375,21 @@ const uint8_t uid_list_h10301[][H10301_DATA_SIZE] = { {0xCA, 0xCA, 0xCA}, // From arha }; -const uint8_t uid_list_ioproxxsf[][IOPROXXSF_DATA_SIZE] = { - {0x00, 0x00, 0x00, 0x00}, // Null bytes - {0xFF, 0xFF, 0xFF, 0xFF}, // Only FF - {0x11, 0x11, 0x11, 0x11}, // Only 11 - {0x22, 0x22, 0x22, 0x22}, // Only 22 - {0x33, 0x33, 0x33, 0x33}, // Only 33 - {0x44, 0x44, 0x44, 0x44}, // Only 44 - {0x55, 0x55, 0x55, 0x55}, // Only 55 - {0x66, 0x66, 0x66, 0x66}, // Only 66 - {0x77, 0x77, 0x77, 0x77}, // Only 77 - {0x88, 0x88, 0x88, 0x88}, // Only 88 - {0x99, 0x99, 0x99, 0x99}, // Only 99 - {0x12, 0x34, 0x56, 0x78}, // Incremental UID - {0x9A, 0x78, 0x56, 0x34}, // Decremental UID - {0x04, 0xd0, 0x9b, 0x0d}, // From arha - {0x34, 0x00, 0x29, 0x3d}, // From arha - {0x04, 0xdf, 0x00, 0x00}, // From arha - {0xCA, 0xCA, 0xCA, 0xCA}, // From arha -}; - -const uint8_t uid_list_paradox[][PARADOX_DATA_SIZE] = { - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // Null bytes - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // Only FF - {0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, // Only 11 - {0x22, 0x22, 0x22, 0x22, 0x22, 0x22}, // Only 22 - {0x33, 0x33, 0x33, 0x33, 0x33, 0x33}, // Only 33 - {0x44, 0x44, 0x44, 0x44, 0x44, 0x44}, // Only 44 - {0x55, 0x55, 0x55, 0x55, 0x55, 0x55}, // Only 55 - {0x66, 0x66, 0x66, 0x66, 0x66, 0x66}, // Only 66 - {0x77, 0x77, 0x77, 0x77, 0x77, 0x77}, // Only 77 - {0x88, 0x88, 0x88, 0x88, 0x88, 0x88}, // Only 88 - {0x99, 0x99, 0x99, 0x99, 0x99, 0x99}, // Only 99 - {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC}, // Incremental UID - {0xFF, 0xDE, 0xBC, 0x9A, 0x78, 0x56}, // Decremental UID - {0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA}, // From arha -}; - -const uint8_t uid_list_indala26[][INDALA26_DATA_SIZE] = { - {0x00, 0x00, 0x00, 0x00}, // Null bytes - {0xFF, 0xFF, 0xFF, 0xFF}, // Only FF - {0x11, 0x11, 0x11, 0x11}, // Only 11 - {0x22, 0x22, 0x22, 0x22}, // Only 22 - {0x33, 0x33, 0x33, 0x33}, // Only 33 - {0x44, 0x44, 0x44, 0x44}, // Only 44 - {0x55, 0x55, 0x55, 0x55}, // Only 55 - {0x66, 0x66, 0x66, 0x66}, // Only 66 - {0x77, 0x77, 0x77, 0x77}, // Only 77 - {0x88, 0x88, 0x88, 0x88}, // Only 88 - {0x99, 0x99, 0x99, 0x99}, // Only 99 - {0x12, 0x34, 0x56, 0x78}, // Incremental UID - {0xFF, 0xDE, 0xBC, 0x9A}, // Decremental UID - {0xCA, 0xCA, 0xCA, 0xCA}, // From arha -}; - -const uint8_t uid_list_viking[][VIKING_DATA_SIZE] = { - {0x00, 0x00, 0x00, 0x00}, // Null bytes - {0xFF, 0xFF, 0xFF, 0xFF}, // Only FF - {0x11, 0x11, 0x11, 0x11}, // Only 11 - {0x22, 0x22, 0x22, 0x22}, // Only 22 - {0x33, 0x33, 0x33, 0x33}, // Only 33 - {0x44, 0x44, 0x44, 0x44}, // Only 44 - {0x55, 0x55, 0x55, 0x55}, // Only 55 - {0x66, 0x66, 0x66, 0x66}, // Only 66 - {0x77, 0x77, 0x77, 0x77}, // Only 77 - {0x88, 0x88, 0x88, 0x88}, // Only 88 - {0x99, 0x99, 0x99, 0x99}, // Only 99 - {0x12, 0x34, 0x56, 0x78}, // Incremental UID - {0xFF, 0xDE, 0xBC, 0x9A}, // Decremental UID - {0xCA, 0xCA, 0xCA, 0xCA}, // From arha -}; - -const uint8_t uid_list_pyramid[][PYRAMID_DATA_SIZE] = { - {0x00, 0x00, 0x00, 0x00}, // Null bytes - {0xFF, 0xFF, 0xFF, 0xFF}, // Only FF - {0x11, 0x11, 0x11, 0x11}, // Only 11 - {0x22, 0x22, 0x22, 0x22}, // Only 22 - {0x33, 0x33, 0x33, 0x33}, // Only 33 - {0x44, 0x44, 0x44, 0x44}, // Only 44 - {0x55, 0x55, 0x55, 0x55}, // Only 55 - {0x66, 0x66, 0x66, 0x66}, // Only 66 - {0x77, 0x77, 0x77, 0x77}, // Only 77 - {0x88, 0x88, 0x88, 0x88}, // Only 88 - {0x99, 0x99, 0x99, 0x99}, // Only 99 - {0x12, 0x34, 0x56, 0x78}, // Incremental UID - {0xFF, 0xDE, 0xBC, 0x9A}, // Decremental UID - {0xCA, 0xCA, 0xCA, 0xCA}, // From arha -}; - -const uint8_t uid_list_keri[][KERI_DATA_SIZE] = { - {0x00, 0x00, 0x00, 0x00}, // Null bytes - {0xFF, 0xFF, 0xFF, 0xFF}, // Only FF - {0x11, 0x11, 0x11, 0x11}, // Only 11 - {0x22, 0x22, 0x22, 0x22}, // Only 22 - {0x33, 0x33, 0x33, 0x33}, // Only 33 - {0x44, 0x44, 0x44, 0x44}, // Only 44 - {0x55, 0x55, 0x55, 0x55}, // Only 55 - {0x66, 0x66, 0x66, 0x66}, // Only 66 - {0x77, 0x77, 0x77, 0x77}, // Only 77 - {0x88, 0x88, 0x88, 0x88}, // Only 88 - {0x99, 0x99, 0x99, 0x99}, // Only 99 - {0x12, 0x34, 0x56, 0x78}, // Incremental UID - {0xFF, 0xDE, 0xBC, 0x9A}, // Decremental UID - {0xCA, 0xCA, 0xCA, 0xCA}, // From arha -}; - -const uint8_t uid_list_jablotron[][JABLOTRON_DATA_SIZE] = { - {0x00, 0x00, 0x00, 0x00, 0x00}, // Null bytes - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // Only FF - {0x11, 0x11, 0x11, 0x11, 0x11}, // Only 11 - {0x22, 0x22, 0x22, 0x22, 0x22}, // Only 22 - {0x33, 0x33, 0x33, 0x33, 0x33}, // Only 33 - {0x44, 0x44, 0x44, 0x44, 0x44}, // Only 44 - {0x55, 0x55, 0x55, 0x55, 0x55}, // Only 55 - {0x66, 0x66, 0x66, 0x66, 0x66}, // Only 66 - {0x77, 0x77, 0x77, 0x77, 0x77}, // Only 77 - {0x88, 0x88, 0x88, 0x88, 0x88}, // Only 88 - {0x99, 0x99, 0x99, 0x99, 0x99}, // Only 99 - {0x12, 0x34, 0x56, 0x78, 0x9A}, // Incremental UID - {0xFF, 0xDE, 0xBC, 0x9A, 0x78}, // Decremental UID - {0xCA, 0xCA, 0xCA, 0xCA, 0xCA}, // From arha +const uint8_t uid_list_8byte[][EIGHTBYTE_DATA_SIZE] = { + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // Null bytes + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // Only FF + {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, // Only 11 + {0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22}, // Only 22 + {0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33}, // Only 33 + {0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44}, // Only 44 + {0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55}, // Only 55 + {0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66}, // Only 66 + {0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77}, // Only 77 + {0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88}, // Only 88 + {0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99}, // Only 99 + {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xFF}, // Incremental UID + {0xFF, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12}, // Decremental UID + {0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA, 0xCA}, // From arha }; #if defined(RFID_125_PROTOCOL) @@ -284,111 +397,151 @@ const FuzzerProtocol fuzzer_proto_items[] = { // EM4100 { .name = "EM4100", - .data_size = EM4100_DATA_SIZE, + .data_size = FIVEBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_em4100, - .len = COUNT_OF(uid_list_em4100), + .val = (const uint8_t*)&uid_list_5byte, + .len = COUNT_OF(uid_list_5byte), }, }, // HIDProx { .name = "HIDProx", - .data_size = HIDProx_DATA_SIZE, + .data_size = SIXBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_hid, - .len = COUNT_OF(uid_list_hid), + .val = (const uint8_t*)&uid_list_6byte, + .len = COUNT_OF(uid_list_6byte), }, }, // PAC { .name = "PAC/Stanley", - .data_size = PAC_DATA_SIZE, + .data_size = FOURBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_pac, - .len = COUNT_OF(uid_list_pac), + .val = (const uint8_t*)&uid_list_4byte, + .len = COUNT_OF(uid_list_4byte), }, }, // H10301 { .name = "H10301", - .data_size = H10301_DATA_SIZE, + .data_size = THREEBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_h10301, - .len = COUNT_OF(uid_list_h10301), + .val = (const uint8_t*)&uid_list_3byte, + .len = COUNT_OF(uid_list_3byte), }, }, // IoProxXSF { .name = "IoProxXSF", - .data_size = IOPROXXSF_DATA_SIZE, + .data_size = FOURBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_ioproxxsf, - .len = COUNT_OF(uid_list_ioproxxsf), + .val = (const uint8_t*)&uid_list_4byte, + .len = COUNT_OF(uid_list_4byte), }, }, // Paradox { .name = "Paradox", - .data_size = PARADOX_DATA_SIZE, + .data_size = SIXBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_paradox, - .len = COUNT_OF(uid_list_paradox), + .val = (const uint8_t*)&uid_list_6byte, + .len = COUNT_OF(uid_list_6byte), }, }, // Indala26 { .name = "Indala26", - .data_size = INDALA26_DATA_SIZE, + .data_size = FOURBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_indala26, - .len = COUNT_OF(uid_list_indala26), + .val = (const uint8_t*)&uid_list_4byte, + .len = COUNT_OF(uid_list_4byte), }, }, // Viking { .name = "Viking", - .data_size = VIKING_DATA_SIZE, + .data_size = FOURBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_viking, - .len = COUNT_OF(uid_list_viking), + .val = (const uint8_t*)&uid_list_4byte, + .len = COUNT_OF(uid_list_4byte), }, }, // Pyramid { .name = "Pyramid", - .data_size = PYRAMID_DATA_SIZE, + .data_size = FOURBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_pyramid, - .len = COUNT_OF(uid_list_pyramid), + .val = (const uint8_t*)&uid_list_4byte, + .len = COUNT_OF(uid_list_4byte), }, }, // Keri { .name = "Keri", - .data_size = KERI_DATA_SIZE, + .data_size = FOURBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_keri, - .len = COUNT_OF(uid_list_keri), + .val = (const uint8_t*)&uid_list_4byte, + .len = COUNT_OF(uid_list_4byte), }, }, // Jablotron { .name = "Jablotron", - .data_size = JABLOTRON_DATA_SIZE, + .data_size = FIVEBYTE_DATA_SIZE, + .dict = + { + .val = (const uint8_t*)&uid_list_5byte, + .len = COUNT_OF(uid_list_5byte), + }, + }, + // Electra + { + .name = "Electra", + .data_size = EIGHTBYTE_DATA_SIZE, + .dict = + { + .val = (const uint8_t*)&uid_list_8byte, + .len = COUNT_OF(uid_list_8byte), + }, + }, + // Idteck + { + .name = "Idteck", + .data_size = EIGHTBYTE_DATA_SIZE, + .dict = + { + .val = (const uint8_t*)&uid_list_8byte, + .len = COUNT_OF(uid_list_8byte), + }, + }, + // Gallagher + { + .name = "Gallagher", + .data_size = EIGHTBYTE_DATA_SIZE, + .dict = + { + .val = (const uint8_t*)&uid_list_8byte, + .len = COUNT_OF(uid_list_8byte), + }, + }, + // Nexwatch + { + .name = "Nexwatch", + .data_size = EIGHTBYTE_DATA_SIZE, .dict = { - .val = (const uint8_t*)&uid_list_jablotron, - .len = COUNT_OF(uid_list_jablotron), + .val = (const uint8_t*)&uid_list_8byte, + .len = COUNT_OF(uid_list_8byte), }, }, }; diff --git a/multi_fuzzer/lib/worker/protocol_i.h b/multi_fuzzer/lib/worker/protocol_i.h index 6679b6d7f..51b328567 100644 --- a/multi_fuzzer/lib/worker/protocol_i.h +++ b/multi_fuzzer/lib/worker/protocol_i.h @@ -3,7 +3,7 @@ #include "protocol.h" #if defined(RFID_125_PROTOCOL) -#define MAX_PAYLOAD_SIZE (6) +#define MAX_PAYLOAD_SIZE (8) #define PROTOCOL_DEF_IDLE_TIME (4) #define PROTOCOL_DEF_EMU_TIME (5) #define PROTOCOL_TIME_DELAY_MIN PROTOCOL_DEF_IDLE_TIME + PROTOCOL_DEF_EMU_TIME @@ -48,4 +48,4 @@ struct FuzzerProtocol { // #define FUZZ_TIME_DELAY_DEFAULT (8) // #define FUZZ_TIME_DELAY_MAX (80) -extern const FuzzerProtocol fuzzer_proto_items[]; \ No newline at end of file +extern const FuzzerProtocol fuzzer_proto_items[]; diff --git a/nfc_magic/.gitsubtree b/nfc_magic/.gitsubtree index 8dc6fd2ca..62f532c20 100644 --- a/nfc_magic/.gitsubtree +++ b/nfc_magic/.gitsubtree @@ -1,2 +1,2 @@ -https://github.com/xMasterX/all-the-plugins dev base_pack/nfc_magic e7a5fb24f091746d1dce138de2041d070a2902e0 +https://github.com/xMasterX/all-the-plugins dev base_pack/nfc_magic a0eeb19e4385a178793c673cbd073fcd37a94928 https://github.com/flipperdevices/flipperzero-good-faps dev nfc_magic e28aceb103154518364ab262653a82d8637d331a diff --git a/nfc_magic/scenes/nfc_magic_scene_file_select.c b/nfc_magic/scenes/nfc_magic_scene_file_select.c index 20e40a420..242378603 100644 --- a/nfc_magic/scenes/nfc_magic_scene_file_select.c +++ b/nfc_magic/scenes/nfc_magic_scene_file_select.c @@ -8,7 +8,7 @@ static bool nfc_magic_scene_file_select_is_file_suitable(NfcMagicApp* instance) bool suitable = false; if(instance->protocol == NfcMagicProtocolGen1) { - if((uid_len == 4) && (protocol == NfcProtocolMfClassic)) { + if((uid_len == 4 || uid_len == 7) && (protocol == NfcProtocolMfClassic)) { const MfClassicData* mfc_data = nfc_device_get_data(instance->source_dev, NfcProtocolMfClassic); if(mfc_data->type == MfClassicType1k) { diff --git a/nfc_playlist/.gitsubtree b/nfc_playlist/.gitsubtree index 35e9b21b5..d35a1e7ff 100644 --- a/nfc_playlist/.gitsubtree +++ b/nfc_playlist/.gitsubtree @@ -1,2 +1,2 @@ -https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/nfc_playlist 6925308df62214abd3e19982444dc733cf098d92 +https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/nfc_playlist 35b8ffdadb1a2caf757875edf815ea3b3a4c4dcb https://github.com/acegoal07/FlipperZero_NFC_Playlist main / diff --git a/pinball0/.github/FUNDING.yml b/pinball0/.github/FUNDING.yml new file mode 100644 index 000000000..21bac9d03 --- /dev/null +++ b/pinball0/.github/FUNDING.yml @@ -0,0 +1 @@ +buy_me_a_coffee: rdefeo diff --git a/pinball0/.github/ISSUE_TEMPLATE/bug_report.md b/pinball0/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..5b3feb2c2 --- /dev/null +++ b/pinball0/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. Is this a game play bug? A build/compilation bug? More information is better. + +**To Reproduce** +Steps to reproduce the behavior: + +**Logs** +Please include log output - this can be extremely helpful. To capture the logs, you will need to run `ufbt cli` and then the `log` command. Pinball0 will print various levels of logging data to the console. You can learn more about ufbt here: [github.com/flipperdevices/flipperzero-ufbt](https://github.com/flipperdevices/flipperzero-ufbt) + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Software Info** + - Pinball0 version [e.g. v0.2] + - ufbt version [e.g. 1.1.2] - if this is a build issue diff --git a/pinball0/.github/ISSUE_TEMPLATE/new-table-template.md b/pinball0/.github/ISSUE_TEMPLATE/new-table-template.md new file mode 100644 index 000000000..ff3acf946 --- /dev/null +++ b/pinball0/.github/ISSUE_TEMPLATE/new-table-template.md @@ -0,0 +1,18 @@ +--- +name: New table template +about: Suggest a new table. Have you posted this on Discussions yet? +title: '' +labels: '' +assignees: '' + +--- + +**Screenshots** +A picture is worth a thousand words! Show off your table with a screenshot. + +**Files** +Include the json file of your new table + +**Description** +- Name of your table +- Reason why you feel it should be included with the shipped versions of Pinball0 diff --git a/pinball0/.github/workflows/build.yml b/pinball0/.github/workflows/build.yml new file mode 100644 index 000000000..143847c4a --- /dev/null +++ b/pinball0/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: "FAP: Build for multiple SDK sources" +# This will build your app for dev and release channels on GitHub. +# It will also build your app every day to make sure it's up to date with the latest SDK changes. +# See https://github.com/marketplace/actions/build-flipper-application-package-fap for more information + +on: + push: + ## put your main branch name under "branches" + #branches: + # - master + pull_request: + schedule: + # do a build every day + - cron: "1 1 * * *" + +jobs: + ufbt-build: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: dev channel + sdk-channel: dev + - name: release channel + sdk-channel: release + # You can add unofficial channels here. See ufbt action docs for more info. + name: 'ufbt: Build for ${{ matrix.name }}' + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build with ufbt + uses: flipperdevices/flipperzero-ufbt-action@v0.1 + id: build-app + with: + sdk-channel: ${{ matrix.sdk-channel }} + - name: Upload app artifacts + uses: actions/upload-artifact@v3 + with: + # See ufbt action docs for other output variables + name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }} + path: ${{ steps.build-app.outputs.fap-artifacts }} diff --git a/pinball0/.gitignore b/pinball0/.gitignore new file mode 100644 index 000000000..2bcc38cf0 --- /dev/null +++ b/pinball0/.gitignore @@ -0,0 +1,8 @@ +dist/* +.vscode +.clang-format +.clangd +.editorconfig +.env +.ufbt +temp/* diff --git a/pinball0/.gitsubtree b/pinball0/.gitsubtree new file mode 100644 index 000000000..e9192b932 --- /dev/null +++ b/pinball0/.gitsubtree @@ -0,0 +1 @@ +https://github.com/rdefeo/pinball0 master / diff --git a/pinball0/CHANGELOG.md b/pinball0/CHANGELOG.md new file mode 100644 index 000000000..821e975d4 --- /dev/null +++ b/pinball0/CHANGELOG.md @@ -0,0 +1,22 @@ +## 0.4.0 + +- Table Tilt! +- Solid flipper rendering +- Code refactor / cleanup + +## 0.3.0 + +- Added Idle timeout of 2 minutes +- Changed Manual mode to Debug mode + +## 0.2.0 + +- User tables from /apps_data/pinball0 folder +- Sounds, LED blinking, vibrations +- Basic scores +- Collision bug fixes +- Mem leak fix + +## 0.1.0 + +- BETA release diff --git a/pinball0/README.md b/pinball0/README.md new file mode 100644 index 000000000..e31e10e03 --- /dev/null +++ b/pinball0/README.md @@ -0,0 +1,124 @@ +# Pinball0 (Pinball Zero) +Play pinball on your Flipperzero! + +Still a work in progress... + +[Latest version v0.4](https://github.com/rdefeo/pinball0/releases) + +![build status badge](https://github.com/rdefeo/pinball0/actions/workflows/build.yml/badge.svg) + +> The default tables and example tables may / will change + +## Screenshots + +![menu](screenshots/screenshot_menu.png) +![basic](screenshots/screenshot_basic.png) +![el ocho](screenshots/screenshot_el_ocho.png) +![chamber](screenshots/screenshot_chamber.png) + +## Features +* Realistic physics and collisions +* User-defined tables via JSON files +* Bumpers, flat surfaces, curved surfaces +* Scores! (no high scores yet, just a running tally as you play) +* Table bumps! (Don't tilt the table!) +* Portals! +* Rollover items +* Sounds! Blinky lights! Annoying vibrations! +* Customizable notification settings: sound, LED, vibration +* Idle timeout to save battery - will exit after ~120 seconds of no key-presses + +## Controls +* **Ok** to release the ball +* **Left** and **Right** to activate the left and right flippers +* **Back** to return to the main menu or exit +* **Up** to "bump" the table if the ball gets stuck + +I find it easiest to hold the flipper with both hands so I can hit left/right with my thumbs! + +## Settings +The **SETTINGS** menu will be the "last" table listed. You can Enable / Disable the following: Sound, LED light, Vibration, and Debug mode. Move Up/Down to select your setting and press **OK** to toggle. Settings are saved in `/data/.pinball0.conf` as a native Flipper Format file. **Back** will return you to the main menu. + +**Debug** mode allows you to move the ball using the directional pad _before_ the ball is launched. This is useful for testing and may be removed in the future. (May result in unexpected behavior.) It also displays test tables on the main menu. The test tables will only show/hide after you exit and restart the app. This feature is mainly for me - lol. + +## Tables +Pinball0 ships with several default tables. These tables are automatically deployed into the assets folder (`/apps_data/pinball0`) on your SD card. Tables are simple JSON which means you can define your own! Your tables should be stored in the data folder (`/apps_data/pinball0`). On the main menu, tables are sorted alphabetically. In order to "force" a sorting order, you can prepend any filename with `NN_` where `NN` is between `00` and `99`. When the files are displayed on the menu, if they start with `NN_`, that will be stripped - but their sorted order will be preserved. + +> The default tables may change over time. + +In **Debug** mode, test tables will be shown. A test table is one that begins with the text `dbg`. Given that you can prefix table names for sorting purposes, here are two valid table filenames for a test table called `my FLIPS`: `dbg my FLIPS.json` and `04_dbg my FLIPS.json`. In both cases it will be displayed as `dbg my FLIPS` on the menu. I doubt that you will use this feature, but I'm documenting it anyway. + + +### File Format +Table units are specified at a 10x scale. This means our table is **630 x 1270** in size (as the F0 display is 64 pixels x 128 pixels). Our origin is in the top-left at 0, 0. Check out the default tables in the `assets/tables` folder for example usage. + +These JSON elements are all defined at the top-level. The JSON can include comments - because why not! + +> **DISCLAIMER:** The file format may change from release to release. Sorry. There is some basic error checking when reading / parsing the table files. If the error is serious enough, you will see an error message in the app. Otherwise, check the console logs. For those familiar with `ufbt`, simply run `ufbt cli` and issue the `log` command. Then launch Pinball0. All informational and higher logs will be displayed. These logs are useful when reporting bugs/issues! + +#### lives : object (optional) +Defines how many lives/balls you start with, and display information + +* `"display": bool` : optional, defaults to false +* `"position": [ X, Y ]` +* `"value": N` : Number of balls - optional, defaults to 3 +* `"align": A` : optional, defaults to `"HORIZONTAL"` (also, `"VERTICAL"`) + +#### balls : list of objects +Every table needs at least 1 ball, otherwise it will fail to load. + +* `"position": [ X, Y ]` +* `"velocity": [ VX, VY ]` : optional, defaults to `[ 0, 0 ]`. The default tables have an initial velocity magnitude in the 10-18 range. Test your own values! +* `"radius" : N` : optional, defaults to `20` + +#### score : object (optional) +* `"display" : bool` : optional, defaults to false +* `"position" : [ X, Y ]` : optional, defaults to `[ 63, 0 ]` + +The position units are in absolute LCD pixels within the range [0..63], [0..127]. + +#### flippers : list of objects (optional) +* `"position": [ X, Y ]` : location of the pivot point +* `"side": S` : valid values are `"LEFT"` or `"RIGHT"` +* `"size": N` : optional, defaults to `120` + +You can have more than 2 flippers! Try it! + +#### bumpers : list of objects (optional) +* `"position": [ X, Y ]` +* `"radius": [ N ]` : optional, defaults to `40` + +#### rails : list of objects (optional) +* `"start": [ X, Y ]` +* `"end": [ X, Y ]` +* `"double_sided": bool` : optional, defaults to `false` + +The "surface" of a rail is "on the left" as we move from the `"start"` to the `"end"`. This means that when thinking about your table, the outer perimiter should be defined in a counter-clockwise order. Arriving at a rail from it's revrese side will result in a pass-through - think of it as a one-way mirror. + +#### arcs : list of objects (optional) +* `"position": [ X, Y ]` +* `"radius" : N` +* `"start_angle": N` : in degrees from 0 to 360 +* `"end_angle": N` : in degreens from 0 to 360 +* `"surface": S` : valid values are `"INSIDE"` or `"OUTSIDE"` + +Start and End angles should be specified in **counter-clockwise** order. The **surface** defines which side will bounce/reflect. + +#### rollovers : list of objects (optional) +* `"position": [ X, Y ]` +* `"symbol": C` : where C is a string representing a single character, like `"Z"` + +When the ball passes over/through a rollover object, the symbol will appear. Only the first character of the string is used. + +#### portals : list of objects (optional) +* `"a_start": [ X, Y]` +* `"a_end": [ X, Y]` +* `"b_start": [ X, Y]` +* `"b_end": [ X, Y]` + +Defines two portals, **a** and **b**. They are bi-drectional. Like rails, their "surface" - or in this case, their "entry surface" - is "on the left" from their respective start to end direction. You can't "enter" a portal from it's reverse side, you will pass through. + +#### tilt_detect : boolean +* `"tilt_detect": bool` : optional, defaults to `true` + +Mainly used to turn off tilt detection. Useful for tables that promote free-play and multiple table bumps without penalty. \ No newline at end of file diff --git a/pinball0/README_flipperlab.md b/pinball0/README_flipperlab.md new file mode 100644 index 000000000..6d8e247c2 --- /dev/null +++ b/pinball0/README_flipperlab.md @@ -0,0 +1,29 @@ +# Pinball0 (Pinball Zero) +Play pinball on your Flipperzero! + +## Features +* Realistic physics and collisions +* User-defined tables via JSON files +* Bumpers, flat surfaces, curved surfaces +* Table bumps +* Portals! +* Rollover items +* Sounds! Blinky lights! Annoying vibrations! +* Customizable notification settings: sound, LED, vibration +* Idle timeout + +## Controls +* **Ok** to release the ball +* **Left** and **Right** to activate the left and right flippers +* **Back** to return to the main menu or exit +* **Up** to "bump" the table if the ball gets stuck + +I find it easiest to hold the flipper with both hands so I can hit left/right with my thumbs! + +## Tables +Pinball0 ships with several default tables. These tables are automatically deployed into the assets folder (/apps_data/pinball0). Tables are simple JSON which means you can define your own! Your tables should be stored in the data folder (/apps_data/pinball0). + +View the github repo for the JSON format specification: https://github.com/rdefeo/pinball0 + +**The default tables may change over time.** + diff --git a/pinball0/application.fam b/pinball0/application.fam new file mode 100644 index 000000000..c8e86cf8b --- /dev/null +++ b/pinball0/application.fam @@ -0,0 +1,19 @@ +# For details & more options, see documentation/AppManifests.md in firmware repo + +App( + appid="pinball0", + name="Pinball0", + apptype=FlipperAppType.EXTERNAL, + entry_point="pinball0_app", + stack_size=2 * 1024, # neede? + fap_category="Games", + requires=["gui"], + # Optional values + fap_version="0.4", + fap_icon="pinball0.png", # 10x10 1-bit PNG + fap_description="Pinball game", + fap_author="Roberto De Feo", + fap_weburl="https://github.com/rdefeo/pinball0", + fap_icon_assets="images", # Image assets to compile for this application + fap_file_assets="assets", +) diff --git a/pinball0/assets/tables/01_Basic.json b/pinball0/assets/tables/01_Basic.json new file mode 100644 index 000000000..b1e220d08 --- /dev/null +++ b/pinball0/assets/tables/01_Basic.json @@ -0,0 +1,143 @@ +{ + "name": "Basic", + "lives": { + "display": true + }, + "score": { + "display": true + }, + "balls": [ + { + "position": [ 600, 1110 ], + "velocity": [ 0, -16.0 ] + } + ], + // "plunger": { + // "position": [ 600, 1200 ], + // "size": 100 + // }, + "flippers": [ + { + "position": [ 130, 1200 ], + "side": "LEFT", + "size": 130 + }, + { + "position": [ 490, 1200 ], + "side": "RIGHT", + "size": 130 + } + ], + "bumpers": [ + { + "position": [ 180, 280 ], + "radius": 60 + }, + { + "position": [ 470, 280 ], + "radius": 60 + }, + { + "position": [ 320, 430 ], + "radius": 50 + }, + { + "position": [ 200, 600 ], + "radius": 25 + }, + { + "position": [ 440, 900 ], + "radius": 20 + }, + { + "position": [ 220, 920 ], + "radius": 30 + } + ], + "arcs": [ + { + // top dome + "position": [ 320, 320 ], + "radius": 310, + "start_angle": 0, + "end_angle": 180, + "surface": "INSIDE" + } + ], + "rails": [ + // left wall + { + "start": [ 0, 320 ], + "end": [ 0, 1150 ] + }, + // right wall + { + "start": [ 630, 1150 ], + "end": [ 630, 320 ] + }, + // left bottom rail + { + "start": [ 70, 900 ], + "end": [ 70, 1150 ], + "double_sided": true + }, + { + "start": [ 70, 1150 ], + "end": [ 130, 1180 ], + "double_sided": true + }, + // right bottom rail + { + "start": [ 560, 1150 ], + "end": [ 560, 900 ], + "double_sided": true + }, + { + "start": [ 490, 1180 ], + "end": [ 560, 1150 ], + "double_sided": true + }, + { + // left shooter stopper + "start": [ 40, 180 ], + "end": [ 120, 240 ], + "bounce": 0.65 + }, + { + // right shooter pass-through + "start": [ 510, 240 ], + "end": [ 600, 180 ], + "bounce": 0.65 + }, + { + // top middle rail + "start": [ 320, 140 ], + "end": [ 320, 220 ], + "double_sided": true + }, + { + // right side triangle thing 1/3 + "start": [ 560, 390 ], + "end": [ 560, 700 ] + }, + { + // right side triangle thing 2/3 + "start": [ 560, 700 ], + "end": [ 490, 640 ] + }, + { + // right side triangle thing 3/3 + "start": [ 490, 640 ], + "end": [ 560, 390 ] + }, + { + // left side thing 1/2 + "start": [ 0, 430 ], + "end": [ 70, 570 ] + }, + { + "start": [ 70, 570 ], + "end": [ 0, 750 ] + } + ] +} \ No newline at end of file diff --git a/pinball0/assets/tables/02_Classic.json b/pinball0/assets/tables/02_Classic.json new file mode 100644 index 000000000..7165a86f3 --- /dev/null +++ b/pinball0/assets/tables/02_Classic.json @@ -0,0 +1,143 @@ +{ + "name": "Classic", + "lives": { + "display": true, + "position": [ 20, 480 ], + "align": "VERTICAL" + }, + "score": { + "display": true, + "position": [ 23, 0 ] + }, + "balls": [ + { + "position": [ 600, 1110 ], + "velocity": [ 0, -12.0 ] + } + ], + "flippers": [ + { + "position": [ 130, 1200 ], + "side": "LEFT", + "size": 130 + }, + { + "position": [ 490, 1200 ], + "side": "RIGHT", + "size": 130 + } + ], + "bumpers": [ + { + "position": [ 200, 260 ], + "radius": 60 + }, + { + "position": [ 450, 200 ], + "radius": 60 + }, + { + "position": [ 280, 550 ], + "radius": 40 + }, + { + "position": [ 480, 500 ], + "radius": 40 + } + ], + "arcs": [ + { + // top right curve + "position": [ 440, 200 ], + "radius": 200, + "start_angle": 0, + "end_angle": 95, + "surface": "INSIDE" + }, + { + // top left curve + "position": [ 160, 240 ], + "radius": 160, + "start_angle": 95, + "end_angle": 180, + "surface": "INSIDE" + } + ], + "rails": [ + // left wall + { + "start": [ 0, 240 ], + "end": [ 0, 400 ] + }, + { + "start": [ 0, 740 ], + "end": [ 0, 1200 ] + }, + // right wall + { + "start": [ 630, 1200 ], + "end": [ 630, 160 ] + }, + // top roof + { + "start": [ 412, 1 ], + "end": [ 137, 81 ] + }, + // left wall fixture + { + "start": [ 0, 400 ], + "end": [ 80, 480 ], + "bounce": 1.08 + }, + { + "start": [ 80, 480 ], + "end": [ 80, 660 ], + "bounce": 1.1 + }, + { + "start": [ 80, 660 ], + "end": [ 0, 740 ], + "bounce": 1.08 + }, + // left bottom rail + { + "start": [ 70, 900 ], + "end": [ 70, 1150 ], + "double_sided": true + }, + { + "start": [ 70, 1150 ], + "end": [ 130, 1180 ], + "double_sided": true + }, + // right bottom rail + { + "start": [ 560, 1150 ], + "end": [ 560, 900 ], + "double_sided": true + }, + { + "start": [ 490, 1180 ], + "end": [ 560, 1150 ], + "double_sided": true + } + ], + "rollovers": [ + { + "position": [ 200, 800 ], + "symbol": "Z" + }, + { + "position": [ 280, 770 ], + "symbol": "E" + }, + { + "position": [ 360, 770 ], + "symbol": "R" + }, + { + "position": [ 440, 800 ], + "symbol": "O" + } + ] +} \ No newline at end of file diff --git a/pinball0/assets/tables/03_El Ocho.json b/pinball0/assets/tables/03_El Ocho.json new file mode 100644 index 000000000..1c7eadbcf --- /dev/null +++ b/pinball0/assets/tables/03_El Ocho.json @@ -0,0 +1,76 @@ +{ + "name": "El Ocho", + "balls": [ + { + "position": [ 580, 580 ], + "velocity": [ -9.0, -0.2 ] + } + ], + "flippers": [ + { + "position": [ 130, 1200 ], + "side": "LEFT", + "size": 125 + }, + { + "position": [ 490, 1200 ], + "side": "RIGHT", + "size": 125 + } + ], + "arcs": [ + { + // top + "position": [ 320, 330 ], + "radius": 310, + "start_angle": 290, + "end_angle": 250, // 250 / 610 + "surface": "INSIDE" + }, + { + // bottom left + "position": [ 320, 920 ], + "radius": 310, + "start_angle": 110, + "end_angle": 240, + "surface": "INSIDE" + }, + { + // bottom right + "position": [ 320, 920 ], + "radius": 310, + "start_angle": 300, + "end_angle": 70, + "surface": "INSIDE" + }, + { + "position": [ 320, 330 ], + "radius": 80, + "start_angle": 180, + "end_angle": 360, + "bounce": 1.1 + }, + { + "position": [ 320, 920 ], + "radius": 80, + "start_angle": 0, + "end_angle": 180, + "bounce": 1.1 + } + + ], + "portals": [ + { + "a_start": [ 400, 920 ], + "a_end": [ 240, 920 ], + "b_start": [ 240, 330 ], + "b_end": [ 400, 330 ] + } + ], + "bumpers": [ + { + "position": [ 320, 220 ], + "radius": 30 + } + ] +} \ No newline at end of file diff --git a/pinball0/assets/tables/04_Chamber.json b/pinball0/assets/tables/04_Chamber.json new file mode 100644 index 000000000..d30a82cfb --- /dev/null +++ b/pinball0/assets/tables/04_Chamber.json @@ -0,0 +1,114 @@ +{ + "name": "Chamber", + "balls": [ + { + "position": [ 390, 400 ] + } + ], + "flippers": [ + { + "position": [ 20, 1160 ], + "side": "LEFT", + "size": 220 + }, + { + "position": [ 610, 1160 ], + "side": "RIGHT", + "size": 220 + } + ], + "arcs": [ + { + "position": [ 147, 200 ], + "radius": 200, + "start_angle": 30, + "end_angle": 90, + "surface": "INSIDE" + }, + { + "position": [ 493, 200 ], + "radius": 200, + "start_angle": 90, + "end_angle": 150, + "surface": "INSIDE" + }, + { + "position": [ 147, 160 ], + "radius": 200, + "start_angle": 270, + "end_angle": 330, + "surface": "INSIDE" + }, + { + "position": [ 493, 160 ], + "radius": 200, + "start_angle": 210, + "end_angle": 270, + "surface": "INSIDE" + } + ], + "rails": [ + { + "start": [ 0, 0 ], + "end": [ 147, 0 ] + }, + { + "start": [ 493, 0 ], + "end": [ 630, 0 ] + }, + { + "start": [ 0, 360 ], + "end": [ 147, 360 ] + }, + { + "start": [ 493, 360 ], + "end": [ 630, 360 ] + }, + { + "start": [ 0, 360 ], + "end": [ 0, 1160 ] + }, + { + "start": [ 630, 1160 ], + "end": [ 630, 360 ] + }, + { + "start": [ 630, 360 ], + "end": [ 0, 360 ] + }, + { + "start": [ 0, 60 ], + "end": [ 0, 300 ] + }, + { + "start": [ 630, 300 ], + "end": [ 630, 60 ] + } + ], + "portals": [ + { + "a_start": [ 0, 300 ], + "a_end": [ 0, 360 ], + "b_start": [ 0, 1000 ], + "b_end": [ 120, 1000 ] + }, + { + "a_start": [ 630, 360 ], + "a_end": [ 630, 300 ], + "b_start": [ 260, 500 ], + "b_end": [ 160, 500 ] + }, + { + "a_start": [ 0, 0 ], + "a_end": [ 0, 60 ], + "b_start": [ 340, 800 ], + "b_end": [ 460, 800 ] + }, + { + "a_start": [ 630, 60 ], + "a_end": [ 630, 0 ], + "b_start": [ 630, 700 ], + "b_end": [ 510, 620 ] + } + ] +} \ No newline at end of file diff --git a/pinball0/assets/tables/05_Endless.json b/pinball0/assets/tables/05_Endless.json new file mode 100644 index 000000000..4e43de4ec --- /dev/null +++ b/pinball0/assets/tables/05_Endless.json @@ -0,0 +1,85 @@ +{ + "name": "Endless", + "lives": { + "value": 1 + }, + "tilt_detect": false, + "balls": [ + { + "position": [ 600, 510 ], + "velocity": [ 0, -10.0 ] + } + ], + "flippers": [ + { + "position": [ 20, 1160 ], + "side": "LEFT", + "size": 240 + }, + { + "position": [ 610, 1160 ], + "side": "RIGHT", + "size": 240 + } + ], + "bumpers": [ + { + "position": [ 200, 260 ], + "radius": 80 + }, + { + "position": [ 450, 200 ], + "radius": 60 + }, + { + "position": [ 300, 750 ], + "radius": 50 + }, + { + "position": [ 460, 500 ], + "radius": 60 + }, + { + "position": [ -30.0, 600 ], + "radius": 130 + } + ], + "arcs": [ + { + // top right curve + "position": [ 430, 200 ], + "radius": 200, + "start_angle": 0, + "end_angle": 90, + "surface": "INSIDE" + }, + { + // top left curve + "position": [ 200, 200 ], + "radius": 200, + "start_angle": 90, + "end_angle": 180, + "surface": "INSIDE" + } + ], + "rails": [ + // left wall + { + "start": [ 0, 200 ], + "end": [ 0, 1160 ] + }, + // right wall + { + "start": [ 630, 1160 ], + "end": [ 630, 200 ] + } + ], + "portals": [ + { + "a_start": [ 200, 1270 ], + "a_end": [ 440, 1270 ], + "b_start": [ 440, 0 ], + "b_end": [ 200, 0 ] + } + ] +} \ No newline at end of file diff --git a/pinball0/assets/tables/40_dbg Arc Test.json b/pinball0/assets/tables/40_dbg Arc Test.json new file mode 100644 index 000000000..56b05079e --- /dev/null +++ b/pinball0/assets/tables/40_dbg Arc Test.json @@ -0,0 +1,19 @@ +{ + "lives": { + "value": 1 + }, + "balls": [ + { + "position": [ 50, 140 ] + } + ], + "arcs": [ + { + "position": [ 320, 800 ], + "radius": 310, + "start_angle": 90, + "end_angle": 360, + "surface": "INSIDE" + } + ] +} \ No newline at end of file diff --git a/pinball0/assets/tables/50_dbg Bumpers.json b/pinball0/assets/tables/50_dbg Bumpers.json new file mode 100644 index 000000000..9298df446 --- /dev/null +++ b/pinball0/assets/tables/50_dbg Bumpers.json @@ -0,0 +1,60 @@ +{ + "balls": [ + { + "position": [ 250, 50 ] + } + ], + "flippers": [ + { + "position": [ 170, 1080 ], + "side": "LEFT", + "size": 120 + }, + { + "position": [ 470, 1080 ], + "side": "RIGHT", + "size": 120 + } + ], + "bumpers": [ + { + "position": [ 200, 260 ], + "radius": 60 + }, + { + "position": [ 450, 200 ], + "radius": 60 + }, + { + "position": [ 280, 550 ], + "radius": 80 + }, + { + "position": [ 480, 500 ], + "radius": 85 + } + ], + "rails": [ + // left wall + { + "start": [ 0, 0 ], + "end": [ 0, 1080 ] + }, + // right wall + { + "start": [ 630, 1080 ], + "end": [ 630, 0 ] + }, + // bottom left + { + "start": [ 0, 1080 ], + "end": [ 220, 1180 ] + + }, + // bottom right + { + "start": [ 420, 1180 ], + "end": [ 630, 1080 ] + } + ] +} \ No newline at end of file diff --git a/pinball0/assets/tables/70_dbg Platforms.json b/pinball0/assets/tables/70_dbg Platforms.json new file mode 100644 index 000000000..b525bd60b --- /dev/null +++ b/pinball0/assets/tables/70_dbg Platforms.json @@ -0,0 +1,49 @@ +{ + "name": "Platforms", + "balls": [ + { + "position": [ + 220, 60 + ] + } + ], + "rails": [ + { + "start": [ 0, 200 ], + "end": [ 280, 280 ] + }, + { + "start": [ 360, 500 ], + "end": [ 630, 420 ] + }, + { + "start": [ 0, 600 ], + "end": [ 280, 680 ] + }, + { + "start": [ 360, 880 ], + "end": [ 630, 800 ] + }, + // left wall + { + "start": [ 0, 0 ], + "end": [ 0, 1080 ] + }, + // right wall + { + "start": [ 630, 1080 ], + "end": [ 630, 0 ] + }, + // bottom left + { + "start": [ 0, 1080 ], + "end": [ 220, 1180 ] + + }, + // bottom right + { + "start": [ 420, 1180 ], + "end": [ 630, 1080 ] + } + ] +} \ No newline at end of file diff --git a/pinball0/assets/tables/95_dbg Error.json b/pinball0/assets/tables/95_dbg Error.json new file mode 100644 index 000000000..fc7cbdd93 --- /dev/null +++ b/pinball0/assets/tables/95_dbg Error.json @@ -0,0 +1,16 @@ +{ + "balls": [ + { + // oh noes! we don't have a position! + } + ], + "arcs": [ + { + "position": [ 320, 800 ], + "radius": 310, + "start_angle": 90, + "end_angle": 360, + "surface": "INSIDE" + } + ] +} \ No newline at end of file diff --git a/pinball0/graphics.cxx b/pinball0/graphics.cxx new file mode 100644 index 000000000..806cc3a6d --- /dev/null +++ b/pinball0/graphics.cxx @@ -0,0 +1,528 @@ +#include "graphics.h" + +#define SCALE 10 + +namespace { + +// Another algo - https://www.research-collection.ethz.ch/handle/20.500.11850/68976 + +/* + * Thick line methods courtesy + * https://github.com/ArminJo/Arduino-BlueDisplay/blob/master/src/LocalGUI/ThickLine.hpp + */ +const int LOCAL_DISPLAY_WIDTH = 64; +const int LOCAL_DISPLAY_HEIGHT = 128; +/* + * Overlap means drawing additional pixel when changing minor direction + * Needed for drawThickLine, otherwise some pixels will be missing in the thick line + */ +const int LINE_OVERLAP_NONE = 0; // No line overlap, like in standard Bresenham +const int LINE_OVERLAP_MAJOR = + 0x01; // Overlap - first go major then minor direction. Pixel is drawn as extension after actual line +const int LINE_OVERLAP_MINOR = + 0x02; // Overlap - first go minor then major direction. Pixel is drawn as extension before next line +const int LINE_OVERLAP_BOTH = 0x03; // Overlap - both + +const int LINE_THICKNESS_MIDDLE = 0; // Start point is on the line at center of the thick line +const int LINE_THICKNESS_DRAW_CLOCKWISE = 1; // Start point is on the counter clockwise border line +const int LINE_THICKNESS_DRAW_COUNTERCLOCKWISE = 2; // Start point is on the clockwise border line + +/** + * Draws a line from aXStart/aYStart to aXEnd/aYEnd including both ends + * @param aOverlap One of LINE_OVERLAP_NONE, LINE_OVERLAP_MAJOR, LINE_OVERLAP_MINOR, LINE_OVERLAP_BOTH + */ +void drawLineOverlap( + Canvas* canvas, + unsigned int aXStart, + unsigned int aYStart, + unsigned int aXEnd, + unsigned int aYEnd, + uint8_t aOverlap) { + int16_t tDeltaX, tDeltaY, tDeltaXTimes2, tDeltaYTimes2, tError, tStepX, tStepY; + + /* + * Clip to display size + */ + if(aXStart >= LOCAL_DISPLAY_WIDTH) { + aXStart = LOCAL_DISPLAY_WIDTH - 1; + } + + if(aXEnd >= LOCAL_DISPLAY_WIDTH) { + aXEnd = LOCAL_DISPLAY_WIDTH - 1; + } + + if(aYStart >= LOCAL_DISPLAY_HEIGHT) { + aYStart = LOCAL_DISPLAY_HEIGHT - 1; + } + + if(aYEnd >= LOCAL_DISPLAY_HEIGHT) { + aYEnd = LOCAL_DISPLAY_HEIGHT - 1; + } + + if((aXStart == aXEnd) || (aYStart == aYEnd)) { + // horizontal or vertical line -> fillRect() is faster than drawLine() + // fillRect( + // aXStart, + // aYStart, + // aXEnd, + // aYEnd, + // aColor); // you can remove the check and this line if you have no fillRect() or drawLine() available. + canvas_draw_box(canvas, aXStart, aYStart, aXEnd - aXStart, aYEnd - aYStart); + } else { + // calculate direction + tDeltaX = aXEnd - aXStart; + tDeltaY = aYEnd - aYStart; + if(tDeltaX < 0) { + tDeltaX = -tDeltaX; + tStepX = -1; + } else { + tStepX = +1; + } + if(tDeltaY < 0) { + tDeltaY = -tDeltaY; + tStepY = -1; + } else { + tStepY = +1; + } + tDeltaXTimes2 = tDeltaX << 1; + tDeltaYTimes2 = tDeltaY << 1; + // draw start pixel + // drawPixel(aXStart, aYStart, aColor); + canvas_draw_dot(canvas, aXStart, aYStart); + if(tDeltaX > tDeltaY) { + // start value represents a half step in Y direction + tError = tDeltaYTimes2 - tDeltaX; + while(aXStart != aXEnd) { + // step in main direction + aXStart += tStepX; + if(tError >= 0) { + if(aOverlap & LINE_OVERLAP_MAJOR) { + // draw pixel in main direction before changing + // drawPixel(aXStart, aYStart, aColor); + canvas_draw_dot(canvas, aXStart, aYStart); + } + // change Y + aYStart += tStepY; + if(aOverlap & LINE_OVERLAP_MINOR) { + // draw pixel in minor direction before changing + // drawPixel(aXStart - tStepX, aYStart, aColor); + canvas_draw_dot(canvas, aXStart - tStepX, aYStart); + } + tError -= tDeltaXTimes2; + } + tError += tDeltaYTimes2; + // drawPixel(aXStart, aYStart, aColor); + canvas_draw_dot(canvas, aXStart, aYStart); + } + } else { + tError = tDeltaXTimes2 - tDeltaY; + while(aYStart != aYEnd) { + aYStart += tStepY; + if(tError >= 0) { + if(aOverlap & LINE_OVERLAP_MAJOR) { + // draw pixel in main direction before changing + // drawPixel(aXStart, aYStart, aColor); + canvas_draw_dot(canvas, aXStart, aYStart); + } + aXStart += tStepX; + if(aOverlap & LINE_OVERLAP_MINOR) { + // draw pixel in minor direction before changing + // drawPixel(aXStart, aYStart - tStepY, aColor); + canvas_draw_dot(canvas, aXStart, aYStart - tStepY); + } + tError -= tDeltaYTimes2; + } + tError += tDeltaXTimes2; + // drawPixel(aXStart, aYStart, aColor); + canvas_draw_dot(canvas, aXStart, aYStart); + } + } + } +} + +/** + * Bresenham with thickness + * No pixel missed and every pixel only drawn once! + * The code is bigger and more complicated than drawThickLineSimple() but it tends to be faster, since drawing a pixel is often a slow operation. + * aThicknessMode can be one of LINE_THICKNESS_MIDDLE, LINE_THICKNESS_DRAW_CLOCKWISE, LINE_THICKNESS_DRAW_COUNTERCLOCKWISE + */ +void drawThickLine( + Canvas* canvas, + unsigned int aXStart, + unsigned int aYStart, + unsigned int aXEnd, + unsigned int aYEnd, + unsigned int aThickness, + uint8_t aThicknessMode) { + int16_t i, tDeltaX, tDeltaY, tDeltaXTimes2, tDeltaYTimes2, tError, tStepX, tStepY; + + if(aThickness <= 1) { + drawLineOverlap(canvas, aXStart, aYStart, aXEnd, aYEnd, LINE_OVERLAP_NONE); + } + /* + * Clip to display size + */ + if(aXStart >= LOCAL_DISPLAY_WIDTH) { + aXStart = LOCAL_DISPLAY_WIDTH - 1; + } + + if(aXEnd >= LOCAL_DISPLAY_WIDTH) { + aXEnd = LOCAL_DISPLAY_WIDTH - 1; + } + + if(aYStart >= LOCAL_DISPLAY_HEIGHT) { + aYStart = LOCAL_DISPLAY_HEIGHT - 1; + } + + if(aYEnd >= LOCAL_DISPLAY_HEIGHT) { + aYEnd = LOCAL_DISPLAY_HEIGHT - 1; + } + + /** + * For coordinate system with 0.0 top left + * Swap X and Y delta and calculate clockwise (new delta X inverted) + * or counterclockwise (new delta Y inverted) rectangular direction. + * The right rectangular direction for LINE_OVERLAP_MAJOR toggles with each octant + */ + tDeltaY = aXEnd - aXStart; + tDeltaX = aYEnd - aYStart; + // mirror 4 quadrants to one and adjust deltas and stepping direction + bool tSwap = true; // count effective mirroring + if(tDeltaX < 0) { + tDeltaX = -tDeltaX; + tStepX = -1; + tSwap = !tSwap; + } else { + tStepX = +1; + } + if(tDeltaY < 0) { + tDeltaY = -tDeltaY; + tStepY = -1; + tSwap = !tSwap; + } else { + tStepY = +1; + } + tDeltaXTimes2 = tDeltaX << 1; + tDeltaYTimes2 = tDeltaY << 1; + bool tOverlap; + // adjust for right direction of thickness from line origin + int tDrawStartAdjustCount = aThickness / 2; + if(aThicknessMode == LINE_THICKNESS_DRAW_COUNTERCLOCKWISE) { + tDrawStartAdjustCount = aThickness - 1; + } else if(aThicknessMode == LINE_THICKNESS_DRAW_CLOCKWISE) { + tDrawStartAdjustCount = 0; + } + + /* + * Now tDelta* are positive and tStep* define the direction + * tSwap is false if we mirrored only once + */ + // which octant are we now + if(tDeltaX >= tDeltaY) { + // Octant 1, 3, 5, 7 (between 0 and 45, 90 and 135, ... degree) + if(tSwap) { + tDrawStartAdjustCount = (aThickness - 1) - tDrawStartAdjustCount; + tStepY = -tStepY; + } else { + tStepX = -tStepX; + } + /* + * Vector for draw direction of the starting points of lines is rectangular and counterclockwise to main line direction + * Therefore no pixel will be missed if LINE_OVERLAP_MAJOR is used on change in minor rectangular direction + */ + // adjust draw start point + tError = tDeltaYTimes2 - tDeltaX; + for(i = tDrawStartAdjustCount; i > 0; i--) { + // change X (main direction here) + aXStart -= tStepX; + aXEnd -= tStepX; + if(tError >= 0) { + // change Y + aYStart -= tStepY; + aYEnd -= tStepY; + tError -= tDeltaXTimes2; + } + tError += tDeltaYTimes2; + } + // draw start line. We can alternatively use drawLineOverlap(aXStart, aYStart, aXEnd, aYEnd, LINE_OVERLAP_NONE, aColor) here. + // drawLine(aXStart, aYStart, aXEnd, aYEnd); + // canvas_draw_line(canvas, aXStart, aYStart, aXEnd, aYEnd); + drawLineOverlap(canvas, aXStart, aYStart, aXEnd, aYEnd, LINE_OVERLAP_NONE); + // draw aThickness number of lines + tError = tDeltaYTimes2 - tDeltaX; + for(i = aThickness; i > 1; i--) { + // change X (main direction here) + aXStart += tStepX; + aXEnd += tStepX; + tOverlap = LINE_OVERLAP_NONE; + if(tError >= 0) { + // change Y + aYStart += tStepY; + aYEnd += tStepY; + tError -= tDeltaXTimes2; + /* + * Change minor direction reverse to line (main) direction + * because of choosing the right (counter)clockwise draw vector + * Use LINE_OVERLAP_MAJOR to fill all pixel + * + * EXAMPLE: + * 1,2 = Pixel of first 2 lines + * 3 = Pixel of third line in normal line mode + * - = Pixel which will additionally be drawn in LINE_OVERLAP_MAJOR mode + * 33 + * 3333-22 + * 3333-222211 + * 33-22221111 + * 221111 ^ + * 11 Main direction of start of lines draw vector + * -> Line main direction + * <- Minor direction of counterclockwise of start of lines draw vector + */ + tOverlap = LINE_OVERLAP_MAJOR; + } + tError += tDeltaYTimes2; + drawLineOverlap(canvas, aXStart, aYStart, aXEnd, aYEnd, tOverlap); + } + } else { + // the other octant 2, 4, 6, 8 (between 45 and 90, 135 and 180, ... degree) + if(tSwap) { + tStepX = -tStepX; + } else { + tDrawStartAdjustCount = (aThickness - 1) - tDrawStartAdjustCount; + tStepY = -tStepY; + } + // adjust draw start point + tError = tDeltaXTimes2 - tDeltaY; + for(i = tDrawStartAdjustCount; i > 0; i--) { + aYStart -= tStepY; + aYEnd -= tStepY; + if(tError >= 0) { + aXStart -= tStepX; + aXEnd -= tStepX; + tError -= tDeltaYTimes2; + } + tError += tDeltaXTimes2; + } + //draw start line + // drawLine(aXStart, aYStart, aXEnd, aYEnd); + canvas_draw_line(canvas, aXStart, aYStart, aXEnd, aYEnd); + // draw aThickness number of lines + tError = tDeltaXTimes2 - tDeltaY; + for(i = aThickness; i > 1; i--) { + aYStart += tStepY; + aYEnd += tStepY; + tOverlap = LINE_OVERLAP_NONE; + if(tError >= 0) { + aXStart += tStepX; + aXEnd += tStepX; + tError -= tDeltaYTimes2; + tOverlap = LINE_OVERLAP_MAJOR; + } + tError += tDeltaXTimes2; + drawLineOverlap(canvas, aXStart, aYStart, aXEnd, aYEnd, tOverlap); + } + } +} +/** + * The same as before, but no clipping to display range, some pixel are drawn twice (because of using LINE_OVERLAP_BOTH) + * and direction of thickness changes for each octant (except for LINE_THICKNESS_MIDDLE and aThickness value is odd) + * aThicknessMode can be LINE_THICKNESS_MIDDLE or any other value + * + */ +/* +void drawThickLineSimple( + Canvas* canvas, + unsigned int aXStart, + unsigned int aYStart, + unsigned int aXEnd, + unsigned int aYEnd, + unsigned int aThickness, + uint8_t aThicknessMode) { + int16_t i, tDeltaX, tDeltaY, tDeltaXTimes2, tDeltaYTimes2, tError, tStepX, tStepY; + + tDeltaY = aXStart - aXEnd; + tDeltaX = aYEnd - aYStart; + // mirror 4 quadrants to one and adjust deltas and stepping direction + if(tDeltaX < 0) { + tDeltaX = -tDeltaX; + tStepX = -1; + } else { + tStepX = +1; + } + if(tDeltaY < 0) { + tDeltaY = -tDeltaY; + tStepY = -1; + } else { + tStepY = +1; + } + tDeltaXTimes2 = tDeltaX << 1; + tDeltaYTimes2 = tDeltaY << 1; + bool tOverlap; + // which octant are we now + if(tDeltaX > tDeltaY) { + if(aThicknessMode == LINE_THICKNESS_MIDDLE) { + // adjust draw start point + tError = tDeltaYTimes2 - tDeltaX; + for(i = aThickness / 2; i > 0; i--) { + // change X (main direction here) + aXStart -= tStepX; + aXEnd -= tStepX; + if(tError >= 0) { + // change Y + aYStart -= tStepY; + aYEnd -= tStepY; + tError -= tDeltaXTimes2; + } + tError += tDeltaYTimes2; + } + } + // draw start line. We can alternatively use drawLineOverlap(aXStart, aYStart, aXEnd, aYEnd, LINE_OVERLAP_NONE, aColor) here. + // drawLine(aXStart, aYStart, aXEnd, aYEnd, aColor); + canvas_draw_line(canvas, aXStart, aYStart, aXEnd, aYEnd); + // draw aThickness lines + tError = tDeltaYTimes2 - tDeltaX; + for(i = aThickness; i > 1; i--) { + // change X (main direction here) + aXStart += tStepX; + aXEnd += tStepX; + tOverlap = LINE_OVERLAP_NONE; + if(tError >= 0) { + // change Y + aYStart += tStepY; + aYEnd += tStepY; + tError -= tDeltaXTimes2; + tOverlap = LINE_OVERLAP_BOTH; + } + tError += tDeltaYTimes2; + drawLineOverlap(canvas, aXStart, aYStart, aXEnd, aYEnd, tOverlap); + } + } else { + // adjust draw start point + if(aThicknessMode == LINE_THICKNESS_MIDDLE) { + tError = tDeltaXTimes2 - tDeltaY; + for(i = aThickness / 2; i > 0; i--) { + aYStart -= tStepY; + aYEnd -= tStepY; + if(tError >= 0) { + aXStart -= tStepX; + aXEnd -= tStepX; + tError -= tDeltaYTimes2; + } + tError += tDeltaXTimes2; + } + } + // draw start line. We can alternatively use drawLineOverlap(aXStart, aYStart, aXEnd, aYEnd, LINE_OVERLAP_NONE, aColor) here. + // drawLine(aXStart, aYStart, aXEnd, aYEnd, aColor); + canvas_draw_line(canvas, aXStart, aYStart, aXEnd, aYEnd); + tError = tDeltaXTimes2 - tDeltaY; + for(i = aThickness; i > 1; i--) { + aYStart += tStepY; + aYEnd += tStepY; + tOverlap = LINE_OVERLAP_NONE; + if(tError >= 0) { + aXStart += tStepX; + aXEnd += tStepX; + tError -= tDeltaYTimes2; + tOverlap = LINE_OVERLAP_BOTH; + } + tError += tDeltaXTimes2; + drawLineOverlap(canvas, aXStart, aYStart, aXEnd, aYEnd, tOverlap); + } + } +} +*/ + +}; // namespace + +/* + Fontname: micro + Copyright: Public domain font. Share and enjoy. + Glyphs: 18/128 + BBX Build Mode: 0 +*/ +const uint8_t u8g2_font_micro_tn[148] = + "\22\0\2\3\2\3\1\4\4\3\5\0\0\5\0\5\0\0\0\0\0\0w \4`\63*\10\67\62Q" + "j\312\0+\7or\321\24\1,\5*r\3-\5\247\62\3.\5*\62\4/\10\67\262\251\60\12" + "\1\60\10\67r)U\12\0\61\6\66rS\6\62\7\67\62r\224\34\63\7\67\62r$\22\64\7\67" + "\62\221\212\14\65\7\67\62\244<\1\66\6\67r#E\67\10\67\62c*\214\0\70\6\67\62TE\71" + "\7\67\62\24\71\1:\6\66\62$\1\0\0\0\4\377\377\0"; + +// TODO: allow points to be located outside the canvas. currently, the canvas_* methods +// choke on this in some cases, resulting in large vertical/horizontal lines +void gfx_draw_line(Canvas* canvas, float x1, float y1, float x2, float y2) { + canvas_draw_line( + canvas, roundf(x1 / SCALE), roundf(y1 / SCALE), roundf(x2 / SCALE), roundf(y2 / SCALE)); +} + +void gfx_draw_line(Canvas* canvas, const Vec2& p1, const Vec2& p2) { + gfx_draw_line(canvas, p1.x, p1.y, p2.x, p2.y); +} + +void gfx_draw_line_thick(Canvas* canvas, float x1, float y1, float x2, float y2, int thickness) { + x1 = roundf(x1 / SCALE); + y1 = roundf(y1 / SCALE); + x2 = roundf(x2 / SCALE); + y2 = roundf(y2 / SCALE); + + drawThickLine(canvas, x1, y1, x2, y2, thickness, LINE_THICKNESS_MIDDLE); +} + +void gfx_draw_line_thick(Canvas* canvas, const Vec2& p1, const Vec2& p2, int thickness) { + gfx_draw_line_thick(canvas, p1.x, p1.y, p2.x, p2.y, thickness); +} + +void gfx_draw_disc(Canvas* canvas, float x, float y, float r) { + canvas_draw_disc(canvas, roundf(x / SCALE), roundf(y / SCALE), roundf(r / SCALE)); +} +void gfx_draw_disc(Canvas* canvas, const Vec2& p, float r) { + gfx_draw_disc(canvas, p.x, p.y, r); +} + +void gfx_draw_circle(Canvas* canvas, float x, float y, float r) { + canvas_draw_circle(canvas, roundf(x / SCALE), roundf(y / SCALE), roundf(r / SCALE)); +} +void gfx_draw_circle(Canvas* canvas, const Vec2& p, float r) { + gfx_draw_circle(canvas, p.x, p.y, r); +} + +void gfx_draw_dot(Canvas* canvas, float x, float y) { + canvas_draw_dot(canvas, roundf(x / SCALE), roundf(y / SCALE)); +} +void gfx_draw_dot(Canvas* canvas, const Vec2& p) { + gfx_draw_dot(canvas, p.x, p.y); +} + +void gfx_draw_arc(Canvas* canvas, const Vec2& p, float r, float start, float end) { + float adj_end = end; + if(end < start) { + adj_end += (float)M_PI * 2; + } + // initialize to start of arc + float sx = p.x + r * cosf(start); + float sy = p.y - r * sinf(start); + size_t segments = r / 8; + for(size_t i = 1; i <= segments; i++) { // for now, use r to determin number of segments + float nx = p.x + r * cosf(start + i / (segments / (adj_end - start))); + float ny = p.y - r * sinf(start + i / (segments / (adj_end - start))); + gfx_draw_line(canvas, sx, sy, nx, ny); + sx = nx; + sy = ny; + } +} + +void gfx_draw_str(Canvas* canvas, int x, int y, Align h, Align v, const char* str) { + canvas_set_custom_u8g2_font(canvas, u8g2_font_micro_tn); + + canvas_set_color(canvas, ColorWhite); + int w = canvas_string_width(canvas, str); + if(h == AlignRight) { + canvas_draw_box(canvas, x - 1 - w, y, w + 2, 6); + } else if(h == AlignLeft) { + canvas_draw_box(canvas, x - 1, y, w + 2, 6); + } + + canvas_set_color(canvas, ColorBlack); + canvas_draw_str_aligned(canvas, x, y, h, v, str); + + canvas_set_font(canvas, FontSecondary); // reset? +} diff --git a/pinball0/graphics.h b/pinball0/graphics.h new file mode 100644 index 000000000..6a0239d27 --- /dev/null +++ b/pinball0/graphics.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "vec2.h" + +// Use to draw table elements, which live on a 640 x 1280 grid +// These methods will scale and round the coordinates +// Also, they will (eventually) handle cases where the thing we're drawing +// lies outside the table bounds. + +void gfx_draw_line(Canvas* canvas, float x1, float y1, float x2, float y2); +void gfx_draw_line(Canvas* canvas, const Vec2& p1, const Vec2& p2); + +void gfx_draw_line_thick(Canvas* canvas, float x1, float y1, float x2, float y2, int thickness); +void gfx_draw_line_thick(Canvas* canvas, const Vec2& p1, const Vec2& p2, int thickness); + +void gfx_draw_disc(Canvas* canvas, float x, float y, float r); +void gfx_draw_disc(Canvas* canvas, const Vec2& p, float r); + +void gfx_draw_circle(Canvas* canvas, float x, float y, float r); +void gfx_draw_circle(Canvas* canvas, const Vec2& p, float r); + +void gfx_draw_dot(Canvas* canvas, float x, float y); +void gfx_draw_dot(Canvas* canvas, const Vec2& p); + +void gfx_draw_arc(Canvas* canvas, const Vec2& p, float r, float start, float end); + +// Uses the micro font +void gfx_draw_str(Canvas* canvas, int x, int y, Align h, Align v, const char* str); diff --git a/pinball0/images/.gitkeep b/pinball0/images/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/pinball0/images/Arcade_A.png b/pinball0/images/Arcade_A.png new file mode 100644 index 000000000..630611ae4 Binary files /dev/null and b/pinball0/images/Arcade_A.png differ diff --git a/pinball0/images/Arcade_E.png b/pinball0/images/Arcade_E.png new file mode 100644 index 000000000..657d6d492 Binary files /dev/null and b/pinball0/images/Arcade_E.png differ diff --git a/pinball0/images/Arcade_G.png b/pinball0/images/Arcade_G.png new file mode 100644 index 000000000..28ec94a28 Binary files /dev/null and b/pinball0/images/Arcade_G.png differ diff --git a/pinball0/images/Arcade_I.png b/pinball0/images/Arcade_I.png new file mode 100644 index 000000000..9afbf603c Binary files /dev/null and b/pinball0/images/Arcade_I.png differ diff --git a/pinball0/images/Arcade_L.png b/pinball0/images/Arcade_L.png new file mode 100644 index 000000000..407b06c97 Binary files /dev/null and b/pinball0/images/Arcade_L.png differ diff --git a/pinball0/images/Arcade_M.png b/pinball0/images/Arcade_M.png new file mode 100644 index 000000000..fe84e71e4 Binary files /dev/null and b/pinball0/images/Arcade_M.png differ diff --git a/pinball0/images/Arcade_O.png b/pinball0/images/Arcade_O.png new file mode 100644 index 000000000..35efaf1bf Binary files /dev/null and b/pinball0/images/Arcade_O.png differ diff --git a/pinball0/images/Arcade_R.png b/pinball0/images/Arcade_R.png new file mode 100644 index 000000000..e7cd5df72 Binary files /dev/null and b/pinball0/images/Arcade_R.png differ diff --git a/pinball0/images/Arcade_T.png b/pinball0/images/Arcade_T.png new file mode 100644 index 000000000..1084519fe Binary files /dev/null and b/pinball0/images/Arcade_T.png differ diff --git a/pinball0/images/Arcade_V.png b/pinball0/images/Arcade_V.png new file mode 100644 index 000000000..01b600fcc Binary files /dev/null and b/pinball0/images/Arcade_V.png differ diff --git a/pinball0/images/pinball0_logo.png b/pinball0/images/pinball0_logo.png new file mode 100644 index 000000000..a31607cc9 Binary files /dev/null and b/pinball0/images/pinball0_logo.png differ diff --git a/pinball0/notifications.cxx b/pinball0/notifications.cxx new file mode 100644 index 000000000..a039bec1c --- /dev/null +++ b/pinball0/notifications.cxx @@ -0,0 +1,276 @@ +#include "notifications.h" +#include "pinball0.h" + +static const NotificationMessage* nm_list[32]; + +// static FuriMutex* nm_mutex; + +// void notify_init() { +// nm_mutex = furi_mutex_alloc(FuriMutexTypeNormal); +// } +// void notify_free() { +// furi_mutex_free(nm_mutex); +// } + +void notify_ball_released(void* ctx) { + PinballApp* app = (PinballApp*)ctx; + int n = 0; + if(app->settings.vibrate_enabled) { + nm_list[n++] = &message_vibro_on; + } + nm_list[n++] = &message_delay_100; + if(app->settings.vibrate_enabled) { + nm_list[n++] = &message_vibro_off; + } + nm_list[n] = NULL; + notification_message(app->notify, &nm_list); +} + +void notify_table_bump(void* ctx) { + PinballApp* app = (PinballApp*)ctx; + int n = 0; + if(app->settings.vibrate_enabled) { + nm_list[n++] = &message_vibro_on; + } + if(app->settings.led_enabled) { + nm_list[n++] = &message_red_255; + } + nm_list[n++] = &message_delay_100; + if(app->settings.vibrate_enabled) { + nm_list[n++] = &message_vibro_off; + } + if(app->settings.led_enabled) { + nm_list[n++] = &message_red_0; + } + nm_list[n] = NULL; + notification_message(app->notify, &nm_list); +} + +void notify_table_tilted(void* ctx) { + PinballApp* app = (PinballApp*)ctx; + int n = 0; + + for(int i = 0; i < 2; i++) { + nm_list[n++] = &message_display_backlight_off; + if(app->settings.vibrate_enabled) { + nm_list[n++] = &message_vibro_on; + } + if(app->settings.led_enabled) { + nm_list[n++] = &message_red_255; + } + nm_list[n++] = &message_delay_500; + + nm_list[n++] = &message_display_backlight_on; + if(app->settings.vibrate_enabled) { + nm_list[n++] = &message_vibro_off; + } + if(app->settings.led_enabled) { + nm_list[n++] = &message_red_0; + } + } + nm_list[n] = NULL; + notification_message(app->notify, &nm_list); +} + +void notify_error_message(void* ctx) { + PinballApp* app = (PinballApp*)ctx; + int n = 0; + if(app->settings.sound_enabled) { + nm_list[n++] = &message_note_c6; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_sound_off; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_c5; + nm_list[n++] = &message_delay_250; + nm_list[n++] = &message_sound_off; + } + nm_list[n] = NULL; + notification_message(app->notify, &nm_list); +} + +void notify_game_over(void* ctx) { + PinballApp* app = (PinballApp*)ctx; + int n = 0; + if(app->settings.sound_enabled) { + nm_list[n++] = &message_delay_500; + nm_list[n++] = &message_note_b5; + nm_list[n++] = &message_delay_250; + nm_list[n++] = &message_note_f6; + nm_list[n++] = &message_delay_250; + nm_list[n++] = &message_sound_off; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_f6; + nm_list[n++] = &message_delay_100; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_f6; + nm_list[n++] = &message_delay_100; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_e6; + nm_list[n++] = &message_delay_100; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_d6; + nm_list[n++] = &message_delay_100; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_c6; + nm_list[n++] = &message_delay_1000; + nm_list[n++] = &message_sound_off; + } + nm_list[n] = NULL; + notification_message(app->notify, &nm_list); +} + +void notify_bumper_hit(void* ctx) { + FURI_LOG_I(TAG, "notify_bumper_hit"); + PinballApp* app = (PinballApp*)ctx; + int n = 0; + if(app->settings.led_enabled) { + nm_list[n++] = &message_blue_255; + } + if(app->settings.sound_enabled) { + nm_list[n++] = &message_note_f4; + nm_list[n++] = &message_delay_10; + nm_list[n++] = &message_note_f5; + nm_list[n++] = &message_delay_10; + nm_list[n++] = &message_sound_off; + } + if(app->settings.led_enabled) { + nm_list[n++] = &message_blue_0; + } + nm_list[n] = NULL; + notification_message(app->notify, &nm_list); +} + +void notify_rail_hit(void* ctx) { + PinballApp* app = (PinballApp*)ctx; + int n = 0; + if(app->settings.sound_enabled) { + nm_list[n++] = &message_note_d4; + nm_list[n++] = &message_delay_10; + nm_list[n++] = &message_note_d5; + nm_list[n++] = &message_delay_10; + nm_list[n++] = &message_sound_off; + } + nm_list[n] = NULL; + notification_message(app->notify, &nm_list); +} + +void notify_portal(void* ctx) { + PinballApp* app = (PinballApp*)ctx; + int n = 0; + if(app->settings.led_enabled) { + nm_list[n++] = &message_blue_255; + nm_list[n++] = &message_red_255; + } + if(app->settings.sound_enabled) { + nm_list[n++] = &message_note_c4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_e4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_b4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_c5; + nm_list[n++] = &message_delay_50; + } + if(app->settings.led_enabled) { + nm_list[n++] = &message_blue_255; + nm_list[n++] = &message_red_0; + } + if(app->settings.sound_enabled) { + nm_list[n++] = &message_note_e4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_g4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_c5; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_e5; + nm_list[n++] = &message_delay_50; + + nm_list[n++] = &message_sound_off; + } + if(app->settings.led_enabled) { + nm_list[n++] = &message_blue_0; + } + nm_list[n] = NULL; + notification_message(app->notify, &nm_list); +} + +void notify_lost_life(void* ctx) { + PinballApp* app = (PinballApp*)ctx; + int n = 0; + if(app->settings.led_enabled) { + nm_list[n++] = &message_red_255; + nm_list[n++] = &message_green_255; + } + if(app->settings.sound_enabled) { + nm_list[n++] = &message_note_c5; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_c4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_b4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_b3; + nm_list[n++] = &message_delay_50; + } + if(app->settings.led_enabled) { + nm_list[n++] = &message_green_0; + } + if(app->settings.sound_enabled) { + nm_list[n++] = &message_note_as4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_as3; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_a4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_a3; + nm_list[n++] = &message_delay_50; + } + if(app->settings.led_enabled) { + nm_list[n++] = &message_green_255; + } + if(app->settings.sound_enabled) { + nm_list[n++] = &message_note_gs4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_gs3; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_g4; + nm_list[n++] = &message_delay_50; + nm_list[n++] = &message_note_g4; + nm_list[n++] = &message_delay_50; + + nm_list[n++] = &message_sound_off; + } + if(app->settings.led_enabled) { + nm_list[n++] = &message_red_0; + nm_list[n++] = &message_green_0; + } + furi_assert(n < 32); + nm_list[n] = NULL; + notification_message_block(app->notify, &nm_list); +} + +void notify_flipper(void* ctx) { + PinballApp* app = (PinballApp*)ctx; + int n = 0; + if(app->settings.sound_enabled) { + nm_list[n++] = &message_note_c4; + nm_list[n++] = &message_delay_10; + nm_list[n++] = &message_note_cs4; + nm_list[n++] = &message_delay_10; + nm_list[n++] = &message_sound_off; + } + nm_list[n] = NULL; + notification_message(app->notify, &nm_list); +} + +/* +Mario coin sound - ish + nm_list[n++] = &message_note_b5; + nm_list[n++] = &message_delay_10; + nm_list[n++] = &message_delay_10; + nm_list[n++] = &message_delay_10; + nm_list[n++] = &message_sound_off; + nm_list[n++] = &message_note_e6; + nm_list[n++] = &message_delay_250; + nm_list[n++] = &message_delay_100; + +*/ diff --git a/pinball0/notifications.h b/pinball0/notifications.h new file mode 100644 index 000000000..acaa646f5 --- /dev/null +++ b/pinball0/notifications.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +// void notify_init(); +// void notify_free(); + +void notify_ball_released(void* ctx); +void notify_table_bump(void* ctx); +void notify_table_tilted(void* ctx); + +void notify_error_message(void* ctx); +void notify_game_over(void* ctx); + +void notify_bumper_hit(void* ctx); +void notify_rail_hit(void* ctx); + +void notify_portal(void* ctx); +void notify_lost_life(void* ctx); + +void notify_flipper(void* ctx); diff --git a/pinball0/nxjson/README.md b/pinball0/nxjson/README.md new file mode 100644 index 000000000..e742b652c --- /dev/null +++ b/pinball0/nxjson/README.md @@ -0,0 +1,142 @@ +NXJSON +================================ + +Very small JSON parser written in C. + +## Features + +- Parses JSON from null-terminated string +- Easy to use tree traversal API +- Allows // line and /\* block \*/ comments (except before colon ':') +- Operates on single-byte or multi-byte characters (like UTF-8), but not on wide characters +- Unescapes string values (including Unicode codepoints & surrogates) +- Can use custom Unicode encoder, UTF-8 encoder built in +- Can use custom memory allocator +- Can use custom macro to print errors +- Test suite included + +## Limitations + +- Non-validating parser; might accept invalid JSON (eg., extra or missing commas, comments, octal or hex numeric values, etc.) + +## API + +Parsed JSON tree consists of nodes. Each node has type: + + typedef enum nx_json_type { + NX_JSON_NULL, // this is null value + NX_JSON_OBJECT, // this is an object; properties can be found in child nodes + NX_JSON_ARRAY, // this is an array; items can be found in child nodes + NX_JSON_STRING, // this is a string; value can be found in text_value field + NX_JSON_INTEGER, // this is an integer; value can be found in int_value field + NX_JSON_float, // this is a float; value can be found in dbl_value field + NX_JSON_BOOL // this is a boolean; value can be found in int_value field + } nx_json_type; + +The node itself: + + typedef struct nx_json { + nx_json_type type; // type of json node, see above + const char* key; // key of the property; for object's children only + const char* text_value; // text value of STRING node + long int_value; // the value of INTEGER or BOOL node + float dbl_value; // the value of float node + int length; // number of children of OBJECT or ARRAY + nx_json* child; // points to first child + nx_json* next; // points to next child + } nx_json; + +#### Parsing + + const nx_json* nx_json_parse(char* text, nx_json_unicode_encoder encoder); + +Parses null-terminated string `text` into `nx_json` tree structure. The string is **modified in place**. + +Parsing ends right after retrieving first valid JSON value. Remainder of the text is not analysed. + +Returns `NULL` on syntax error. Error details are printed out using user-redefinable macro `NX_JSON_REPORT_ERROR(msg, ptr)`. + +Inside parse function `nx_json` nodes get allocated using user-redefinable macro `NX_JSON_CALLOC()` and freed by `NX_JSON_FREE(json)`. + +All `text_value` pointers refer to the content of original `text` string, which is modified in place to unescape and null-terminate JSON string literals. + +`encoder` is a function defined as follows: + + int unicode_to_my_encoding(unsigned int codepoint, char* p, char** endp) { ... } + +Encoder takes Unicode codepoint and writes corresponding encoded value into buffer pointed by `p`. It should store pointer to the end of encoded value into `*endp`. The function should return 1 on success and 0 on error. Number of bytes written must not exceed 6. + +NXJSON includes sample encoder `nx_json_unicode_to_utf8`, which converts all `\uXXXX` escapes into UTF-8 sequences. + +In case `encoder` parameter is `NULL` all unicode escape sequences (`\uXXXX`) are ignored (remain untouched). + + const nx_json* nx_json_parse_utf8(char* text); + +This is shortcut for `nx_json_parse(text, nx_json_unicode_to_utf8)` where `nx_json_unicode_to_utf8` is unicode to UTF-8 encoder provided by NXJSON. + + void nx_json_free(const nx_json* js); + +Frees resources (`nx_json` nodes) allocated by `nx_json_parse()`. + +#### Traversal + + const nx_json* nx_json_get(const nx_json* json, const char* key); + +Gets object's property by key. + +If `json` points to `OBJECT` node returns first the object's property identified by key `key`. + +If there is no such property returns *dummy* node of type `NX_JSON_NULL`. Never returns literal `NULL`. + + const nx_json* nx_json_item(const nx_json* json, int idx); + +Gets array's item by its index. + +If `json` points to `ARRAY` node returns array's element identified by index `idx`. + +If `json` points to `OBJECT` node returns object's property identified by index `idx`. + +If there is no such item/property returns *dummy* node of type `NX_JSON_NULL`. Never returns literal `NULL`. + +## Usage Example + +JSON code: + + { + "some-int": 195, + "array": [ 3, 5.1, -7, "nine", /*11*/ ], + "some-bool": true, + "some-dbl": -1e-4, + "some-null": null, + "hello": "world!", + //"other": "/OTHER/", + "obj": {"KEY": "VAL"} + } + +C API: + + const nx_json* json=nx_json_parse(code, 0); + if (json) { + printf("some-int=%ld\n", nx_json_get(json, "some-int")->int_value); + printf("some-dbl=%lf\n", nx_json_get(json, "some-dbl")->dbl_value); + printf("some-bool=%s\n", nx_json_get(json, "some-bool")->int_value? "true":"false"); + printf("some-null=%s\n", nx_json_get(json, "some-null")->text_value); + printf("hello=%s\n", nx_json_get(json, "hello")->text_value); + printf("other=%s\n", nx_json_get(json, "other")->text_value); + printf("KEY=%s\n", nx_json_get(nx_json_get(json, "obj"), "KEY")->text_value); + const nx_json* arr=nx_json_get(json, "array"); + int i; + for (i=0; ilength; i++) { + const nx_json* item=nx_json_item(arr, i); + printf("arr[%d]=(%d) %ld %lf %s\n", i, (int)item->type, item->int_value, item->dbl_value, item->text_value); + } + nx_json_free(json); + } + +## License + +LGPL v3 + +## Copyright + +Copyright (c) 2013 Yaroslav Stavnichiy diff --git a/pinball0/nxjson/nxjson.c b/pinball0/nxjson/nxjson.c new file mode 100644 index 000000000..a55ad8dc2 --- /dev/null +++ b/pinball0/nxjson/nxjson.c @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2013 Yaroslav Stavnichiy + * + * This file is part of NXJSON. + * + * NXJSON is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * NXJSON is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with NXJSON. If not, see . + */ + +// this file can be #included in your code +#ifndef NXJSON_C +#define NXJSON_C + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include + +#include "nxjson.h" + +// redefine NX_JSON_CALLOC & NX_JSON_FREE to use custom allocator +#ifndef NX_JSON_CALLOC +#define NX_JSON_CALLOC() calloc(1, sizeof(nx_json)) +#define NX_JSON_FREE(json) free((void*)(json)) +#endif + +// redefine NX_JSON_REPORT_ERROR to use custom error reporting +#ifndef NX_JSON_REPORT_ERROR +#define NX_JSON_REPORT_ERROR(msg, p) FURI_LOG_E("nxjson", "PARSE ERROR (%d): at %s", __LINE__, p) +#endif + +#define IS_WHITESPACE(c) ((unsigned char)(c) <= (unsigned char)' ') + +static nx_json* create_json(nx_json_type type, const char* key, nx_json* parent) { + nx_json* js = NX_JSON_CALLOC(); + assert(js); + js->type = type; + js->key = key; + if(!parent->children.last) { + parent->children.first = parent->children.last = js; + } else { + parent->children.last->next = js; + parent->children.last = js; + } + parent->children.length++; + return js; +} + +void nx_json_free(const nx_json* js) { + if(!js) { + return; + } + if(js->type == NX_JSON_OBJECT || js->type == NX_JSON_ARRAY) { + nx_json* p = js->children.first; + nx_json* p1; + while(p) { + p1 = p->next; + nx_json_free(p); + p = p1; + } + } + NX_JSON_FREE(js); +} + +static int unicode_to_utf8(unsigned int codepoint, char* p, char** endp) { + // code from http://stackoverflow.com/a/4609989/697313 + if(codepoint < 0x80) + *p++ = codepoint; + else if(codepoint < 0x800) + *p++ = 192 + codepoint / 64, *p++ = 128 + codepoint % 64; + else if(codepoint - 0xd800u < 0x800) + return 0; // surrogate must have been treated earlier + else if(codepoint < 0x10000) + *p++ = 224 + codepoint / 4096, *p++ = 128 + codepoint / 64 % 64, + *p++ = 128 + codepoint % 64; + else if(codepoint < 0x110000) + *p++ = 240 + codepoint / 262144, *p++ = 128 + codepoint / 4096 % 64, + *p++ = 128 + codepoint / 64 % 64, *p++ = 128 + codepoint % 64; + else + return 0; // error + *endp = p; + return 1; +} + +nx_json_unicode_encoder nx_json_unicode_to_utf8 = unicode_to_utf8; + +static inline int hex_val(char c) { + if(c >= '0' && c <= '9') return c - '0'; + if(c >= 'a' && c <= 'f') return c - 'a' + 10; + if(c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static char* unescape_string(char* s, char** end, nx_json_unicode_encoder encoder) { + char* p = s; + char* d = s; + char c; + while((c = *p++)) { + if(c == '"') { + *d = '\0'; + *end = p; + return s; + } else if(c == '\\') { + switch(*p) { + case '\\': + case '/': + case '"': + *d++ = *p++; + break; + case 'b': + *d++ = '\b'; + p++; + break; + case 'f': + *d++ = '\f'; + p++; + break; + case 'n': + *d++ = '\n'; + p++; + break; + case 'r': + *d++ = '\r'; + p++; + break; + case 't': + *d++ = '\t'; + p++; + break; + case 'u': // unicode + if(!encoder) { + // leave untouched + *d++ = c; + break; + } + char* ps = p - 1; + int h1, h2, h3, h4; + if((h1 = hex_val(p[1])) < 0 || (h2 = hex_val(p[2])) < 0 || + (h3 = hex_val(p[3])) < 0 || (h4 = hex_val(p[4])) < 0) { + NX_JSON_REPORT_ERROR("invalid unicode escape", p - 1); + return 0; + } + unsigned int codepoint = h1 << 12 | h2 << 8 | h3 << 4 | h4; + if((codepoint & 0xfc00) == + 0xd800) { // high surrogate; need one more unicode to succeed + p += 6; + if(p[-1] != '\\' || *p != 'u' || (h1 = hex_val(p[1])) < 0 || + (h2 = hex_val(p[2])) < 0 || (h3 = hex_val(p[3])) < 0 || + (h4 = hex_val(p[4])) < 0) { + NX_JSON_REPORT_ERROR("invalid unicode surrogate", ps); + return 0; + } + unsigned int codepoint2 = h1 << 12 | h2 << 8 | h3 << 4 | h4; + if((codepoint2 & 0xfc00) != 0xdc00) { + NX_JSON_REPORT_ERROR("invalid unicode surrogate", ps); + return 0; + } + codepoint = 0x10000 + ((codepoint - 0xd800) << 10) + (codepoint2 - 0xdc00); + } + if(!encoder(codepoint, d, &d)) { + NX_JSON_REPORT_ERROR("invalid codepoint", ps); + return 0; + } + p += 5; + break; + default: + // leave untouched + *d++ = c; + break; + } + } else { + *d++ = c; + } + } + NX_JSON_REPORT_ERROR("no closing quote for string", s); + return 0; +} + +static char* skip_block_comment(char* p) { + // assume p[-2]=='/' && p[-1]=='*' + char* ps = p - 2; + if(!*p) { + NX_JSON_REPORT_ERROR("endless comment", ps); + return 0; + } +REPEAT: + p = strchr(p + 1, '/'); + if(!p) { + NX_JSON_REPORT_ERROR("endless comment", ps); + return 0; + } + if(p[-1] != '*') goto REPEAT; + return p + 1; +} + +static char* parse_key(const char** key, char* p, nx_json_unicode_encoder encoder) { + // on '}' return with *p=='}' + char c; + while((c = *p++)) { + if(c == '"') { + *key = unescape_string(p, &p, encoder); + if(!*key) return 0; // propagate error + while(*p && IS_WHITESPACE(*p)) + p++; + if(*p == ':') return p + 1; + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; + } else if(IS_WHITESPACE(c) || c == ',') { + // continue + } else if(c == '}') { + return p - 1; + } else if(c == '/') { + if(*p == '/') { // line comment + char* ps = p - 1; + p = strchr(p + 1, '\n'); + if(!p) { + NX_JSON_REPORT_ERROR("endless comment", ps); + return 0; // error + } + p++; + } else if(*p == '*') { // block comment + p = skip_block_comment(p + 1); + if(!p) return 0; + } else { + NX_JSON_REPORT_ERROR("unexpected chars", p - 1); + return 0; // error + } + } else { + NX_JSON_REPORT_ERROR("unexpected chars", p - 1); + return 0; // error + } + } + NX_JSON_REPORT_ERROR("unexpected chars", p - 1); + return 0; // error +} + +static char* + parse_value(nx_json* parent, const char* key, char* p, nx_json_unicode_encoder encoder) { + nx_json* js; + while(1) { + switch(*p) { + case '\0': + NX_JSON_REPORT_ERROR("unexpected end of text", p); + return 0; // error + case ' ': + case '\t': + case '\n': + case '\r': + case ',': + // skip + p++; + break; + case '{': + js = create_json(NX_JSON_OBJECT, key, parent); + p++; + while(1) { + const char* new_key = NULL; + p = parse_key(&new_key, p, encoder); + if(!p) return 0; // error + if(*p == '}') return p + 1; // end of object + p = parse_value(js, new_key, p, encoder); + if(!p) return 0; // error + } + case '[': + js = create_json(NX_JSON_ARRAY, key, parent); + p++; + while(1) { + p = parse_value(js, 0, p, encoder); + if(!p) return 0; // error + if(*p == ']') return p + 1; // end of array + } + case ']': + return p; + case '"': + p++; + js = create_json(NX_JSON_STRING, key, parent); + js->text_value = unescape_string(p, &p, encoder); + if(!js->text_value) return 0; // propagate error + return p; + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + js = create_json(NX_JSON_INTEGER, key, parent); + char* pe; + if(*p == '-') { + js->num.s_value = (nxjson_s64)strtol(p, &pe, 0); // was strtoll + } else { + js->num.u_value = (nxjson_u64)strtoul(p, &pe, 0); // was stroull + } + if(pe == p || errno == ERANGE) { + NX_JSON_REPORT_ERROR("invalid number", p); + return 0; // error + } + if(*pe == '.' || *pe == 'e' || *pe == 'E') { // float value + js->type = NX_JSON_float; + js->num.dbl_value = strtod(p, &pe); + if(pe == p || errno == ERANGE) { + NX_JSON_REPORT_ERROR("invalid number", p); + return 0; // error + } + } else { + if(*p == '-') { + js->num.dbl_value = js->num.s_value; + } else { + js->num.dbl_value = js->num.u_value; + } + } + return pe; + } + case 't': + if(!strncmp(p, "true", 4)) { + js = create_json(NX_JSON_BOOL, key, parent); + js->num.u_value = 1; + return p + 4; + } + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; // error + case 'f': + if(!strncmp(p, "false", 5)) { + js = create_json(NX_JSON_BOOL, key, parent); + js->num.u_value = 0; + return p + 5; + } + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; // error + case 'n': + if(!strncmp(p, "null", 4)) { + create_json(NX_JSON_NULL, key, parent); + return p + 4; + } + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; // error + case '/': // comment + if(p[1] == '/') { // line comment + char* ps = p; + p = strchr(p + 2, '\n'); + if(!p) { + NX_JSON_REPORT_ERROR("endless comment", ps); + return 0; // error + } + p++; + } else if(p[1] == '*') { // block comment + p = skip_block_comment(p + 2); + if(!p) return 0; + } else { + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; // error + } + break; + default: + NX_JSON_REPORT_ERROR("unexpected chars", p); + return 0; // error + } + } +} + +const nx_json* nx_json_parse_utf8(char* text) { + return nx_json_parse(text, unicode_to_utf8); +} + +const nx_json* nx_json_parse(char* text, nx_json_unicode_encoder encoder) { + nx_json js = {0}; + if(!parse_value(&js, 0, text, encoder)) { + if(js.children.first) nx_json_free(js.children.first); + return 0; + } + return js.children.first; +} + +const nx_json* nx_json_get(const nx_json* json, const char* key) { + nx_json* js; + for(js = json->children.first; js; js = js->next) { + if(js->key && !strcmp(js->key, key)) return js; + } + return NULL; +} + +const nx_json* nx_json_item(const nx_json* json, int idx) { + nx_json* js; + for(js = json->children.first; js; js = js->next) { + if(!idx--) return js; + } + return NULL; +} + +#ifdef __cplusplus +} +#endif + +#endif /* NXJSON_C */ diff --git a/pinball0/nxjson/nxjson.h b/pinball0/nxjson/nxjson.h new file mode 100644 index 000000000..abb3f8e2a --- /dev/null +++ b/pinball0/nxjson/nxjson.h @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2013 Yaroslav Stavnichiy + * + * This file is part of NXJSON. + * + * NXJSON is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * NXJSON is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with NXJSON. If not, see . + */ + +#ifndef NXJSON_H +#define NXJSON_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef NXJSON_TYPE_U64 + +#include + +typedef uint64_t nxjson_u64; +#endif + +#ifndef NXJSON_TYPE_S64 + +#include + +typedef uint64_t nxjson_s64; +#endif + +typedef enum nx_json_type { + NX_JSON_NULL, // this is null value + NX_JSON_OBJECT, // this is an object; properties can be found in child nodes + NX_JSON_ARRAY, // this is an array; items can be found in child nodes + NX_JSON_STRING, // this is a string; value can be found in text_value field + NX_JSON_INTEGER, // this is an integer; value can be found in int_value field + NX_JSON_float, // this is a float; value can be found in dbl_value field + NX_JSON_BOOL // this is a boolean; value can be found in int_value field +} nx_json_type; + +typedef struct nx_json { + nx_json_type type; // type of json node, see above + const char* key; // key of the property; for object's children only + union { + const char* text_value; // text value of STRING node + struct { + union { + nxjson_u64 u_value; // the value of INTEGER or BOOL node + nxjson_s64 s_value; + }; + float dbl_value; // the value of float node + } num; + struct { // children of OBJECT or ARRAY + int length; + struct nx_json* first; + struct nx_json* last; + } children; + }; + struct nx_json* next; // points to next child +} nx_json; + +typedef int (*nx_json_unicode_encoder)(unsigned int codepoint, char* p, char** endp); + +extern nx_json_unicode_encoder nx_json_unicode_to_utf8; + +const nx_json* nx_json_parse(char* text, nx_json_unicode_encoder encoder); + +const nx_json* nx_json_parse_utf8(char* text); + +void nx_json_free(const nx_json* js); + +const nx_json* nx_json_get(const nx_json* json, const char* key); // get object's property by key +const nx_json* nx_json_item(const nx_json* json, int idx); // get array element by index + +#ifdef __cplusplus +} +#endif + +#endif /* NXJSON_H */ diff --git a/pinball0/objects.cxx b/pinball0/objects.cxx new file mode 100644 index 000000000..4308cf912 --- /dev/null +++ b/pinball0/objects.cxx @@ -0,0 +1,670 @@ +#include +#include + +#include "objects.h" +#include "pinball0.h" +#include "graphics.h" + +Object::Object(const Vec2& p_, float r_) + : p(p_) + , prev_p(p_) + , a({0.0, 0.0}) + , r(r_) + , physical(true) + , bounce(1.0f) + , fixed(false) + , score(0) { +} + +void Object::update(float dt) { + if(fixed) { + return; + } + Vec2 velocity = p - prev_p; + // table friction / damping + velocity *= 0.9999f; + prev_p = p; + p = p + velocity + a + (dt * dt); + a = {0.0, 0.0}; +} + +void Ball::draw(Canvas* canvas) { + gfx_draw_disc(canvas, p, r); +} + +Flipper::Flipper(const Vec2& p_, Side side_, size_t size_) + : p(p_) + , side(side_) + , size(size_) + , r(20.0f) + , max_rotation(1.0f) + , omega(4.0f) + , rotation(0.0f) + , powered(false) + , score(50) + , notification(nullptr) { + if(side_ == Side::LEFT) { + rest_angle = -0.4f; + sign = 1; + } else { + rest_angle = M_PI + 0.4; + sign = -1; + } +} + +void Flipper::draw(Canvas* canvas) { + // tip + float angle = rest_angle + sign * rotation; + Vec2 dir(cos(angle), -sin(angle)); + + // draw the tip + Vec2 tip = p + dir * size; + gfx_draw_line_thick(canvas, p, tip, (r * 1.5f) / 10.0f); + gfx_draw_disc(canvas, tip, r * 0.6f); + + // // base / pivot + // gfx_draw_circle(canvas, p, r); + + // // tip + // float angle = rest_angle + sign * rotation; + // Vec2 dir(cos(angle), -sin(angle)); + + // // draw the tip + // Vec2 tip = p + dir * size; + // gfx_draw_circle(canvas, tip, r); + + // // top and bottom lines + // Vec2 perp(-dir.y, dir.x); + // perp.normalize(); + // Vec2 start = p + perp * r; + // Vec2 end = start + dir * size; + // gfx_draw_line(canvas, start, end); + + // perp *= -1.0f; + // start = p + perp * r; + // end = start + dir * size; + // gfx_draw_line(canvas, start, end); +} + +void Flipper::update(float dt) { + float prev_rotation = rotation; + if(powered) { + rotation = fmin(rotation + dt * omega, max_rotation); + } else { + rotation = fmax(rotation - dt * omega, 0.0f); + } + current_omega = sign * (rotation - prev_rotation) / dt; +} + +bool Flipper::collide(Ball& ball) { + Vec2 closest = Vec2_closest(p, get_tip(), ball.p); + Vec2 dir = ball.p - closest; + float dist = dir.mag(); + if(dist <= VEC2_EPSILON || dist > ball.r + r) { + return false; + } + dir = dir / dist; + + Vec2 ball_v = ball.p - ball.prev_p; + + // adjust ball position + float corr = ball.r + r - dist; + ball.p += dir * corr; + + closest += dir * r; + closest -= p; + Vec2 perp(-closest.y, closest.x); + perp *= -1.0f; + perp.normalize(); + Vec2 surface_velocity = perp * 1.7f; // TODO: flipper power?? + FURI_LOG_I(TAG, "sv: %.3f,%.3f", (double)surface_velocity.x, (double)surface_velocity.y); + if(current_omega != 0.0f) surface_velocity *= current_omega; + FURI_LOG_I(TAG, "sv: %.3f,%.3f", (double)surface_velocity.x, (double)surface_velocity.y); + + // TODO: Flippers currently aren't "bouncy" when they are still + float v = ball_v.dot(dir); + float v_new = surface_velocity.dot(dir); + FURI_LOG_I(TAG, "v_new: %.4f, v: %.4f", (double)v_new, (double)v); + ball_v += dir * (v_new - v); + ball.prev_p = ball.p - ball_v; + return true; +} + +Vec2 Flipper::get_tip() const { + float angle = rest_angle + sign * rotation; + Vec2 dir(cos(angle), -sin(angle)); + Vec2 tip = p + dir * size; + return tip; +} + +void Polygon::draw(Canvas* canvas) { + if(!hidden) { + for(size_t i = 0; i < points.size() - 1; i++) { + gfx_draw_line(canvas, points[i], points[i + 1]); +#ifdef DRAW_NORMALS + Vec2 c = (points[i] + points[i + 1]) / 2.0f; + Vec2 e = c + normals[i] * 40.0f; + gfX_draw_line(canvas, c, e); +#endif + } + } +} + +// Attempt to handle double_sided rails better +bool Polygon::collide(Ball& ball) { + Vec2 ball_v = ball.p - ball.prev_p; + Vec2 dir; + Vec2 closest = points[0]; + Vec2 normal = normals[0]; + float min_dist = infinityf(); + + for(size_t i = 0; i < points.size() - 1; i++) { + Vec2& p1 = points[i]; + Vec2& p2 = points[i + 1]; + + Vec2 c = Vec2_closest(p1, p2, ball.p); + dir = ball.p - c; + float dist = dir.mag(); + if(dist < min_dist) { + min_dist = dist; + closest = c; + normal = normals[i]; + } + } + dir = ball.p - closest; + float dist = dir.mag(); + if(dist > ball.r) { + return false; + } + + if(dist <= VEC2_EPSILON) { + dir = normal; + dist = normal.mag(); + } + dir = dir / dist; + if(ball_v.dot(normal) < 0.0f) { + // FURI_LOG_I(TAG, "Collision Moving TOWARDS"); + ball.p += dir * (ball.r - dist); + } else { + // TODO: This is key - we're moving away, so don't alter our v / prev_p! + // FURI_LOG_I(TAG, "Collision Moving AWAY"); + return false; + // ball.p += dir * -(dist + ball.r); + } + + // FURI_LOG_I( + // TAG, + // "p: %.3f,%.3f dir: %.3f,%.3f norm: %.3f,%.3f", + // (double)ball.p.x, + // (double)ball.p.y, + // (double)dir.x, + // (double)dir.y, + // (double)normal.x, + // (double)normal.y); + float v = ball_v.dot(dir); + float v_new = fabs(v) * bounce; + ball_v += dir * (v_new - v); + ball.prev_p = ball.p - ball_v; + return true; +} + +// Works-ish - 11/5/2024 +// bool Polygon::collide(Ball& ball) { +// Vec2 ball_v = ball.p - ball.prev_p; +// // We need to check for collisions across all line segments +// for(size_t i = 0; i < points.size() - 1; i++) { +// // If ball is moving away from the line, we can't have a collision! +// if(normals[i].dot(ball_v) > 0) { +// continue; +// } + +// Vec2& p1 = points[i]; +// Vec2& p2 = points[i + 1]; +// // bool isLeft_prev = Vec2_ccw(p1, p2, ball.prev_p); +// // bool isLeft = Vec2_ccw(p1, p2, ball.p); +// Vec2 closest = Vec2_closest(p1, p2, ball.p); +// float dist = ball.p.dist(closest); + +// if(dist < ball.r) { +// // FURI_LOG_I(TAG, "... within collision distance!"); +// // ball_v.dot + +// // float factor = (ball.r - dist) / ball.r; +// // ball.p -= normals[i] * factor; +// float depth = ball.r - dist; +// ball.p -= normals[i] * depth * 1.05f; + +// Vec2 rel_v = ball_v * -1; +// float velAlongNormal = rel_v.dot(normals[i]); +// float j = (-(1 + 1) * velAlongNormal); +// Vec2 impulse = j * normals[i]; +// ball_v -= impulse; + +// ball.prev_p = ball.p - ball_v; +// return true; +// } +// } +// return false; +// } + +void Polygon::finalize() { + if(points.size() < 2) { + FURI_LOG_E(TAG, "Polygon: FINALIZE_ERROR - insufficient points"); + return; + } + // compute and store normals on all segments + for(size_t i = 0; i < points.size() - 1; i++) { + Vec2 normal(points[i + 1].y - points[i].y, points[i].x - points[i + 1].x); + normal.normalize(); + normals.push_back(normal); + } +} + +void Portal::draw(Canvas* canvas) { + if(!hidden) { + Vec2 d; + Vec2 e; + + // Portal A + gfx_draw_line(canvas, a1, a2); + d = a1 + au * amag * 0.33f; + e = d + na * 20.0f; + gfx_draw_line(canvas, d, e); + d += au * amag * 0.33f; + e = d + na * 20.0f; + gfx_draw_line(canvas, d, e); + + // Portal B + gfx_draw_line(canvas, b1, b2); + d = b1 + bu * bmag * 0.33f; + e = d + nb * 20.0f; + gfx_draw_line(canvas, d, e); + d += bu * bmag * 0.33f; + e = d + nb * 20.0f; + gfx_draw_line(canvas, d, e); + + if(decay > 0) { + gfx_draw_circle(canvas, enter_p, 20); + } + } +#ifdef DRAW_NORMALS + Vec2 c = (a1 + a2) / 2.0f; + Vec2 e = c + na * 40.0f; + gfx_draw_line(canvas, c, e); + c = (b1 + b2) / 2.0f; + e = c + nb * 40.0f; + gfx_draw_line(canvas, c, e); +#endif +} + +// TODO: simplify this code? +bool Portal::collide(Ball& ball) { + Vec2 ball_v = ball.p - ball.prev_p; + float dist; + + Vec2 a_cl = Vec2_closest(a1, a2, ball.p); + dist = (ball.p - a_cl).mag(); + if(dist <= ball.r && ball_v.dot(na) < 0.0f) { + // entering portal a! move it to portal b + // how far "along" the portal are we? + enter_p = a_cl; + float offset = (a_cl - a1).mag() / amag; + ball.p = b2 - bu * (bmag * offset); + // ensure we're "outside" the next portal to prevent rapid re-entry + ball.p += nb * ball.r; + + // get projections on entry portal + float m = -ball_v.dot(au); // tangent magnitude + float n = ball_v.dot(na); // normal magnitude + + // FURI_LOG_I( + // TAG, + // "v: %.3f,%.3f u: %.3f,%.3f n: %.3f,%.3f M: %.3f N: %.3f", + // (double)ball_v.x, + // (double)ball_v.y, + // (double)au.x, + // (double)au.y, + // (double)na.x, + // (double)na.y, + // (double)m, + // (double)n); + + // transform to exit portal + ball_v.x = bu.x * m - nb.x * n; + ball_v.y = bu.y * m - nb.y * n; + FURI_LOG_I(TAG, "new v: %.3f,%.3f", (double)ball_v.x, (double)ball_v.y); + + ball.prev_p = ball.p - ball_v; + return true; + } + + Vec2 b_cl = Vec2_closest(b1, b2, ball.p); + dist = (ball.p - b_cl).mag(); + if(dist <= ball.r && ball_v.dot(nb) < 0.0f) { + // entering portal b! move it to portal a + // how far "along" the portal are we? + enter_p = b_cl; + float offset = (b_cl - b1).mag() / bmag; + ball.p = a2 - au * (amag * offset); + // ensure we're "outside" the next portal to prevent rapid re-entry + ball.p += na * ball.r; + + // get projections on entry portal + float m = -ball_v.dot(bu); // tangent magnitude + float n = ball_v.dot(nb); // normal magnitude + + // FURI_LOG_I( + // TAG, + // "v: %.3f,%.3f u: %.3f,%.3f n: %.3f,%.3f M: %.3f N: %.3f", + // (double)ball_v.x, + // (double)ball_v.y, + // (double)bu.x, + // (double)bu.y, + // (double)nb.x, + // (double)nb.y, + // (double)m, + // (double)n); + + // transform to exit portal + ball_v.x = au.x * m - na.x * n; + ball_v.y = au.y * m - na.y * n; + FURI_LOG_I(TAG, "new v: %.3f,%.3f", (double)ball_v.x, (double)ball_v.y); + + ball.prev_p = ball.p - ball_v; + return true; + } + return false; +} + +void Portal::reset_animation() { + decay = 8; +} +void Portal::step_animation() { + if(decay > 0) { + decay--; + } else { + decay = 0; + } +} + +void Portal::finalize() { + na = Vec2(a2.y - a1.y, a1.x - a2.x); + na.normalize(); + amag = (a2 - a1).mag(); + au = (a2 - a1) / amag; + nb = Vec2(b2.y - b1.y, b1.x - b2.x); + nb.normalize(); + bmag = (b2 - b1).mag(); + bu = (b2 - b1) / bmag; +} + +Arc::Arc(const Vec2& p_, float r_, float s_, float e_, Surface surf_) + : FixedObject() + , p(p_) + , r(r_) + , start(s_) + , end(e_) + , surface(surf_) { +} + +void Arc::draw(Canvas* canvas) { + if(start == 0 && end == (float)M_PI * 2) { + gfx_draw_circle(canvas, p, r); + } else { + float adj_end = end; + if(end < start) { + adj_end += (float)M_PI * 2; + } + // initialize to start of arc + float sx = p.x + r * cosf(start); + float sy = p.y - r * sinf(start); + size_t segments = r / 8; + for(size_t i = 1; i <= segments; i++) { // for now, use r to determin number of segments + float nx = p.x + r * cosf(start + i / (segments / (adj_end - start))); + float ny = p.y - r * sinf(start + i / (segments / (adj_end - start))); + gfx_draw_line(canvas, sx, sy, nx, ny); + sx = nx; + sy = ny; + } + } +} + +// returns value between 0 and 2 PI +// assumes x,y are on cartesean plane, thus you should pass it a neg y +// since the display on flipper is y-inverted +float vector_to_angle(float x, float y) { + if(x == 0) // special cases UP or DOWN + return (y > 0) ? M_PI_2 : (y == 0) ? 0 : M_PI + M_PI_2; + else if(y == 0) // special cases LEFT or RIGHT + return (x >= 0) ? 0 : M_PI; + float ret = atanf(y / x); // quadrant I + if(x < 0 && y < 0) // quadrant III + ret = (float)M_PI + ret; + else if(x < 0) // quadrant II + ret = (float)M_PI + ret; // it actually substracts + else if(y < 0) // quadrant IV + ret = (float)M_PI + (float)M_PI_2 + ((float)M_PI_2 + ret); // it actually substracts + return ret; +} + +// Matthias research - 10 minute physics +bool Arc::collide(Ball& ball) { + Vec2 dir = ball.p - p; + float dist = dir.mag(); + // FURI_LOG_I( + // TAG, + // "ball.p: %.3f,%.3f p: %.3f,%.3f", + // (double)ball.p.x, + // (double)ball.p.y, + // (double)p.x, + // (double)p.y); + // FURI_LOG_I(TAG, "dir: %.3f,%.3f dist: %.3f", (double)dir.x, (double)dir.y, (double)dist); + + if(surface == OUTSIDE) { + if(dist > r + ball.r) { + return false; + } + // FURI_LOG_I(TAG, "hitting arc"); + float angle = vector_to_angle(dir.x, -dir.y); + if((start < end && start <= angle && angle <= end) || + (start > end && (angle >= start || angle <= end))) { + // FURI_LOG_I(TAG, "colliding with arc"); + dir.normalize(); + + Vec2 ball_v = ball.p - ball.prev_p; + float corr = ball.r + r - dist; + ball.p += dir * corr; + float v = ball_v.dot(dir); + ball_v += dir * (3.0f - v); // TODO: pushVel, this should be a prop + ball.prev_p = ball.p - ball_v; + return true; + } + } + if(surface == INSIDE) { + Vec2 prev_dir = ball.prev_p - p; + float prev_dist = prev_dir.mag(); + if(prev_dist < r && dist + ball.r > r) { + // FURI_LOG_I(TAG, "Inside an arc!"); + float angle = vector_to_angle(dir.x, -dir.y); + // FURI_LOG_I(TAG, "%f : %f : %f", (double)start, (double)angle, (double)end); + // if(angle >= start && angle <= end) { + if((start < end && start <= angle && angle <= end) || + (start > end && (angle >= start || angle <= end))) { + // FURI_LOG_I(TAG, "Within the arc angle"); + + dir.normalize(); + Vec2 ball_v = ball.p - ball.prev_p; + + // correct our position to be "on" the arc + float corr = dist + ball.r - r; + ball.p -= dir * corr; + + // Adjust restitution on tangent and normals independently + Vec2 tangent = {-dir.y, dir.x}; + float T = (ball_v.x * tangent.x + ball_v.y * tangent.y) * ARC_TANGENT_RESTITUTION; + float N = (ball_v.x * tangent.y - ball_v.y * tangent.x) * ARC_NORMAL_RESTITUTION; + + ball_v.x = tangent.x * T - tangent.y * N; + ball_v.y = tangent.y * T + tangent.x * N; + + // Current collision - works good, but handles restitution holistically + // float v = ball_v.dot(dir); + // ball_v -= dir * v * 2.0f * bounce; + + ball.prev_p = ball.p - ball_v; + return true; + } + } + } + return false; +} + +Bumper::Bumper(const Vec2& p_, float r_) + : Arc(p_, r_) { + score = 500; +} + +void Bumper::draw(Canvas* canvas) { + Arc::draw(canvas); + if(decay) { + // canvas_draw_disc(canvas, p.x / 10, p.y / 10, (r / 10) * 0.8f * (decay / 30.0f)); + gfx_draw_disc(canvas, p, r * 0.8f * (decay / 30.0f)); + } +} +void Bumper::reset_animation() { + decay = 30; +} + +void Bumper::step_animation() { + if(decay > 20) { + decay--; + } else { + decay = 0; + } +} + +void Rollover::draw(Canvas* canvas) { + if(activated) { + canvas_draw_str_aligned(canvas, p.x / 10, p.y / 10, AlignCenter, AlignCenter, c); + } else { + gfx_draw_dot(canvas, p); + } +} + +bool Rollover::collide(Ball& ball) { + Vec2 dir = ball.p - p; + float dist = dir.mag(); + if(dist < 30) { + activated = true; + } + return false; +} + +void Turbo::draw(Canvas* canvas) { + gfx_draw_line(canvas, chevron_1[0], chevron_1[1]); + gfx_draw_line(canvas, chevron_1[1], chevron_1[2]); + + gfx_draw_line(canvas, chevron_2[0], chevron_2[1]); + gfx_draw_line(canvas, chevron_2[1], chevron_2[2]); +} + +bool Turbo::collide(Ball& ball) { + Vec2 dir = ball.p - p; + float dist = dir.mag(); + if(dist < 30) { + // apply the turbo in 'dir' with force of 'boost' + FURI_LOG_I(TAG, "TURBO! dir: %.3f,%.3f", (double)dir.x, (double)dir.y); + ball.accelerate(dir * (boost / 50.0f)); + } + return false; +} + +Plunger::Plunger(const Vec2& p_) + : Object(p_, 20) + , size(100) { + compression = 0; +} + +void Plunger::draw(Canvas* canvas) { + // draw the end / striker + canvas_draw_circle(canvas, p.x / 10, p.y / 10, r / 10); + // draw a line, adjusted for compression + // canvas_draw_line( + // canvas, + // roundf(p.x), + // roundf(p.y), + // roundf(p2.x), + // //roundf(me->p.y - (plunger->size - plunger->compression)) + // roundf(p2.y)); +} + +void Chaser::draw(Canvas* canvas) { + Vec2& p1 = points[0]; + Vec2& p2 = points[1]; + + // TODO: feels like we can do all this with less code? + switch(style) { + case Style::SLASH: // / / / / / / / / / + if(p1.x == p2.x) { + int start = p1.y; + int end = p2.y; + if(start < end) { + for(int y = start + offset; y < end; y += gap) { + canvas_draw_line(canvas, p1.x - 2, y + 2, p1.x + 2, y - 2); + } + } else { + for(int y = start - offset; y > end; y -= gap) { + canvas_draw_line(canvas, p1.x - 2, y + 2, p1.x + 2, y - 2); + } + } + } else if(p1.y == p2.y) { + int start = p1.x; + int end = p2.x; + if(start < end) { + for(int x = start + offset; x < end; x += gap) { + canvas_draw_line(canvas, x - 2, p1.y + 2, x + 2, p1.y - 2); + } + } else { + for(int x = start - offset; x > end; x -= gap) { + canvas_draw_line(canvas, x - 2, p1.y + 2, x + 2, p1.y - 2); + } + } + } + break; + default: // Style::SIMPLE, just dots + // for all pixels between p and q, draw them with offset and gap + if(p1.x == p2.x) { + int start = p1.y; + int end = p2.y; + if(start < end) { + for(int y = start + offset; y < end; y += gap) { + canvas_draw_disc(canvas, p1.x, y, 1); + } + } else { + for(int y = start - offset; y > end; y -= gap) { + canvas_draw_disc(canvas, p1.x, y, 1); + } + } + } else if(p1.y == p2.y) { + int start = p1.x; + int end = p2.x; + if(start < end) { + for(int x = start + offset; x < end; x += gap) { + canvas_draw_disc(canvas, x, p1.y, 1); + } + } else { + for(int x = start - offset; x > end; x -= gap) { + canvas_draw_disc(canvas, x, p1.y, 1); + } + } + } + break; + } +} + +void Chaser::step_animation() { + tick++; + if(tick % (speed) == 0) { + offset = (offset + 1) % gap; + } +} diff --git a/pinball0/objects.h b/pinball0/objects.h new file mode 100644 index 000000000..a3030e27f --- /dev/null +++ b/pinball0/objects.h @@ -0,0 +1,295 @@ +#pragma once +#include +#include "vec2.h" +#include // for Canvas* + +#define DEF_BALL_RADIUS 20 +#define DEF_BUMPER_RADIUS 40 +#define DEF_BUMPER_BOUNCE 1.0f +#define DEF_FLIPPER_SIZE 120 +#define DEF_RAIL_BOUNCE 0.9f + +#define ARC_TANGENT_RESTITUTION 1.0f +#define ARC_NORMAL_RESTITUTION 0.8f + +// A dynamic, moveable object with acceleration +class Object { +public: + Object(const Vec2& p_, float r_); + virtual ~Object() = default; + + // Verlet data + Vec2 p; // position + Vec2 prev_p; // previous position + Vec2 a; + float r; + + bool physical; // is this a real object that can be hit? + float bounce; // < 1 dampens, > 1 adds power + bool fixed; // should this move? + int score; // incremental score for hitting this + + void update(float dt); // updates position + inline void accelerate(const Vec2& da) { + a += da; + } + inline void add_velocity(const Vec2& v, float dt) { + prev_p -= v * dt; + } + + virtual void draw(Canvas* canvas) = 0; +}; + +class Ball : public Object { +public: + Ball(const Vec2& p_ = Vec2(), float r_ = DEF_BALL_RADIUS) + : Object(p_, r_) { + } + void draw(Canvas* canvas); +}; + +class Flipper { +public: + enum Side { + LEFT, + RIGHT + }; + + Flipper(const Vec2& p_, Side side, size_t size_ = DEF_FLIPPER_SIZE); + + void draw(Canvas* canvas); + void update(float dt); // updates position to new position + bool collide(Ball& ball); + + Vec2 get_tip() const; + + Vec2 p; + Side side; + size_t size; + float r; + + float rest_angle; + float max_rotation; + float sign; + float omega; // angular velocity + + float rotation; + float current_omega; + + bool powered; // is this flipper being activated? i.e. is keypad pressed? + + int score; + void (*notification)(void* app); +}; + +// A static object that never moves and can be any shape +class FixedObject { +public: + FixedObject() + : bounce(1.0f) + , physical(true) + , hidden(false) + , score(0) + , notification(nullptr) { + } + virtual ~FixedObject() = default; + + float bounce; + bool physical; // can be hit + bool hidden; // do not draw + int score; + + void (*notification)(void* app); + + virtual void draw(Canvas* canvas) = 0; + virtual bool collide(Ball& ball) = 0; + virtual void reset_animation() {}; + virtual void step_animation() {}; +}; + +class Polygon : public FixedObject { +public: + Polygon() + : FixedObject() {}; + + std::vector points; + std::vector normals; + + void draw(Canvas* canvas); + bool collide(Ball& ball); + void add_point(const Vec2& np) { + points.push_back(np); + } + void finalize(); +}; + +class Portal : public FixedObject { +public: + Portal(const Vec2& a1_, const Vec2& a2_, const Vec2& b1_, const Vec2& b2_) + : FixedObject() + , a1(a1_) + , a2(a2_) + , b1(b1_) + , b2(b2_) { + score = 200; + } + Vec2 a1, a2; // portal 'a' + Vec2 b1, b2; // portal 'b' + Vec2 na, nb; // normals + Vec2 au, bu; // unit vectors + float amag, bmag; // length of portals + bool bidirectional{true}; // TODO: ehhh? + + Vec2 enter_p; // where we entered portal + size_t decay{0}; // used for animation + + void draw(Canvas* canvas); + bool collide(Ball& ball); + void reset_animation(); + void step_animation(); + void finalize(); +}; + +class Arc : public FixedObject { +public: + enum Surface { + OUTSIDE, + INSIDE, + BOTH + }; + + Arc(const Vec2& p_, + float r_, + float s_ = 0, + float e_ = (float)M_PI * 2, + Surface surf_ = OUTSIDE); + + Vec2 p; + float r; + float start; + float end; + Surface surface; + void draw(Canvas* canvas); + bool collide(Ball& ball); +}; + +class Bumper : public Arc { +public: + Bumper(const Vec2& p_, float r_); + + size_t decay; + + void draw(Canvas* canvas); + void reset_animation(); + void step_animation(); +}; + +class Plunger : public Object { +public: + Plunger(const Vec2& p_); + + void draw(Canvas* canvas); + + int size; // how tall is it + int compression; // how much is it pulled back? +}; + +// Simply displays a letter after a rollover +class Rollover : public FixedObject { +public: + Rollover(const Vec2& p_, char c_) + : FixedObject() + , p(p_) { + c[0] = c_; + c[1] = '\0'; + score = 400; + } + + Vec2 p; + char c[2]; + bool activated{false}; + + void draw(Canvas* canvas); + bool collide(Ball& ball); +}; + +class Turbo : public FixedObject { +public: + Turbo(const Vec2& p_, float angle_, float boost_) + : FixedObject() + , p(p_) + , angle(angle_) + , boost(boost_) { + dir = Vec2(cosf(angle), -sinf(angle)); + + // for now, fix the radius to 30 or whatever + size_t r = 30; + chevron_1[0] = Vec2(p.x, p.y - r); + chevron_1[1] = Vec2(p.x + r, p.y); + chevron_1[2] = Vec2(p.x, p.y + r); + + chevron_2[0] = Vec2(p.x - r, p.y - r); + chevron_2[1] = Vec2(p.x, p.y); + chevron_2[2] = Vec2(p.x - r, p.y + r); + + for(size_t i = 0; i < 3; i++) { + Vec2& v = chevron_1[i]; + Vec2 d = v - p; + v.x = p.x + d.x * cosf(angle) - d.y * sinf(angle); + v.y = p.y + d.x * -sinf(angle) + d.y * -cosf(angle); + } + for(size_t i = 0; i < 3; i++) { + Vec2& v = chevron_2[i]; + Vec2 d = v - p; + v.x = p.x + d.x * cosf(angle) - d.y * sinf(angle); + v.y = p.y + d.x * -sinf(angle) + d.y * -cosf(angle); + } + } + + Vec2 p; + float angle; + float boost; + + Vec2 dir; // unit normal of turbo direction + + Vec2 chevron_1[3]; + Vec2 chevron_2[3]; + + void draw(Canvas* canvas); + bool collide(Ball& ball); +}; + +// Visual item only - chase of dots in one direction +// AXIS-ALIGNED! +class Chaser : public Polygon { +public: + enum Style { + SIMPLE, + SLASH + }; + + Chaser(const Vec2& p1, const Vec2& p2, size_t gap_ = 8, size_t speed_ = 3, Style style_ = SIMPLE) + : Polygon() + , tick(0) + , offset(0) + , gap(gap_) + , speed(speed_) + , style(style_) { + physical = false; + points.push_back(p1); + points.push_back(p2); + } + + size_t tick; + size_t offset; + size_t gap; + size_t speed; + Style style; + + void draw(Canvas* canvas); + void step_animation(); +}; + +// class IconImage : public Object { +// Vec2 v; +// }; diff --git a/pinball0/pinball0.cxx b/pinball0/pinball0.cxx new file mode 100644 index 000000000..33efc0097 --- /dev/null +++ b/pinball0/pinball0.cxx @@ -0,0 +1,661 @@ +#include + +#include +#include +#include "pinball0.h" +#include "table.h" +#include "notifications.h" +#include "settings.h" + +/* generated by fbt from .png files in images folder */ +#include + +// Gravity should be lower than 9.8 m/s^2 since the ball is on +// an angled table. We could calc this and derive the actual +// vertical vector based on the angle of the table yadda yadda yadda +#define GRAVITY 3.0f // 9.8f +#define PHYSICS_SUB_STEPS 5 +#define GAME_FPS 30 +#define MANUAL_ADJUSTMENT 20 +#define IDLE_TIMEOUT 120 * 1000 // 120 seconds * 1000 ticks/sec +#define BUMP_DELAY 2 * 1000 // 2 seconds +#define BUMP_MAX 3 + +void solve(PinballApp* pb, float dt) { + Table* table = pb->table; + + float sub_dt = dt / PHYSICS_SUB_STEPS; + for(int ss = 0; ss < PHYSICS_SUB_STEPS; ss++) { + // apply gravity (and any other forces?) + // FURI_LOG_I(TAG, "Applying gravity"); + if(table->balls_released) { + float bump_amt = 1.0f; + if(pb->keys[InputKeyUp]) { + bump_amt = -1.04f; + } + for(auto& b : table->balls) { + // We multiply GRAVITY by dt since gravity is based on seconds + b.accelerate(Vec2(0, GRAVITY * bump_amt * sub_dt)); + } + } + + // apply collisions (among moving objects) + // only needed for multi-ball! - is this true? what about flippers... + for(size_t b1 = 0; b1 < table->balls.size(); b1++) { + for(size_t b2 = b1 + 1; b2 < table->balls.size(); b2++) { + if(b1 != b2) { + auto& ball1 = table->balls[b1]; + auto& ball2 = table->balls[b2]; + + Vec2 axis = ball1.p - ball2.p; + float dist2 = axis.mag2(); + float dist = sqrtf(dist2); + float rr = ball1.r + ball2.r; + if(dist < rr) { + Vec2 v1 = ball1.p - ball1.prev_p; + Vec2 v2 = ball2.p - ball2.prev_p; + + float factor = (dist - rr) / dist; + ball1.p -= axis * factor * 0.5f; + ball2.p -= axis * factor * 0.5f; + + float damping = 1.01f; + float f1 = (damping * (axis.x * v1.x + axis.y * v1.y)) / dist2; + float f2 = (damping * (axis.x * v2.x + axis.y * v2.y)) / dist2; + + v1.x += f2 * axis.x - f1 * axis.x; + v2.x += f1 * axis.x - f2 * axis.x; + v1.y += f2 * axis.y - f1 * axis.y; + v2.y += f1 * axis.y - f2 * axis.y; + + ball1.prev_p = ball1.p - v1; + ball2.prev_p = ball2.p - v2; + } + } + } + } + + // collisions with static objects and flippers + for(auto& b : table->balls) { + for(auto& o : table->objects) { + if(o->physical && o->collide(b)) { + if(pb->game_mode == GM_Tilted) { + continue; + } + if(o->notification) { + (*o->notification)(pb); + } + table->score.value += o->score; + o->reset_animation(); + continue; + } + } + for(auto& f : table->flippers) { + if(f.collide(b)) { + if(pb->game_mode == GM_Tilted) { + continue; + } + if(f.notification) { + (*f.notification)(pb); + } + table->score.value += f.score; + continue; + } + } + } + + // update positions - of balls AND flippers + if(table->balls_released) { + for(auto& b : table->balls) { + b.update(sub_dt); + } + } + for(auto& f : table->flippers) { + f.update(sub_dt); + } + } + + // Did any balls fall off the table? + if(table->balls.size()) { + auto num_in_play = table->balls.size(); + auto i = table->balls.begin(); + while(i != table->balls.end()) { + if(i->p.y > 1280 + 100) { + FURI_LOG_I(TAG, "ball off table!"); + i = table->balls.erase(i); + num_in_play--; + notify_lost_life(pb); + } else { + ++i; + } + } + if(num_in_play == 0) { + table->balls_released = false; + table->lives.value--; + if(table->lives.value > 0) { + // Reset our ball to it's starting position + table->balls = table->balls_initial; + if(pb->game_mode == GM_Tilted) { + pb->game_mode = GM_Playing; + } + } else { + table->game_over = true; + } + } + } +} + +static void pinball_draw_callback(Canvas* const canvas, void* ctx) { + furi_assert(ctx); + PinballApp* pb = (PinballApp*)ctx; + furi_mutex_acquire(pb->mutex, FuriWaitForever); + + // What are we drawing? table select / menu or the actual game? + switch(pb->game_mode) { + case GM_TableSelect: { + canvas_draw_icon(canvas, 0, 0, &I_pinball0_logo); // our sweet logo + // draw the list of table names: display it as a carousel - where the list repeats + // and the currently selected item is always in the middle, surrounded by pinballs + const TableList& list = pb->table_list; + int32_t y = 25; + auto half_way = list.display_size / 2; + + for(auto i = 0; i < list.display_size; i++) { + int index = + (list.selected - half_way + i + list.menu_items.size()) % list.menu_items.size(); + const auto& menu_item = list.menu_items[index]; + canvas_draw_str_aligned( + canvas, + LCD_WIDTH / 2, + y, + AlignCenter, + AlignTop, + furi_string_get_cstr(menu_item.name)); + if(i == half_way) { + canvas_draw_disc(canvas, 8, y + 3, 2); + canvas_draw_disc(canvas, 56, y + 3, 2); + } + y += 12; + } + + pb->table->draw(canvas); + } break; + case GM_Playing: + pb->table->draw(canvas); + break; + case GM_GameOver: { + pb->table->draw(canvas); + + const int32_t y = 56; + const size_t interval = 40; + const float theta = (float)((pb->tick % interval) / (interval * 1.0f)) * (float)(M_PI * 2); + const float sin_theta_4 = sinf(theta) * 4; + + const int border = 3; + canvas_set_color(canvas, ColorWhite); + canvas_draw_box( + canvas, 16 - border, y + sin_theta_4 - border, 32 + border * 2, 16 + border * 2); + canvas_set_color(canvas, ColorBlack); + + canvas_draw_icon(canvas, 16, y + sin_theta_4, &I_Arcade_G); + canvas_draw_icon(canvas, 24, y + sin_theta_4, &I_Arcade_A); + canvas_draw_icon(canvas, 32, y + sin_theta_4, &I_Arcade_M); + canvas_draw_icon(canvas, 40, y + sin_theta_4, &I_Arcade_E); + + canvas_draw_icon(canvas, 16, y + sin_theta_4 + 8, &I_Arcade_O); + canvas_draw_icon(canvas, 24, y + sin_theta_4 + 8, &I_Arcade_V); + canvas_draw_icon(canvas, 32, y + sin_theta_4 + 8, &I_Arcade_E); + canvas_draw_icon(canvas, 40, y + sin_theta_4 + 8, &I_Arcade_R); + } break; + case GM_Error: { + // pb->text contains error message + canvas_draw_icon(canvas, 0, 10, &I_Arcade_E); + canvas_draw_icon(canvas, 8, 10, &I_Arcade_R); + canvas_draw_icon(canvas, 16, 10, &I_Arcade_R); + canvas_draw_icon(canvas, 24, 10, &I_Arcade_O); + canvas_draw_icon(canvas, 32, 10, &I_Arcade_R); + + int x = 10; + int y = 30; + // split the string on \n and display each line + // strtok is disabled - whyyy + char buf[256]; + strncpy(buf, pb->text, 256); + char* str = buf; + char* p = buf; + bool at_end = false; + while(str != NULL) { + while(p && *p != '\n' && *p != '\0') + p++; + if(p && *p == '\0') at_end = true; + *p = '\0'; + canvas_draw_str_aligned(canvas, x, y, AlignLeft, AlignTop, str); + if(at_end) { + str = NULL; + break; + } + str = p + 1; + p = str; + y += 12; + } + + pb->table->draw(canvas); + } break; + case GM_Settings: { + // TODO: like... do better here. maybe vector of settings strings, etc + canvas_draw_str_aligned(canvas, 2, 10, AlignLeft, AlignTop, "SETTINGS"); + + int x = 55; + int y = 30; + + canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "Sound"); + canvas_draw_circle(canvas, x, y + 3, 4); + if(pb->settings.sound_enabled) { + canvas_draw_disc(canvas, x, y + 3, 2); + } + if(pb->settings.selected_setting == 0) { + canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight); + } + y += 12; + + canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "LED"); + canvas_draw_circle(canvas, x, y + 3, 4); + if(pb->settings.led_enabled) { + canvas_draw_disc(canvas, x, y + 3, 2); + } + if(pb->settings.selected_setting == 1) { + canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight); + } + y += 12; + + canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "Vibrate"); + canvas_draw_circle(canvas, x, y + 3, 4); + if(pb->settings.vibrate_enabled) { + canvas_draw_disc(canvas, x, y + 3, 2); + } + if(pb->settings.selected_setting == 2) { + canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight); + } + y += 12; + + canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "Debug"); + canvas_draw_circle(canvas, x, y + 3, 4); + if(pb->settings.debug_mode) { + canvas_draw_disc(canvas, x, y + 3, 2); + } + if(pb->settings.selected_setting == 3) { + canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight); + } + + // About information + canvas_draw_str_aligned(canvas, 2, 88, AlignLeft, AlignTop, "Pinball0 " VERSION); + canvas_draw_str_aligned(canvas, 2, 98, AlignLeft, AlignTop, "github.com/"); + canvas_draw_str_aligned(canvas, 2, 108, AlignLeft, AlignTop, " rdefeo/"); + canvas_draw_str_aligned(canvas, 2, 118, AlignLeft, AlignTop, " pinball0"); + + pb->table->draw(canvas); + } break; + case GM_Tilted: { + pb->table->draw(canvas); + + const int32_t y = 56; + const int border = 8; + canvas_set_color(canvas, ColorWhite); + canvas_draw_box(canvas, 16 - border, y - border, 32 + border * 2, 8 + border * 2); + canvas_set_color(canvas, ColorBlack); + + bool display = furi_get_tick() % 1000 < 500; + if(display) { + canvas_draw_icon(canvas, 17, y, &I_Arcade_T); + canvas_draw_icon(canvas, 25, y, &I_Arcade_I); + canvas_draw_icon(canvas, 33, y, &I_Arcade_L); + canvas_draw_icon(canvas, 40, y, &I_Arcade_T); + } + + int dots = 5; + int x_start = 16; + int x_gap = (48 - 16) / (dots - 1); + for(int x = 0; x < 5; x++, x_start += x_gap) { + if(x % 2 != display) { + canvas_draw_disc(canvas, x_start, 50, 2); + canvas_draw_disc(canvas, x_start, 70, 2); + } else { + canvas_draw_dot(canvas, x_start, 50); + canvas_draw_dot(canvas, x_start, 70); + } + } + + } break; + default: + FURI_LOG_E(TAG, "Unknown Game Mode"); + break; + } + + furi_mutex_release(pb->mutex); +} + +static void pinball_input_callback(InputEvent* input_event, void* ctx) { + furi_assert(ctx); + FuriMessageQueue* event_queue = (FuriMessageQueue*)ctx; + // PinballEvent event = {.type = EventTypeKey, .input = *input_event}; + furi_message_queue_put(event_queue, input_event, FuriWaitForever); +} + +PinballApp::PinballApp() { + initialized = false; + + mutex = furi_mutex_alloc(FuriMutexTypeNormal); + if(!mutex) { + FURI_LOG_E(TAG, "Cannot create mutex!"); + return; + } + + storage = (Storage*)furi_record_open(RECORD_STORAGE); + notify = (NotificationApp*)furi_record_open(RECORD_NOTIFICATION); + // notify_init(); + notification_message(notify, &sequence_display_backlight_enforce_on); + + table = NULL; + tick = 0; + + game_mode = GM_TableSelect; + keys[InputKeyUp] = false; + keys[InputKeyDown] = false; + keys[InputKeyRight] = false; + keys[InputKeyLeft] = false; + + initialized = true; +} + +PinballApp::~PinballApp() { + furi_mutex_free(mutex); + delete table; + // notify_free(); + + notification_message(notify, &sequence_display_backlight_enforce_auto); + notification_message(notify, &sequence_reset_rgb); + + furi_record_close(RECORD_STORAGE); + furi_record_close(RECORD_NOTIFICATION); +} + +extern "C" int32_t pinball0_app(void* p) { + UNUSED(p); + + PinballApp app; + if(!app.initialized) { + FURI_LOG_E(TAG, "Failed to initialize Pinball0! Exiting."); + return 0; + } + + pinball_load_settings(app); + + // read the list of tables from storage + table_table_list_init(&app); + + table_load_table(&app, TABLE_SELECT); + + FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated); + + ViewPort* view_port = view_port_alloc(); + view_port_set_orientation(view_port, ViewPortOrientationVertical); + view_port_draw_callback_set(view_port, pinball_draw_callback, &app); + view_port_input_callback_set(view_port, pinball_input_callback, event_queue); + + // Open the GUI and register view_port + Gui* gui = (Gui*)furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + // TODO: Dolphin deed actions + // dolphin_deed(DolphinDeedPluginGameStart); + + app.processing = true; + + float dt = 0.0f; + uint32_t last_frame_time = furi_get_tick(); + app.idle_start = last_frame_time; + + // I'm not thrilled with this event loop - kinda messy but it'll do for now + InputEvent event; + while(app.processing) { + FuriStatus event_status = furi_message_queue_get(event_queue, &event, 10); + furi_mutex_acquire(app.mutex, FuriWaitForever); + + if(event_status == FuriStatusOk) { + if(event.type == InputTypePress || event.type == InputTypeLong || + event.type == InputTypeRepeat) { + switch(event.key) { + case InputKeyBack: // navigate to previous screen or exit + switch(app.game_mode) { + case GM_TableSelect: + app.processing = false; + break; + case GM_Settings: + pinball_save_settings(app); + // fall through + default: + app.game_mode = GM_TableSelect; + table_load_table(&app, TABLE_SELECT); + break; + } + break; + case InputKeyRight: { + if(app.game_mode == GM_Tilted) { + break; + } + + app.keys[InputKeyRight] = true; + + if(app.settings.debug_mode && app.table->balls_released == false) { + app.table->balls[0].p.x += MANUAL_ADJUSTMENT; + app.table->balls[0].prev_p.x += MANUAL_ADJUSTMENT; + } + bool flipper_pressed = false; + for(auto& f : app.table->flippers) { + if(f.side == Flipper::RIGHT) { + f.powered = true; + if(f.rotation != f.max_rotation) { + flipper_pressed = true; + } + } + } + if(flipper_pressed) { + notify_flipper(&app); + } + } break; + case InputKeyLeft: { + if(app.game_mode == GM_Tilted) { + break; + } + + app.keys[InputKeyLeft] = true; + + if(app.settings.debug_mode && app.table->balls_released == false) { + app.table->balls[0].p.x -= MANUAL_ADJUSTMENT; + app.table->balls[0].prev_p.x -= MANUAL_ADJUSTMENT; + } + bool flipper_pressed = false; + for(auto& f : app.table->flippers) { + if(f.side == Flipper::LEFT) { + f.powered = true; + if(f.rotation != f.max_rotation) { + flipper_pressed = true; + } + } + } + if(flipper_pressed) { + notify_flipper(&app); + } + } break; + case InputKeyUp: + switch(app.game_mode) { + case GM_Playing: + if(event.type == InputTypePress) { + // Table bump and Tilt tracking + uint32_t current_tick = furi_get_tick(); + if(current_tick - app.table->last_bump >= BUMP_DELAY) { + app.table->bump_count++; + app.table->last_bump = current_tick; + if(!app.table->tilt_detect_enabled || + app.table->bump_count < BUMP_MAX) { + app.keys[InputKeyUp] = true; + notify_table_bump(&app); + } else { + FURI_LOG_W(TAG, "TABLE TILTED!"); + app.game_mode = GM_Tilted; + app.table->bump_count = 0; + notify_table_tilted(&app); + } + } + } + if(app.settings.debug_mode && app.table->balls_released == false) { + app.table->balls[0].p.y -= MANUAL_ADJUSTMENT; + app.table->balls[0].prev_p.y -= MANUAL_ADJUSTMENT; + } + break; + case GM_TableSelect: + app.table_list.selected = + (app.table_list.selected - 1 + app.table_list.menu_items.size()) % + app.table_list.menu_items.size(); + break; + case GM_Settings: + if(app.settings.selected_setting > 0) { + app.settings.selected_setting--; + } + break; + default: + FURI_LOG_W(TAG, "Table tilted, UP does nothing!"); + break; + } + break; + case InputKeyDown: + switch(app.game_mode) { + case GM_Playing: + app.keys[InputKeyDown] = true; + if(app.settings.debug_mode && app.table->balls_released == false) { + app.table->balls[0].p.y += MANUAL_ADJUSTMENT; + app.table->balls[0].prev_p.y += MANUAL_ADJUSTMENT; + } + break; + case GM_TableSelect: + app.table_list.selected = + (app.table_list.selected + 1 + app.table_list.menu_items.size()) % + app.table_list.menu_items.size(); + break; + case GM_Settings: + if(app.settings.selected_setting < app.settings.max_settings - 1) { + app.settings.selected_setting++; + } + break; + default: + break; + } + break; + case InputKeyOk: + switch(app.game_mode) { + case GM_Playing: + if(!app.table->balls_released) { + app.table->balls_released = true; + notify_ball_released(&app); + } + break; + case GM_TableSelect: { + size_t sel = app.table_list.selected; + if(sel == app.table_list.menu_items.size() - 1) { + app.game_mode = GM_Settings; + table_load_table(&app, TABLE_SETTINGS); + } else if(!table_load_table(&app, sel + TABLE_INDEX_OFFSET)) { + app.game_mode = GM_Error; + table_load_table(&app, TABLE_ERROR); + notify_error_message(&app); + } else { + app.game_mode = GM_Playing; + } + } break; + case GM_Settings: + switch(app.settings.selected_setting) { + case 0: + app.settings.sound_enabled = !app.settings.sound_enabled; + break; + case 1: + app.settings.led_enabled = !app.settings.led_enabled; + break; + case 2: + app.settings.vibrate_enabled = !app.settings.vibrate_enabled; + break; + case 3: + app.settings.debug_mode = !app.settings.debug_mode; + break; + default: + break; + } + break; + default: + break; + } + break; + default: + break; + } + } else if(event.type == InputTypeRelease) { + if(event.key != InputKeyOk && event.key != InputKeyBack) { + app.keys[event.key] = false; + for(auto& f : app.table->flippers) { + if(event.key == InputKeyLeft && f.side == Flipper::LEFT) { + f.powered = false; + } else if(event.key == InputKeyRight && f.side == Flipper::RIGHT) { + f.powered = false; + } + } + } + } + // a key was pressed, reset idle counter + app.idle_start = furi_get_tick(); + } + + // update physics / motion + solve(&app, dt); + for(auto& o : app.table->objects) { + o->step_animation(); + } + + // check game state + if(app.game_mode != GM_GameOver && app.table->game_over) { + FURI_LOG_I(TAG, "GAME OVER!"); + app.game_mode = GM_GameOver; + notify_game_over(&app); + } + + // render + view_port_update(view_port); + furi_mutex_release(app.mutex); + + // game timing + idle check + uint32_t current_tick = furi_get_tick(); + if(current_tick - app.idle_start >= IDLE_TIMEOUT) { + FURI_LOG_W(TAG, "Idle timeout! Exiting Pinball0..."); + app.processing = false; + break; + } + + uint32_t time_lapsed = current_tick - last_frame_time; + dt = time_lapsed / 1000.0f; + while(dt < 1.0f / GAME_FPS) { + time_lapsed = furi_get_tick() - last_frame_time; + dt = time_lapsed / 1000.0f; + } + app.tick++; + last_frame_time = furi_get_tick(); + } + + // general cleanup + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + furi_record_close(RECORD_GUI); + view_port_free(view_port); + furi_message_queue_free(event_queue); + + furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal); + return 0; +} diff --git a/pinball0/pinball0.h b/pinball0/pinball0.h new file mode 100644 index 000000000..b58e98a70 --- /dev/null +++ b/pinball0/pinball0.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +// #include +// #include +#include +#include +#include +#include +#include + +#include "vec2.h" +#include "objects.h" +#include "settings.h" + +// #define DRAW_NORMALS + +#define TAG "Pinball0" +#define VERSION "v0.4" + +// Vertical orientation +#define LCD_WIDTH 64 +#define LCD_HEIGHT 128 + +typedef enum GameMode { + GM_TableSelect, + GM_Playing, + GM_GameOver, + GM_Error, + GM_Settings, + GM_Tilted +} GameMode; + +class TableList { +public: + TableList() = default; + ~TableList() { + for(auto& mi : menu_items) { + furi_string_free(mi.name); + furi_string_free(mi.filename); + } + } + + typedef struct { + FuriString* name; + FuriString* filename; + } TableMenuItem; + + std::vector menu_items; + int display_size; // how many can fit on screen + int selected; +}; + +class Table; + +typedef struct PinballApp { + PinballApp(); + ~PinballApp(); + + bool initialized; + + FuriMutex* mutex; + + TableList table_list; + + GameMode game_mode; + Table* table; // data for the current table + uint32_t tick; + + bool keys[4]; // which key was pressed? + bool processing; // controls game loop and game objects + uint32_t idle_start; // tracks time of last key press + + // user settings + PinballSettings settings; + + // system objects + Storage* storage; + NotificationApp* notify; // allows us to blink/buzz during game + char text[256]; // general temp buffer + +} PinballApp; diff --git a/pinball0/pinball0.png b/pinball0/pinball0.png new file mode 100644 index 000000000..a175d2e38 Binary files /dev/null and b/pinball0/pinball0.png differ diff --git a/pinball0/screenshots/lab_basic.png b/pinball0/screenshots/lab_basic.png new file mode 100644 index 000000000..0ee3795dc Binary files /dev/null and b/pinball0/screenshots/lab_basic.png differ diff --git a/pinball0/screenshots/lab_classic.png b/pinball0/screenshots/lab_classic.png new file mode 100644 index 000000000..0aabcd37b Binary files /dev/null and b/pinball0/screenshots/lab_classic.png differ diff --git a/pinball0/screenshots/lab_el_ocho.png b/pinball0/screenshots/lab_el_ocho.png new file mode 100644 index 000000000..af3d355b8 Binary files /dev/null and b/pinball0/screenshots/lab_el_ocho.png differ diff --git a/pinball0/screenshots/lab_menu.png b/pinball0/screenshots/lab_menu.png new file mode 100644 index 000000000..82bc5e5c1 Binary files /dev/null and b/pinball0/screenshots/lab_menu.png differ diff --git a/pinball0/screenshots/lab_splash.png b/pinball0/screenshots/lab_splash.png new file mode 100644 index 000000000..522be5752 Binary files /dev/null and b/pinball0/screenshots/lab_splash.png differ diff --git a/pinball0/screenshots/screenshot_basic.png b/pinball0/screenshots/screenshot_basic.png new file mode 100644 index 000000000..ef1a3c3a7 Binary files /dev/null and b/pinball0/screenshots/screenshot_basic.png differ diff --git a/pinball0/screenshots/screenshot_chamber.png b/pinball0/screenshots/screenshot_chamber.png new file mode 100644 index 000000000..492f92fa7 Binary files /dev/null and b/pinball0/screenshots/screenshot_chamber.png differ diff --git a/pinball0/screenshots/screenshot_el_ocho.png b/pinball0/screenshots/screenshot_el_ocho.png new file mode 100644 index 000000000..33dbe665e Binary files /dev/null and b/pinball0/screenshots/screenshot_el_ocho.png differ diff --git a/pinball0/screenshots/screenshot_menu.png b/pinball0/screenshots/screenshot_menu.png new file mode 100644 index 000000000..a3ee67a07 Binary files /dev/null and b/pinball0/screenshots/screenshot_menu.png differ diff --git a/pinball0/screenshots/splash.png b/pinball0/screenshots/splash.png new file mode 100644 index 000000000..3748cfda2 Binary files /dev/null and b/pinball0/screenshots/splash.png differ diff --git a/pinball0/settings.cxx b/pinball0/settings.cxx new file mode 100644 index 000000000..679e54d0a --- /dev/null +++ b/pinball0/settings.cxx @@ -0,0 +1,100 @@ +#include + +#include "settings.h" +#include "pinball0.h" + +#define PINBALL_SETTINGS_FILENAME ".pinball0.conf" +#define PINBALL_SETTINGS_PATH APP_DATA_PATH(PINBALL_SETTINGS_FILENAME) +#define PINBALL_SETTINGS_FILE_TYPE "Pinball0 Settings File" +#define PINBALL_SETTINGS_FILE_VERSION 1 + +void pinball_load_settings(PinballApp& pb) { + FlipperFormat* fff_settings = flipper_format_file_alloc(pb.storage); + FuriString* tmp_str = furi_string_alloc(); + uint32_t tmp_data32 = 0; + + PinballSettings& settings = pb.settings; + // init the settings to default values, then overwrite them if found in the settings file + settings.sound_enabled = true; + settings.led_enabled = true; + settings.vibrate_enabled = true; + settings.debug_mode = false; + settings.selected_setting = 0; + settings.max_settings = 4; + + do { + if(!flipper_format_file_open_existing(fff_settings, PINBALL_SETTINGS_PATH)) { + FURI_LOG_I(TAG, "SETTINGS: File not found, using defaults"); + break; + } + if(!flipper_format_read_header(fff_settings, tmp_str, &tmp_data32)) { + FURI_LOG_E(TAG, "SETTINGS: Missing or incorrect header"); + break; + } + if(!strcmp(furi_string_get_cstr(tmp_str), PINBALL_SETTINGS_FILE_TYPE) && + (tmp_data32 == PINBALL_SETTINGS_FILE_VERSION)) { + } else { + FURI_LOG_E(TAG, "SETTINGS: Type or version mismatch"); + break; + } + if(flipper_format_read_uint32(fff_settings, "Sound", &tmp_data32, 1)) { + settings.sound_enabled = (tmp_data32 == 0) ? false : true; + } + if(flipper_format_read_uint32(fff_settings, "LED", &tmp_data32, 1)) { + settings.led_enabled = (tmp_data32 == 0) ? false : true; + } + if(flipper_format_read_uint32(fff_settings, "Vibrate", &tmp_data32, 1)) { + settings.vibrate_enabled = (tmp_data32 == 0) ? false : true; + } + if(flipper_format_read_uint32(fff_settings, "Debug", &tmp_data32, 1)) { + settings.debug_mode = (tmp_data32 == 0) ? false : true; + } + + } while(false); + + furi_string_free(tmp_str); + flipper_format_free(fff_settings); +} + +void pinball_save_settings(PinballApp& pb) { + FlipperFormat* fff_settings = flipper_format_file_alloc(pb.storage); + uint32_t tmp_data32 = 0; + PinballSettings& settings = pb.settings; + + FURI_LOG_I(TAG, "SETTINGS: Saving settings"); + do { + if(!flipper_format_file_open_always(fff_settings, PINBALL_SETTINGS_PATH)) { + FURI_LOG_E(TAG, "SETTINGS: Unable to open file for save!"); + break; + } + if(!flipper_format_write_header_cstr( + fff_settings, PINBALL_SETTINGS_FILE_TYPE, PINBALL_SETTINGS_FILE_VERSION)) { + FURI_LOG_E(TAG, "SETTINGS: Failed writing file type and version"); + break; + } + // now write out our settings data + tmp_data32 = settings.sound_enabled ? 1 : 0; + if(!flipper_format_write_uint32(fff_settings, "Sound", &tmp_data32, 1)) { + FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Sound'"); + break; + } + tmp_data32 = settings.led_enabled ? 1 : 0; + if(!flipper_format_write_uint32(fff_settings, "LED", &tmp_data32, 1)) { + FURI_LOG_E(TAG, "SETTINGS: Failed to write 'LED'"); + break; + } + tmp_data32 = settings.vibrate_enabled ? 1 : 0; + if(!flipper_format_write_uint32(fff_settings, "Vibrate", &tmp_data32, 1)) { + FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Vibrate'"); + break; + } + tmp_data32 = settings.debug_mode ? 1 : 0; + if(!flipper_format_write_uint32(fff_settings, "Debug", &tmp_data32, 1)) { + FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Debug'"); + break; + } + } while(false); + + flipper_format_file_close(fff_settings); + flipper_format_free(fff_settings); +} diff --git a/pinball0/settings.h b/pinball0/settings.h new file mode 100644 index 000000000..68ce7d91d --- /dev/null +++ b/pinball0/settings.h @@ -0,0 +1,18 @@ +#pragma once + +typedef struct { + bool sound_enabled; + bool vibrate_enabled; + bool led_enabled; + bool debug_mode; + + int selected_setting; + int max_settings; +} PinballSettings; + +struct PinballApp; +// Read game settings from .pinball0.conf +void pinball_load_settings(PinballApp& pb); + +// Save game settings to .pinball0.conf +void pinball_save_settings(PinballApp& pb); diff --git a/pinball0/table.cxx b/pinball0/table.cxx new file mode 100644 index 000000000..02f2fc95c --- /dev/null +++ b/pinball0/table.cxx @@ -0,0 +1,243 @@ +#include +#include +#include +#include +#include + +#include "pinball0.h" +#include "graphics.h" +#include "table.h" +// #include "notifications.h" + +// Table defaults +#define LIVES 3 +#define LIVES_POS Vec2(20, 20) + +void Lives::draw(Canvas* canvas) { + // we don't draw the last one, as it's in play! + constexpr float r = 20; + if(display && value > 0) { + float x = p.x; + float y = p.y; + float x_off = alignment == Align::Horizontal ? (2 * r) + r : 0; + float y_off = alignment == Align::Vertical ? (2 * r) + r : 0; + for(auto l = 0; l < value - 1; x += x_off, y += y_off, l++) { + gfx_draw_disc(canvas, x + r, y + r, 20); + } + } +} + +void Score::draw(Canvas* canvas) { + if(display) { + char buf[32]; + snprintf(buf, 32, "%d", value); + gfx_draw_str(canvas, p.x, p.y, AlignRight, AlignTop, buf); + } +} + +Table::Table() + : game_over(false) + , balls_released(false) + , plunger(nullptr) + , tilt_detect_enabled(true) + , last_bump(furi_get_tick()) + , bump_count(0) { +} + +Table::~Table() { + for(size_t i = 0; i < objects.size(); i++) { + delete objects[i]; + } + if(plunger != nullptr) { + delete plunger; + } +} + +void Table::draw(Canvas* canvas) { + lives.draw(canvas); + + // da balls + for(auto& b : balls) { + b.draw(canvas); + } + + // loop through objects on the table and draw them + for(auto& o : objects) { + o->draw(canvas); + } + + // now draw flippers + for(auto& f : flippers) { + f.draw(canvas); + } + + // is there a plunger in the house? + if(plunger) { + plunger->draw(canvas); + } + + score.draw(canvas); +} + +Table* table_init_table_select(void* ctx) { + UNUSED(ctx); + Table* table = new Table(); + + table->balls.push_back(Ball(Vec2(20, 880), 35)); + table->balls.back().add_velocity(Vec2(7, 0), .10f); + table->balls.push_back(Ball(Vec2(610, 920), 30)); + table->balls.back().add_velocity(Vec2(-8, 0), .10f); + table->balls.push_back(Ball(Vec2(250, 980), 20)); + table->balls.back().add_velocity(Vec2(10, 0), .10f); + + table->balls_released = true; + + Polygon* new_rail = new Polygon(); + new_rail->add_point({-1, 840}); + new_rail->add_point({-1, 1280}); + new_rail->finalize(); + new_rail->hidden = true; + table->objects.push_back(new_rail); + + new_rail = new Polygon(); + new_rail->add_point({-1, 1280}); + new_rail->add_point({640, 1280}); + new_rail->finalize(); + new_rail->hidden = true; + table->objects.push_back(new_rail); + + new_rail = new Polygon(); + new_rail->add_point({640, 1280}); + new_rail->add_point({640, 840}); + new_rail->finalize(); + new_rail->hidden = true; + table->objects.push_back(new_rail); + + int gap = 8; + int speed = 3; + float top = 20; + // right side + table->objects.push_back(new Chaser(Vec2(32, top), Vec2(62, top), gap, speed)); + table->objects.push_back(new Chaser(Vec2(62, top), Vec2(62, 84), gap, speed)); + table->objects.push_back(new Chaser(Vec2(62, 84), Vec2(32, 84), gap, speed)); + + // left side + table->objects.push_back(new Chaser(Vec2(32, top), Vec2(1, top), gap, speed)); + table->objects.push_back(new Chaser(Vec2(1, top), Vec2(1, 84), gap, speed)); + table->objects.push_back(new Chaser(Vec2(1, 84), Vec2(32, 84), gap, speed)); + + return table; +} + +Table* table_init_table_error(void* ctx) { + UNUSED(ctx); + // PinballApp* pb = (PinballApp*)ctx; + Table* table = new Table(); + + table->balls.push_back(Ball(Vec2(20, 880), 30)); + table->balls.back().add_velocity(Vec2(7, 0), .10f); + // table->balls.push_back(Ball(Vec2(610, 920), 30)); + // table->balls.back().add_velocity(Vec2(-8, 0), .10f); + // table->balls.push_back(Ball(Vec2(250, 980), 20)); + // table->balls.back().add_velocity(Vec2(10, 0), .10f); + + table->balls_released = true; + + Polygon* new_rail = new Polygon(); + new_rail->add_point({-1, 840}); + new_rail->add_point({-1, 1280}); + new_rail->finalize(); + new_rail->hidden = true; + table->objects.push_back(new_rail); + + new_rail = new Polygon(); + new_rail->add_point({-1, 1280}); + new_rail->add_point({640, 1280}); + new_rail->finalize(); + new_rail->hidden = true; + table->objects.push_back(new_rail); + + new_rail = new Polygon(); + new_rail->add_point({640, 1280}); + new_rail->add_point({640, 840}); + new_rail->finalize(); + new_rail->hidden = true; + table->objects.push_back(new_rail); + + int gap = 8; + int speed = 3; + float top = 20; + + table->objects.push_back(new Chaser(Vec2(2, top), Vec2(61, top), gap, speed, Chaser::SLASH)); + table->objects.push_back(new Chaser(Vec2(2, top), Vec2(2, 84), gap, speed, Chaser::SLASH)); + table->objects.push_back(new Chaser(Vec2(2, 84), Vec2(61, 84), gap, speed, Chaser::SLASH)); + table->objects.push_back(new Chaser(Vec2(61, top), Vec2(61, 84), gap, speed, Chaser::SLASH)); + + return table; +} + +Table* table_init_table_settings(void* ctx) { + UNUSED(ctx); + Table* table = new Table(); + + // table->balls.push_back(Ball(Vec2(20, 880), 10)); + // table->balls.back().add_velocity(Vec2(7, 0), .10f); + // table->balls.push_back(Ball(Vec2(610, 920), 10)); + // table->balls.back().add_velocity(Vec2(-8, 0), .10f); + // table->balls.push_back(Ball(Vec2(250, 980), 10)); + // table->balls.back().add_velocity(Vec2(10, 0), .10f); + + table->balls_released = true; + + Polygon* new_rail = new Polygon(); + new_rail->add_point({-1, 840}); + new_rail->add_point({-1, 1280}); + new_rail->finalize(); + new_rail->hidden = true; + table->objects.push_back(new_rail); + + new_rail = new Polygon(); + new_rail->add_point({-1, 1280}); + new_rail->add_point({640, 1280}); + new_rail->finalize(); + new_rail->hidden = true; + table->objects.push_back(new_rail); + + new_rail = new Polygon(); + new_rail->add_point({640, 1280}); + new_rail->add_point({640, 840}); + new_rail->finalize(); + new_rail->hidden = true; + table->objects.push_back(new_rail); + + return table; +} + +bool table_load_table(void* ctx, size_t index) { + PinballApp* pb = (PinballApp*)ctx; + + // read the index'th file in pb->table_list and allocate + FURI_LOG_I(TAG, "Loading table %u", index); + + // if there's already a table loaded, free it + if(pb->table) { + delete pb->table; + pb->table = nullptr; + } + + switch(index) { + case TABLE_SELECT: + pb->table = table_init_table_select(ctx); + break; + case TABLE_ERROR: + pb->table = table_init_table_error(ctx); + break; + case TABLE_SETTINGS: + pb->table = table_init_table_settings(ctx); + break; + default: + pb->table = table_load_table_from_file(pb, index - TABLE_INDEX_OFFSET); + break; + } + return pb->table != NULL; +} diff --git a/pinball0/table.h b/pinball0/table.h new file mode 100644 index 000000000..9586da6ae --- /dev/null +++ b/pinball0/table.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include "pinball0.h" +#include "objects.h" + +#define TABLE_SELECT 0 +#define TABLE_ERROR 1 +#define TABLE_SETTINGS 2 +#define TABLE_INDEX_OFFSET 3 + +// Table display elements, rendered on the physical display coordinates, +// not the table's scaled coords +class DataDisplay { +public: + enum Align { + Horizontal, + Vertical + }; + DataDisplay(const Vec2& pos, int val, bool disp, Align align) + : p(pos) + , value(val) + , display(disp) + , alignment(align) { + } + Vec2 p; + int value; + bool display; + Align alignment; + virtual void draw(Canvas* canvas) = 0; +}; +class Lives : public DataDisplay { +public: + Lives() + : DataDisplay(Vec2(), 3, false, Horizontal) { + } + void draw(Canvas* canvas); +}; + +class Score : public DataDisplay { +public: + Score() + : DataDisplay(Vec2(64 - 1, 1), 0, false, Horizontal) { + } + void draw(Canvas* canvas); +}; + +// Defines all of the elements on a pinball table: +// edges, bumpers, flipper locations, scoreboard +// +// Also used for other app "views", like the main menu (table select) +// and the Settings screen. +// TODO: make this better? eh, it works for now... +class Table { +public: + Table(); + + ~Table(); + + std::vector objects; + std::vector balls; // current state of balls + std::vector balls_initial; // original positions, before release + std::vector flippers; + + bool game_over; + bool balls_released; // is ball in play? + Lives lives; + Score score; + + Plunger* plunger; + + // table bump / tilt tracking + bool tilt_detect_enabled; + uint32_t last_bump; + uint32_t bump_count; + + void draw(Canvas* canvas); +}; + +// Read the list tables from the data folder and store in the state +void table_table_list_init(void* ctx); + +// Reads the table file and creates the new table. +Table* table_load_table_from_file(PinballApp* ctx, size_t index); + +// Loads the index'th table from the list +bool table_load_table(void* ctx, size_t index); diff --git a/pinball0/table_parser.cxx b/pinball0/table_parser.cxx new file mode 100644 index 000000000..e34963ff6 --- /dev/null +++ b/pinball0/table_parser.cxx @@ -0,0 +1,577 @@ +#include +#include +#include +#include +#include + +#include "nxjson/nxjson.h" +#include "pinball0.h" +#include "table.h" +#include "notifications.h" + +namespace { +bool ON_TABLE(const Vec2& p) { + return 0 <= p.x && p.x <= 630 && 0 <= p.y && p.y <= 1270; +} +}; + +void table_table_list_init(void* ctx) { + PinballApp* pb = (PinballApp*)ctx; + // using the asset file path, read the table files, and for each one, extract their + // display name (oof). let's just use their filenames for now (stripping any XX_ prefix) + // sort tables by original filename + + const char* paths[] = {APP_ASSETS_PATH("tables"), APP_DATA_PATH("tables")}; + const size_t ext_len_max = 32; + char ext[ext_len_max]; + + for(size_t p = 0; p < 2; p++) { + const char* path = paths[p]; + // const char* asset_path = APP_ASSETS_PATH("tables"); + FURI_LOG_I(TAG, "Loading table list from: %s", path); + + FuriString* table_path = furi_string_alloc(); + + DirWalk* dir_walk = dir_walk_alloc(pb->storage); + dir_walk_set_recursive(dir_walk, false); + if(dir_walk_open(dir_walk, path)) { + while(dir_walk_read(dir_walk, table_path, NULL) == DirWalkOK) { + path_extract_extension(table_path, ext, ext_len_max); + if(strcmp(ext, ".json") != 0) { + FURI_LOG_W( + TAG, "Skipping non-json file: %s", furi_string_get_cstr(table_path)); + continue; + } + const char* cpath = furi_string_get_cstr(table_path); + + FuriString* filename_no_ext = furi_string_alloc(); + path_extract_filename_no_ext(cpath, filename_no_ext); + + // If filename starts with XX_ (for custom sorting) strip the prefix + char c = furi_string_get_char(filename_no_ext, 2); + if(c == '_') { + char a = furi_string_get_char(filename_no_ext, 0); + char b = furi_string_get_char(filename_no_ext, 1); + if(a >= '0' && a <= '9' && b >= '0' && b <= '9') { + furi_string_right(filename_no_ext, 3); + } + } + + if(!pb->settings.debug_mode && + !strncmp("dbg", furi_string_get_cstr(filename_no_ext), 3)) { + furi_string_free(filename_no_ext); + continue; + } + + FURI_LOG_I( + TAG, + "Found table: name=%s | path=%s", + furi_string_get_cstr(filename_no_ext), + furi_string_get_cstr(table_path)); + + // set display 'name' and 'filename' + TableList::TableMenuItem tmi; + tmi.filename = furi_string_alloc_set_str(cpath); + tmi.name = filename_no_ext; + + // Insert in sorted order + size_t i = 0; + auto it = pb->table_list.menu_items.begin(); + for(; it != pb->table_list.menu_items.end(); it++, i++) { + if(strcmp( + furi_string_get_cstr(tmi.filename), + furi_string_get_cstr(it->filename)) > 0) { + continue; + } + pb->table_list.menu_items.insert(it, tmi); + break; + } + if(pb->table_list.menu_items.size() == i) { + pb->table_list.menu_items.push_back(tmi); + } + } + } + furi_string_free(table_path); + dir_walk_free(dir_walk); + } + + // Add 'Settings' as last element + TableList::TableMenuItem settings; + settings.filename = furi_string_alloc_set_str("99_Settings"); + settings.name = furi_string_alloc_set_str("SETTINGS"); + pb->table_list.menu_items.push_back(settings); + + FURI_LOG_I(TAG, "Found %d tables", pb->table_list.menu_items.size()); + for(auto& tmi : pb->table_list.menu_items) { + FURI_LOG_I(TAG, "%s", furi_string_get_cstr(tmi.name)); + } + pb->table_list.display_size = 5; // how many tables to display at once + pb->table_list.selected = 0; +} + +// json parse helper function +bool table_file_parse_vec2(const nx_json* json, const char* key, Vec2& v) { + const nx_json* item = nx_json_get(json, key); + if(!item || item->children.length != 2) { + return false; + } + v.x = nx_json_item(item, 0)->num.dbl_value; + v.y = nx_json_item(item, 1)->num.dbl_value; + return true; +} + +bool table_file_parse_int(const nx_json* json, const char* key, int& v) { + const nx_json* item = nx_json_get(json, key); + if(!item) return false; + v = item->num.u_value; + return true; +} + +bool table_file_parse_bool(const nx_json* json, const char* key, bool& v) { + int value = v == true ? 1 : 0; // set default value + if(table_file_parse_int(json, key, value)) { + v = value > 0 ? true : false; + return true; + } + return false; +} + +bool table_file_parse_float(const nx_json* json, const char* key, float& v) { + const nx_json* item = nx_json_get(json, key); + if(!item) return false; + v = item->num.dbl_value; + return true; +} + +Table* table_load_table_from_file(PinballApp* pb, size_t index) { + auto& tmi = pb->table_list.menu_items[index]; + + FURI_LOG_I(TAG, "Reading file: %s", furi_string_get_cstr(tmi.filename)); + + File* file = storage_file_alloc(pb->storage); + FileInfo fileinfo; + FS_Error error = + storage_common_stat(pb->storage, furi_string_get_cstr(tmi.filename), &fileinfo); + if(error != FSE_OK) { + FURI_LOG_E(TAG, "Could not find file"); + storage_file_free(file); + return NULL; + } + // TODO: determine an appropriate max file size and make configurable + FURI_LOG_I(TAG, "Found file ok!"); + if(fileinfo.size >= 8192) { + FURI_LOG_E(TAG, "Table file size too big"); + snprintf(pb->text, 256, "Table file\nis too big!\n> 8192 bytes"); + storage_file_free(file); + return NULL; + } + FURI_LOG_I(TAG, "File size is ok!"); + bool ok = + storage_file_open(file, furi_string_get_cstr(tmi.filename), FSAM_READ, FSOM_OPEN_EXISTING); + FURI_LOG_I(TAG, "File opened? %s", ok ? "YES" : "NO"); + + // read the file as a string + uint8_t* buffer; + uint64_t file_size = storage_file_size(file); + if(file_size > 8192) { // TODO - what's the right size? + FURI_LOG_E(TAG, "Table file is too large! (> 8192 bytes)"); + snprintf(pb->text, 256, "Table file\nis too big!\n> 8192 bytes"); + storage_file_free(file); + return NULL; + } + buffer = (uint8_t*)malloc(file_size); + size_t read_count = storage_file_read(file, buffer, file_size); + // if(storage_file_get_error(file) != FSE_OK) { + // FURI_LOG_E(TAG, "Um, couldn't read file"); + // storage_file_free(file); + // return NULL; + // } + storage_file_free(file); + + if(read_count != file_size) { + FURI_LOG_E(TAG, "Error reading file. expected %lld, got %d", file_size, read_count); + free(buffer); + return NULL; + } + FURI_LOG_I(TAG, "Read file into buffer! %d bytes", read_count); + + // let's parse this shit + char* json_buffer = (char*)malloc(read_count * sizeof(char) + 1); + for(uint16_t i = 0; i < read_count; i++) { + json_buffer[i] = buffer[i]; + } + json_buffer[read_count] = 0; + free(buffer); + + const nx_json* json = nx_json_parse(json_buffer, 0); + + if(!json) { + FURI_LOG_E(TAG, "Failed to parse table json!"); + snprintf(pb->text, 256, "Failed to\nparse table\njson!!"); + free(json_buffer); + return NULL; + } + + Table* table = new Table(); + + do { + const nx_json* lives = nx_json_get(json, "lives"); + if(lives) { + table_file_parse_int(lives, "value", table->lives.value); + table_file_parse_bool(lives, "display", table->lives.display); + table_file_parse_vec2(lives, "position", table->lives.p); + const nx_json* align = nx_json_get(lives, "align"); + if(align && !strcmp(align->text_value, "VERTICAL")) { + table->lives.alignment = Lives::Vertical; + } + } + const nx_json* tilt = nx_json_get(json, "tilt_detect"); + if(tilt) { + table->tilt_detect_enabled = tilt->num.u_value > 0 ? true : false; + } + const nx_json* score = nx_json_get(json, "score"); + if(score) { + table_file_parse_bool(score, "display", table->score.display); + table_file_parse_vec2(score, "position", table->score.p); + } + + const nx_json* balls = nx_json_get(json, "balls"); + if(balls) { + for(int i = 0; i < balls->children.length; i++) { + const nx_json* ball = nx_json_item(balls, i); + if(!ball) continue; + + Vec2 p; + if(!table_file_parse_vec2(ball, "position", p)) { + FURI_LOG_E(TAG, "Ball missing \"position\", skipping"); + continue; + } + if(!ON_TABLE(p)) { + FURI_LOG_W( + TAG, + "Ball with position %.1f,%.1f is not on table!", + (double)p.x, + (double)p.y); + } + + Ball new_ball(p); + table_file_parse_float(ball, "radius", new_ball.r); + + Vec2 v = (Vec2){0, 0}; + table_file_parse_vec2(ball, "velocity", v); + new_ball.accelerate(v); + + table->balls_initial.push_back(new_ball); + table->balls.push_back(new_ball); + } + } + if(table->balls.size() == 0) { + FURI_LOG_E(TAG, "Table has NO BALLS"); + snprintf(pb->text, 256, "No balls\nfound in\ntable file!"); + delete table; + table = NULL; + break; + } + + // TODO: plungers need work + const nx_json* plunger = nx_json_get(json, "plunger"); + if(plunger) { + Vec2 p; + table_file_parse_vec2(plunger, "position", p); + int s = 100; + table_file_parse_int(plunger, "size", s); + table->plunger = new Plunger(p); + } else { + FURI_LOG_W( + TAG, "Table has NO PLUNGER - s'ok, we don't really support one anyway (yet)"); + } + + const nx_json* flippers = nx_json_get(json, "flippers"); + if(flippers) { + for(int i = 0; i < flippers->children.length; i++) { + const nx_json* flipper = nx_json_item(flippers, i); + + Vec2 p; + if(!table_file_parse_vec2(flipper, "position", p)) { + FURI_LOG_E(TAG, "Flipper missing \"position\", skipping"); + continue; + } + if(!ON_TABLE(p)) { + FURI_LOG_W( + TAG, + "Flipper with position %.1f,%.1f is not on table!", + (double)p.x, + (double)p.y); + } + + const nx_json* side = nx_json_get(flipper, "side"); + Flipper::Side sd = Flipper::LEFT; + if(side && !strcmp(side->text_value, "RIGHT")) { + sd = Flipper::RIGHT; + } + + int sz = DEF_FLIPPER_SIZE; + table_file_parse_int(flipper, "size", sz); + Flipper flip(p, sd, sz); + // flip.notification = ¬ify_flipper; + table->flippers.push_back(flip); + } + } + + const nx_json* bumpers = nx_json_get(json, "bumpers"); + if(bumpers) { + for(int i = 0; i < bumpers->children.length; i++) { + const nx_json* bumper = nx_json_item(bumpers, i); + + Vec2 p; + if(!table_file_parse_vec2(bumper, "position", p)) { + FURI_LOG_E(TAG, "Bumper missing \"position\", skipping"); + continue; + } + if(!ON_TABLE(p)) { + FURI_LOG_W( + TAG, + "Bumper with position %.1f,%.1f is not on table!", + (double)p.x, + (double)p.y); + } + + int r = DEF_BUMPER_RADIUS; + table_file_parse_int(bumper, "radius", r); + + float bnc = DEF_BUMPER_BOUNCE; + table_file_parse_float(bumper, "bounce", bnc); + + Bumper* new_bumper = new Bumper(p, r); + new_bumper->bounce = bnc; + new_bumper->notification = notify_bumper_hit; + table->objects.push_back(new_bumper); + } + } + + constexpr float pi_180 = M_PI / 180; + const nx_json* arcs = nx_json_get(json, "arcs"); + if(arcs) { + for(int i = 0; i < arcs->children.length; i++) { + const nx_json* arc = nx_json_item(arcs, i); + + Vec2 p; + if(!table_file_parse_vec2(arc, "position", p)) { + FURI_LOG_E(TAG, "Arc missing \"position\""); + continue; + } + if(!ON_TABLE(p)) { + FURI_LOG_W( + TAG, + "Arc with position %.1f,%.1f is not on table!", + (double)p.x, + (double)p.y); + } + + int r = DEF_BUMPER_RADIUS; + table_file_parse_int(arc, "radius", r); + + float bnc = 0.95f; // DEF_BUMPER_BOUNCE? + table_file_parse_float(arc, "bounce", bnc); + + float start_angle = 0.0; + table_file_parse_float(arc, "start_angle", start_angle); + start_angle *= pi_180; + float end_angle = 0.0; + table_file_parse_float(arc, "end_angle", end_angle); + end_angle *= pi_180; + + Arc::Surface surface = Arc::OUTSIDE; + const nx_json* stype = nx_json_get(arc, "surface"); + if(stype && !strcmp(stype->text_value, "INSIDE")) { + surface = Arc::INSIDE; + } + + Arc* new_bumper = new Arc(p, r, start_angle, end_angle, surface); + new_bumper->bounce = bnc; + table->objects.push_back(new_bumper); + } + } + + const nx_json* rails = nx_json_get(json, "rails"); + if(rails) { + for(int i = 0; i < rails->children.length; i++) { + const nx_json* rail = nx_json_item(rails, i); + + Vec2 s; + if(!table_file_parse_vec2(rail, "start", s)) { + FURI_LOG_E(TAG, "Rail missing \"start\", skipping"); + continue; + } + if(!ON_TABLE(s)) { + FURI_LOG_W( + TAG, + "Rail with starting position %.1f,%.1f is not on table!", + (double)s.x, + (double)s.y); + } + Vec2 e; + if(!table_file_parse_vec2(rail, "end", e)) { + FURI_LOG_E(TAG, "Rail missing \"end\", skipping"); + continue; + } + if(!ON_TABLE(e)) { + FURI_LOG_W( + TAG, + "Rail with ending position %.1f,%.1f is not on table!", + (double)e.x, + (double)e.y); + } + + Polygon* new_rail = new Polygon(); + new_rail->add_point(s); + new_rail->add_point(e); + + float bnc = DEF_RAIL_BOUNCE; + table_file_parse_float(rail, "bounce", bnc); + new_rail->bounce = bnc; + + int double_sided = 0; + table_file_parse_int(rail, "double_sided", double_sided); + + new_rail->finalize(); + new_rail->notification = ¬ify_rail_hit; + table->objects.push_back(new_rail); + + if(double_sided) { + new_rail = new Polygon(); + new_rail->add_point(e); + new_rail->add_point(s); + new_rail->bounce = bnc; + new_rail->finalize(); + new_rail->notification = ¬ify_rail_hit; + table->objects.push_back(new_rail); + } + } + } + + const nx_json* portals = nx_json_get(json, "portals"); + if(portals) { + for(int i = 0; i < portals->children.length; i++) { + const nx_json* portal = nx_json_item(portals, i); + + Vec2 a1; + if(!table_file_parse_vec2(portal, "a_start", a1)) { + FURI_LOG_E(TAG, "Portal missing \"a_start\", skipping"); + continue; + } + if(!ON_TABLE(a1)) { + FURI_LOG_W( + TAG, + "Portal A with starting position %.1f,%.1f is not on table!", + (double)a1.x, + (double)a1.y); + } + Vec2 a2; + if(!table_file_parse_vec2(portal, "a_end", a2)) { + FURI_LOG_E(TAG, "Portal missing \"a_end\", skipping"); + continue; + } + if(!ON_TABLE(a2)) { + FURI_LOG_W( + TAG, + "Portal A with ending position %.1f,%.1f is not on table!", + (double)a2.x, + (double)a2.y); + } + Vec2 b1; + if(!table_file_parse_vec2(portal, "b_start", b1)) { + FURI_LOG_E(TAG, "Portal missing \"b_start\", skipping"); + continue; + } + if(!ON_TABLE(b1)) { + FURI_LOG_W( + TAG, + "Portal B with starting position %.1f,%.1f is not on table!", + (double)b1.x, + (double)b1.y); + } + Vec2 b2; + if(!table_file_parse_vec2(portal, "b_end", b2)) { + FURI_LOG_E(TAG, "Portal missing \"b_end\", skipping"); + continue; + } + if(!ON_TABLE(b2)) { + FURI_LOG_W( + TAG, + "Portal B with ending position %.1f,%.1f is not on table!", + (double)b2.x, + (double)b2.y); + } + + Portal* new_portal = new Portal(a1, a2, b1, b2); + new_portal->finalize(); + new_portal->notification = ¬ify_portal; + table->objects.push_back(new_portal); + } + } + + const nx_json* rollovers = nx_json_get(json, "rollovers"); + if(rollovers) { + for(int i = 0; i < rollovers->children.length; i++) { + const nx_json* rollover = nx_json_item(rollovers, i); + + Vec2 p; + if(!table_file_parse_vec2(rollover, "position", p)) { + FURI_LOG_E(TAG, "Rollover missing \"position\", skipping"); + continue; + } + if(!ON_TABLE(p)) { + FURI_LOG_W( + TAG, + "Rollover with position %.1f,%.1f is not on table!", + (double)p.x, + (double)p.y); + } + char sym = '*'; + const nx_json* symbol = nx_json_get(rollover, "symbol"); + if(symbol) { + sym = symbol->text_value[0]; + } + Rollover* new_rollover = new Rollover(p, sym); + table->objects.push_back(new_rollover); + } + } + + const nx_json* turbos = nx_json_get(json, "turbos"); + if(turbos) { + for(int i = 0; i < turbos->children.length; i++) { + const nx_json* turbo = nx_json_item(turbos, i); + + Vec2 p; + if(!table_file_parse_vec2(turbo, "position", p)) { + FURI_LOG_E(TAG, "Turbo missing \"position\""); + continue; + } + if(!ON_TABLE(p)) { + FURI_LOG_W( + TAG, + "Turbo with position %.1f,%.1f is not on table!", + (double)p.x, + (double)p.y); + } + float angle = 0; + table_file_parse_float(turbo, "angle", angle); + angle *= pi_180; + + float boost = 10; + table_file_parse_float(turbo, "boost", boost); + + Turbo* new_turbo = new Turbo(p, angle, boost); + + table->objects.push_back(new_turbo); + } + } + break; + } while(false); + + nx_json_free(json); + free(json_buffer); + + return table; +} diff --git a/pinball0/vec2.cxx b/pinball0/vec2.cxx new file mode 100644 index 000000000..237b3989e --- /dev/null +++ b/pinball0/vec2.cxx @@ -0,0 +1,16 @@ +#include +#include + +#include "vec2.h" + +// Returns the closest point to the line segment ab and p +Vec2 Vec2_closest(const Vec2& a, const Vec2& b, const Vec2& p) { + // vector along line ab + Vec2 ab = b - a; + float t = ab.dot(ab); + if(t == 0.0f) { + return a; + } + t = fmax(0.0f, fmin(1.0f, (p.dot(ab) - a.dot(ab)) / t)); + return a + ab * t; +} diff --git a/pinball0/vec2.h b/pinball0/vec2.h new file mode 100644 index 000000000..0b7f573af --- /dev/null +++ b/pinball0/vec2.h @@ -0,0 +1,102 @@ +#pragma once +#include +#include + +#define VEC2_EPSILON (float)0.001 + +class Vec2 { +public: + float x; + float y; + + Vec2() + : x(0) + , y(0) { + } + Vec2(float x_, float y_) + : x(x_) + , y(y_) { + } + + Vec2 operator+(const Vec2& rhs) const { + return Vec2(x + rhs.x, y + rhs.y); + } + Vec2 operator+(float s) const { + return Vec2(x + s, y + s); + } + void operator+=(const Vec2& rhs) { + x += rhs.x; + y += rhs.y; + } + Vec2 operator-(const Vec2& rhs) const { + return Vec2(x - rhs.x, y - rhs.y); + } + Vec2 operator-(float s) const { + return Vec2(x - s, y - s); + } + void operator-=(const Vec2& rhs) { + x -= rhs.x; + y -= rhs.y; + } + Vec2 operator*(float s) const { + return Vec2(x * s, y * s); + } + void operator*=(float s) { + x *= s; + y *= s; + } + + Vec2 operator/(float s) const { + return Vec2(x / s, y / s); + } + + bool operator==(const Vec2& rhs) const { + return x == rhs.x && y == rhs.y; + } + + // Magnitude / length of vector + float mag() const { + return sqrtf(x * x + y * y); + } + // Magnitude squared + float mag2() const { + return x * x + y * y; + } + + // Dot product: this.x * v.x + this.y * v.y + float dot(const Vec2& v) const { + return x * v.x + y * v.y; + } + + // Cross product + float cross(const Vec2& v) const { + return x * v.y - y * v.x; + } + + void normalize(void) { + float len = mag(); + if(len > VEC2_EPSILON) { + float inverse_len = 1.0f / len; + x *= inverse_len; + y *= inverse_len; + } + } + + // Distance squared between this and next + float dist2(const Vec2& v) const { + float dx = x - v.x; + float dy = y - v.y; + return dx * dx + dy * dy; + } + // Distance between tihs and next + float dist(const Vec2& v) const { + return sqrtf(dist2(v)); + } +}; + +inline Vec2 operator*(float s, const Vec2& v) { + return Vec2(s * v.x, s * v.y); +} + +// // Returns the closest point to the line segment ab and p +Vec2 Vec2_closest(const Vec2& a, const Vec2& b, const Vec2& p); diff --git a/pokemon_trading/README.md b/pokemon_trading/README.md index 8cc359b63..b97bf3a98 100644 --- a/pokemon_trading/README.md +++ b/pokemon_trading/README.md @@ -1,4 +1,4 @@ -# GAME BOY Pokemon Trading MALVEKE +# Pokemon Trade Tool for Flipper ## Watch it in Action The video below trades a Bulbasaur from the Flipper to a Game Boy Color with Pokemon Silver. The Game Boy trades its Cyndaquil. The Flipper is then used to modify the Cyndaquil to infect it with Pokerus, modify its EVs and IVs, and have the Cyndaquil hold an Antidote before it is traded back to the Game Boy. diff --git a/pokemon_trading/application.fam b/pokemon_trading/application.fam index 211bf7826..f31855121 100644 --- a/pokemon_trading/application.fam +++ b/pokemon_trading/application.fam @@ -1,11 +1,11 @@ App( appid="pokemon", - name="[GB] Pokemon Trading", + name="[GB] Pokemon Trade Tool", apptype=FlipperAppType.EXTERNAL, entry_point="pokemon_app", requires=["gui"], stack_size=2 * 1024, - fap_version=[2, 1], + fap_version=[2, 2], fap_category="GPIO", fap_icon="pokemon_10px.png", fap_icon_assets="assets", @@ -16,6 +16,8 @@ App( fap_private_libs=[ Lib( name="flipper-gblink", + fap_include_paths=["gblink/include", "./"], + sources=["gblink/*.c"], ), ], ) diff --git a/pokemon_trading/changelog.md b/pokemon_trading/changelog.md index a4244d173..b4be455da 100644 --- a/pokemon_trading/changelog.md +++ b/pokemon_trading/changelog.md @@ -1,5 +1,9 @@ # Changelog - Patch Notes +## Version 2.2 +**New Features** +- Update to gblink v0.63 which includes saving/loading of pin configurations for the EXT link interface + ## Version 2.1 **New Features** - Add ability to reset trade state without affecting current Pokemon being configured diff --git a/pokemon_trading/lib/flipper-gblink/.gitsubtree b/pokemon_trading/lib/flipper-gblink/.gitsubtree index a3baed34d..382565fda 100644 --- a/pokemon_trading/lib/flipper-gblink/.gitsubtree +++ b/pokemon_trading/lib/flipper-gblink/.gitsubtree @@ -1 +1 @@ -https://github.com/kbembedded/flipper-gblink 93931d71fffd0476a591dc18d3263a02e93a169a / +https://github.com/kbembedded/flipper-gblink ecce5b6363adb067cef424eab09fead2f038baf5 / diff --git a/pokemon_trading/lib/flipper-gblink/Doxyfile b/pokemon_trading/lib/flipper-gblink/Doxyfile new file mode 100644 index 000000000..72a59f5f5 --- /dev/null +++ b/pokemon_trading/lib/flipper-gblink/Doxyfile @@ -0,0 +1,2660 @@ +# Doxyfile 1.9.1 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "flipper-gblink" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = 0.62 + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "Game Boy Link interface API for Flipper Zero" + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all generated output in the proper direction. +# Possible values are: None, LTR, RTL and Context. +# The default value is: None. + +OUTPUT_TEXT_DIRECTION = None + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# By default Python docstrings are displayed as preformatted text and doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines (in the resulting output). You can put ^^ in the value part of an +# alias to insert a newline as if a physical newline was in the original file. +# When you need a literal { or } or , in the value part of an alias you have to +# escape them by means of a backslash (\), this can lead to conflicts with the +# commands \{ and \} for these it is advised to use the version @{ and @} or use +# a double escape (\\{ and \\}) + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL, +# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 5. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 5 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use +# during processing. When set to 0 doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which efficively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# With the correct setting of option CASE_SENSE_NAMES doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and MacOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# The default value is: system dependent. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. If +# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the doxygen process doxygen will return with a non-zero status. +# Possible values are: NO, YES and FAIL_ON_WARNINGS. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), +# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl, +# *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f18 \ + *.f \ + *.for \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which doxygen's built-in parser lacks the necessary type information. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If clang assisted parsing is enabled and the CLANG_ADD_INC_PATHS tag is set to +# YES then doxygen will add the directory of each input to the include path. +# The default value is: YES. + +CLANG_ADD_INC_PATHS = YES + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: +# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the main .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to +# run qhelpgenerator on the generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +SHOW_ENUM_VALUES = YES + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. +# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2 + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /