diff --git a/cli_bridge/cli_control.c b/cli_bridge/cli_control.c index 1ff6226c4..7e529c4e5 100644 --- a/cli_bridge/cli_control.c +++ b/cli_bridge/cli_control.c @@ -16,7 +16,8 @@ static void tx_handler(const uint8_t* buffer, size_t size) { furi_stream_buffer_send(cli_tx_stream, buffer, size, FuriWaitForever); } -static void tx_handler_stdout(const char* buffer, size_t size) { +static void tx_handler_stdout(const char* buffer, size_t size, void* context) { + UNUSED(context); tx_handler((const uint8_t*)buffer, size); } @@ -32,9 +33,9 @@ static size_t real_rx_handler(uint8_t* buffer, size_t size, uint32_t timeout) { rx_cnt += len; } if(restore_tx_stdout) { - furi_thread_set_stdout_callback(cli_vcp.tx_stdout); + furi_thread_set_stdout_callback(cli_vcp.tx_stdout, NULL); } else { - furi_thread_set_stdout_callback(tx_handler_stdout); + furi_thread_set_stdout_callback(tx_handler_stdout, NULL); } return rx_cnt; } @@ -87,7 +88,7 @@ void clicontrol_hijack(size_t tx_size, size_t rx_size) { cli_session_close(global_cli); restore_tx_stdout = false; cli_session_open(global_cli, session); - furi_thread_set_stdout_callback(prev_stdout); + furi_thread_set_stdout_callback(prev_stdout, NULL); furi_record_close(RECORD_CLI); } @@ -134,7 +135,7 @@ void clicontrol_unhijack(bool persist) { FuriThreadStdoutWriteCallback prev_stdout = furi_thread_get_stdout_callback(); cli_session_close(global_cli); cli_session_open(global_cli, &cli_vcp); - furi_thread_set_stdout_callback(prev_stdout); + furi_thread_set_stdout_callback(prev_stdout, NULL); furi_record_close(RECORD_CLI); // Unblock waiting rx handler, restore old cli_vcp.tx_stdout diff --git a/cntdown_timer/application.fam b/cntdown_timer/application.fam index 8968cacbf..0dc52aacf 100644 --- a/cntdown_timer/application.fam +++ b/cntdown_timer/application.fam @@ -13,6 +13,6 @@ App( fap_category="Tools", fap_author="@0w0mewo", fap_weburl="https://github.com/0w0mewo/fpz_cntdown_timer", - fap_version="1.4", + fap_version="1.5", fap_description="Simple count down timer", ) diff --git a/cntdown_timer/views/countdown_view.h b/cntdown_timer/views/countdown_view.h index caa1466b3..a234a5efd 100644 --- a/cntdown_timer/views/countdown_view.h +++ b/cntdown_timer/views/countdown_view.h @@ -11,7 +11,7 @@ #define SCREEN_CENTER_X (SCREEN_WIDTH / 2) #define SCREEN_CENTER_Y (SCREEN_HEIGHT / 2) -#define INIT_COUNT 10 +#define INIT_COUNT 60 typedef enum { CountDownTimerMinuteUp, diff --git a/color_guess/application.fam b/color_guess/application.fam index cd9be5635..268307072 100644 --- a/color_guess/application.fam +++ b/color_guess/application.fam @@ -11,7 +11,7 @@ App( order=10, fap_icon="icons/color_guess_10px.png", fap_icon_assets="icons", - fap_version="1.5", + fap_version="1.6", fap_category="Games", fap_author="Leedave", fap_description="Color Guessing Game", diff --git a/color_guess/changelog.md b/color_guess/changelog.md deleted file mode 100644 index c4d4c1b0a..000000000 --- a/color_guess/changelog.md +++ /dev/null @@ -1,7 +0,0 @@ -## v1.1 - -Added GFX to start screen - -## v1.0 - -First release to Application Catalog \ No newline at end of file diff --git a/color_guess/color_guess.c b/color_guess/color_guess.c index 9d447bf67..89cb879ae 100644 --- a/color_guess/color_guess.c +++ b/color_guess/color_guess.c @@ -24,7 +24,6 @@ ColorGuess* color_guess_app_alloc() { ColorGuess* app = malloc(sizeof(ColorGuess)); app->gui = furi_record_open(RECORD_GUI); app->notification = furi_record_open(RECORD_NOTIFICATION); - app->error = false; // Set Defaults if no config exists app->haptic = 1; @@ -38,7 +37,6 @@ ColorGuess* color_guess_app_alloc() { //Scene additions app->view_dispatcher = view_dispatcher_alloc(); - app->scene_manager = scene_manager_alloc(&color_guess_scene_handlers, app); view_dispatcher_set_event_callback_context(app->view_dispatcher, app); view_dispatcher_set_navigation_event_callback( @@ -112,9 +110,6 @@ void color_guess_app_free(ColorGuess* app) { int32_t color_guess_app(void* p) { UNUSED(p); ColorGuess* app = color_guess_app_alloc(); - if(app->error) { - return 255; - } view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); diff --git a/color_guess/color_guess.h b/color_guess/color_guess.h index 79a95861d..c8ccbf215 100644 --- a/color_guess/color_guess.h +++ b/color_guess/color_guess.h @@ -17,7 +17,7 @@ #include "views/color_guess_startscreen.h" #include "helpers/color_guess_storage.h" -#define COLOR_GUESS_VERSION "1.5" +#define COLOR_GUESS_VERSION "1.6" #define TAG "Color_Guess" typedef struct { @@ -32,9 +32,7 @@ typedef struct { ColorGuessPlay* color_guess_play; ColorGuessStartscreen* color_guess_startscreen; Submenu* color_guess_settings; - bool error; uint32_t haptic; - //uint32_t speaker; uint32_t led; uint32_t save_settings; } ColorGuess; @@ -52,11 +50,6 @@ typedef enum { ColorGuessHapticOn, } ColorGuessHapticState; -typedef enum { - ColorGuessSpeakerOff, - ColorGuessSpeakerOn, -} ColorGuessSpeakerState; - typedef enum { ColorGuessLedOff, ColorGuessLedOn, diff --git a/color_guess/docs/changelog.md b/color_guess/docs/changelog.md index 4aafc252d..17a2d3928 100644 --- a/color_guess/docs/changelog.md +++ b/color_guess/docs/changelog.md @@ -1,21 +1,26 @@ -## 1.5 +## v1.6 +- Reducing code +- Removal of deprecated view dispatcher queue + +## v1.5 - Additional Memory Management fixes by Willy-JL - Redraw Issue fixed by Willy-JL - Added GNU License - Added version number -## 1.4 -- Prevent value changing on win view -- Fix issues with FW build 0.99.x +## v1.4 +- Compatibility for FW above 0.99.0 +- Fix for Value changing on success screen -## 1.3 -- Patched Memory Leak in storage +## v1.3 +- Fixed minor memory leak in storage ## v1.2 -- Updated compatibility to 0.95.0-rc +- Updated Application for Flipper Zero 0.95.0-rc ## v1.1 -- Updated Launch Screen GFX + +Added GFX to start screen ## v1.0 diff --git a/color_guess/scenes/color_guess_scene_settings.c b/color_guess/scenes/color_guess_scene_settings.c index d23fa3fb3..a24a7fcd9 100644 --- a/color_guess/scenes/color_guess_scene_settings.c +++ b/color_guess/scenes/color_guess_scene_settings.c @@ -11,22 +11,12 @@ const char* const haptic_text[2] = { "OFF", "ON", }; + const uint32_t haptic_value[2] = { ColorGuessHapticOff, ColorGuessHapticOn, }; -/* Speaker currently not used -const char* const speaker_text[2] = { - "OFF", - "ON", -}; -const uint32_t speaker_value[2] = { - ColorGuessSpeakerOff, - ColorGuessSpeakerOn, -}; -*/ - /* Game doesn't make sense with LED off, but the setting is there */ const char* const led_text[2] = { "OFF", @@ -45,15 +35,6 @@ static void color_guess_scene_settings_set_haptic(VariableItem* item) { app->haptic = haptic_value[index]; } -/* -static void color_guess_scene_settings_set_speaker(VariableItem* item) { - ColorGuess* app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - variable_item_set_current_value_text(item, speaker_text[index]); - app->speaker = speaker_value[index]; -} -*/ - static void color_guess_scene_settings_set_led(VariableItem* item) { ColorGuess* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -78,18 +59,6 @@ void color_guess_scene_settings_on_enter(void* context) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, haptic_text[value_index]); - // Sound on/off - /* - item = variable_item_list_add( - app->variable_item_list, - "Sound:", - 2, - color_guess_scene_settings_set_speaker, - app); - value_index = value_index_uint32(app->speaker, speaker_value, 2); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, speaker_text[value_index]);*/ - // LED Effects on/off item = variable_item_list_add( app->variable_item_list, "LED FX:", 2, color_guess_scene_settings_set_led, app); diff --git a/cross_remote/.gitignore b/cross_remote/.gitignore new file mode 100644 index 000000000..bad38f9a7 --- /dev/null +++ b/cross_remote/.gitignore @@ -0,0 +1,9 @@ +dist/* +.vscode +.clang-format +.clangd +.editorconfig +.env +.ufbt +.clang-format +.vscode/settings.json diff --git a/cross_remote/README.md b/cross_remote/README.md index 012a9bc47..8717ee509 100644 --- a/cross_remote/README.md +++ b/cross_remote/README.md @@ -29,6 +29,7 @@ Wouldn't it be nicer to simply click one button and let everything happen? This - Save chained commands to a file
- Add pauses, becaue target systems are not always fast enough for multiple commands
- Run file containing chained IR & SubGhz commands
+- Loop Transmissions until quit ### Settings - LED FX, allow the LED to blink @@ -52,5 +53,10 @@ Then run the command: ``` The application will be compiled and copied onto your device. +## Thank you notes +- [Willy-JL](https://github.com/Willy-JL) for distributing in Momentum Firmware +- [Roguemaster](https://github.com/RogueMaster/flipperzero-firmware-wPlugins) for distributing in Roguemaster Firmware +- [Miccayo](https://github.com/miccayo) for contributing the loop transmit feature + ## Licensing This code is open-source and may be used for whatever you want to do with it. \ No newline at end of file diff --git a/cross_remote/application.fam b/cross_remote/application.fam index 3538b48ef..6919ad55a 100644 --- a/cross_remote/application.fam +++ b/cross_remote/application.fam @@ -6,7 +6,7 @@ App( stack_size=3 * 1024, fap_icon="icons/xremote_10px.png", fap_icon_assets="icons", - fap_version="2.7", + fap_version="3.0", fap_category="Infrared", fap_author="Leedave", fap_description="One-Click, sends multiple commands", diff --git a/cross_remote/docs/README.md b/cross_remote/docs/README.md index 24f758fd6..e3cd326dd 100644 --- a/cross_remote/docs/README.md +++ b/cross_remote/docs/README.md @@ -11,6 +11,7 @@ This app combines your IR and SubGhz commands into a playlist that can be run wi - Disable LED effects if not wanted - Configure duration of IR Signals - Configure default duration of Encoded SubGhz Signals +- Loop Transmissions until quit ## What good is this? diff --git a/cross_remote/docs/changelog.md b/cross_remote/docs/changelog.md index 0b09234ef..f45ec135b 100644 --- a/cross_remote/docs/changelog.md +++ b/cross_remote/docs/changelog.md @@ -1,3 +1,11 @@ +## 3.0 +- Added loop transmit feature (thanks to miccayo) +- Replaced transmission counter with animations +- Refactored the transmission part to allow interruption of command chains + +## 2.8 +- Update SubGhz Protocoll to include flippers official rolling code support + ## 2.7 - Replaced custom keyboard for timing with new number_input from Firmware - Requires minimum OFW version 0.105.0 or custom firmware based on this diff --git a/cross_remote/helpers/subghz/subghz.c b/cross_remote/helpers/subghz/subghz.c index 61dcddac2..0ac5f1f2c 100644 --- a/cross_remote/helpers/subghz/subghz.c +++ b/cross_remote/helpers/subghz/subghz.c @@ -9,6 +9,7 @@ SubGhz* subghz_alloc() { subghz->file_path = furi_string_alloc(); subghz->txrx = subghz_txrx_alloc(); + subghz_txrx_set_need_save_callback(subghz->txrx, subghz_save_to_file, subghz); subghz->dialogs = furi_record_open(RECORD_DIALOGS); return subghz; diff --git a/cross_remote/helpers/subghz/subghz_i.c b/cross_remote/helpers/subghz/subghz_i.c index c75739da3..099f6e0a1 100644 --- a/cross_remote/helpers/subghz/subghz_i.c +++ b/cross_remote/helpers/subghz/subghz_i.c @@ -188,6 +188,60 @@ SubGhzLoadTypeFile subghz_get_load_type_file(SubGhz* subghz) { return subghz->load_type_file; } +bool subghz_save_protocol_to_file( + SubGhz* subghz, + FlipperFormat* flipper_format, + const char* dev_file_name) { + furi_assert(subghz); + furi_assert(flipper_format); + furi_assert(dev_file_name); + + Storage* storage = furi_record_open(RECORD_STORAGE); + Stream* flipper_format_stream = flipper_format_get_raw_stream(flipper_format); + + bool saved = false; + FuriString* file_dir = furi_string_alloc(); + + path_extract_dirname(dev_file_name, file_dir); + do { + //removing additional fields + flipper_format_delete_key(flipper_format, "Repeat"); + flipper_format_delete_key(flipper_format, "Manufacture"); + + // Create subghz folder directory if necessary + if(!storage_simply_mkdir(storage, furi_string_get_cstr(file_dir))) { + dialog_message_show_storage_error(subghz->dialogs, "Cannot create\nfolder"); + break; + } + + if(!storage_simply_remove(storage, dev_file_name)) { + break; + } + stream_seek(flipper_format_stream, 0, StreamOffsetFromStart); + stream_save_to_file(flipper_format_stream, storage, dev_file_name, FSOM_CREATE_ALWAYS); + + if(storage_common_stat(storage, dev_file_name, NULL) != FSE_OK) { + break; + } + + saved = true; + } while(0); + furi_string_free(file_dir); + furi_record_close(RECORD_STORAGE); + return saved; +} + +void subghz_save_to_file(void* context) { + furi_assert(context); + SubGhz* subghz = context; + if(subghz_path_is_file(subghz->file_path)) { + subghz_save_protocol_to_file( + subghz, + subghz_txrx_get_fff_data(subghz->txrx), + furi_string_get_cstr(subghz->file_path)); + } +} + bool subghz_load_protocol_from_file(SubGhz* subghz, const char* path) { furi_assert(subghz); @@ -214,9 +268,9 @@ bool subghz_load_protocol_from_file(SubGhz* subghz, const char* path) { return ret; }*/ -/*bool subghz_path_is_file(FuriString* path) { +bool subghz_path_is_file(FuriString* path) { return furi_string_end_with(path, SUBGHZ_APP_FILENAME_EXTENSION); -}*/ +} /*void subghz_lock(SubGhz* subghz) { furi_assert(subghz); diff --git a/cross_remote/helpers/subghz/subghz_i.h b/cross_remote/helpers/subghz/subghz_i.h index fd9581d70..44e88cca5 100644 --- a/cross_remote/helpers/subghz/subghz_i.h +++ b/cross_remote/helpers/subghz/subghz_i.h @@ -37,6 +37,7 @@ #include "subghz_txrx.h" #define SUBGHZ_MAX_LEN_NAME 64 +#define SUBGHZ_APP_FILENAME_EXTENSION ".sub" typedef struct SubGhz SubGhz; @@ -67,6 +68,12 @@ struct SubGhz { //void subghz_set_default_preset(SubGhz* subghz); //void subghz_blink_start(SubGhz* subghz); //void subghz_blink_stop(SubGhz* subghz); +bool subghz_save_protocol_to_file( + SubGhz* subghz, + FlipperFormat* flipper_format, + const char* dev_file_name); +void subghz_save_to_file(void* context); +bool subghz_path_is_file(FuriString* path); // Used on Encoded SubGhz bool subghz_tx_start(SubGhz* subghz, FlipperFormat* flipper_format); diff --git a/cross_remote/helpers/subghz/subghz_txrx.c b/cross_remote/helpers/subghz/subghz_txrx.c index c0a2be045..b7e394fb0 100644 --- a/cross_remote/helpers/subghz/subghz_txrx.c +++ b/cross_remote/helpers/subghz/subghz_txrx.c @@ -27,7 +27,7 @@ static void subghz_txrx_radio_device_power_off(SubGhzTxRx* instance) { if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg(); } -SubGhzTxRx* subghz_txrx_alloc() { +SubGhzTxRx* subghz_txrx_alloc(void) { SubGhzTxRx* instance = malloc(sizeof(SubGhzTxRx)); instance->setting = subghz_setting_alloc(); subghz_setting_load(instance->setting, EXT_PATH("subghz/assets/setting_user")); @@ -71,14 +71,11 @@ SubGhzTxRx* subghz_txrx_alloc() { instance->radio_device_type = subghz_txrx_radio_device_set(instance, SubGhzRadioDeviceTypeExternalCC1101); - FURI_LOG_D(TAG, "completed TXRX alloc"); - return instance; } void subghz_txrx_free(SubGhzTxRx* instance) { furi_assert(instance); - FURI_LOG_D(TAG, "freeing TXRX"); if(instance->radio_device_type != SubGhzRadioDeviceTypeInternal) { subghz_txrx_radio_device_power_off(instance); @@ -194,7 +191,6 @@ static void subghz_txrx_idle(SubGhzTxRx* instance) { subghz_txrx_speaker_off(instance); instance->txrx_state = SubGhzTxRxStateIDLE; } - FURI_LOG_D(TAG, "completed subghz_txrx_idle"); } /*static void subghz_txrx_rx_end(SubGhzTxRx* instance) { @@ -228,7 +224,6 @@ static bool subghz_txrx_tx(SubGhzTxRx* instance, uint32_t frequency) { instance->txrx_state = SubGhzTxRxStateTx; } - FURI_LOG_D(TAG, "completed subghz_txrx_tx"); return ret; } @@ -241,10 +236,7 @@ SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* SubGhzTxRxStartTxState ret = SubGhzTxRxStartTxStateErrorParserOthers; FuriString* temp_str = furi_string_alloc(); uint32_t repeat = 200; - - FURI_LOG_D(TAG, "starting loop in subghz_txrx_tx_start"); do { - FURI_LOG_D(TAG, "looping"); if(!flipper_format_rewind(flipper_format)) { FURI_LOG_E(TAG, "Rewind error"); break; @@ -257,7 +249,6 @@ SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* FURI_LOG_E(TAG, "Unable Repeat"); break; } - //FURI_LOG_D(TAG, "File loaded"); ret = SubGhzTxRxStartTxStateOk; SubGhzRadioPreset* preset = instance->preset; @@ -265,24 +256,18 @@ SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* subghz_transmitter_alloc_init(instance->environment, furi_string_get_cstr(temp_str)); if(instance->transmitter) { - FURI_LOG_D(TAG, "transmitter found"); if(subghz_transmitter_deserialize(instance->transmitter, flipper_format) == SubGhzProtocolStatusOk) { - //if (false) { - FURI_LOG_D(TAG, "deserialization"); if(strcmp(furi_string_get_cstr(preset->name), "") != 0) { - FURI_LOG_D(TAG, "got preset name"); subghz_txrx_begin( instance, subghz_setting_get_preset_data_by_name( instance->setting, furi_string_get_cstr(preset->name))); - FURI_LOG_D(TAG, "loaded preset by name"); if(preset->frequency) { if(!subghz_txrx_tx(instance, preset->frequency)) { FURI_LOG_E(TAG, "Only Rx"); ret = SubGhzTxRxStartTxStateErrorOnlyRx; } - FURI_LOG_D(TAG, "got frequency"); } else { ret = SubGhzTxRxStartTxStateErrorParserOthers; } @@ -295,12 +280,10 @@ SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* if(ret == SubGhzTxRxStartTxStateOk) { //Start TX - FURI_LOG_D(TAG, "starting Async TX"); subghz_devices_start_async_tx( instance->radio_device, subghz_transmitter_yield, instance->transmitter); } } else { - FURI_LOG_D(TAG, "no deserialization"); ret = SubGhzTxRxStartTxStateErrorParserOthers; } } else { @@ -328,14 +311,14 @@ SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* subghz_txrx_rx(instance, instance->preset->frequency); }*/ -/*void subghz_txrx_set_need_save_callback( +void subghz_txrx_set_need_save_callback( SubGhzTxRx* instance, SubGhzTxRxNeedSaveCallback callback, void* context) { furi_assert(instance); instance->need_save_callback = callback; instance->need_save_context = context; -}*/ +} static void subghz_txrx_tx_stop(SubGhzTxRx* instance) { furi_assert(instance); @@ -346,11 +329,11 @@ static void subghz_txrx_tx_stop(SubGhzTxRx* instance) { subghz_transmitter_free(instance->transmitter); //if protocol dynamic then we save the last upload - /*if(instance->decoder_result->protocol->type == SubGhzProtocolTypeDynamic) { + if(instance->decoder_result->protocol->type == SubGhzProtocolTypeDynamic) { if(instance->need_save_callback) { instance->need_save_callback(instance->need_save_context); } - }*/ + } subghz_txrx_idle(instance); subghz_txrx_speaker_off(instance); } diff --git a/cross_remote/helpers/subghz/subghz_txrx.h b/cross_remote/helpers/subghz/subghz_txrx.h index a74051ade..d8c6aba87 100644 --- a/cross_remote/helpers/subghz/subghz_txrx.h +++ b/cross_remote/helpers/subghz/subghz_txrx.h @@ -221,10 +221,10 @@ SubGhzProtocolDecoderBase* subghz_txrx_get_decoder(SubGhzTxRx* instance); * @param callback Callback for save data * @param context Context for callback */ -/*void subghz_txrx_set_need_save_callback( +void subghz_txrx_set_need_save_callback( SubGhzTxRx* instance, SubGhzTxRxNeedSaveCallback callback, - void* context);*/ + void* context); /** * Get pointer to a load data key diff --git a/cross_remote/helpers/xremote_storage.c b/cross_remote/helpers/xremote_storage.c index 7077b01a1..c63e39c33 100644 --- a/cross_remote/helpers/xremote_storage.c +++ b/cross_remote/helpers/xremote_storage.c @@ -59,6 +59,8 @@ void xremote_save_settings(void* context) { fff_file, XREMOTE_SETTINGS_KEY_SAVE_SETTINGS, &app->save_settings, 1); flipper_format_write_uint32(fff_file, XREMOTE_SETTINGS_KEY_IR_TIMING, &app->ir_timing, 1); flipper_format_write_uint32(fff_file, XREMOTE_SETTINGS_KEY_SG_TIMING, &app->sg_timing, 1); + flipper_format_write_uint32( + fff_file, XREMOTE_SETTINGS_KEY_LOOP_TRANSMIT, &app->loop_transmit, 1); if(!flipper_format_rewind(fff_file)) { xremote_close_config_file(fff_file); @@ -112,6 +114,8 @@ void xremote_read_settings(void* context) { fff_file, XREMOTE_SETTINGS_KEY_SAVE_SETTINGS, &app->save_settings, 1); flipper_format_read_uint32(fff_file, XREMOTE_SETTINGS_KEY_IR_TIMING, &app->ir_timing, 1); flipper_format_read_uint32(fff_file, XREMOTE_SETTINGS_KEY_SG_TIMING, &app->sg_timing, 1); + flipper_format_read_uint32( + fff_file, XREMOTE_SETTINGS_KEY_LOOP_TRANSMIT, &app->loop_transmit, 1); flipper_format_rewind(fff_file); diff --git a/cross_remote/helpers/xremote_storage.h b/cross_remote/helpers/xremote_storage.h index 650fac2c8..166faaee5 100644 --- a/cross_remote/helpers/xremote_storage.h +++ b/cross_remote/helpers/xremote_storage.h @@ -13,6 +13,7 @@ #define XREMOTE_SETTINGS_KEY_SAVE_SETTINGS "SaveSettings" #define XREMOTE_SETTINGS_KEY_IR_TIMING "IRTiming" #define XREMOTE_SETTINGS_KEY_SG_TIMING "SGTiming" +#define XREMOTE_SETTINGS_KEY_LOOP_TRANSMIT "LoopTransmit" void xremote_save_settings(void* context); void xremote_read_settings(void* context); \ No newline at end of file diff --git a/cross_remote/icons/KeyBackspaceSelected_16x9.png b/cross_remote/icons/KeyBackspaceSelected_16x9.png deleted file mode 100644 index 7cc0759a8..000000000 Binary files a/cross_remote/icons/KeyBackspaceSelected_16x9.png and /dev/null differ diff --git a/cross_remote/icons/KeyBackspace_16x9.png b/cross_remote/icons/KeyBackspace_16x9.png deleted file mode 100644 index 9946232d9..000000000 Binary files a/cross_remote/icons/KeyBackspace_16x9.png and /dev/null differ diff --git a/cross_remote/icons/KeySaveSelected_24x11.png b/cross_remote/icons/KeySaveSelected_24x11.png deleted file mode 100644 index eeb3569d3..000000000 Binary files a/cross_remote/icons/KeySaveSelected_24x11.png and /dev/null differ diff --git a/cross_remote/icons/KeySave_24x11.png b/cross_remote/icons/KeySave_24x11.png deleted file mode 100644 index e7dba987a..000000000 Binary files a/cross_remote/icons/KeySave_24x11.png and /dev/null differ diff --git a/cross_remote/icons/ir_ani_1_32x22.png b/cross_remote/icons/ir_ani_1_32x22.png new file mode 100644 index 000000000..c11f9f1cf Binary files /dev/null and b/cross_remote/icons/ir_ani_1_32x22.png differ diff --git a/cross_remote/icons/ir_ani_2_32x22.png b/cross_remote/icons/ir_ani_2_32x22.png new file mode 100644 index 000000000..3e33bc834 Binary files /dev/null and b/cross_remote/icons/ir_ani_2_32x22.png differ diff --git a/cross_remote/icons/ir_ani_32x22.png b/cross_remote/icons/ir_ani_32x22.png new file mode 100644 index 000000000..ec7b52ec1 Binary files /dev/null and b/cross_remote/icons/ir_ani_32x22.png differ diff --git a/cross_remote/icons/ir_ani_3_32x22.png b/cross_remote/icons/ir_ani_3_32x22.png new file mode 100644 index 000000000..75acfa2ad Binary files /dev/null and b/cross_remote/icons/ir_ani_3_32x22.png differ diff --git a/cross_remote/icons/pause_ani_1_22x23.png b/cross_remote/icons/pause_ani_1_22x23.png new file mode 100644 index 000000000..055c8ee86 Binary files /dev/null and b/cross_remote/icons/pause_ani_1_22x23.png differ diff --git a/cross_remote/icons/pause_ani_2_22x23.png b/cross_remote/icons/pause_ani_2_22x23.png new file mode 100644 index 000000000..b8884185d Binary files /dev/null and b/cross_remote/icons/pause_ani_2_22x23.png differ diff --git a/cross_remote/icons/pause_ani_3_22x23.png b/cross_remote/icons/pause_ani_3_22x23.png new file mode 100644 index 000000000..c8cb97727 Binary files /dev/null and b/cross_remote/icons/pause_ani_3_22x23.png differ diff --git a/cross_remote/icons/sg_ani_1_19x13.png b/cross_remote/icons/sg_ani_1_19x13.png new file mode 100644 index 000000000..6e3c8d0cf Binary files /dev/null and b/cross_remote/icons/sg_ani_1_19x13.png differ diff --git a/cross_remote/icons/sg_ani_2_19x13.png b/cross_remote/icons/sg_ani_2_19x13.png new file mode 100644 index 000000000..92ac3c054 Binary files /dev/null and b/cross_remote/icons/sg_ani_2_19x13.png differ diff --git a/cross_remote/icons/sg_ani_3_19x13.png b/cross_remote/icons/sg_ani_3_19x13.png new file mode 100644 index 000000000..966cc04af Binary files /dev/null and b/cross_remote/icons/sg_ani_3_19x13.png differ diff --git a/cross_remote/scenes/xremote_scene_settings.c b/cross_remote/scenes/xremote_scene_settings.c index 7e66d2551..d60115d67 100644 --- a/cross_remote/scenes/xremote_scene_settings.c +++ b/cross_remote/scenes/xremote_scene_settings.c @@ -33,7 +33,14 @@ const uint32_t led_value[2] = { XRemoteLedOff, XRemoteLedOn, }; - +const char* const loop_text[2] = { + "OFF", + "ON", +}; +const uint32_t loop_value[2] = { + XRemoteLoopOff, + XRemoteLoopOn, +}; const char* const settings_text[2] = { "OFF", "ON", @@ -72,6 +79,13 @@ static void xremote_scene_settings_set_save_settings(VariableItem* item) { app->save_settings = settings_value[index]; } +static void xremote_scene_settings_set_loop(VariableItem* item) { + XRemote* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, loop_text[index]); + app->loop_transmit = loop_value[index]; +} + static void xremote_scene_settings_set_ir_timing(VariableItem* item) { XRemote* app = variable_item_get_context(item); uint32_t index = variable_item_get_current_value_index(item); @@ -116,11 +130,18 @@ void xremote_scene_settings_on_enter(void* context) { // LED Effects on/off item = variable_item_list_add( - app->variable_item_list, "LED FX:", 2, xremote_scene_settings_set_led, app); + app->variable_item_list, "LED FX", 2, xremote_scene_settings_set_led, app); value_index = value_index_uint32(app->led, led_value, 2); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, led_text[value_index]); + /* NEW: Loop saved command functionality */ + item = variable_item_list_add( + app->variable_item_list, "Loop Transmit", 2, xremote_scene_settings_set_loop, app); + value_index = value_index_uint32(app->loop_transmit, loop_value, 2); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, loop_text[value_index]); + // Save Settings to File item = variable_item_list_add( app->variable_item_list, "Save Settings", 2, xremote_scene_settings_set_save_settings, app); diff --git a/cross_remote/scenes/xremote_scene_transmit.c b/cross_remote/scenes/xremote_scene_transmit.c index 339d4b7cb..053f40819 100644 --- a/cross_remote/scenes/xremote_scene_transmit.c +++ b/cross_remote/scenes/xremote_scene_transmit.c @@ -18,9 +18,10 @@ static const NotificationSequence* xremote_notification_sequences[] = { &sequence_blink_stop, }; -void xremote_transmit_callback(XRemoteCustomEvent event, void* context) { +void xremote_scene_transmit_callback(XRemoteCustomEvent event, void* context) { furi_assert(context); XRemote* app = context; + FURI_LOG_D(TAG, "trigger xremote_transmit_callback"); view_dispatcher_send_custom_event(app->view_dispatcher, event); } @@ -114,44 +115,51 @@ void xremote_scene_transmit_send_signal(void* context, CrossRemoteItem* item) { } } -void xremote_scene_transmit_run_remote(void* context) { - furi_assert(context); - XRemote* app = context; +static void xremote_scene_transmit_end_scene(XRemote* app) { + xremote_scene_ir_notification_message(app, InfraredNotificationMessageBlinkStop); + scene_manager_previous_scene(app->scene_manager); +} + +static void xremote_scene_transmit_run_single_transmit(XRemote* app) { CrossRemote* remote = app->cross_remote; + if(xremote_cross_remote_get_transmitting(remote) == XRemoteTransmittingIdle) { + xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingStart); + CrossRemoteItem* item = xremote_cross_remote_get_item(remote, app->transmit_item); + xremote_scene_transmit_send_signal(app, item); + } else if(xremote_cross_remote_get_transmitting(remote) == XRemoteTransmittingStopSubghz) { + app->transmit_item++; + app->state_notifications = SubGhzNotificationStateIDLE; + app->transmitting = false; + subghz_txrx_stop(app->subghz->txrx); + xremote_scene_ir_notification_message(app, SubGhzNotificationMessageBlinkStop); + xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingIdle); + } else if(xremote_cross_remote_get_transmitting(remote) == XRemoteTransmittingStop) { + app->transmit_item++; + xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingIdle); + } +} +static void xremote_scene_transmit_run_next_transmission(XRemote* app) { + CrossRemote* remote = app->cross_remote; size_t item_count = xremote_cross_remote_get_item_count(remote); - for(size_t i = 0; i < item_count;) { - if(xremote_cross_remote_get_transmitting(remote) == XRemoteTransmittingIdle) { - xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingStart); - CrossRemoteItem* item = xremote_cross_remote_get_item(remote, i); - xremote_scene_transmit_send_signal(app, item); - //furi_thread_flags_wait(0, FuriFlagWaitAny, 1000); - } else if(xremote_cross_remote_get_transmitting(remote) == XRemoteTransmittingStopSubghz) { - i++; - app->state_notifications = SubGhzNotificationStateIDLE; - app->transmitting = false; - subghz_txrx_stop(app->subghz->txrx); - xremote_scene_ir_notification_message(app, SubGhzNotificationMessageBlinkStop); - xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingIdle); - //furi_thread_flags_wait(0, FuriFlagWaitAny, 1000); - } else if(xremote_cross_remote_get_transmitting(remote) == XRemoteTransmittingStop) { - i++; - xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingIdle); - } + if(app->transmit_item < item_count) { + xremote_scene_transmit_run_single_transmit(app); + return; } - xremote_scene_ir_notification_message(app, InfraredNotificationMessageBlinkStop); - - scene_manager_previous_scene(app->scene_manager); - //xremote_transmit_model_set_name(app->xremote_transmit, xremote_cross_remote_get_name(remote)); + if(app->loop_transmit && !app->stop_transmit) { + app->transmit_item = 0; + return; + } + xremote_scene_transmit_end_scene(app); } void xremote_scene_transmit_on_enter(void* context) { furi_assert(context); XRemote* app = context; - xremote_transmit_set_callback(app->xremote_transmit, xremote_transmit_callback, app); + app->transmit_item = 0; + xremote_transmit_set_callback(app->xremote_transmit, xremote_scene_transmit_callback, app); view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdTransmit); - xremote_scene_transmit_run_remote(app); } bool xremote_scene_transmit_on_event(void* context, SceneManagerEvent event) { @@ -162,13 +170,14 @@ bool xremote_scene_transmit_on_event(void* context, SceneManagerEvent event) { FURI_LOG_D(TAG, "Custom Event"); switch(event.event) { case XRemoteCustomEventViewTransmitterSendStop: - FURI_LOG_D("SUBGHZ", "stop event"); // doesn't trigger + app->stop_transmit = true; break; default: break; } } else if(event.type == SceneManagerEventTypeTick) { FURI_LOG_D(TAG, "Tick Event"); + xremote_scene_transmit_run_next_transmission(app); with_view_model( xremote_transmit_get_view(app->xremote_transmit), void* model, @@ -177,6 +186,10 @@ bool xremote_scene_transmit_on_event(void* context, SceneManagerEvent event) { if(app->state_notifications == SubGhzNotificationStateTx && app->led == 1) { //blink for subghz } + if(app->stop_transmit == true) { + app->stop_transmit = false; + xremote_scene_transmit_end_scene(app); + } } return consumed; diff --git a/cross_remote/views/xremote_transmit.c b/cross_remote/views/xremote_transmit.c index f87bf7d2d..ea657c373 100644 --- a/cross_remote/views/xremote_transmit.c +++ b/cross_remote/views/xremote_transmit.c @@ -50,12 +50,16 @@ void xremote_transmit_draw_ir(Canvas* canvas, XRemoteTransmitModel* model) { canvas_set_font(canvas, FontPrimary); canvas_draw_str_aligned(canvas, 74, 5, AlignLeft, AlignTop, "Sending"); canvas_set_font(canvas, FontSecondary); - canvas_draw_str_aligned(canvas, 74, 20, AlignLeft, AlignTop, "Infrared"); - canvas_draw_str_aligned(canvas, 74, 30, AlignLeft, AlignTop, model->name); - - char temp_str[18]; - snprintf(temp_str, 18, "%u", model->time); - canvas_draw_str_aligned(canvas, 74, 40, AlignLeft, AlignTop, temp_str); + canvas_draw_str_aligned(canvas, 74, 15, AlignLeft, AlignTop, "Infrared"); + canvas_draw_str_aligned(canvas, 74, 25, AlignLeft, AlignTop, model->name); + + if(model->time == 0) { + canvas_draw_icon(canvas, 36, 2, &I_ir_ani_1_32x22); + } else if(model->time == 1) { + canvas_draw_icon(canvas, 36, 2, &I_ir_ani_2_32x22); + } else if(model->time == 2) { + canvas_draw_icon(canvas, 36, 2, &I_ir_ani_3_32x22); + } } void xremote_transmit_draw_pause(Canvas* canvas, XRemoteTransmitModel* model) { @@ -66,12 +70,16 @@ void xremote_transmit_draw_pause(Canvas* canvas, XRemoteTransmitModel* model) { canvas_set_font(canvas, FontPrimary); canvas_draw_str_aligned(canvas, 74, 5, AlignLeft, AlignTop, "Waiting"); canvas_set_font(canvas, FontSecondary); - canvas_draw_str_aligned(canvas, 74, 20, AlignLeft, AlignTop, "Sequence"); - canvas_draw_str_aligned(canvas, 74, 30, AlignLeft, AlignTop, model->name); - - char temp_str[18]; - snprintf(temp_str, 18, "%u", model->time); - canvas_draw_str_aligned(canvas, 74, 40, AlignLeft, AlignTop, temp_str); + canvas_draw_str_aligned(canvas, 74, 15, AlignLeft, AlignTop, "Sequence"); + canvas_draw_str_aligned(canvas, 74, 25, AlignLeft, AlignTop, model->name); + + if(model->time == 0) { + canvas_draw_icon(canvas, 9, 28, &I_pause_ani_1_22x23); + } else if(model->time == 1) { + canvas_draw_icon(canvas, 9, 28, &I_pause_ani_2_22x23); + } else if(model->time == 2) { + canvas_draw_icon(canvas, 9, 28, &I_pause_ani_3_22x23); + } } void xremote_transmit_draw_subghz(Canvas* canvas, XRemoteTransmitModel* model) { @@ -82,12 +90,16 @@ void xremote_transmit_draw_subghz(Canvas* canvas, XRemoteTransmitModel* model) { canvas_set_font(canvas, FontPrimary); canvas_draw_str_aligned(canvas, 74, 5, AlignLeft, AlignTop, "Sending"); canvas_set_font(canvas, FontSecondary); - canvas_draw_str_aligned(canvas, 74, 20, AlignLeft, AlignTop, "SubGhz"); - canvas_draw_str_aligned(canvas, 74, 30, AlignLeft, AlignTop, model->name); - - char temp_str[18]; - snprintf(temp_str, 18, "%u", model->time); - canvas_draw_str_aligned(canvas, 74, 40, AlignLeft, AlignTop, temp_str); + canvas_draw_str_aligned(canvas, 74, 15, AlignLeft, AlignTop, "SubGhz"); + canvas_draw_str_aligned(canvas, 74, 25, AlignLeft, AlignTop, model->name); + + if(model->time == 0) { + canvas_draw_icon(canvas, 15, 1, &I_sg_ani_1_19x13); + } else if(model->time == 1) { + canvas_draw_icon(canvas, 15, 1, &I_sg_ani_2_19x13); + } else if(model->time == 2) { + canvas_draw_icon(canvas, 15, 1, &I_sg_ani_3_19x13); + } } void xremote_transmit_draw(Canvas* canvas, XRemoteTransmitModel* model) { @@ -98,6 +110,10 @@ void xremote_transmit_draw(Canvas* canvas, XRemoteTransmitModel* model) { } else if(model->type == XRemoteRemoteItemTypePause) { xremote_transmit_draw_pause(canvas, model); } + if(model->time > 2) { + model->time = 0; + } + elements_button_right(canvas, "exit"); } bool xremote_transmit_input(InputEvent* event, void* context) { @@ -106,6 +122,7 @@ bool xremote_transmit_input(InputEvent* event, void* context) { if(event->type == InputTypeRelease) { switch(event->key) { case InputKeyBack: + case InputKeyRight: with_view_model( instance->view, XRemoteTransmitModel * model, @@ -123,6 +140,16 @@ bool xremote_transmit_input(InputEvent* event, void* context) { return true; } +void xremote_transmit_enter(void* context) { + furi_assert(context); + XRemoteTransmit* instance = (XRemoteTransmit*)context; + with_view_model( + instance->view, + XRemoteTransmitModel * model, + { xremote_transmit_model_init(model); }, + true); +} + XRemoteTransmit* xremote_transmit_alloc() { XRemoteTransmit* instance = malloc(sizeof(XRemoteTransmit)); instance->view = view_alloc(); @@ -130,7 +157,7 @@ XRemoteTransmit* xremote_transmit_alloc() { view_set_context(instance->view, instance); view_set_draw_callback(instance->view, (ViewDrawCallback)xremote_transmit_draw); view_set_input_callback(instance->view, xremote_transmit_input); - view_set_enter_callback(instance->view, xremote_transmit_enter); + //view_set_enter_callback(instance->view, xremote_transmit_enter); with_view_model( instance->view, @@ -141,16 +168,6 @@ XRemoteTransmit* xremote_transmit_alloc() { return instance; } -void xremote_transmit_enter(void* context) { - furi_assert(context); - XRemoteTransmit* instance = (XRemoteTransmit*)context; - with_view_model( - instance->view, - XRemoteTransmitModel * model, - { xremote_transmit_model_init(model); }, - true); -} - void xremote_transmit_free(XRemoteTransmit* instance) { furi_assert(instance); diff --git a/cross_remote/xremote.c b/cross_remote/xremote.c index 6d87a6ba3..e3ddbb860 100644 --- a/cross_remote/xremote.c +++ b/cross_remote/xremote.c @@ -51,6 +51,8 @@ XRemote* xremote_app_alloc() { app->sg_timing = 500; app->sg_timing_char = "500"; app->stop_transmit = false; + app->loop_transmit = 0; + app->transmit_item = 0; // Load configs xremote_read_settings(app); diff --git a/cross_remote/xremote.h b/cross_remote/xremote.h index 3314d9a37..244b68eb0 100644 --- a/cross_remote/xremote.h +++ b/cross_remote/xremote.h @@ -42,6 +42,7 @@ typedef struct { uint32_t speaker; uint32_t led; uint32_t save_settings; + uint32_t loop_transmit; uint32_t edit_item; uint32_t ir_timing; char* ir_timing_char; @@ -49,6 +50,7 @@ typedef struct { char* sg_timing_char; bool transmitting; bool stop_transmit; + size_t transmit_item; char text_store[XREMOTE_TEXT_STORE_NUM][XREMOTE_TEXT_STORE_SIZE + 1]; SubGhz* subghz; NumberInput* number_input; @@ -85,6 +87,11 @@ typedef enum { XRemoteLedOn, } XRemoteLedState; +typedef enum { + XRemoteLoopOff, + XRemoteLoopOn, +} XRemoteLoopState; + typedef enum { XRemoteSettingsOff, XRemoteSettingsOn, diff --git a/cross_remote/xremote_i.h b/cross_remote/xremote_i.h index 5886644a6..890f47037 100644 --- a/cross_remote/xremote_i.h +++ b/cross_remote/xremote_i.h @@ -51,10 +51,10 @@ #define XREMOTE_TEXT_STORE_SIZE 128 #define XREMOTE_MAX_ITEM_NAME_LENGTH 22 #define XREMOTE_MAX_REMOTE_NAME_LENGTH 22 -#define XREMOTE_VERSION "2.7" +#define XREMOTE_VERSION "3.0" #define INFRARED_APP_EXTENSION ".ir" -#define INFRARED_APP_FOLDER ANY_PATH("infrared") +#define INFRARED_APP_FOLDER EXT_PATH("infrared") #define TAG "XRemote" diff --git a/esp_flasher/.gitsubtree b/esp_flasher/.gitsubtree index e2f0b4677..a98d418ea 100644 --- a/esp_flasher/.gitsubtree +++ b/esp_flasher/.gitsubtree @@ -1,2 +1,2 @@ -https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/esp_flasher 4558d74c9da36abc851edd96a95d18f7d5511a75 +https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/esp_flasher c450eaa657997d2a8776df11b837e22a69fabd77 https://github.com/0xchocolate/flipperzero-esp-flasher main / diff --git a/esp_flasher/resources/apps_data/esp_flasher/assets/flipperhttp/s2/flipper_http_bootloader.bin b/esp_flasher/resources/apps_data/esp_flasher/assets/flipperhttp/s2/flipper_http_bootloader.bin index 15e26392d..ded603b18 100644 Binary files a/esp_flasher/resources/apps_data/esp_flasher/assets/flipperhttp/s2/flipper_http_bootloader.bin and b/esp_flasher/resources/apps_data/esp_flasher/assets/flipperhttp/s2/flipper_http_bootloader.bin differ 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 c10bca3a6..dd66e4f76 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/resources/apps_data/esp_flasher/assets/marauder/s2/esp32_marauder.flipper.bin b/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/s2/esp32_marauder.flipper.bin index d089c7b62..93a908d60 100644 Binary files a/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/s2/esp32_marauder.flipper.bin and b/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/s2/esp32_marauder.flipper.bin differ diff --git a/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/s3/esp32_marauder.multiboardS3.bin b/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/s3/esp32_marauder.multiboardS3.bin index 4f6f02ef0..058bd80ac 100644 Binary files a/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/s3/esp32_marauder.multiboardS3.bin and b/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/s3/esp32_marauder.multiboardS3.bin differ diff --git a/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/wroom/esp32_marauder.dev_board_pro.bin b/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/wroom/esp32_marauder.dev_board_pro.bin index 8d93838bc..e8e020158 100644 Binary files a/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/wroom/esp32_marauder.dev_board_pro.bin and b/esp_flasher/resources/apps_data/esp_flasher/assets/marauder/wroom/esp32_marauder.dev_board_pro.bin differ diff --git a/flip_library/jsmn/jsmn.c b/flip_library/jsmn/jsmn.c index eb33b3cc7..b85ab83b5 100644 --- a/flip_library/jsmn/jsmn.c +++ b/flip_library/jsmn/jsmn.c @@ -13,11 +13,13 @@ /** * 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; +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) { + if (parser->toknext >= num_tokens) + { return NULL; } tok = &tokens[parser->toknext++]; @@ -32,8 +34,9 @@ static jsmntok_t* /** * Fills token type and boundaries. */ -static void - jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { +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; @@ -43,19 +46,19 @@ static void /** * 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; +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]) { + 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 ':': @@ -72,7 +75,8 @@ static int jsmn_parse_primitive( /* to quiet a warning from gcc*/ break; } - if(js[parser->pos] < 32 || js[parser->pos] >= 127) { + if (js[parser->pos] < 32 || js[parser->pos] >= 127) + { parser->pos = start; return JSMN_ERROR_INVAL; } @@ -84,12 +88,14 @@ static int jsmn_parse_primitive( #endif found: - if(tokens == NULL) { + if (tokens == NULL) + { parser->pos--; return 0; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { parser->pos = start; return JSMN_ERROR_NOMEM; } @@ -104,29 +110,31 @@ static int jsmn_parse_primitive( /** * 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; +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++) { + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { char c = js[parser->pos]; /* Quote: end of string */ - if(c == '\"') { - if(tokens == NULL) { + if (c == '\"') + { + if (tokens == NULL) + { return 0; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { parser->pos = start; return JSMN_ERROR_NOMEM; } @@ -138,10 +146,12 @@ static int jsmn_parse_string( } /* Backslash: Quoted symbol expected */ - if(c == '\\' && parser->pos + 1 < len) { + if (c == '\\' && parser->pos + 1 < len) + { int i; parser->pos++; - switch(js[parser->pos]) { + switch (js[parser->pos]) + { /* Allowed escaped symbols */ case '\"': case '/': @@ -155,11 +165,13 @@ static int jsmn_parse_string( /* Allows escaped symbol \uXXXX */ case 'u': parser->pos++; - for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + 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 */ + 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; } @@ -181,7 +193,8 @@ static int jsmn_parse_string( /** * Create JSON parser over an array of tokens */ -void jsmn_init(jsmn_parser* parser) { +void jsmn_init(jsmn_parser *parser) +{ parser->pos = 0; parser->toknext = 0; parser->toksuper = -1; @@ -190,38 +203,41 @@ void jsmn_init(jsmn_parser* parser) { /** * 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 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; + jsmntok_t *token; int count = parser->toknext; - for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { char c; jsmntype_t type; c = js[parser->pos]; - switch(c) { + switch (c) + { case '{': case '[': count++; - if(tokens == NULL) { + if (tokens == NULL) + { break; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { return JSMN_ERROR_NOMEM; } - if(parser->toksuper != -1) { - jsmntok_t* t = &tokens[parser->toksuper]; + 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) { + if (t->type == JSMN_OBJECT) + { return JSMN_ERROR_INVAL; } #endif @@ -236,26 +252,33 @@ int jsmn_parse( break; case '}': case ']': - if(tokens == NULL) { + if (tokens == NULL) + { break; } type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); #ifdef JSMN_PARENT_LINKS - if(parser->toknext < 1) { + 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) { + 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) { + if (token->parent == -1) + { + if (token->type != type || parser->toksuper == -1) + { return JSMN_ERROR_INVAL; } break; @@ -263,10 +286,13 @@ int jsmn_parse( token = &tokens[token->parent]; } #else - for(i = parser->toknext - 1; i >= 0; i--) { + for (i = parser->toknext - 1; i >= 0; i--) + { token = &tokens[i]; - if(token->start != -1 && token->end == -1) { - if(token->type != type) { + if (token->start != -1 && token->end == -1) + { + if (token->type != type) + { return JSMN_ERROR_INVAL; } parser->toksuper = -1; @@ -275,12 +301,15 @@ int jsmn_parse( } } /* Error if unmatched closing bracket */ - if(i == -1) { + if (i == -1) + { return JSMN_ERROR_INVAL; } - for(; i >= 0; i--) { + for (; i >= 0; i--) + { token = &tokens[i]; - if(token->start != -1 && token->end == -1) { + if (token->start != -1 && token->end == -1) + { parser->toksuper = i; break; } @@ -289,11 +318,13 @@ int jsmn_parse( break; case '\"': r = jsmn_parse_string(parser, js, len, tokens, num_tokens); - if(r < 0) { + if (r < 0) + { return r; } count++; - if(parser->toksuper != -1 && tokens != NULL) { + if (parser->toksuper != -1 && tokens != NULL) + { tokens[parser->toksuper].size++; } break; @@ -306,15 +337,19 @@ int jsmn_parse( 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) { + 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) { + 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; } @@ -340,9 +375,12 @@ int jsmn_parse( 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)) { + 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; } } @@ -351,11 +389,13 @@ int jsmn_parse( default: #endif r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); - if(r < 0) { + if (r < 0) + { return r; } count++; - if(parser->toksuper != -1 && tokens != NULL) { + if (parser->toksuper != -1 && tokens != NULL) + { tokens[parser->toksuper].size++; } break; @@ -368,10 +408,13 @@ int jsmn_parse( } } - if(tokens != NULL) { - for(i = parser->toknext - 1; i >= 0; i--) { + 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) { + if (tokens[i].start != -1 && tokens[i].end == -1) + { return JSMN_ERROR_PART; } } @@ -381,10 +424,12 @@ int jsmn_parse( } // 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) { +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); @@ -392,30 +437,36 @@ 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) { - if(tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && - strncmp(json + tok->start, s, tok->end - tok->start) == 0) { +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) { +char *get_json_value(char *key, char *json_data, uint32_t max_tokens) +{ // Parse the JSON feed - if(json_data != NULL) { + 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) { + 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) { + if (ret < 0) + { // Handle parsing errors FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); free(tokens); @@ -423,19 +474,23 @@ char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { } // Ensure that the root element is an object - if(ret < 1 || tokens[0].type != JSMN_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) { + 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) { + char *value = malloc(length + 1); + if (value == NULL) + { FURI_LOG_E("JSMM.H", "Failed to allocate memory for value."); free(tokens); return NULL; @@ -450,7 +505,9 @@ char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { // Free the token array if key was not found free(tokens); - } else { + } + else + { FURI_LOG_E("JSMM.H", "JSON data is NULL"); } FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON."); @@ -458,10 +515,12 @@ 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) { +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) { + 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; } @@ -471,8 +530,9 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t jsmn_init(&parser); // Allocate memory for JSON tokens - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); - if(tokens == NULL) { + 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; @@ -480,7 +540,8 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t // Parse the JSON array int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); - if(ret < 0) { + if (ret < 0) + { FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); free(tokens); free(array_str); @@ -488,7 +549,8 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // Ensure the root element is an array - if(ret < 1 || tokens[0].type != JSMN_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); @@ -496,12 +558,9 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // 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); + 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; @@ -509,20 +568,27 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t // 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 (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) { + } + else if (tokens[current_token].type == JSMN_ARRAY) + { // For nested arrays, skip all elements current_token += 1 + tokens[current_token].size; - } else { + } + else + { // For primitive types, simply move to the next token current_token += 1; } // Safety check to prevent out-of-bounds - if(current_token >= ret) { + if (current_token >= ret) + { FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array."); free(tokens); free(array_str); @@ -533,8 +599,9 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t // Extract the array element jsmntok_t element = tokens[current_token]; int length = element.end - element.start; - char* value = malloc(length + 1); - if(value == NULL) { + 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); @@ -553,10 +620,12 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // 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) { +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) { + 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; } @@ -566,8 +635,9 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in jsmn_init(&parser); // Allocate memory for JSON tokens - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); // Allocate on the heap - if(tokens == NULL) { + 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; @@ -575,7 +645,8 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // Parse the JSON array int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); - if(ret < 0) { + if (ret < 0) + { FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); free(tokens); free(array_str); @@ -583,7 +654,8 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in } // Ensure the root element is an array - if(tokens[0].type != JSMN_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); @@ -592,8 +664,9 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // 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) { + 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); @@ -604,15 +677,18 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // 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) { + 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) { + 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; @@ -622,10 +698,12 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in 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) { + 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++) { + for (int j = 0; j < actual_num_values; j++) + { free(values[j]); } free(values); @@ -647,14 +725,17 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in *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) { + 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++) { + for (int i = actual_num_values; i < array_size; i++) + { free(values[i]); } } diff --git a/flip_library/jsmn/jsmn.h b/flip_library/jsmn/jsmn.h index cd95a0e58..74cdccf95 100644 --- a/flip_library/jsmn/jsmn.h +++ b/flip_library/jsmn/jsmn.h @@ -19,7 +19,8 @@ #include #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif #ifdef JSMN_STATIC @@ -28,71 +29,71 @@ extern "C" { #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 -}; - -/** + 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; + typedef struct + { + jsmntype_t type; + int start; + int end; + int size; #ifdef JSMN_PARENT_LINKS - int parent; + int parent; #endif -} jsmntok_t; + } 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; - -/** + 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); + 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); + 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 */ @@ -116,16 +117,16 @@ JSMN_API int jsmn_parse( #include // Helper function to create a JSON object -char* jsmn(const char* key, const char* value); +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); +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); +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); +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); +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 7f29ee275..5b09349b1 100644 --- a/flip_social/README.md +++ b/flip_social/README.md @@ -3,12 +3,9 @@ The first social media app for Flipper Zero. Connect with other users directly o The highlight of this app is customizable pre-saves, which, as explained below, aim to address the challenges of typing with the directional pad. -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 Developer Board, Raspberry Pi, or ESP32 Device with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP -- WiFi Access Point - +- 2.4 Ghz WiFi Access Point ## Features - Login/Logout @@ -16,15 +13,16 @@ FlipSocial uses the FlipperHTTP flash for the WiFi Devboard, first introduced in - Feed - Profile - Customizable Pre-Saves -- Explore (NEW) -- Friends (NEW) -- Direct Messaging (NEW) +- Explore +- Friends +- Direct Messaging + **Login/Logout:** Log in to your account to view and post on the Feed. You can also change your password and log out when needed. **Registration:** Create an account with just a username and password—no email or personal information required or collected. -**Feed:** View up to 50 of the latest posts, create your own posts, and "Flip" a post—FlipSocial’s version of liking or favoriting a post. +**Feed:** View the latest posts, create your own posts, and "Flip" a post—FlipSocial’s version of liking or favoriting a post. **Customizable Pre-Saves:** The biggest challenge with a social media app on the Flipper Zero is using only the directional pad for input. To address this, I implemented a pre-saved text system. The pre-saves are stored in a pre_saved_messages.txt file on your SD card. You can edit the pre-saves by opening qFlipper, downloading the file from the /apps_data/flip_social/ folder, adding your pre-saves (separated by new lines), and then copying it back to your SD card. You can also create pre-saves directly within the app. @@ -32,66 +30,4 @@ FlipSocial uses the FlipperHTTP flash for the WiFi Devboard, first introduced in **Friends:** View and remove friends. -**Direct Messaging:** Send direct messages to other Flipper users and view your conversations. - -## Roadmap -**v0.2** -- Stability Patch - -**v0.3** -- Explore Page -- Friends - -**v0.4** -- Direct Messaging - -**v0.5** -- Improve memory allocation -- Improve Feed Page -- Raspberry Pi Pico W Support - -**v0.6** -- Improve memory allocation -- Update the Direct Messaging View -- Update the Pre-Save View - -**v0.7** -- Improve memory allocation -- Loading screens. - -**v0.8** -- Improve User Profile -- Improve Explore Page - -**v1.0** -- Official Release - -## Contribution -This is a big project, 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 any button other than the BACK button in the Feed view, post creation view, messages view, or the friends view, the app doesn't respond to inputs. -- **Solution:** Restart your Flipper device. - -2. When trying to log in, the app shows "Awaiting response..." and nothing happens for more than 30 seconds. -- **Solution 1:** Update your WiFi credentials. Sometimes, you just need to hit Save again on either the SSID or password. -- **Solution 2:** Ensure your WiFi Devboard is plugged in, then restart the app. -- **Solution 3:** Ensure your WiFi Devboard is plugged in, then restart your Flipper device. - -3. When accessing the Feed, I keep getting the message "Either the feed didn’t load or there was a server error." -- **Solution 1:** Update your WiFi credentials. Sometimes, you just need to hit Save again on either the SSID or password. -- **Solution 2:** Ensure your WiFi Devboard is plugged in, then restart the app. -- **Solution 3:** Ensure your WiFi Devboard is plugged in, then restart your Flipper device. - -4. The Feed is empty. -- **Solution 1:** Update your WiFi credentials. Sometimes, you just need to hit Save again on either the SSID or password. -- **Solution 2:** Ensure your WiFi Devboard is plugged in, then restart the app. -- **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 1:** Restart your Flipper device. -- **Solution 2:** Update the app to version 0.7 (or higher). - -6. I can no longer access the Messages. -- **Solution 1:** Uppdate the app to version 0.6.3 (or higher) -- **Solution 2:** Click the logout button then login again. Make sure your password is correct before clicking "Login". +**Direct Messaging:** Send direct messages to other Flipper users and view your conversations. \ No newline at end of file diff --git a/flip_social/alloc/alloc.c b/flip_social/alloc/alloc.c new file mode 100644 index 000000000..7966b35a0 --- /dev/null +++ b/flip_social/alloc/alloc.c @@ -0,0 +1,894 @@ +#include +bool went_to_friends = false; +void auth_headers_alloc(void) +{ + if (!app_instance) + { + snprintf(auth_headers, sizeof(auth_headers), "{\"Content-Type\":\"application/json\"}"); + return; + } + + if (app_instance->login_username_logged_out && app_instance->login_password_logged_out && strlen(app_instance->login_username_logged_out) > 0 && strlen(app_instance->login_password_logged_out) > 0) + { + 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 && strlen(app_instance->login_username_logged_in) > 0 && strlen(app_instance->change_password_logged_in) > 0) + { + 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\"}"); + } +} + +FlipSocialFeedMini *flip_feed_info_alloc(void) +{ + FlipSocialFeedMini *feed_info = (FlipSocialFeedMini *)malloc(sizeof(FlipSocialFeedMini)); + if (!feed_info) + { + FURI_LOG_E(TAG, "Failed to allocate memory for feed_info"); + return NULL; + } + feed_info->count = 0; + feed_info->index = 0; + return feed_info; +} +bool messages_dialog_alloc(bool free_first) +{ + if (free_first) + { + flip_social_free_messages_dialog(); + } + if (!app_instance->dialog_messages) + { + if (!easy_flipper_set_dialog_ex( + &app_instance->dialog_messages, + FlipSocialViewMessagesDialog, + flip_social_messages->usernames[flip_social_messages->index], + 0, + 0, + updated_user_message(flip_social_messages->messages[flip_social_messages->index]), + 0, + 10, + flip_social_messages->index != 0 ? "Prev" : NULL, + flip_social_messages->index != flip_social_messages->count - 1 ? "Next" : NULL, + "Create", + messages_dialog_callback, + flip_social_callback_to_messages_logged_in, + &app_instance->view_dispatcher, + app_instance)) + { + return false; + } + return true; + } + return false; +} +char *updated_user_message(const char *user_message) +{ + if (user_message == NULL) + { + FURI_LOG_E(TAG, "User message is NULL."); + return NULL; + } + + size_t msg_length = strlen(user_message); + size_t start = 0; + int line_num = 0; + + // Allocate memory for the updated message + char *updated_message = malloc(MAX_MESSAGE_LENGTH + 10); + if (updated_message == NULL) + { + FURI_LOG_E(TAG, "Failed to allocate memory for updated_message."); + return NULL; + } + size_t current_pos = 0; // Tracks the current position in updated_message + updated_message[0] = '\0'; // Initialize as empty string + + while (start < msg_length && line_num < 4) + { + size_t remaining = msg_length - start; + size_t len = (remaining > MAX_LINE_LENGTH) ? MAX_LINE_LENGTH : remaining; + + // Adjust length to the last space if the line exceeds MAX_LINE_LENGTH + if (remaining > MAX_LINE_LENGTH) + { + size_t last_space = len; + while (last_space > 0 && user_message[start + last_space - 1] != ' ') + { + last_space--; + } + + if (last_space > 0) + { + len = last_space; // Adjust len to the position of the last space + } + } + + // Check if the new line fits in the updated_message buffer + if (current_pos + len + 1 >= (MAX_MESSAGE_LENGTH + 10)) + { + FURI_LOG_E(TAG, "Updated message exceeds maximum length."); + // break and return what we have so far + break; + } + + // Copy the line and append a newline character + memcpy(updated_message + current_pos, user_message + start, len); + current_pos += len; + updated_message[current_pos++] = '\n'; // Append newline + + // 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 && user_message[start] == ' ') + { + start++; + } + + // Increment the line number + line_num++; + } + + // Null-terminate the final string + if (current_pos < (MAX_MESSAGE_LENGTH + 10)) + { + updated_message[current_pos] = '\0'; + } + else + { + FURI_LOG_E(TAG, "Buffer overflow while null-terminating."); + free(updated_message); + return NULL; + } + + return updated_message; +} + +typedef enum +{ + ActionNone, + ActionBack, + ActionNext, + ActionPrev, + ActionFlip, +} Action; + +static Action action = ActionNone; + +void on_input(const void *event, void *ctx) +{ + UNUSED(ctx); + + InputKey key = ((InputEvent *)event)->key; + InputType type = ((InputEvent *)event)->type; + + if (type != InputTypeRelease) + { + return; + } + + switch (key) + { + case InputKeyOk: + action = ActionFlip; + break; + case InputKeyBack: + action = ActionBack; + break; + case InputKeyRight: + action = ActionNext; + break; + case InputKeyLeft: + action = ActionPrev; + break; + case InputKeyUp: + action = ActionPrev; + break; + case InputKeyDown: + action = ActionNext; + break; + default: + action = ActionNone; + break; + } +} + +// Make sure to define a suitable MAX_LINE_LENGTH +// For example: + +#define MAX_LINES 6 +#define LINE_HEIGHT 8 +#define MAX_LINE_WIDTH_PX 128 +#define TEMP_BUF_SIZE 128 + +static void draw_user_message(Canvas *canvas, const char *user_message, int x, int y) +{ + if (!user_message) + { + FURI_LOG_E(TAG, "User message is NULL."); + return; + } + + // We will read through user_message and extract words manually + const char *p = user_message; + + // Skip leading spaces + while (*p == ' ') + p++; + + char line[TEMP_BUF_SIZE]; + size_t line_len = 0; + line[0] = '\0'; + int line_num = 0; + + while (*p && line_num < MAX_LINES) + { + // Find the end of the next word + const char *word_start = p; + while (*p && *p != ' ') + p++; + size_t word_len = p - word_start; + + // Extract the word into a temporary buffer + char word[TEMP_BUF_SIZE]; + if (word_len > TEMP_BUF_SIZE - 1) + { + word_len = TEMP_BUF_SIZE - 1; // Just to avoid overflow if extremely large + } + memcpy(word, word_start, word_len); + word[word_len] = '\0'; + + // Skip trailing spaces for the next iteration + while (*p == ' ') + p++; + + if (word_len == 0) + { + // Empty word (consecutive spaces?), just continue + continue; + } + + // Check how the word fits into the current line + char test_line[TEMP_BUF_SIZE + 128]; + if (line_len == 0) + { + // If line is empty, the line would just be this word + strncpy(test_line, word, sizeof(test_line) - 1); + test_line[sizeof(test_line) - 1] = '\0'; + } + else + { + // If not empty, we add a space and then the word + snprintf(test_line, sizeof(test_line), "%s %s", line, word); + } + + uint16_t width = canvas_string_width(canvas, test_line); + if (width <= MAX_LINE_WIDTH_PX) + { + // The word fits on this line + strcpy(line, test_line); + line_len = strlen(line); + } + else + { + // The word doesn't fit on this line + // First, draw the current line if it's not empty + if (line_len > 0) + { + canvas_draw_str_aligned(canvas, x, y + line_num * LINE_HEIGHT, AlignLeft, AlignTop, line); + line_num++; + if (line_num >= MAX_LINES) + break; + } + + // Now we try to put the current word on a new line + // Check if the word itself fits on an empty line + width = canvas_string_width(canvas, word); + if (width <= MAX_LINE_WIDTH_PX) + { + // The whole word fits on a new line + strcpy(line, word); + line_len = word_len; + } + else + { + // The word alone doesn't fit. We must truncate it. + // We'll find the largest substring of the word that fits. + size_t truncate_len = word_len; + while (truncate_len > 0) + { + char truncated[TEMP_BUF_SIZE]; + strncpy(truncated, word, truncate_len); + truncated[truncate_len] = '\0'; + if (canvas_string_width(canvas, truncated) <= MAX_LINE_WIDTH_PX) + { + // Found a substring that fits + strcpy(line, truncated); + line_len = truncate_len; + break; + } + truncate_len--; + } + + if (line_len == 0) + { + // Could not fit a single character. Skip this word. + } + } + } + } + + // Draw any remaining text in the buffer if we have lines left + if (line_len > 0 && line_num < MAX_LINES) + { + canvas_draw_str_aligned(canvas, x, y + line_num * LINE_HEIGHT, AlignLeft, AlignTop, line); + } +} + +static void flip_social_feed_draw_callback(Canvas *canvas, void *model) +{ + UNUSED(model); + canvas_clear(canvas); + canvas_set_font_custom(canvas, FONT_SIZE_LARGE); + canvas_draw_str(canvas, 0, 7, flip_feed_item->username); + canvas_set_font_custom(canvas, FONT_SIZE_MEDIUM); + draw_user_message(canvas, flip_feed_item->message, 0, 12); + canvas_set_font_custom(canvas, FONT_SIZE_SMALL); + char flip_message[32]; + snprintf(flip_message, sizeof(flip_message), "%u %s", flip_feed_item->flips, flip_feed_item->flips == 1 ? "flip" : "flips"); + canvas_draw_str(canvas, 0, 60, flip_message); // Draw the number of flips + char flip_status[16]; + snprintf(flip_status, sizeof(flip_status), flip_feed_item->is_flipped ? "Unflip" : "Flip"); + canvas_draw_str(canvas, 32, 60, flip_status); // Draw the flip status + canvas_draw_str(canvas, 64, 60, flip_feed_item->date_created); // Draw the date +} + +static bool flip_social_feed_input_callback(InputEvent *event, void *context) +{ + UNUSED(context); + furi_assert(app_instance); + + // if back button is pressed + if (event->type == InputTypePress && event->key == InputKeyBack) + { + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoggedInSubmenu); + return true; + } + + if (event->type == InputTypePress && event->key == InputKeyLeft) // Previous message + { + if (flip_feed_info->index > 0) + { + flip_feed_info->index--; + } + // switch view, free dialog, re-alloc dialog, switch back to dialog + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewWidgetResult); + flip_social_free_feed_view(); + // load feed item + if (!flip_social_load_feed_post(flip_feed_info->ids[flip_feed_info->index])) + { + FURI_LOG_E(TAG, "Failed to load nexy feed post"); + fhttp.state = ISSUE; + return false; + } + if (feed_view_alloc()) + { + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoggedInFeed); + } + else + { + FURI_LOG_E(TAG, "Failed to allocate feed dialog"); + fhttp.state = ISSUE; + return false; + } + } + else if (event->type == InputTypePress && event->key == InputKeyRight) // Next message + { + // if next message is the last message, then use flip_social_load_initial_feed + if (flip_feed_info->index == flip_feed_info->count - 1) + { + char series_index[16]; + load_char("series_index", series_index, sizeof(series_index)); + flip_feed_info->series_index = atoi(series_index) + 1; + char new_series_index[16]; + snprintf(new_series_index, sizeof(new_series_index), "%d", flip_feed_info->series_index); + + save_char("series_index", new_series_index); + + if (!flip_social_load_initial_feed(true, flip_feed_info->series_index)) + { + FURI_LOG_E(TAG, "Failed to load initial feed"); + fhttp.state = ISSUE; + return false; + } + // switch view, free dialog, re-alloc dialog, switch back to dialog + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewWidgetResult); + flip_social_free_feed_view(); + // load feed item + if (!flip_social_load_feed_post(flip_feed_info->ids[flip_feed_info->index])) + { + FURI_LOG_E(TAG, "Failed to load nexy feed post"); + fhttp.state = ISSUE; + return false; + } + if (feed_view_alloc()) + { + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoggedInFeed); + } + else + { + FURI_LOG_E(TAG, "Failed to allocate feed dialog"); + fhttp.state = ISSUE; + return false; + } + } + if (flip_feed_info->index < flip_feed_info->count - 1) + { + flip_feed_info->index++; + } + // switch view, free dialog, re-alloc dialog, switch back to dialog + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewWidgetResult); + flip_social_free_feed_view(); + // load feed item + if (!flip_social_load_feed_post(flip_feed_info->ids[flip_feed_info->index])) + { + FURI_LOG_E(TAG, "Failed to load nexy feed post"); + fhttp.state = ISSUE; + return false; + } + if (feed_view_alloc()) + { + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoggedInFeed); + } + else + { + FURI_LOG_E(TAG, "Failed to allocate feed dialog"); + fhttp.state = ISSUE; + return false; + } + } + else if (event->type == InputTypePress && event->key == InputKeyOk) // Flip/Unflip + { + // Moved to above the is_flipped check + if (!flip_feed_item->is_flipped) + { + // increase the flip count + flip_feed_item->flips++; + } + else + { + // decrease the flip count + if (flip_feed_item->flips > 0) + flip_feed_item->flips--; + } + // change the flip status + flip_feed_item->is_flipped = !flip_feed_item->is_flipped; + + // send post request to flip the message + if (app_instance->login_username_logged_in == NULL) + { + FURI_LOG_E(TAG, "Username is NULL"); + return false; + } + if (!flipper_http_init(flipper_http_rx_callback, app_instance)) + { + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); + return false; + } + auth_headers_alloc(); + char payload[256]; + snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"post_id\":\"%u\"}", app_instance->login_username_logged_in, flip_feed_item->id); + if (flipper_http_post_request_with_headers("https://www.flipsocial.net/api/feed/flip/", auth_headers, payload)) + { + // save feed item + char new_save[512]; + snprintf(new_save, sizeof(new_save), "{\"id\":%u,\"username\":\"%s\",\"message\":\"%s\",\"flip_count\":%u,\"flipped\":%s,\"date_created\":\"%s\"}", + flip_feed_item->id, flip_feed_item->username, flip_feed_item->message, flip_feed_item->flips, flip_feed_item->is_flipped ? "true" : "false", flip_feed_item->date_created); + char id[16]; + snprintf(id, sizeof(id), "%u", flip_feed_item->id); + if (!flip_social_save_post(id, new_save)) + { + FURI_LOG_E(TAG, "Failed to save the feed post"); + flipper_http_deinit(); + return false; + } + } + // switch view, free dialog, re-alloc dialog, switch back to dialog + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewWidgetResult); + flip_social_free_feed_view(); + // load feed item + if (!flip_social_load_feed_post(flip_feed_info->ids[flip_feed_info->index])) + { + FURI_LOG_E(TAG, "Failed to load nexy feed post"); + fhttp.state = ISSUE; + return false; + } + if (feed_view_alloc()) + { + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoggedInFeed); + } + else + { + FURI_LOG_E(TAG, "Failed to allocate feed dialog"); + } + flipper_http_deinit(); + } + return false; +} + +bool feed_view_alloc() +{ + if (!app_instance) + { + return false; + } + if (!flip_feed_item) + { + FURI_LOG_E(TAG, "Feed item is NULL"); + return false; + } + flip_social_free_feed_view(); + if (!app_instance->view_feed) + { + if (!easy_flipper_set_view( + &app_instance->view_feed, + FlipSocialViewLoggedInFeed, + flip_social_feed_draw_callback, + flip_social_feed_input_callback, + flip_social_callback_to_submenu_logged_in, + &app_instance->view_dispatcher, + app_instance)) + { + return false; + } + return true; + } + return false; +} + +bool alloc_text_input(uint32_t view_id) +{ + if (!app_instance) + { + return false; + } + if (!app_instance->text_input) + { + switch (view_id) + { + case FlipSocialViewLoggedOutWifiSettingsSSIDInput: + // memset(app_instance->wifi_ssid_logged_out_temp_buffer, 0, app_instance->wifi_ssid_logged_out_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter SSID", app_instance->wifi_ssid_logged_out_temp_buffer, app_instance->wifi_ssid_logged_out_temp_buffer_size, flip_social_logged_out_wifi_settings_ssid_updated, flip_social_callback_to_wifi_settings_logged_out, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedOutWifiSettingsPasswordInput: + // memset(app_instance->wifi_password_logged_out_temp_buffer, 0, app_instance->wifi_password_logged_out_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Password", app_instance->wifi_password_logged_out_temp_buffer, app_instance->wifi_password_logged_out_temp_buffer_size, flip_social_logged_out_wifi_settings_password_updated, flip_social_callback_to_wifi_settings_logged_out, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedOutLoginUsernameInput: + // memset(app_instance->login_username_logged_out_temp_buffer, 0, app_instance->login_username_logged_out_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Username", app_instance->login_username_logged_out_temp_buffer, app_instance->login_username_logged_out_temp_buffer_size, flip_social_logged_out_login_username_updated, flip_social_callback_to_login_logged_out, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedOutLoginPasswordInput: + // memset(app_instance->login_password_logged_out_temp_buffer, 0, app_instance->login_password_logged_out_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Password", app_instance->login_password_logged_out_temp_buffer, app_instance->login_password_logged_out_temp_buffer_size, flip_social_logged_out_login_password_updated, flip_social_callback_to_login_logged_out, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedOutRegisterUsernameInput: + memset(app_instance->register_username_logged_out_temp_buffer, 0, app_instance->register_username_logged_out_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Username", app_instance->register_username_logged_out_temp_buffer, app_instance->register_username_logged_out_temp_buffer_size, flip_social_logged_out_register_username_updated, flip_social_callback_to_register_logged_out, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedOutRegisterPasswordInput: + memset(app_instance->register_password_logged_out_temp_buffer, 0, app_instance->register_password_logged_out_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Password", app_instance->register_password_logged_out_temp_buffer, app_instance->register_password_logged_out_temp_buffer_size, flip_social_logged_out_register_password_updated, flip_social_callback_to_register_logged_out, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedOutRegisterPassword2Input: + memset(app_instance->register_password_2_logged_out_temp_buffer, 0, app_instance->register_password_2_logged_out_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Confirm Password", app_instance->register_password_2_logged_out_temp_buffer, app_instance->register_password_2_logged_out_temp_buffer_size, flip_social_logged_out_register_password_2_updated, flip_social_callback_to_register_logged_out, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedInChangePasswordInput: + // memset(app_instance->change_password_logged_in_temp_buffer, 0, app_instance->change_password_logged_in_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Change Password", app_instance->change_password_logged_in_temp_buffer, app_instance->change_password_logged_in_temp_buffer_size, flip_social_logged_in_profile_change_password_updated, flip_social_callback_to_profile_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedInChangeBioInput: + // memset(app_instance->change_bio_logged_in_temp_buffer, 0, app_instance->change_bio_logged_in_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Bio", app_instance->change_bio_logged_in_temp_buffer, app_instance->change_bio_logged_in_temp_buffer_size, flip_social_logged_in_profile_change_bio_updated, flip_social_callback_to_profile_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedInComposeAddPreSaveInput: + memset(app_instance->compose_pre_save_logged_in_temp_buffer, 0, app_instance->compose_pre_save_logged_in_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Pre-Save Message", app_instance->compose_pre_save_logged_in_temp_buffer, app_instance->compose_pre_save_logged_in_temp_buffer_size, flip_social_logged_in_compose_pre_save_updated, flip_social_callback_to_compose_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedInWifiSettingsSSIDInput: + // memset(app_instance->wifi_ssid_logged_in_temp_buffer, 0, app_instance->wifi_ssid_logged_in_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter SSID", app_instance->wifi_ssid_logged_in_temp_buffer, app_instance->wifi_ssid_logged_in_temp_buffer_size, flip_social_logged_in_wifi_settings_ssid_updated, flip_social_callback_to_wifi_settings_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedInWifiSettingsPasswordInput: + // memset(app_instance->wifi_password_logged_in_temp_buffer, 0, app_instance->wifi_password_logged_in_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Password", app_instance->wifi_password_logged_in_temp_buffer, app_instance->wifi_password_logged_in_temp_buffer_size, flip_social_logged_in_wifi_settings_password_updated, flip_social_callback_to_wifi_settings_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedInMessagesNewMessageInput: + memset(app_instance->messages_new_message_logged_in_temp_buffer, 0, app_instance->messages_new_message_logged_in_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Message", app_instance->messages_new_message_logged_in_temp_buffer, app_instance->messages_new_message_logged_in_temp_buffer_size, flip_social_logged_in_messages_new_message_updated, flip_social_callback_to_messages_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedInMessagesNewMessageUserChoicesInput: + memset(app_instance->message_user_choice_logged_in_temp_buffer, 0, app_instance->message_user_choice_logged_in_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Message", app_instance->message_user_choice_logged_in_temp_buffer, app_instance->message_user_choice_logged_in_temp_buffer_size, flip_social_logged_in_messages_user_choice_message_updated, flip_social_callback_to_messages_user_choices, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedInExploreInput: + memset(app_instance->explore_logged_in_temp_buffer, 0, app_instance->explore_logged_in_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Username or Keyword", app_instance->explore_logged_in_temp_buffer, app_instance->explore_logged_in_temp_buffer_size, flip_social_logged_in_explore_updated, flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + case FlipSocialViewLoggedInMessageUsersInput: + memset(app_instance->message_users_logged_in_temp_buffer, 0, app_instance->message_users_logged_in_temp_buffer_size); + if (!easy_flipper_set_uart_text_input(&app_instance->text_input, FlipSocialViewTextInput, "Enter Username or Keyword", app_instance->message_users_logged_in_temp_buffer, app_instance->message_users_logged_in_temp_buffer_size, flip_social_logged_in_message_users_updated, flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + break; + default: + return false; + } + } + return true; +} + +bool about_widget_alloc(bool is_logged_in) +{ + if (!is_logged_in) + { + if (!app_instance->widget_logged_out_about) + { + return easy_flipper_set_widget(&app_instance->widget_logged_out_about, FlipSocialViewLoggedOutAbout, "Welcome to FlipSocial\n---\nThe social media app for\nFlipper Zero, created by\nJBlanked: www.flipsocial.net\n---\nPress BACK to return.", flip_social_callback_to_submenu_logged_out, &app_instance->view_dispatcher); + } + } + else + { + if (!app_instance->widget_logged_in_about) + { + return easy_flipper_set_widget(&app_instance->widget_logged_in_about, FlipSocialViewLoggedInSettingsAbout, "Welcome to FlipSocial\n---\nThe social media app for\nFlipper Zero, created by\nJBlanked: www.flipsocial.net\n---\nPress BACK to return.", flip_social_callback_to_settings_logged_in, &app_instance->view_dispatcher); + } + } + return true; +} + +bool alloc_submenu(uint32_t view_id) +{ + if (!app_instance) + { + return false; + } + if (!app_instance->submenu) + { + switch (view_id) + { + case FlipSocialViewLoggedInSettings: + if (!easy_flipper_set_submenu(&app_instance->submenu, FlipSocialViewSubmenu, "Settings", flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher)) + { + return false; + } + submenu_reset(app_instance->submenu); + submenu_add_item(app_instance->submenu, "About", FlipSocialSubmenuLoggedInIndexAbout, flip_social_callback_submenu_choices, app_instance); + submenu_add_item(app_instance->submenu, "WiFi", FlipSocialSubmenuLoggedInIndexWifiSettings, flip_social_callback_submenu_choices, app_instance); + submenu_add_item(app_instance->submenu, "User", FlipSocialSubmenuLoggedInIndexUserSettings, flip_social_callback_submenu_choices, app_instance); + break; + case FlipSocialViewLoggedInCompose: + if (!easy_flipper_set_submenu(&app_instance->submenu, FlipSocialViewSubmenu, "Create A Post", flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher)) + { + return false; + } + submenu_reset(app_instance->submenu); + submenu_add_item(app_instance->submenu, "Add Pre-Save", FlipSocialSubmenuComposeIndexAddPreSave, flip_social_callback_submenu_choices, app_instance); + + // Load the playlist + if (load_playlist(&app_instance->pre_saved_messages)) + { + // Update the playlist submenu + for (uint32_t i = 0; i < app_instance->pre_saved_messages.count; i++) + { + if (app_instance->pre_saved_messages.messages[i][0] != '\0') // Check if the string is not empty + { + submenu_add_item(app_instance->submenu, app_instance->pre_saved_messages.messages[i], FlipSocialSubemnuComposeIndexStartIndex + i, flip_social_callback_submenu_choices, app_instance); + } + } + } + break; + case FlipSocialViewLoggedInFriendsSubmenu: + if (!easy_flipper_set_submenu(&app_instance->submenu, FlipSocialViewSubmenu, "Friends", flip_social_callback_to_profile_logged_in, &app_instance->view_dispatcher)) + { + FURI_LOG_E(TAG, "Failed to set submenu for friends"); + return false; + } + submenu_reset(app_instance->submenu); + went_to_friends = true; + break; + case FlipSocialViewLoggedInMessagesUserChoices: + if (!easy_flipper_set_submenu(&app_instance->submenu, FlipSocialViewSubmenu, "Users", flip_social_callback_to_messages_logged_in, &app_instance->view_dispatcher)) + { + FURI_LOG_E(TAG, "Failed to set submenu for user choices"); + return false; + } + submenu_reset(app_instance->submenu); + break; + case FlipSocialViewLoggedInMessagesSubmenu: + if (!easy_flipper_set_submenu(&app_instance->submenu, FlipSocialViewSubmenu, "Messages", flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher)) + { + return false; + } + submenu_reset(app_instance->submenu); + break; + case FlipSocialViewLoggedInExploreSubmenu: + if (!easy_flipper_set_submenu(&app_instance->submenu, FlipSocialViewSubmenu, "Explore", flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher)) + { + return false; + } + submenu_reset(app_instance->submenu); + break; + } + } + return true; +} +static void flip_social_feed_type_change(VariableItem *item) +{ + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, flip_social_feed_type[index]); + flip_social_feed_type_index = index; + variable_item_set_current_value_index(item, index); + + // save the feed type + save_char("user_feed_type", strstr(flip_social_feed_type[index], "Global") ? "global" : "friends"); +} +static void flip_social_notification_type_change(VariableItem *item) +{ + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, flip_social_notification_type[index]); + flip_social_notification_type_index = index; + variable_item_set_current_value_index(item, index); + + // save the notification type + save_char("user_notifications", strstr(flip_social_notification_type[index], "ON") ? "on" : "off"); +} + +bool alloc_variable_item_list(uint32_t view_id) +{ + if (!app_instance) + { + return false; + } + if (!app_instance->variable_item_list) + { + switch (view_id) + { + case FlipSocialViewLoggedOutWifiSettings: + if (!easy_flipper_set_variable_item_list(&app_instance->variable_item_list, FlipSocialViewVariableItemList, flip_social_text_input_logged_out_wifi_settings_item_selected, flip_social_callback_to_submenu_logged_out, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + app_instance->variable_item_logged_out_wifi_settings_ssid = variable_item_list_add(app_instance->variable_item_list, "SSID", 1, NULL, app_instance); + app_instance->variable_item_logged_out_wifi_settings_password = variable_item_list_add(app_instance->variable_item_list, "Password", 1, NULL, app_instance); + if (app_instance->wifi_ssid_logged_out) + variable_item_set_current_value_text(app_instance->variable_item_logged_out_wifi_settings_ssid, app_instance->wifi_ssid_logged_out); + return true; + case FlipSocialViewLoggedOutLogin: + if (!easy_flipper_set_variable_item_list(&app_instance->variable_item_list, FlipSocialViewVariableItemList, flip_social_text_input_logged_out_login_item_selected, flip_social_callback_to_submenu_logged_out, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + app_instance->variable_item_logged_out_login_username = variable_item_list_add(app_instance->variable_item_list, "Username", 1, NULL, app_instance); + app_instance->variable_item_logged_out_login_password = variable_item_list_add(app_instance->variable_item_list, "Password", 1, NULL, app_instance); + app_instance->variable_item_logged_out_login_button = variable_item_list_add(app_instance->variable_item_list, "Login", 0, NULL, app_instance); + if (app_instance->login_username_logged_out) + variable_item_set_current_value_text(app_instance->variable_item_logged_out_login_username, app_instance->login_username_logged_out); + return true; + case FlipSocialViewLoggedOutRegister: + if (!easy_flipper_set_variable_item_list(&app_instance->variable_item_list, FlipSocialViewVariableItemList, flip_social_text_input_logged_out_register_item_selected, flip_social_callback_to_submenu_logged_out, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + app_instance->variable_item_logged_out_register_username = variable_item_list_add(app_instance->variable_item_list, "Username", 1, NULL, app_instance); + app_instance->variable_item_logged_out_register_password = variable_item_list_add(app_instance->variable_item_list, "Password", 1, NULL, app_instance); + app_instance->variable_item_logged_out_register_password_2 = variable_item_list_add(app_instance->variable_item_list, "Confirm Password", 1, NULL, app_instance); + app_instance->variable_item_logged_out_register_button = variable_item_list_add(app_instance->variable_item_list, "Register", 0, NULL, app_instance); + return true; + case FlipSocialViewLoggedInProfile: + if (!easy_flipper_set_variable_item_list(&app_instance->variable_item_list, FlipSocialViewVariableItemList, flip_social_text_input_logged_in_profile_item_selected, flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + app_instance->variable_item_logged_in_profile_username = variable_item_list_add(app_instance->variable_item_list, "Username", 1, NULL, app_instance); + app_instance->variable_item_logged_in_profile_change_password = variable_item_list_add(app_instance->variable_item_list, "Password", 1, NULL, app_instance); + app_instance->variable_item_logged_in_profile_change_bio = variable_item_list_add(app_instance->variable_item_list, "Bio", 1, NULL, app_instance); + app_instance->variable_item_logged_in_profile_friends = variable_item_list_add(app_instance->variable_item_list, "Friends", 0, NULL, app_instance); + if (app_instance->login_username_logged_in) + variable_item_set_current_value_text(app_instance->variable_item_logged_in_profile_username, app_instance->login_username_logged_in); + if (app_instance->change_bio_logged_in) + variable_item_set_current_value_text(app_instance->variable_item_logged_in_profile_change_bio, app_instance->change_bio_logged_in); + return true; + case FlipSocialViewLoggedInSettingsWifi: + if (!easy_flipper_set_variable_item_list(&app_instance->variable_item_list, FlipSocialViewVariableItemList, flip_social_text_input_logged_in_wifi_settings_item_selected, flip_social_callback_to_settings_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + app_instance->variable_item_logged_in_wifi_settings_ssid = variable_item_list_add(app_instance->variable_item_list, "SSID", 1, NULL, app_instance); + app_instance->variable_item_logged_in_wifi_settings_password = variable_item_list_add(app_instance->variable_item_list, "Password", 1, NULL, app_instance); + if (app_instance->wifi_ssid_logged_in) + variable_item_set_current_value_text(app_instance->variable_item_logged_in_wifi_settings_ssid, app_instance->wifi_ssid_logged_in); + return true; + case FlipSocialViewLoggedInSettingsUser: + if (!easy_flipper_set_variable_item_list(&app_instance->variable_item_list, FlipSocialViewVariableItemList, flip_social_logged_in_user_settings_item_selected, flip_social_callback_to_settings_logged_in, &app_instance->view_dispatcher, app_instance)) + { + return false; + } + app_instance->variable_item_logged_in_user_settings_feed_type = variable_item_list_add(app_instance->variable_item_list, "Feed Type", 2, flip_social_feed_type_change, app_instance); + app_instance->variable_item_logged_in_user_settings_notifications = variable_item_list_add(app_instance->variable_item_list, "Notifications", 2, flip_social_notification_type_change, app_instance); + variable_item_set_current_value_text(app_instance->variable_item_logged_in_user_settings_feed_type, flip_social_feed_type[flip_social_feed_type_index]); + variable_item_set_current_value_index(app_instance->variable_item_logged_in_user_settings_feed_type, flip_social_feed_type_index); + variable_item_set_current_value_text(app_instance->variable_item_logged_in_user_settings_notifications, flip_social_notification_type[flip_social_notification_type_index]); + variable_item_set_current_value_index(app_instance->variable_item_logged_in_user_settings_notifications, flip_social_notification_type_index); + char user_feed_type[32]; + char user_notifications[32]; + if (load_char("user_feed_type", user_feed_type, sizeof(user_feed_type))) + { + flip_social_feed_type_index = strstr(user_feed_type, "friends") ? 1 : 0; + variable_item_set_current_value_text(app_instance->variable_item_logged_in_user_settings_feed_type, flip_social_feed_type[flip_social_feed_type_index]); + variable_item_set_current_value_index(app_instance->variable_item_logged_in_user_settings_feed_type, flip_social_feed_type_index); + } + if (load_char("user_notifications", user_notifications, sizeof(user_notifications))) + { + flip_social_notification_type_index = strstr(user_notifications, "on") ? 1 : 0; + variable_item_set_current_value_text(app_instance->variable_item_logged_in_user_settings_notifications, flip_social_notification_type[flip_social_notification_type_index]); + variable_item_set_current_value_index(app_instance->variable_item_logged_in_user_settings_notifications, flip_social_notification_type_index); + } + return true; + default: + return false; + } + } + return false; +} diff --git a/flip_social/alloc/alloc.h b/flip_social/alloc/alloc.h new file mode 100644 index 000000000..10501aed5 --- /dev/null +++ b/flip_social/alloc/alloc.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include +#include +void auth_headers_alloc(void); +FlipSocialFeedMini *flip_feed_info_alloc(void); +bool messages_dialog_alloc(bool free_first); +bool feed_view_alloc(); +char *updated_user_message(const char *user_message); +bool alloc_text_input(uint32_t view_id); +bool about_widget_alloc(bool is_logged_in); +bool alloc_variable_item_list(uint32_t view_id); +bool alloc_submenu(uint32_t view_id); +extern bool went_to_friends; \ No newline at end of file diff --git a/flip_social/alloc/flip_social_alloc.c b/flip_social/alloc/flip_social_alloc.c index 1ea7cce42..56a14e4f2 100644 --- a/flip_social/alloc/flip_social_alloc.c +++ b/flip_social/alloc/flip_social_alloc.c @@ -8,13 +8,6 @@ FlipSocialApp *flip_social_app_alloc() // Initialize gui Gui *gui = furi_record_open(RECORD_GUI); - // Initialize UART - if (!flipper_http_init(flipper_http_rx_callback, app)) - { - FURI_LOG_E(TAG, "Failed to initialize UART"); - return NULL; - } - // Allocate ViewDispatcher if (!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) { @@ -27,7 +20,7 @@ FlipSocialApp *flip_social_app_alloc() return NULL; } flip_social_loader_init(app->view_loader); - if (!easy_flipper_set_widget(&app->widget_result, FlipSocialViewWidgetResult, "Error, try again.", flip_social_callback_to_submenu_logged_out, &app->view_dispatcher)) + if (!easy_flipper_set_widget(&app->widget_result, FlipSocialViewWidgetResult, "", flip_social_callback_to_submenu_logged_out, &app->view_dispatcher)) { return NULL; } @@ -199,16 +192,11 @@ FlipSocialApp *flip_social_app_alloc() } // Allocate Submenu(s) - if (!easy_flipper_set_submenu(&app->submenu_logged_out, FlipSocialViewLoggedOutSubmenu, "FlipSocial v0.8", flip_social_callback_exit_app, &app->view_dispatcher)) + if (!easy_flipper_set_submenu(&app->submenu_logged_out, FlipSocialViewLoggedOutSubmenu, VERSION_TAG, flip_social_callback_exit_app, &app->view_dispatcher)) { return NULL; } - if (!easy_flipper_set_submenu(&app->submenu_logged_in, FlipSocialViewLoggedInSubmenu, "FlipSocial v0.8", flip_social_callback_exit_app, &app->view_dispatcher)) - { - return NULL; - } - - if (!easy_flipper_set_submenu(&app->submenu_messages_user_choices, FlipSocialViewLoggedInMessagesUserChoices, "Users", flip_social_callback_to_messages_logged_in, &app->view_dispatcher)) + if (!easy_flipper_set_submenu(&app->submenu_logged_in, FlipSocialViewLoggedInSubmenu, VERSION_TAG, flip_social_callback_exit_app, &app->view_dispatcher)) { return NULL; } @@ -226,123 +214,6 @@ FlipSocialApp *flip_social_app_alloc() submenu_add_item(app->submenu_logged_in, "Settings", FlipSocialSubmenuLoggedInIndexSettings, flip_social_callback_submenu_choices, app); submenu_add_item(app->submenu_logged_in, "Sign Out", FlipSocialSubmenuLoggedInSignOutButton, flip_social_callback_submenu_choices, app); - // Setup Variable Item List(s) - if (!easy_flipper_set_variable_item_list(&app->variable_item_list_logged_out_wifi_settings, FlipSocialViewLoggedOutWifiSettings, flip_social_text_input_logged_out_wifi_settings_item_selected, flip_social_callback_to_submenu_logged_out, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_variable_item_list(&app->variable_item_list_logged_out_login, FlipSocialViewLoggedOutLogin, flip_social_text_input_logged_out_login_item_selected, flip_social_callback_to_submenu_logged_out, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_variable_item_list(&app->variable_item_list_logged_out_register, FlipSocialViewLoggedOutRegister, flip_social_text_input_logged_out_register_item_selected, flip_social_callback_to_submenu_logged_out, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_variable_item_list(&app->variable_item_list_logged_in_profile, FlipSocialViewLoggedInProfile, flip_social_text_input_logged_in_profile_item_selected, flip_social_callback_to_submenu_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_variable_item_list(&app->variable_item_list_logged_in_settings, FlipSocialViewLoggedInSettings, flip_social_text_input_logged_in_settings_item_selected, flip_social_callback_to_submenu_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_variable_item_list(&app->variable_item_list_logged_in_settings_wifi, FlipSocialViewLoggedInSettingsWifi, flip_social_text_input_logged_in_wifi_settings_item_selected, flip_social_callback_to_settings_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - - app->variable_item_logged_out_wifi_settings_ssid = variable_item_list_add(app->variable_item_list_logged_out_wifi_settings, "SSID", 1, NULL, app); - app->variable_item_logged_out_wifi_settings_password = variable_item_list_add(app->variable_item_list_logged_out_wifi_settings, "Password", 1, NULL, app); - // - app->variable_item_logged_out_login_username = variable_item_list_add(app->variable_item_list_logged_out_login, "Username", 1, NULL, app); - app->variable_item_logged_out_login_password = variable_item_list_add(app->variable_item_list_logged_out_login, "Password", 1, NULL, app); - app->variable_item_logged_out_login_button = variable_item_list_add(app->variable_item_list_logged_out_login, "Login", 0, NULL, app); - // - app->variable_item_logged_out_register_username = variable_item_list_add(app->variable_item_list_logged_out_register, "Username", 1, NULL, app); - app->variable_item_logged_out_register_password = variable_item_list_add(app->variable_item_list_logged_out_register, "Password", 1, NULL, app); - app->variable_item_logged_out_register_password_2 = variable_item_list_add(app->variable_item_list_logged_out_register, "Confirm Password", 1, NULL, app); - app->variable_item_logged_out_register_button = variable_item_list_add(app->variable_item_list_logged_out_register, "Register", 0, NULL, app); - // - app->variable_item_logged_in_profile_username = variable_item_list_add(app->variable_item_list_logged_in_profile, "Username", 1, NULL, app); - app->variable_item_logged_in_profile_change_password = variable_item_list_add(app->variable_item_list_logged_in_profile, "Password", 1, NULL, app); - app->variable_item_logged_in_profile_change_bio = variable_item_list_add(app->variable_item_list_logged_in_profile, "Bio", 1, NULL, app); - app->variable_item_logged_in_profile_friends = variable_item_list_add(app->variable_item_list_logged_in_profile, "Friends", 0, NULL, app); - // - app->variable_item_logged_in_settings_about = variable_item_list_add(app->variable_item_list_logged_in_settings, "About", 0, NULL, app); - app->variable_item_logged_in_settings_wifi = variable_item_list_add(app->variable_item_list_logged_in_settings, "WiFi", 0, NULL, app); - // - app->variable_item_logged_in_wifi_settings_ssid = variable_item_list_add(app->variable_item_list_logged_in_settings_wifi, "SSID", 1, NULL, app); - app->variable_item_logged_in_wifi_settings_password = variable_item_list_add(app->variable_item_list_logged_in_settings_wifi, "Password", 1, NULL, app); - - // Setup Text Input(s) - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_out_wifi_settings_ssid, FlipSocialViewLoggedOutWifiSettingsSSIDInput, "Enter SSID", app->wifi_ssid_logged_out_temp_buffer, app->wifi_ssid_logged_out_temp_buffer_size, flip_social_logged_out_wifi_settings_ssid_updated, flip_social_callback_to_wifi_settings_logged_out, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_out_wifi_settings_password, FlipSocialViewLoggedOutWifiSettingsPasswordInput, "Enter Password", app->wifi_password_logged_out_temp_buffer, app->wifi_password_logged_out_temp_buffer_size, flip_social_logged_out_wifi_settings_password_updated, flip_social_callback_to_wifi_settings_logged_out, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_out_login_username, FlipSocialViewLoggedOutLoginUsernameInput, "Enter Username", app->login_username_logged_out_temp_buffer, app->login_username_logged_out_temp_buffer_size, flip_social_logged_out_login_username_updated, flip_social_callback_to_login_logged_out, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_out_login_password, FlipSocialViewLoggedOutLoginPasswordInput, "Enter Password", app->login_password_logged_out_temp_buffer, app->login_password_logged_out_temp_buffer_size, flip_social_logged_out_login_password_updated, flip_social_callback_to_login_logged_out, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_out_register_username, FlipSocialViewLoggedOutRegisterUsernameInput, "Enter Username", app->register_username_logged_out_temp_buffer, app->register_username_logged_out_temp_buffer_size, flip_social_logged_out_register_username_updated, flip_social_callback_to_register_logged_out, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_out_register_password, FlipSocialViewLoggedOutRegisterPasswordInput, "Enter Password", app->register_password_logged_out_temp_buffer, app->register_password_logged_out_temp_buffer_size, flip_social_logged_out_register_password_updated, flip_social_callback_to_register_logged_out, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_out_register_password_2, FlipSocialViewLoggedOutRegisterPassword2Input, "Confirm Password", app->register_password_2_logged_out_temp_buffer, app->register_password_2_logged_out_temp_buffer_size, flip_social_logged_out_register_password_2_updated, flip_social_callback_to_register_logged_out, &app->view_dispatcher, app)) - { - return NULL; - } - // - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_in_change_password, FlipSocialViewLoggedInChangePasswordInput, "Password", app->change_password_logged_in_temp_buffer, app->change_password_logged_in_temp_buffer_size, flip_social_logged_in_profile_change_password_updated, flip_social_callback_to_profile_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_in_change_bio, FlipSocialViewLoggedInChangeBioInput, "Bio", app->change_bio_logged_in_temp_buffer, app->change_bio_logged_in_temp_buffer_size, flip_social_logged_in_profile_change_bio_updated, flip_social_callback_to_profile_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_in_compose_pre_save_input, FlipSocialViewLoggedInComposeAddPreSaveInput, "Enter Pre-Save Message", app->compose_pre_save_logged_in_temp_buffer, app->compose_pre_save_logged_in_temp_buffer_size, flip_social_logged_in_compose_pre_save_updated, flip_social_callback_to_compose_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_in_wifi_settings_ssid, FlipSocialViewLoggedInWifiSettingsSSIDInput, "Enter SSID", app->wifi_ssid_logged_in_temp_buffer, app->wifi_ssid_logged_in_temp_buffer_size, flip_social_logged_in_wifi_settings_ssid_updated, flip_social_callback_to_wifi_settings_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_in_wifi_settings_password, FlipSocialViewLoggedInWifiSettingsPasswordInput, "Enter Password", app->wifi_password_logged_in_temp_buffer, app->wifi_password_logged_in_temp_buffer_size, flip_social_logged_in_wifi_settings_password_updated, flip_social_callback_to_wifi_settings_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - // - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_in_messages_new_message, FlipSocialViewLoggedInMessagesNewMessageInput, "Enter Message", app->messages_new_message_logged_in_temp_buffer, app->messages_new_message_logged_in_temp_buffer_size, flip_social_logged_in_messages_new_message_updated, flip_social_callback_to_messages_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_in_messages_new_message_user_choices, FlipSocialViewLoggedInMessagesNewMessageUserChoicesInput, "Enter Message", app->message_user_choice_logged_in_temp_buffer, app->message_user_choice_logged_in_temp_buffer_size, flip_social_logged_in_messages_user_choice_message_updated, flip_social_callback_to_messages_user_choices, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_in_explore, FlipSocialViewLoggedInExploreInput, "Enter Username or Keyword", app->explore_logged_in_temp_buffer, app->explore_logged_in_temp_buffer_size, flip_social_logged_in_explore_updated, flip_social_callback_to_submenu_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->text_input_logged_in_message_users, FlipSocialViewLoggedInMessageUsersInput, "Enter Username or Keyword", app->message_users_logged_in_temp_buffer, app->message_users_logged_in_temp_buffer_size, flip_social_logged_in_message_users_updated, flip_social_callback_to_submenu_logged_in, &app->view_dispatcher, app)) - { - return NULL; - } - // Load the settings if (!load_settings(app->wifi_ssid_logged_out, app->wifi_ssid_logged_out_temp_buffer_size, @@ -508,20 +379,14 @@ FlipSocialApp *flip_social_app_alloc() 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); - variable_item_set_current_value_text(app->variable_item_logged_out_wifi_settings_ssid, app->wifi_ssid_logged_out); - variable_item_set_current_value_text(app->variable_item_logged_out_login_username, app->login_username_logged_out); - variable_item_set_current_value_text(app->variable_item_logged_in_profile_username, app->login_username_logged_in); - variable_item_set_current_value_text(app->variable_item_logged_in_profile_change_bio, app->change_bio_logged_in); - // - if (app->is_logged_in != NULL && strcmp(app->is_logged_in, "true") == 0) { + save_char("is_logged_in", "true"); view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInSubmenu); } else { + save_char("is_logged_in", "false"); view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutSubmenu); } } diff --git a/flip_social/alloc/free.c b/flip_social/alloc/free.c new file mode 100644 index 000000000..03dc7a63d --- /dev/null +++ b/flip_social/alloc/free.c @@ -0,0 +1,146 @@ +#include +void free_all(bool should_free_variable_item_list, bool should_free_submenu) +{ + + if (should_free_submenu) + { + flip_social_free_explore(); + free_submenu(); + } + if (should_free_variable_item_list) + { + free_variable_item_list(); + } + free_text_input(); + flip_social_free_friends(); + flip_social_free_messages(); + flip_social_free_feed_view(); + flip_social_free_compose_dialog(); + flip_social_free_explore_dialog(); + flip_social_free_friends_dialog(); + flip_social_free_messages_dialog(); + flip_feed_info_free(); + free_about_widget(true); + free_about_widget(false); + + if (went_to_friends) + { + flipper_http_deinit(); + went_to_friends = false; + } +} +void free_text_input() +{ + if (app_instance->text_input) + { + text_input_free(app_instance->text_input); + app_instance->text_input = NULL; + view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewTextInput); + } +} +void flip_social_free_explore_dialog() +{ + if (app_instance->dialog_explore) + { + dialog_ex_free(app_instance->dialog_explore); + app_instance->dialog_explore = NULL; + view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewExploreDialog); + } +} +void flip_social_free_friends_dialog() +{ + if (app_instance->dialog_friends) + { + dialog_ex_free(app_instance->dialog_friends); + app_instance->dialog_friends = NULL; + view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewFriendsDialog); + } +} +void flip_social_free_messages_dialog() +{ + if (app_instance->dialog_messages) + { + dialog_ex_free(app_instance->dialog_messages); + app_instance->dialog_messages = NULL; + view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewMessagesDialog); + return; + } +} +void flip_social_free_compose_dialog() +{ + if (app_instance->dialog_compose) + { + dialog_ex_free(app_instance->dialog_compose); + app_instance->dialog_compose = NULL; + view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewComposeDialog); + } +} +void flip_social_free_feed_view() +{ + if (app_instance->view_feed) + { + view_free(app_instance->view_feed); + app_instance->view_feed = NULL; + view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoggedInFeed); + } +} + +void free_about_widget(bool is_logged_in) +{ + if (is_logged_in && app_instance->widget_logged_in_about) + { + widget_free(app_instance->widget_logged_in_about); + app_instance->widget_logged_in_about = NULL; + view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoggedInSettingsAbout); + } + if (!is_logged_in && app_instance->widget_logged_out_about) + { + widget_free(app_instance->widget_logged_out_about); + app_instance->widget_logged_out_about = NULL; + view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoggedOutAbout); + } +} + +void flip_social_free_friends(void) +{ + if (!flip_social_friends) + { + return; + } + free(flip_social_friends); + flip_social_friends = NULL; +} + +void flip_feed_info_free(void) +{ + if (!flip_feed_info) + { + return; + } + free(flip_feed_info); + flip_feed_info = NULL; +} + +void free_variable_item_list(void) +{ + if (app_instance->variable_item_list) + { + variable_item_list_free(app_instance->variable_item_list); + app_instance->variable_item_list = NULL; + view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewVariableItemList); + } +} + +void free_submenu(void) +{ + if (!app_instance) + { + return; + } + if (app_instance->submenu) + { + submenu_free(app_instance->submenu); + app_instance->submenu = NULL; + view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewSubmenu); + } +} \ No newline at end of file diff --git a/flip_social/alloc/free.h b/flip_social/alloc/free.h new file mode 100644 index 000000000..e8208af35 --- /dev/null +++ b/flip_social/alloc/free.h @@ -0,0 +1,15 @@ +#pragma once +#include +#include +void free_all(bool should_free_variable_item_list, bool should_free_submenu); +void free_text_input(); +void flip_social_free_explore_dialog(); +void flip_social_free_friends_dialog(); +void flip_social_free_messages_dialog(); +void flip_social_free_compose_dialog(); +void flip_social_free_feed_view(); +void free_about_widget(bool is_logged_in); +void flip_social_free_friends(void); +void flip_feed_info_free(void); +void free_variable_item_list(void); +void free_submenu(void); diff --git a/flip_social/app.c b/flip_social/app.c index 38a0083d7..939c6afe2 100644 --- a/flip_social/app.c +++ b/flip_social/app.c @@ -17,21 +17,24 @@ int32_t main_flip_social(void *p) if (!app_instance) { // Allocation failed + FURI_LOG_E(TAG, "Failed to allocate FlipSocialApp"); return -1; // Indicate failure } - if (!flipper_http_ping()) + // check if board is connected (Derek Jamison) + uint8_t counter = 10; + // initialize the http + if (flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "Failed to ping the device"); - return -1; - } + fhttp.state = INACTIVE; // set inactive for the ping + + if (!flipper_http_ping()) + { + FURI_LOG_E(TAG, "Failed to ping the device"); + return -1; + } - // Thanks to Derek Jamison for the following edits - if (app_instance->wifi_ssid_logged_out != NULL && - app_instance->wifi_password_logged_out != NULL) - { // Try to wait for pong response. - uint8_t counter = 10; while (fhttp.state == INACTIVE && --counter > 0) { FURI_LOG_D(TAG, "Waiting for PONG"); @@ -40,20 +43,35 @@ int32_t main_flip_social(void *p) 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); + easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed."); + } + else + { + save_char("is_connected", "true"); + } + + flipper_http_deinit(); + } + else + { + easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero."); + } + + // if counter is not 0, check notifications + if (counter != 0) + { + char is_connected[5]; + char is_logged_in[5]; + char is_notifications[5]; + load_char("is_connected", is_connected, 5); + load_char("is_logged_in", is_logged_in, 5); + load_char("user_notifications", is_notifications, 5); + + if (strcmp(is_connected, "true") == 0 && + strcmp(is_notifications, "on") == 0 && + strcmp(is_logged_in, "true") == 0) + { + flip_social_home_notification(); } } diff --git a/flip_social/application.fam b/flip_social/application.fam index 776c69b94..6247772f9 100644 --- a/flip_social/application.fam +++ b/flip_social/application.fam @@ -7,8 +7,8 @@ App( fap_icon="app_new.png", fap_category="GPIO/FlipperHTTP", fap_icon_assets="assets", - fap_author="jblanked", + fap_author="JBlanked", fap_weburl="https://github.com/jblanked/FlipSocial", - fap_version="0.8", + fap_version="1.0.3", fap_description="Social media platform for the Flipper Zero.", ) diff --git a/flip_social/assets/CHANGELOG.md b/flip_social/assets/CHANGELOG.md index e41468ce8..9b54a1a07 100644 --- a/flip_social/assets/CHANGELOG.md +++ b/flip_social/assets/CHANGELOG.md @@ -1,3 +1,24 @@ +## 1.0.3 +- Updated to ensure the flip_social data folder is created when saving settings. + +## 1.0.2 +- Fixed the Feed Type and Notifications toggles, in the User Settings, to work as intended. +- Added flip status to each feed post. + +## 1.0.1 +- Fixed a freeze that occurred when sending a direct message. + +## 1.0 - Official Release +- Final memory improvements. +- Updated the New Message option in the Messages view to allow users to search for recipients to send messages to. +- Updated the display of the Feed to use a custom view and custom font, and to show how long ago each post was created. +- Organized the files saved/utilized in the apps_data folder. +- Fixed bugs in the Direct Messaging View. +- Fixed bugs in the Pre-Save View. +- Added home announcements and notifications. +- Added User Settings (Notifications and Feed Type). With the Feed Type option, users can choose between a private feed, which only consists of posts from their friends, and a global feed, which consists of posts from all Flippers. The Notifications settings allow users to turn home notifications on and off. +- Updated the Feed to be "endless." Once you reach the end of the feed, previous posts will load in sets of 20. + ## 0.8 - New Features - Added support for RPC_KEYBOARD (thanks to Derek Jamison). - Introduced a bio feature in the Profile section: View and update your bio. diff --git a/flip_social/assets/README.md b/flip_social/assets/README.md index 7f29ee275..5b09349b1 100644 --- a/flip_social/assets/README.md +++ b/flip_social/assets/README.md @@ -3,12 +3,9 @@ The first social media app for Flipper Zero. Connect with other users directly o The highlight of this app is customizable pre-saves, which, as explained below, aim to address the challenges of typing with the directional pad. -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 Developer Board, Raspberry Pi, or ESP32 Device with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP -- WiFi Access Point - +- 2.4 Ghz WiFi Access Point ## Features - Login/Logout @@ -16,15 +13,16 @@ FlipSocial uses the FlipperHTTP flash for the WiFi Devboard, first introduced in - Feed - Profile - Customizable Pre-Saves -- Explore (NEW) -- Friends (NEW) -- Direct Messaging (NEW) +- Explore +- Friends +- Direct Messaging + **Login/Logout:** Log in to your account to view and post on the Feed. You can also change your password and log out when needed. **Registration:** Create an account with just a username and password—no email or personal information required or collected. -**Feed:** View up to 50 of the latest posts, create your own posts, and "Flip" a post—FlipSocial’s version of liking or favoriting a post. +**Feed:** View the latest posts, create your own posts, and "Flip" a post—FlipSocial’s version of liking or favoriting a post. **Customizable Pre-Saves:** The biggest challenge with a social media app on the Flipper Zero is using only the directional pad for input. To address this, I implemented a pre-saved text system. The pre-saves are stored in a pre_saved_messages.txt file on your SD card. You can edit the pre-saves by opening qFlipper, downloading the file from the /apps_data/flip_social/ folder, adding your pre-saves (separated by new lines), and then copying it back to your SD card. You can also create pre-saves directly within the app. @@ -32,66 +30,4 @@ FlipSocial uses the FlipperHTTP flash for the WiFi Devboard, first introduced in **Friends:** View and remove friends. -**Direct Messaging:** Send direct messages to other Flipper users and view your conversations. - -## Roadmap -**v0.2** -- Stability Patch - -**v0.3** -- Explore Page -- Friends - -**v0.4** -- Direct Messaging - -**v0.5** -- Improve memory allocation -- Improve Feed Page -- Raspberry Pi Pico W Support - -**v0.6** -- Improve memory allocation -- Update the Direct Messaging View -- Update the Pre-Save View - -**v0.7** -- Improve memory allocation -- Loading screens. - -**v0.8** -- Improve User Profile -- Improve Explore Page - -**v1.0** -- Official Release - -## Contribution -This is a big project, 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 any button other than the BACK button in the Feed view, post creation view, messages view, or the friends view, the app doesn't respond to inputs. -- **Solution:** Restart your Flipper device. - -2. When trying to log in, the app shows "Awaiting response..." and nothing happens for more than 30 seconds. -- **Solution 1:** Update your WiFi credentials. Sometimes, you just need to hit Save again on either the SSID or password. -- **Solution 2:** Ensure your WiFi Devboard is plugged in, then restart the app. -- **Solution 3:** Ensure your WiFi Devboard is plugged in, then restart your Flipper device. - -3. When accessing the Feed, I keep getting the message "Either the feed didn’t load or there was a server error." -- **Solution 1:** Update your WiFi credentials. Sometimes, you just need to hit Save again on either the SSID or password. -- **Solution 2:** Ensure your WiFi Devboard is plugged in, then restart the app. -- **Solution 3:** Ensure your WiFi Devboard is plugged in, then restart your Flipper device. - -4. The Feed is empty. -- **Solution 1:** Update your WiFi credentials. Sometimes, you just need to hit Save again on either the SSID or password. -- **Solution 2:** Ensure your WiFi Devboard is plugged in, then restart the app. -- **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 1:** Restart your Flipper device. -- **Solution 2:** Update the app to version 0.7 (or higher). - -6. I can no longer access the Messages. -- **Solution 1:** Uppdate the app to version 0.6.3 (or higher) -- **Solution 2:** Click the logout button then login again. Make sure your password is correct before clicking "Login". +**Direct Messaging:** Send direct messages to other Flipper users and view your conversations. \ No newline at end of file diff --git a/flip_social/assets/flip-social-main-menu.png b/flip_social/assets/flip-social-main-menu.png index 0503fe708..07808d6aa 100644 Binary files a/flip_social/assets/flip-social-main-menu.png and b/flip_social/assets/flip-social-main-menu.png differ diff --git a/flip_social/callback/flip_social_callback.c b/flip_social/callback/flip_social_callback.c index 7d0920010..bde904af1 100644 --- a/flip_social/callback/flip_social_callback.c +++ b/flip_social/callback/flip_social_callback.c @@ -10,102 +10,6 @@ #define DEV_CRASH() #endif -static bool about_widget_alloc(bool is_logged_in) -{ - if (!is_logged_in) - { - if (!app_instance->widget_logged_out_about) - { - return easy_flipper_set_widget(&app_instance->widget_logged_out_about, FlipSocialViewLoggedOutAbout, "Welcome to FlipSocial\n---\nThe social media app for\nFlipper Zero, created by\nJBlanked: www.flipsocial.net\n---\nPress BACK to return.", flip_social_callback_to_submenu_logged_out, &app_instance->view_dispatcher); - } - } - else - { - if (!app_instance->widget_logged_in_about) - { - return easy_flipper_set_widget(&app_instance->widget_logged_in_about, FlipSocialViewLoggedInSettingsAbout, "Welcome to FlipSocial\n---\nThe social media app for\nFlipper Zero, created by\nJBlanked: www.flipsocial.net\n---\nPress BACK to return.", flip_social_callback_to_settings_logged_in, &app_instance->view_dispatcher); - } - } - return true; -} -static void free_about_widget(bool is_logged_in) -{ - if (is_logged_in && app_instance->widget_logged_in_about) - { - widget_free(app_instance->widget_logged_in_about); - app_instance->widget_logged_in_about = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoggedInSettingsAbout); - } - if (!is_logged_in && app_instance->widget_logged_out_about) - { - widget_free(app_instance->widget_logged_out_about); - app_instance->widget_logged_out_about = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoggedOutAbout); - } -} -static bool pre_saved_messages_alloc(void) -{ - if (!app_instance) - { - return false; - } - if (!app_instance->submenu_compose) - { - if (!easy_flipper_set_submenu(&app_instance->submenu_compose, FlipSocialViewLoggedInCompose, "Create A Post", flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher)) - { - return false; - } - submenu_reset(app_instance->submenu_compose); - submenu_add_item(app_instance->submenu_compose, "Add Pre-Save", FlipSocialSubmenuComposeIndexAddPreSave, flip_social_callback_submenu_choices, app_instance); - // load the playlist - if (load_playlist(&app_instance->pre_saved_messages)) - { - // Update the playlist submenu - for (uint32_t i = 0; i < app_instance->pre_saved_messages.count; i++) - { - if (app_instance->pre_saved_messages.messages[i]) - { - submenu_add_item(app_instance->submenu_compose, app_instance->pre_saved_messages.messages[i], FlipSocialSubemnuComposeIndexStartIndex + i, flip_social_callback_submenu_choices, app_instance); - } - } - } - } - return true; -} -static void free_pre_saved_messages(void) -{ - if (app_instance->submenu_compose) - { - submenu_free(app_instance->submenu_compose); - app_instance->submenu_compose = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoggedInCompose); - } - for (uint32_t i = 0; i < app_instance->pre_saved_messages.count; i++) - { - if (app_instance->pre_saved_messages.messages[i]) - { - free(app_instance->pre_saved_messages.messages[i]); - app_instance->pre_saved_messages.messages[i] = NULL; - } - } -} - -static void flip_social_free_friends(void) -{ - if (!flip_social_friends) - { - return; - } - free(flip_social_friends); - flip_social_friends = NULL; - if (app_instance->submenu_friends) - { - submenu_free(app_instance->submenu_friends); - app_instance->submenu_friends = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoggedInFriendsSubmenu); - } -} - static void flip_social_request_error_draw(Canvas *canvas) { if (canvas == NULL) @@ -162,32 +66,38 @@ static void flip_social_request_error_draw(Canvas *canvas) static bool flip_social_login_fetch(DataLoaderModel *model) { UNUSED(model); + if (!app_instance) + { + FURI_LOG_E(TAG, "app_instance is NULL"); + return false; + } if (!app_instance->login_username_logged_out || !app_instance->login_password_logged_out || strlen(app_instance->login_username_logged_out) == 0 || strlen(app_instance->login_password_logged_out) == 0) { return false; } - char buffer[256]; snprintf(buffer, sizeof(buffer), "{\"username\":\"%s\",\"password\":\"%s\"}", app_instance->login_username_logged_out, app_instance->login_password_logged_out); auth_headers_alloc(); - if (flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/login/", auth_headers, buffer)) - { - fhttp.state = RECEIVING; - return true; - } - else - { - fhttp.state = ISSUE; - return false; - } + return flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/login/", auth_headers, buffer); } static char *flip_social_login_parse(DataLoaderModel *model) { UNUSED(model); + if (!app_instance) + { + FURI_LOG_E(TAG, "app_instance is NULL"); + return "Failed to login..."; + } + if (!fhttp.last_response) + { + flipper_http_deinit(); + return "Failed to login..."; + } // read response if (strstr(fhttp.last_response, "[SUCCESS]") != NULL || strstr(fhttp.last_response, "User found") != NULL) { + flipper_http_deinit(); app_instance->is_logged_in = "true"; // set the logged_in_username and change_password_logged_in @@ -208,10 +118,12 @@ static char *flip_social_login_parse(DataLoaderModel *model) } else if (strstr(fhttp.last_response, "User not found") != NULL) { + flipper_http_deinit(); return "Account not found..."; } else { + flipper_http_deinit(); return "Failed to login..."; } } @@ -237,21 +149,10 @@ static bool flip_social_register_fetch(DataLoaderModel *model) FURI_LOG_E(TAG, "Passwords do not match"); return false; } - char buffer[128]; snprintf(buffer, sizeof(buffer), "{\"username\":\"%s\",\"password\":\"%s\"}", app_instance->register_username_logged_out, app_instance->register_password_logged_out); - if (flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/register/", "{\"Content-Type\":\"application/json\"}", buffer)) - { - // Set the state to RECEIVING to ensure we continue to see the receiving message - fhttp.state = RECEIVING; - return true; - } - else - { - fhttp.state = ISSUE; - return false; - } + return flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/register/", "{\"Content-Type\":\"application/json\"}", buffer); } static char *flip_social_register_parse(DataLoaderModel *model) @@ -260,6 +161,7 @@ static char *flip_social_register_parse(DataLoaderModel *model) // read response if (fhttp.last_response != NULL && (strstr(fhttp.last_response, "[SUCCESS]") != NULL || strstr(fhttp.last_response, "User created") != NULL)) { + flipper_http_deinit(); // set the login credentials if (app_instance->login_username_logged_out) { @@ -289,14 +191,17 @@ static char *flip_social_register_parse(DataLoaderModel *model) } else if (strstr(fhttp.last_response, "Username or password not provided") != NULL) { + flipper_http_deinit(); return "Please enter your credentials.\nPress BACK to return."; } else if (strstr(fhttp.last_response, "User already exists") != NULL || strstr(fhttp.last_response, "Multiple users found") != NULL) { + flipper_http_deinit(); return "Registration failed...\nUsername already exists.\nPress BACK to return."; } else { + flipper_http_deinit(); return "Registration failed...\nUpdate your credentials.\nPress BACK to return."; } } @@ -317,53 +222,6 @@ uint32_t flip_social_callback_to_submenu_logged_out(void *context) return FlipSocialViewLoggedOutSubmenu; } -static void flip_social_free_explore_dialog() -{ - if (app_instance->dialog_explore) - { - dialog_ex_free(app_instance->dialog_explore); - app_instance->dialog_explore = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewExploreDialog); - } -} -static void flip_social_free_friends_dialog() -{ - if (app_instance->dialog_friends) - { - dialog_ex_free(app_instance->dialog_friends); - app_instance->dialog_friends = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewFriendsDialog); - } -} -static void flip_social_free_messages_dialog() -{ - if (app_instance->dialog_messages) - { - dialog_ex_free(app_instance->dialog_messages); - app_instance->dialog_messages = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewMessagesDialog); - return; - } -} -static void flip_social_free_compose_dialog() -{ - if (app_instance->dialog_compose) - { - dialog_ex_free(app_instance->dialog_compose); - app_instance->dialog_compose = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewComposeDialog); - } -} -static void flip_social_free_feed_dialog() -{ - if (app_instance->dialog_feed) - { - dialog_ex_free(app_instance->dialog_feed); - app_instance->dialog_feed = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewFeedDialog); - } -} - /** * @brief Navigation callback to go back to the submenu Logged in. * @param context The context - unused @@ -388,6 +246,7 @@ uint32_t flip_social_callback_to_submenu_logged_in(void *context) flip_social_free_friends_dialog(); flip_social_free_messages_dialog(); flip_social_free_compose_dialog(); + return FlipSocialViewLoggedInSubmenu; } @@ -401,7 +260,7 @@ uint32_t flip_social_callback_to_login_logged_out(void *context) UNUSED(context); flip_social_sent_login_request = false; flip_social_login_success = false; - return FlipSocialViewLoggedOutLogin; + return FlipSocialViewVariableItemList; } /** @@ -414,7 +273,7 @@ uint32_t flip_social_callback_to_register_logged_out(void *context) UNUSED(context); flip_social_sent_register_request = false; flip_social_register_success = false; - return FlipSocialViewLoggedOutRegister; + return FlipSocialViewVariableItemList; } /** @@ -425,7 +284,7 @@ uint32_t flip_social_callback_to_register_logged_out(void *context) uint32_t flip_social_callback_to_wifi_settings_logged_out(void *context) { UNUSED(context); - return FlipSocialViewLoggedOutWifiSettings; + return FlipSocialViewVariableItemList; } /** @@ -436,7 +295,7 @@ uint32_t flip_social_callback_to_wifi_settings_logged_out(void *context) uint32_t flip_social_callback_to_wifi_settings_logged_in(void *context) { UNUSED(context); - return FlipSocialViewLoggedInSettingsWifi; + return FlipSocialViewVariableItemList; } /** @@ -447,7 +306,7 @@ uint32_t flip_social_callback_to_wifi_settings_logged_in(void *context) uint32_t flip_social_callback_to_settings_logged_in(void *context) { UNUSED(context); - return FlipSocialViewLoggedInSettings; + return FlipSocialViewSubmenu; } /** @@ -458,7 +317,7 @@ uint32_t flip_social_callback_to_settings_logged_in(void *context) uint32_t flip_social_callback_to_compose_logged_in(void *context) { UNUSED(context); - return FlipSocialViewLoggedInCompose; + return FlipSocialViewSubmenu; } /** @@ -469,13 +328,13 @@ uint32_t flip_social_callback_to_compose_logged_in(void *context) uint32_t flip_social_callback_to_profile_logged_in(void *context) { UNUSED(context); - return FlipSocialViewLoggedInProfile; + return FlipSocialViewVariableItemList; } /** * @brief Navigation callback to bring the user back to the Explore submenu * @param context The context - unused - * @return next view id (FlipSocialViewLoggedInExploreSubmenu) + * @return next view id (FlipSocialViewSubmenu) */ uint32_t flip_social_callback_to_explore_logged_in(void *context) { @@ -486,7 +345,7 @@ uint32_t flip_social_callback_to_explore_logged_in(void *context) { flip_social_explore->index = 0; } - return FlipSocialViewLoggedInExploreSubmenu; + return FlipSocialViewSubmenu; } /** @@ -500,7 +359,7 @@ uint32_t flip_social_callback_to_friends_logged_in(void *context) flip_social_dialog_stop = false; flip_social_dialog_shown = false; flip_social_friends->index = 0; - return FlipSocialViewLoggedInFriendsSubmenu; + return FlipSocialViewSubmenu; } /** @@ -511,7 +370,7 @@ uint32_t flip_social_callback_to_friends_logged_in(void *context) uint32_t flip_social_callback_to_messages_logged_in(void *context) { UNUSED(context); - return FlipSocialViewLoggedInMessagesSubmenu; + return FlipSocialViewSubmenu; } /** @@ -522,14 +381,7 @@ uint32_t flip_social_callback_to_messages_logged_in(void *context) uint32_t flip_social_callback_to_messages_user_choices(void *context) { UNUSED(context); - return FlipSocialViewLoggedInMessagesUserChoices; -} - -static void free_flip_social_group() -{ - flip_social_free_messages(); - flip_social_free_explore(); - flip_social_free_feed_dialog(); + return FlipSocialViewSubmenu; } /** @@ -541,9 +393,8 @@ uint32_t flip_social_callback_exit_app(void *context) { // Exit the application UNUSED(context); - free_flip_social_group(); - free_pre_saved_messages(); - return VIEW_NONE; // Return VIEW_NONE to exit the app + free_all(true, true); + return VIEW_NONE; } static void explore_dialog_callback(DialogExResult result, void *context) @@ -552,23 +403,33 @@ static void explore_dialog_callback(DialogExResult result, void *context) FlipSocialApp *app = (FlipSocialApp *)context; if (result == DialogExResultLeft) // Remove { - // remove friend - char remove_payload[128]; - snprintf(remove_payload, sizeof(remove_payload), "{\"username\":\"%s\",\"friend\":\"%s\"}", app_instance->login_username_logged_in, flip_social_explore->usernames[flip_social_explore->index]); - auth_headers_alloc(); - flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/remove-friend/", auth_headers, remove_payload); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInExploreSubmenu); - flip_social_free_explore_dialog(); + if (flipper_http_init(flipper_http_rx_callback, app_instance)) + { + // remove friend + char remove_payload[128]; + snprintf(remove_payload, sizeof(remove_payload), "{\"username\":\"%s\",\"friend\":\"%s\"}", app_instance->login_username_logged_in, flip_social_explore->usernames[flip_social_explore->index]); + auth_headers_alloc(); + flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/remove-friend/", auth_headers, remove_payload); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewSubmenu); + flip_social_free_explore_dialog(); + furi_delay_ms(1000); + flipper_http_deinit(); + } } else if (result == DialogExResultRight) { - // add friend - char add_payload[128]; - snprintf(add_payload, sizeof(add_payload), "{\"username\":\"%s\",\"friend\":\"%s\"}", app_instance->login_username_logged_in, flip_social_explore->usernames[flip_social_explore->index]); - auth_headers_alloc(); - flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/add-friend/", auth_headers, add_payload); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInExploreSubmenu); - flip_social_free_explore_dialog(); + if (flipper_http_init(flipper_http_rx_callback, app_instance)) + { + // add friend + char add_payload[128]; + snprintf(add_payload, sizeof(add_payload), "{\"username\":\"%s\",\"friend\":\"%s\"}", app_instance->login_username_logged_in, flip_social_explore->usernames[flip_social_explore->index]); + auth_headers_alloc(); + flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/add-friend/", auth_headers, add_payload); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewSubmenu); + flip_social_free_explore_dialog(); + furi_delay_ms(1000); + flipper_http_deinit(); + } } } static void friends_dialog_callback(DialogExResult result, void *context) @@ -577,16 +438,21 @@ static void friends_dialog_callback(DialogExResult result, void *context) FlipSocialApp *app = (FlipSocialApp *)context; if (result == DialogExResultLeft) // Remove { - // remove friend - char remove_payload[128]; - snprintf(remove_payload, sizeof(remove_payload), "{\"username\":\"%s\",\"friend\":\"%s\"}", app_instance->login_username_logged_in, flip_social_friends->usernames[flip_social_friends->index]); - auth_headers_alloc(); - flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/remove-friend/", auth_headers, remove_payload); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInFriendsSubmenu); - flip_social_free_friends_dialog(); + if (flipper_http_init(flipper_http_rx_callback, app_instance)) + { + // remove friend + char remove_payload[128]; + snprintf(remove_payload, sizeof(remove_payload), "{\"username\":\"%s\",\"friend\":\"%s\"}", app_instance->login_username_logged_in, flip_social_friends->usernames[flip_social_friends->index]); + auth_headers_alloc(); + flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/remove-friend/", auth_headers, remove_payload); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewSubmenu); + flip_social_free_friends_dialog(); + furi_delay_ms(1000); + flipper_http_deinit(); + } } } -static void messages_dialog_callback(DialogExResult result, void *context) +void messages_dialog_callback(DialogExResult result, void *context) { furi_assert(context); FlipSocialApp *app = (FlipSocialApp *)context; @@ -597,7 +463,7 @@ static void messages_dialog_callback(DialogExResult result, void *context) flip_social_messages->index--; dialog_ex_reset(app->dialog_messages); dialog_ex_set_header(app->dialog_messages, flip_social_messages->usernames[flip_social_messages->index], 0, 0, AlignLeft, AlignTop); - dialog_ex_set_text(app->dialog_messages, flip_social_messages->messages[flip_social_messages->index], 0, 10, AlignLeft, AlignTop); + dialog_ex_set_text(app->dialog_messages, updated_user_message(flip_social_messages->messages[flip_social_messages->index]), 0, 10, AlignLeft, AlignTop); if (flip_social_messages->index != 0) { dialog_ex_set_left_button_text(app->dialog_messages, "Prev"); @@ -605,7 +471,7 @@ static void messages_dialog_callback(DialogExResult result, void *context) dialog_ex_set_right_button_text(app->dialog_messages, "Next"); dialog_ex_set_center_button_text(app->dialog_messages, "Create"); // switch view, free dialog, re-alloc dialog, switch back to dialog - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInMessagesSubmenu); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewWidgetResult); flip_social_free_messages_dialog(); messages_dialog_alloc(false); view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewMessagesDialog); @@ -618,7 +484,7 @@ static void messages_dialog_callback(DialogExResult result, void *context) flip_social_messages->index++; dialog_ex_reset(app->dialog_messages); dialog_ex_set_header(app->dialog_messages, flip_social_messages->usernames[flip_social_messages->index], 0, 0, AlignLeft, AlignTop); - dialog_ex_set_text(app->dialog_messages, flip_social_messages->messages[flip_social_messages->index], 0, 10, AlignLeft, AlignTop); + dialog_ex_set_text(app->dialog_messages, updated_user_message(flip_social_messages->messages[flip_social_messages->index]), 0, 10, AlignLeft, AlignTop); dialog_ex_set_left_button_text(app->dialog_messages, "Prev"); if (flip_social_messages->index != flip_social_messages->count - 1) { @@ -626,7 +492,7 @@ static void messages_dialog_callback(DialogExResult result, void *context) } dialog_ex_set_center_button_text(app->dialog_messages, "Create"); // switch view, free dialog, re-alloc dialog, switch back to dialog - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInMessagesSubmenu); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewWidgetResult); flip_social_free_messages_dialog(); messages_dialog_alloc(false); view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewMessagesDialog); @@ -634,40 +500,14 @@ static void messages_dialog_callback(DialogExResult result, void *context) } else if (result == DialogExResultCenter) // new message { - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInMessagesNewMessageInput); - } -} - -bool messages_dialog_alloc(bool free_first) -{ - if (free_first) - { - flip_social_free_messages_dialog(); - } - if (!app_instance->dialog_messages) - { - if (!easy_flipper_set_dialog_ex( - &app_instance->dialog_messages, - FlipSocialViewMessagesDialog, - flip_social_messages->usernames[flip_social_messages->index], - 0, - 0, - flip_social_messages->messages[flip_social_messages->index], - 0, - 10, - flip_social_messages->index != 0 ? "Prev" : NULL, - flip_social_messages->index != flip_social_messages->count - 1 ? "Next" : NULL, - "Create", - messages_dialog_callback, - flip_social_callback_to_messages_logged_in, - &app_instance->view_dispatcher, - app_instance)) + free_text_input(); + if (!alloc_text_input(FlipSocialViewLoggedInMessagesNewMessageInput)) { - return false; + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; } - return true; + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); } - return false; } static void compose_dialog_callback(DialogExResult result, void *context) @@ -676,39 +516,63 @@ static void compose_dialog_callback(DialogExResult result, void *context) FlipSocialApp *app = (FlipSocialApp *)context; if (result == DialogExResultLeft) // Delete { - // delete message - app->pre_saved_messages.messages[app_instance->pre_saved_messages.index] = NULL; + // Ensure index is within bounds + if (app_instance->pre_saved_messages.index >= app_instance->pre_saved_messages.count) + { + FURI_LOG_E(TAG, "Invalid index for deletion: %zu", app_instance->pre_saved_messages.index); + return; + } - for (uint32_t i = app->pre_saved_messages.index; i < app->pre_saved_messages.count - 1; i++) + // Shift messages to remove the selected message + for (size_t i = app_instance->pre_saved_messages.index; i < app_instance->pre_saved_messages.count - 1; i++) { - app->pre_saved_messages.messages[i] = app->pre_saved_messages.messages[i + 1]; + strncpy(app_instance->pre_saved_messages.messages[i], + app_instance->pre_saved_messages.messages[i + 1], + MAX_MESSAGE_LENGTH); } - app->pre_saved_messages.count--; - // add the item to the submenu - submenu_reset(app_instance->submenu_compose); + // Clear the last message after shifting + memset(app_instance->pre_saved_messages.messages[app_instance->pre_saved_messages.count - 1], 0, MAX_MESSAGE_LENGTH); + app_instance->pre_saved_messages.count--; - submenu_add_item(app_instance->submenu_compose, "Add Pre-Save", FlipSocialSubmenuComposeIndexAddPreSave, flip_social_callback_submenu_choices, app); + // Reset and rebuild the submenu + submenu_reset(app_instance->submenu); + submenu_add_item(app_instance->submenu, "Add Pre-Save", FlipSocialSubmenuComposeIndexAddPreSave, flip_social_callback_submenu_choices, app); - for (uint32_t i = 0; i < app->pre_saved_messages.count; i++) + for (size_t i = 0; i < app_instance->pre_saved_messages.count; i++) { - submenu_add_item(app_instance->submenu_compose, app->pre_saved_messages.messages[i], FlipSocialSubemnuComposeIndexStartIndex + i, flip_social_callback_submenu_choices, app); + submenu_add_item(app_instance->submenu, + app_instance->pre_saved_messages.messages[i], + FlipSocialSubemnuComposeIndexStartIndex + i, + flip_social_callback_submenu_choices, + app); } - // save playlist + // Save the updated playlist save_playlist(&app_instance->pre_saved_messages); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInCompose); + + // Switch back to the compose view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewSubmenu); + + // Free the dialog resources flip_social_free_compose_dialog(); } + else if (result == DialogExResultRight) // Post { // post the message // send selected_message + if (!flipper_http_init(flipper_http_rx_callback, app_instance)) + { + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); + return; + } if (selected_message && app_instance->login_username_logged_in) { if (strlen(selected_message) > MAX_MESSAGE_LENGTH) { FURI_LOG_E(TAG, "Message is too long"); + flipper_http_deinit(); return; } // Send the selected_message @@ -721,7 +585,7 @@ static void compose_dialog_callback(DialogExResult result, void *context) command)) { FURI_LOG_E(TAG, "Failed to send HTTP request for feed"); - fhttp.state = ISSUE; + flipper_http_deinit(); return; } @@ -731,255 +595,33 @@ static void compose_dialog_callback(DialogExResult result, void *context) else { FURI_LOG_E(TAG, "Message or username is NULL"); + flipper_http_deinit(); return; } while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) { furi_delay_ms(100); } - if (flip_social_load_initial_feed()) + if (flip_social_load_initial_feed(false, 1)) { flip_social_free_compose_dialog(); } - FURI_LOG_E(TAG, "Failed to load initial feed"); - return; - } -} -static void feed_dialog_callback(DialogExResult result, void *context) -{ - furi_assert(context); - FlipSocialApp *app = (FlipSocialApp *)context; - if (result == DialogExResultLeft) // Previous message - { - if (flip_feed_info->index > 0) - { - flip_feed_info->index--; - } - // switch view, free dialog, re-alloc dialog, switch back to dialog - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInSettings); - // load feed item - if (!flip_social_load_feed_post(flip_feed_info->ids[flip_feed_info->index])) - { - FURI_LOG_E(TAG, "Failed to load nexy feed post"); - fhttp.state = ISSUE; - return; - } - if (feed_dialog_alloc()) - { - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewFeedDialog); - } - else - { - FURI_LOG_E(TAG, "Failed to allocate feed dialog"); - fhttp.state = ISSUE; - return; - } - } - else if (result == DialogExResultRight) // Next message - { - if (flip_feed_info->index < flip_feed_info->count - 1) - { - flip_feed_info->index++; - } - // switch view, free dialog, re-alloc dialog, switch back to dialog - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInSettings); - // load feed item - if (!flip_social_load_feed_post(flip_feed_info->ids[flip_feed_info->index])) - { - FURI_LOG_E(TAG, "Failed to load nexy feed post"); - fhttp.state = ISSUE; - return; - } - if (feed_dialog_alloc()) - { - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewFeedDialog); - } - else - { - FURI_LOG_E(TAG, "Failed to allocate feed dialog"); - fhttp.state = ISSUE; - return; - } - } - else if (result == DialogExResultCenter) // Flip/Unflip - { - // Moved to above the is_flipped check - if (!flip_feed_item->is_flipped) - { - // increase the flip count - flip_feed_item->flips++; - } else { - // decrease the flip count - flip_feed_item->flips--; - } - // change the flip status - flip_feed_item->is_flipped = !flip_feed_item->is_flipped; - // send post request to flip the message - if (app_instance->login_username_logged_in == NULL) - { - FURI_LOG_E(TAG, "Username is NULL"); - return; - } - char payload[256]; - snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"post_id\":\"%u\"}", app_instance->login_username_logged_in, flip_feed_item->id); - if (flipper_http_post_request_with_headers("https://www.flipsocial.net/api/feed/flip/", auth_headers, payload)) - { - // save feed item - char new_save[256]; - snprintf(new_save, sizeof(new_save), "{\"id\":%u,\"username\":\"%s\",\"message\":\"%s\",\"flip_count\":%u,\"flipped\":%s}", - flip_feed_item->id, flip_feed_item->username, flip_feed_item->message, flip_feed_item->flips, flip_feed_item->is_flipped ? "true" : "false"); - // if (!flip_social_save_post((char *)flip_feed_item->id, new_save)) - // { - // FURI_LOG_E(TAG, "Failed to save the feed post"); - // fhttp.state = ISSUE; - // return; - // } - } - // switch view, free dialog, re-alloc dialog, switch back to dialog - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInSettings); - if (feed_dialog_alloc()) - { - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewFeedDialog); - } - else - { - FURI_LOG_E(TAG, "Failed to allocate feed dialog"); - fhttp.state = ISSUE; - return; - } - } -} - -static char *updated_user_message(const char *user_message) -{ - if (user_message == NULL) - { - FURI_LOG_E(TAG, "User message is NULL."); - return NULL; - } - - size_t msg_length = strlen(user_message); - size_t start = 0; - int line_num = 0; - - // Allocate memory for the updated message - char *updated_message = malloc(MAX_MESSAGE_LENGTH + 10); - if (updated_message == NULL) - { - FURI_LOG_E(TAG, "Failed to allocate memory for updated_message."); - return NULL; - } - size_t current_pos = 0; // Tracks the current position in updated_message - updated_message[0] = '\0'; // Initialize as empty string - - while (start < msg_length && line_num < 4) - { - size_t remaining = msg_length - start; - size_t len = (remaining > MAX_LINE_LENGTH) ? MAX_LINE_LENGTH : remaining; - - // Adjust length to the last space if the line exceeds MAX_LINE_LENGTH - if (remaining > MAX_LINE_LENGTH) - { - size_t last_space = len; - while (last_space > 0 && user_message[start + last_space - 1] != ' ') - { - last_space--; - } - - if (last_space > 0) - { - len = last_space; // Adjust len to the position of the last space - } - } - - // Check if the new line fits in the updated_message buffer - if (current_pos + len + 1 >= (MAX_MESSAGE_LENGTH + 10)) - { - FURI_LOG_E(TAG, "Updated message exceeds maximum length."); - // break and return what we have so far - break; - } - - // Copy the line and append a newline character - memcpy(updated_message + current_pos, user_message + start, len); - current_pos += len; - updated_message[current_pos++] = '\n'; // Append newline - - // 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 && user_message[start] == ' ') - { - start++; + FURI_LOG_E(TAG, "Failed to load the initial feed"); + flipper_http_deinit(); } - - // Increment the line number - line_num++; - } - - // Null-terminate the final string - if (current_pos < (MAX_MESSAGE_LENGTH + 10)) - { - updated_message[current_pos] = '\0'; } - else - { - FURI_LOG_E(TAG, "Buffer overflow while null-terminating."); - free(updated_message); - return NULL; - } - - return updated_message; } -bool feed_dialog_alloc() +static bool flip_social_get_user_info() { - if (!flip_feed_item) + if (!flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "Feed item is NULL"); + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); + fhttp.state = ISSUE; return false; } - flip_social_free_feed_dialog(); - if (!app_instance->dialog_feed) - { - char updated_message[MAX_MESSAGE_LENGTH + 10]; - snprintf(updated_message, MAX_MESSAGE_LENGTH + 10, "%s (%u %s)", flip_feed_item->message, flip_feed_item->flips, flip_feed_item->flips == 1 ? "flip" : "flips"); - char *real_message = updated_user_message(updated_message); - if (!real_message) - { - FURI_LOG_E(TAG, "Failed to update the user message"); - return false; - } - if (!easy_flipper_set_dialog_ex( - &app_instance->dialog_feed, - FlipSocialViewFeedDialog, - flip_feed_item->username, - 0, - 0, - updated_message, - 0, - 10, - flip_feed_info->index != 0 ? "Prev" : NULL, - flip_feed_info->index != flip_feed_info->count - 1 ? "Next" : NULL, - flip_feed_item->is_flipped ? "Unflip" : "Flip", - feed_dialog_callback, - flip_social_callback_to_submenu_logged_in, - &app_instance->view_dispatcher, - app_instance)) - { - free(real_message); - return false; - } - free(real_message); - return true; - } - return false; -} -static bool flip_social_get_user_info() -{ char url[256]; snprintf(url, sizeof(url), "https://www.flipsocial.net/api/user/users/%s/extended/", flip_social_explore->usernames[flip_social_explore->index]); if (!flipper_http_get_request_with_headers(url, auth_headers)) @@ -1003,8 +645,9 @@ static bool flip_social_parse_user_info() FURI_LOG_E(TAG, "App instance is NULL"); return false; } - char *bio = get_json_value("bio", fhttp.last_response, 32); - char *friends = get_json_value("friends", fhttp.last_response, 32); + char *bio = get_json_value("bio", fhttp.last_response); + char *friends = get_json_value("friends", fhttp.last_response); + bool parse_success = false; if (bio && friends) { if (strlen(bio) != 0) @@ -1016,9 +659,11 @@ static bool flip_social_parse_user_info() snprintf(app_instance->explore_user_bio, MAX_MESSAGE_LENGTH, "%s friends", friends); } free(bio); - return true; + free(friends); + parse_success = true; } - return false; + flipper_http_deinit(); + return parse_success; } /** @@ -1040,12 +685,24 @@ void flip_social_callback_submenu_choices(void *context, uint32_t index) case FlipSocialSubmenuLoggedOutIndexLogin: flip_social_sent_login_request = false; flip_social_login_success = false; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutLogin); + free_all(true, true); + if (!alloc_variable_item_list(FlipSocialViewLoggedOutLogin)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); break; case FlipSocialSubmenuLoggedOutIndexRegister: flip_social_sent_register_request = false; flip_social_register_success = false; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutRegister); + free_all(true, true); + if (!alloc_variable_item_list(FlipSocialViewLoggedOutRegister)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); break; case FlipSocialSubmenuLoggedOutIndexAbout: if (!about_widget_alloc(false)) @@ -1055,50 +712,111 @@ void flip_social_callback_submenu_choices(void *context, uint32_t index) view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutAbout); break; case FlipSocialSubmenuLoggedOutIndexWifiSettings: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutWifiSettings); + free_all(false, false); + if (!alloc_variable_item_list(FlipSocialViewLoggedOutWifiSettings)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); break; case FlipSocialSubmenuLoggedInIndexProfile: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInProfile); + free_all(true, true); + if (!alloc_variable_item_list(FlipSocialViewLoggedInProfile)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); break; case FlipSocialSubmenuLoggedInIndexMessages: - free_flip_social_group(); + free_all(true, true); + if (!alloc_submenu(FlipSocialViewLoggedInMessagesSubmenu)) + { + FURI_LOG_E(TAG, "Failed to allocate submenu"); + return; + } flipper_http_loading_task( - flip_social_get_message_users, // get the message users - flip_social_parse_json_message_users, // parse the message users - FlipSocialViewLoggedInMessagesSubmenu, // switch to the messages submenu if successful - FlipSocialViewLoggedInSubmenu, // switch back to the main submenu if failed - &app->view_dispatcher); // view dispatcher + flip_social_get_message_users, // get the message users + flip_social_parse_json_message_users, // parse the message users + FlipSocialViewSubmenu, // switch to the messages submenu if successful + FlipSocialViewLoggedInSubmenu, // switch back to the main submenu if failed + &app->view_dispatcher); // view dispatcher break; case FlipSocialSubmenuLoggedInIndexMessagesNewMessage: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInMessageUsersInput); + // they need to search for the user to send a message + free_text_input(); + if (!alloc_text_input(FlipSocialViewLoggedInMessageUsersInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case FlipSocialSubmenuLoggedInIndexFeed: - free_flip_social_group(); - if (!flip_social_load_initial_feed()) + free_all(true, true); + if (!flip_social_load_initial_feed(true, 1)) { FURI_LOG_E(TAG, "Failed to load the initial feed"); return; } - free_pre_saved_messages(); break; case FlipSocialSubmenuExploreIndex: - free_flip_social_group(); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInExploreInput); + free_all(true, true); + if (!alloc_text_input(FlipSocialViewLoggedInExploreInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case FlipSocialSubmenuLoggedInIndexCompose: - free_pre_saved_messages(); - if (!pre_saved_messages_alloc()) + free_all(true, true); + if (!alloc_submenu(FlipSocialViewLoggedInCompose)) { - FURI_LOG_E(TAG, "Failed to allocate pre-saved messages"); + FURI_LOG_E(TAG, "Failed to allocate submenu"); return; } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInCompose); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewSubmenu); break; case FlipSocialSubmenuLoggedInIndexSettings: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInSettings); + free_all(true, true); + if (!alloc_submenu(FlipSocialViewLoggedInSettings)) + { + FURI_LOG_E(TAG, "Failed to allocate submenu"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewSubmenu); + break; + case FlipSocialSubmenuLoggedInIndexAbout: + free_all(true, false); + if (!about_widget_alloc(true)) + { + FURI_LOG_E(TAG, "Failed to allocate about widget"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInSettingsAbout); + break; + case FlipSocialSubmenuLoggedInIndexWifiSettings: + free_all(true, false); + if (!alloc_variable_item_list(FlipSocialViewLoggedInSettingsWifi)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); + break; + case FlipSocialSubmenuLoggedInIndexUserSettings: + free_all(true, false); + if (!alloc_variable_item_list(FlipSocialViewLoggedInSettingsUser)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); break; case FlipSocialSubmenuLoggedInSignOutButton: - free_flip_social_group(); + free_all(true, true); app->is_logged_in = "false"; save_settings(app->wifi_ssid_logged_out, app->wifi_password_logged_out, app->login_username_logged_out, app->login_username_logged_in, app->login_password_logged_out, app->change_password_logged_in, app->change_bio_logged_in, app->is_logged_in); @@ -1106,7 +824,13 @@ void flip_social_callback_submenu_choices(void *context, uint32_t index) view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutSubmenu); break; case FlipSocialSubmenuComposeIndexAddPreSave: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInComposeAddPreSaveInput); + free_text_input(); + if (!alloc_text_input(FlipSocialViewLoggedInComposeAddPreSaveInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; default: // Handle the pre-saved message selection (has a max of 25 items) @@ -1128,7 +852,7 @@ void flip_social_callback_submenu_choices(void *context, uint32_t index) "New Feed Post", 0, 0, - selected_message, + updated_user_message(selected_message), 0, 10, "Delete", @@ -1175,7 +899,7 @@ void flip_social_callback_submenu_choices(void *context, uint32_t index) flip_social_explore->usernames[flip_social_explore->index], 0, 0, - app->explore_user_bio, + updated_user_message(app->explore_user_bio), 0, 10, "Remove", // remove if user is a friend (future update) @@ -1282,7 +1006,13 @@ void flip_social_callback_submenu_choices(void *context, uint32_t index) return; } flip_social_explore->index = index - FlipSocialSubmenuLoggedInIndexMessagesUserChoicesIndexStart; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInMessagesNewMessageUserChoicesInput); + free_text_input(); + if (!alloc_text_input(FlipSocialViewLoggedInMessagesNewMessageUserChoicesInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } // switch to the text input view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); } else { @@ -1325,17 +1055,22 @@ void flip_social_logged_out_wifi_settings_ssid_updated(void *context) // update the wifi settings if (strlen(app->wifi_ssid_logged_out) > 0 && strlen(app->wifi_password_logged_out) > 0) { - if (!flipper_http_save_wifi(app->wifi_ssid_logged_out, app->wifi_password_logged_out)) + if (flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); - FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + if (!flipper_http_save_wifi(app->wifi_ssid_logged_out, app->wifi_password_logged_out)) + { + FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); + FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + } + furi_delay_ms(500); + flipper_http_deinit(); } } // Save the settings save_settings(app_instance->wifi_ssid_logged_out, app_instance->wifi_password_logged_out, app_instance->login_username_logged_out, app_instance->login_username_logged_in, app_instance->login_password_logged_out, app_instance->change_password_logged_in, app_instance->change_bio_logged_in, app_instance->is_logged_in); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutWifiSettings); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); } /** @@ -1370,17 +1105,22 @@ void flip_social_logged_out_wifi_settings_password_updated(void *context) // update the wifi settings if (strlen(app->wifi_ssid_logged_out) > 0 && strlen(app->wifi_password_logged_out) > 0) { - if (!flipper_http_save_wifi(app->wifi_ssid_logged_out, app->wifi_password_logged_out)) + if (flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); - FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + if (!flipper_http_save_wifi(app->wifi_ssid_logged_out, app->wifi_password_logged_out)) + { + FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); + FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + } + furi_delay_ms(500); + flipper_http_deinit(); } } // Save the settings save_settings(app_instance->wifi_ssid_logged_out, app_instance->wifi_password_logged_out, app_instance->login_username_logged_out, app_instance->login_username_logged_in, app_instance->login_password_logged_out, app_instance->change_password_logged_in, app_instance->change_bio_logged_in, app_instance->is_logged_in); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutWifiSettings); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); } /** @@ -1400,10 +1140,24 @@ void flip_social_text_input_logged_out_wifi_settings_item_selected(void *context switch (index) { case 0: // Input SSID - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutWifiSettingsSSIDInput); + // view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutWifiSettingsSSIDInput); + free_text_input(); + if (!alloc_text_input(FlipSocialViewLoggedOutWifiSettingsSSIDInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case 1: // Input Password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutWifiSettingsPasswordInput); + // view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutWifiSettingsPasswordInput); + free_text_input(); + if (!alloc_text_input(FlipSocialViewLoggedOutWifiSettingsPasswordInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; default: FURI_LOG_E(TAG, "Unknown configuration item index"); @@ -1443,7 +1197,7 @@ void flip_social_logged_out_login_username_updated(void *context) // Save the settings save_settings(app_instance->wifi_ssid_logged_out, app_instance->wifi_password_logged_out, app_instance->login_username_logged_out, app_instance->login_username_logged_in, app_instance->login_password_logged_out, app_instance->change_password_logged_in, app_instance->change_bio_logged_in, app_instance->is_logged_in); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutLogin); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); } /** @@ -1480,7 +1234,7 @@ void flip_social_logged_out_login_password_updated(void *context) // Save the settings save_settings(app_instance->wifi_ssid_logged_out, app_instance->wifi_password_logged_out, app_instance->login_username_logged_out, app_instance->login_username_logged_in, app_instance->login_password_logged_out, app_instance->change_password_logged_in, app_instance->change_bio_logged_in, app_instance->is_logged_in); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutLogin); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); } /** @@ -1500,12 +1254,31 @@ void flip_social_text_input_logged_out_login_item_selected(void *context, uint32 switch (index) { case 0: // Input Username - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutLoginUsernameInput); + // view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutLoginUsernameInput); + free_all(false, true); + if (!alloc_text_input(FlipSocialViewLoggedOutLoginUsernameInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case 1: // Input Password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutLoginPasswordInput); + // view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutLoginPasswordInput); + free_all(false, true); + if (!alloc_text_input(FlipSocialViewLoggedOutLoginPasswordInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case 2: // Login Button + if (!flipper_http_init(flipper_http_rx_callback, app_instance)) + { + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); + return; + } flip_social_login_switch_to_view(app); break; default: @@ -1539,7 +1312,7 @@ void flip_social_logged_out_register_username_updated(void *context) variable_item_set_current_value_text(app->variable_item_logged_out_register_username, app->register_username_logged_out); } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutRegister); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); } /** @@ -1567,7 +1340,7 @@ void flip_social_logged_out_register_password_updated(void *context) variable_item_set_current_value_text(app->variable_item_logged_out_register_password, app->register_password_logged_out); } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutRegister); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); } /** @@ -1595,7 +1368,7 @@ void flip_social_logged_out_register_password_2_updated(void *context) variable_item_set_current_value_text(app->variable_item_logged_out_register_password_2, app->register_password_2_logged_out); } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutRegister); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); } /** @@ -1615,15 +1388,38 @@ void flip_social_text_input_logged_out_register_item_selected(void *context, uin switch (index) { case 0: // Input Username - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutRegisterUsernameInput); + free_all(false, true); + if (!alloc_text_input(FlipSocialViewLoggedOutRegisterUsernameInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case 1: // Input Password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutRegisterPasswordInput); + free_all(false, true); + if (!alloc_text_input(FlipSocialViewLoggedOutRegisterPasswordInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case 2: // Input Password 2 - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedOutRegisterPassword2Input); + free_all(false, true); + if (!alloc_text_input(FlipSocialViewLoggedOutRegisterPassword2Input)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case 3: // Register button + if (!flipper_http_init(flipper_http_rx_callback, app_instance)) + { + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); + return; + } flip_social_register_switch_to_view(app); break; default: @@ -1667,14 +1463,19 @@ void flip_social_logged_in_wifi_settings_ssid_updated(void *context) // update the wifi settings if (strlen(app->wifi_ssid_logged_in) > 0 && strlen(app->wifi_password_logged_in) > 0) { - if (!flipper_http_save_wifi(app->wifi_ssid_logged_in, app->wifi_password_logged_in)) + if (flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); - FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + if (!flipper_http_save_wifi(app->wifi_ssid_logged_in, app->wifi_password_logged_in)) + { + FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); + FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + } + furi_delay_ms(500); + flipper_http_deinit(); } } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInSettingsWifi); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); } /** @@ -1713,14 +1514,19 @@ void flip_social_logged_in_wifi_settings_password_updated(void *context) // update the wifi settings if (strlen(app->wifi_ssid_logged_in) > 0 && strlen(app->wifi_password_logged_in) > 0) { - if (!flipper_http_save_wifi(app->wifi_ssid_logged_in, app->wifi_password_logged_in)) + if (flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); - FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + if (!flipper_http_save_wifi(app->wifi_ssid_logged_in, app->wifi_password_logged_in)) + { + FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); + FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + } + furi_delay_ms(500); + flipper_http_deinit(); } } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInSettingsWifi); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewVariableItemList); } /** @@ -1740,10 +1546,24 @@ void flip_social_text_input_logged_in_wifi_settings_item_selected(void *context, switch (index) { case 0: // Input SSID - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInWifiSettingsSSIDInput); + // view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInWifiSettingsSSIDInput); + free_all(false, false); + if (!alloc_text_input(FlipSocialViewLoggedInWifiSettingsSSIDInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for SSID"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case 1: // Input Password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInWifiSettingsPasswordInput); + // view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInWifiSettingsPasswordInput); + free_all(false, false); + if (!alloc_text_input(FlipSocialViewLoggedInWifiSettingsPasswordInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for Password"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; default: FURI_LOG_E(TAG, "Unknown configuration item index"); @@ -1751,6 +1571,25 @@ void flip_social_text_input_logged_in_wifi_settings_item_selected(void *context, } } +void flip_social_logged_in_user_settings_item_selected(void *context, uint32_t index) +{ + FlipSocialApp *app = (FlipSocialApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipSocialApp is NULL"); + return; + } + + // Switch to the appropriate view + switch (index) + { + case 0: // Feed Type + break; + case 1: // Notifications + break; + } +} + /** * @brief Text input callback for when the user finishes entering their message on the compose (logged in) screen for Add Text * @param context The context - FlipSocialApp object. @@ -1765,37 +1604,50 @@ void flip_social_logged_in_compose_pre_save_updated(void *context) return; } - // check if the message is empty or if adding in the message would exceed the MAX_PRE_SAVED_MESSAGES + // Check if the message is empty or if adding the message would exceed MAX_PRE_SAVED_MESSAGES if (app->compose_pre_save_logged_in_temp_buffer_size == 0 || app->pre_saved_messages.count >= MAX_PRE_SAVED_MESSAGES) { FURI_LOG_E(TAG, "Message is empty or would exceed the maximum number of pre-saved messages"); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInCompose); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewSubmenu); return; } - // Store the entered message - strncpy(app->compose_pre_save_logged_in, app->compose_pre_save_logged_in_temp_buffer, app->compose_pre_save_logged_in_temp_buffer_size); + // Copy the entered message into the next available slot + strncpy( + app->pre_saved_messages.messages[app->pre_saved_messages.count], + app->compose_pre_save_logged_in_temp_buffer, + MAX_MESSAGE_LENGTH - 1); // Ensure null-termination - app->compose_pre_save_logged_in[app->compose_pre_save_logged_in_temp_buffer_size - 1] = '\0'; - - // add the item to the submenu - submenu_reset(app->submenu_compose); + app->pre_saved_messages.messages[app->pre_saved_messages.count][MAX_MESSAGE_LENGTH - 1] = '\0'; - // loop through the items and add them to the submenu - app->pre_saved_messages.messages[app->pre_saved_messages.count] = app->compose_pre_save_logged_in; + // Increment the count app->pre_saved_messages.count++; - submenu_add_item(app->submenu_compose, "Add Pre-Save", FlipSocialSubmenuComposeIndexAddPreSave, flip_social_callback_submenu_choices, app); - for (uint32_t i = 0; i < app->pre_saved_messages.count; i++) + // Rebuild the submenu + submenu_reset(app->submenu); + submenu_add_item( + app->submenu, + "Add Pre-Save", + FlipSocialSubmenuComposeIndexAddPreSave, + flip_social_callback_submenu_choices, + app); + + for (size_t i = 0; i < app->pre_saved_messages.count; i++) { - submenu_add_item(app->submenu_compose, app->pre_saved_messages.messages[i], FlipSocialSubemnuComposeIndexStartIndex + i, flip_social_callback_submenu_choices, app); + submenu_add_item( + app->submenu, + app->pre_saved_messages.messages[i], + FlipSocialSubemnuComposeIndexStartIndex + i, + flip_social_callback_submenu_choices, + app); } - // save playlist + // Save the updated playlist save_playlist(&app->pre_saved_messages); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInCompose); + // Switch back to the compose view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewSubmenu); } /** @@ -1832,19 +1684,23 @@ 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, sizeof(payload), "{\"username\":\"%s\",\"old_password\":\"%s\",\"new_password\":\"%s\"}", app->login_username_logged_out, old_password, app->change_password_logged_in); - if (!flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/change-password/", auth_headers, payload)) + if (flipper_http_init(flipper_http_rx_callback, app)) { - 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"); - return; + auth_headers_alloc(); + char payload[256]; + snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"old_password\":\"%s\",\"new_password\":\"%s\"}", app->login_username_logged_out, old_password, app->change_password_logged_in); + if (!flipper_http_post_request_with_headers("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"); + } + flipper_http_deinit(); } // Save the settings save_settings(app_instance->wifi_ssid_logged_out, app_instance->wifi_password_logged_out, app_instance->login_username_logged_out, app_instance->login_username_logged_in, app_instance->login_password_logged_out, app_instance->change_password_logged_in, app_instance->change_bio_logged_in, app_instance->is_logged_in); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInProfile); + // instead of going to a view, just show a success message + easy_flipper_dialog("Success", "Password updated successfully\n\n\nPress BACK to return :D"); } void flip_social_logged_in_profile_change_bio_updated(void *context) @@ -1869,19 +1725,24 @@ void flip_social_logged_in_profile_change_bio_updated(void *context) } // send post request to change bio - auth_headers_alloc(); - char payload[256]; - snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"bio\":\"%s\"}", app->login_username_logged_out, app->change_bio_logged_in); - if (!flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/change-bio/", auth_headers, payload)) + if (flipper_http_init(flipper_http_rx_callback, app)) { - FURI_LOG_E(TAG, "Failed to send post request to change bio"); - FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); - return; + auth_headers_alloc(); + char payload[256]; + snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"bio\":\"%s\"}", app->login_username_logged_out, app->change_bio_logged_in); + if (!flipper_http_post_request_with_headers("https://www.flipsocial.net/api/user/change-bio/", auth_headers, payload)) + { + FURI_LOG_E(TAG, "Failed to send post request to change bio"); + FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + } + furi_delay_ms(500); + flipper_http_deinit(); } // Save the settings - save_settings(app_instance->wifi_ssid_logged_out, app_instance->wifi_password_logged_out, app_instance->login_username_logged_out, app_instance->login_username_logged_in, app_instance->login_password_logged_out, app_instance->change_password_logged_in, app_instance->change_bio_logged_in, app_instance->is_logged_in); + save_settings(app->wifi_ssid_logged_out, app->wifi_password_logged_out, app->login_username_logged_out, app->login_username_logged_in, app->login_password_logged_out, app->change_password_logged_in, app->change_bio_logged_in, app->is_logged_in); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInProfile); + // instead of going to a view, just show a success message + easy_flipper_dialog("Success", "Bio updated successfully\n\n\nPress BACK to return :D"); } /** @@ -1904,57 +1765,46 @@ void flip_social_text_input_logged_in_profile_item_selected(void *context, uint3 // do nothing since username cannot be changed break; case 1: // Change Password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInChangePasswordInput); + // view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInChangePasswordInput); + free_text_input(); + if (!alloc_text_input(FlipSocialViewLoggedInChangePasswordInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for change password"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case 2: // Change Bio - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInChangeBioInput); + // view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInChangeBioInput); + free_text_input(); + if (!alloc_text_input(FlipSocialViewLoggedInChangeBioInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for change bio"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); break; case 3: // Friends - flip_social_free_friends(); - free_flip_social_group(); - if (!app->submenu_friends) + free_all(false, true); + if (!alloc_submenu(FlipSocialViewLoggedInFriendsSubmenu)) { - if (!easy_flipper_set_submenu(&app->submenu_friends, FlipSocialViewLoggedInFriendsSubmenu, "Friends", flip_social_callback_to_profile_logged_in, &app->view_dispatcher)) - { - FURI_LOG_DEV(TAG, "Failed to set submenu for friends"); - return; - } + FURI_LOG_E(TAG, "Failed to allocate submenu for friends"); + return; } - flipper_http_loading_task(flip_social_get_friends, flip_social_parse_json_friends, FlipSocialViewLoggedInFriendsSubmenu, FlipSocialViewLoggedInProfile, &app->view_dispatcher); - break; - default: - FURI_LOG_E(TAG, "Unknown configuration item index"); - break; - } -} - -/** - * @brief Callback when a user selects a menu item in the settings (logged in) screen. - * @param context The context - FlipSocialApp object. - * @param index The index of the selected item. - * @return void - */ -void flip_social_text_input_logged_in_settings_item_selected(void *context, uint32_t index) -{ - FlipSocialApp *app = (FlipSocialApp *)context; - if (!app) - { - FURI_LOG_E(TAG, "FlipSocialApp is NULL"); - return; - } - switch (index) - { - case 0: // About - if (!about_widget_alloc(true)) + if (!flipper_http_init(flipper_http_rx_callback, app)) { + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); return; } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInSettingsAbout); - break; - case 1: // Wifi - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInSettingsWifi); + flipper_http_loading_task( + flip_social_get_friends, + flip_social_parse_json_friends, + FlipSocialViewSubmenu, + FlipSocialViewVariableItemList, + &app->view_dispatcher); break; default: + FURI_LOG_E(TAG, "Unknown configuration item index"); break; } } @@ -1977,7 +1827,7 @@ void flip_social_logged_in_messages_user_choice_message_updated(void *context) if (app->message_user_choice_logged_in_temp_buffer_size == 0) { FURI_LOG_E(TAG, "Message is empty"); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInMessagesNewMessageUserChoicesInput); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewTextInput); return; } @@ -1988,38 +1838,49 @@ void flip_social_logged_in_messages_user_choice_message_updated(void *context) app->message_user_choice_logged_in[app->message_user_choice_logged_in_temp_buffer_size - 1] = '\0'; // send post request to send message + if (!flipper_http_init(flipper_http_rx_callback, app)) + { + FURI_LOG_E(TAG, "Failed to initialize HTTP"); + return; + } auth_headers_alloc(); char url[128]; char payload[256]; snprintf(url, sizeof(url), "https://www.flipsocial.net/api/messages/%s/post/", app->login_username_logged_in); snprintf(payload, sizeof(payload), "{\"receiver\":\"%s\",\"content\":\"%s\"}", flip_social_explore->usernames[flip_social_explore->index], app->message_user_choice_logged_in); - 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; - } - else + if (!flipper_http_post_request_with_headers(url, auth_headers, payload)) // start the async request { 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"); - fhttp.state = ISSUE; - return; } - while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) + furi_delay_ms(1000); + flipper_http_deinit(); + // add user to the top of the list if not already there + for (int i = 0; i < flip_social_message_users->count; i++) + { + if (strcmp(flip_social_message_users->usernames[i], flip_social_explore->usernames[flip_social_explore->index]) == 0) + { + // remove the user from the list + for (int j = i; j < flip_social_message_users->count - 1; j++) + { + strncpy(flip_social_message_users->usernames[j], flip_social_message_users->usernames[j + 1], strlen(flip_social_message_users->usernames[j + 1])); + } + flip_social_message_users->count--; + break; + } + } + // add the user to the top of the list + for (int i = flip_social_message_users->count; i > 0; i--) { - // Wait for the request to be received - furi_delay_ms(10); + strncpy(flip_social_message_users->usernames[i], flip_social_message_users->usernames[i - 1], strlen(flip_social_message_users->usernames[i - 1])); } - furi_timer_stop(fhttp.get_timeout_timer); - - // add user to the message list - strncpy(flip_social_message_users->usernames[flip_social_message_users->count], flip_social_explore->usernames[flip_social_explore->index], strlen(flip_social_explore->usernames[flip_social_explore->index])); + strncpy(flip_social_message_users->usernames[0], flip_social_explore->usernames[flip_social_explore->index], strlen(flip_social_explore->usernames[flip_social_explore->index])); flip_social_message_users->count++; // redraw submenu flip_social_update_messages_submenu(); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInMessagesSubmenu); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewSubmenu); } /** @@ -2050,32 +1911,47 @@ void flip_social_logged_in_messages_new_message_updated(void *context) // Ensure null-termination app->messages_new_message_logged_in[app->messages_new_message_logged_in_temp_buffer_size - 1] = '\0'; - // send post request to send message - auth_headers_alloc(); - char url[128]; - char payload[256]; - snprintf(url, sizeof(url), "https://www.flipsocial.net/api/messages/%s/post/", app->login_username_logged_in); - snprintf(payload, sizeof(payload), "{\"receiver\":\"%s\",\"content\":\"%s\"}", flip_social_message_users->usernames[flip_social_message_users->index], app->messages_new_message_logged_in); - - if (flipper_http_post_request_with_headers(url, auth_headers, payload)) // start the async request + bool send_message_to_user() { - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + // send post request to send message + if (!flipper_http_init(flipper_http_rx_callback, app)) + { + FURI_LOG_E(TAG, "Failed to initialize HTTP"); + return false; + } + auth_headers_alloc(); + char url[128]; + char payload[256]; + snprintf(url, sizeof(url), "https://www.flipsocial.net/api/messages/%s/post/", app->login_username_logged_in); + snprintf(payload, sizeof(payload), "{\"receiver\":\"%s\",\"content\":\"%s\"}", flip_social_message_users->usernames[flip_social_message_users->index], app->messages_new_message_logged_in); + if (!flipper_http_post_request_with_headers(url, auth_headers, payload)) + { + 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"); + easy_flipper_dialog("Error", "Failed to send message\n\n\nPress BACK to return :D"); + flipper_http_deinit(); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewSubmenu); + return false; + } fhttp.state = RECEIVING; + return true; } - 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"); - fhttp.state = ISSUE; - return; - } - while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) + bool parse_message_to_user() { - // Wait for the request to be received - furi_delay_ms(10); + while (fhttp.state != IDLE) + { + furi_delay_ms(10); + } + flipper_http_deinit(); + return true; } - furi_timer_stop(fhttp.get_timeout_timer); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipSocialViewLoggedInMessagesSubmenu); + // well, we got a freeze here, so let's use the loading task to switch views and force refresh + flipper_http_loading_task( + send_message_to_user, + parse_message_to_user, + FlipSocialViewSubmenu, + FlipSocialViewLoggedInMessagesNewMessageInput, + &app->view_dispatcher); } void flip_social_logged_in_explore_updated(void *context) @@ -2101,14 +1977,22 @@ void flip_social_logged_in_explore_updated(void *context) // Ensure null-termination app->explore_logged_in[app->explore_logged_in_temp_buffer_size - 1] = '\0'; + free_submenu(); + + if (!alloc_submenu(FlipSocialViewLoggedInExploreSubmenu)) + { + FURI_LOG_E(TAG, "Failed to allocate submenu for explore"); + return; + } flipper_http_loading_task( - flip_social_get_explore, // get the explore users - flip_social_parse_json_explore, // parse the explore users - FlipSocialViewLoggedInExploreSubmenu, // switch to the explore submenu if successful - FlipSocialViewLoggedInSubmenu, // switch back to the main submenu if failed - &app->view_dispatcher); // view dispatcher + flip_social_get_explore, // get the explore users + flip_social_parse_json_explore, // parse the explore users + FlipSocialViewSubmenu, // switch to the explore submenu if successful + FlipSocialViewLoggedInSubmenu, // switch back to the main submenu if failed + &app->view_dispatcher); // view dispatcher } + void flip_social_logged_in_message_users_updated(void *context) { FlipSocialApp *app = (FlipSocialApp *)context; @@ -2119,7 +2003,7 @@ void flip_social_logged_in_message_users_updated(void *context) } // check if the message is empty - if (app->message_users_logged_in_temp_buffer_size == 0) + if (!app->message_users_logged_in_temp_buffer || app->message_users_logged_in_temp_buffer_size == 0) { FURI_LOG_E(TAG, "Message is empty"); strncpy(app->message_users_logged_in, "a", 2); @@ -2133,10 +2017,19 @@ void flip_social_logged_in_message_users_updated(void *context) // Ensure null-termination app->message_users_logged_in[app->message_users_logged_in_temp_buffer_size - 1] = '\0'; + free_submenu(); + + if (!alloc_submenu(FlipSocialViewLoggedInExploreSubmenu)) + { + FURI_LOG_E(TAG, "Failed to allocate submenu for explore"); + return; + } + + // get users flipper_http_loading_task( flip_social_get_explore_2, // get the explore users flip_social_parse_json_message_user_choices, // parse the explore users - FlipSocialViewLoggedInMessagesUserChoices, // switch to the user choices if successful + FlipSocialViewSubmenu, // switch to the explore submenu if successful FlipSocialViewLoggedInSubmenu, // switch back to the main submenu if failed &app->view_dispatcher); // view dispatcher } @@ -2434,7 +2327,7 @@ static void flip_social_loader_process_callback(void *context) } else { - flip_social_widget_set_text(model->data_text != NULL ? model->data_text : "Empty result", &app_instance->widget_result); + flip_social_widget_set_text(model->data_text != NULL ? model->data_text : "", &app_instance->widget_result); if (model->data_text != NULL) { free(model->data_text); @@ -2599,3 +2492,190 @@ void flip_social_generic_switch_to_view(FlipSocialApp *app, char *title, DataLoa view_dispatcher_switch_to_view(app->view_dispatcher, view_id); } + +static bool flip_social_get_home_notification() +{ + if (!app_instance) + { + FURI_LOG_E(TAG, "app_instance is NULL"); + return false; + } + if (!flipper_http_init(flipper_http_rx_callback, app_instance)) + { + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); + return false; + } + // Create the directory for saving settings + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/data"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + furi_record_close(RECORD_STORAGE); + auth_headers_alloc(); + snprintf( + fhttp.file_path, + sizeof(fhttp.file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/data/notification.json"); + + fhttp.save_received_data = true; + return flipper_http_get_request_with_headers("https://www.flipsocial.net/api/flip-social-notifications/", auth_headers); +} +static bool flip_social_parse_home_notification() +{ + FuriString *notification = flipper_http_load_from_file(fhttp.file_path); + if (notification == NULL) + { + FURI_LOG_E(TAG, "Failed to load notification from file"); + flipper_http_deinit(); + return false; + } + flipper_http_deinit(); + + // Check if announcement and analytics key exists + FuriString *announcement_json = get_json_value_furi("announcement", notification); + FuriString *analytics_json = get_json_value_furi("analytics", notification); + if (announcement_json == NULL || analytics_json == NULL) + { + FURI_LOG_E(TAG, "Failed to get announcement or analytics from notification"); + if (announcement_json) + { + furi_string_free(announcement_json); + } + if (analytics_json) + { + furi_string_free(analytics_json); + } + furi_string_free(notification); + return false; + } + + // Extract values from JSON + FuriString *announcement_value = get_json_value_furi("content", announcement_json); + FuriString *announcement_time = get_json_value_furi("date_created", announcement_json); + FuriString *analytics_value = get_json_value_furi("count", analytics_json); + FuriString *analytics_time = get_json_value_furi("time", analytics_json); + if (!announcement_value || !announcement_time || !analytics_value || !analytics_time) + { + FURI_LOG_E(TAG, "Failed to get announcement or analytics value from notification"); + if (announcement_value) + { + furi_string_free(announcement_value); + } + if (announcement_time) + { + furi_string_free(announcement_time); + } + if (analytics_value) + { + furi_string_free(analytics_value); + } + if (analytics_time) + { + furi_string_free(analytics_time); + } + furi_string_free(announcement_json); + furi_string_free(analytics_json); + furi_string_free(notification); + return false; + } + + // Load previous announcement and analytics times + char past_analytics_time[32] = {0}; + char past_announcement_time[32] = {0}; + bool analytics_time_loaded = load_char("analytics_time", past_analytics_time, sizeof(past_analytics_time)); + bool announcement_time_loaded = load_char("announcement_time", past_announcement_time, sizeof(past_announcement_time)); + + bool new_announcement = false; + bool new_analytics = false; + + // Check for new announcement + if (!announcement_time_loaded || strcmp(furi_string_get_cstr(announcement_time), past_announcement_time) != 0) + { + new_announcement = true; + } + + // Check for new analytics + if (!analytics_time_loaded || strcmp(furi_string_get_cstr(analytics_time), past_analytics_time) != 0) + { + new_analytics = true; + } + + // If no new announcement and no new analytics, exit early + if (!new_announcement && !new_analytics) + { + FURI_LOG_D(TAG, "No new announcement or analytics"); + furi_string_free(announcement_value); + furi_string_free(announcement_time); + furi_string_free(analytics_value); + furi_string_free(analytics_time); + furi_string_free(announcement_json); + furi_string_free(analytics_json); + furi_string_free(notification); + return true; + } + + // Save the new announcement and analytics times if they are new + if (new_announcement) + { + save_char("announcement_time", furi_string_get_cstr(announcement_time)); + } + + if (new_analytics) + { + save_char("analytics_time", furi_string_get_cstr(analytics_time)); + } + + // Prepare and show dialogs based on what is new + if (new_announcement) + { + easy_flipper_dialog("Announcement", (char *)furi_string_get_cstr(announcement_value)); + } + + if (new_analytics) + { + char analytics_text[128] = {0}; + // Determine the new posts count + if (atoi(furi_string_get_cstr(analytics_value)) > 0) + { + char past_analytics_value[32] = {0}; + int new_posts = 0; + if (load_char("analytics_value", past_analytics_value, sizeof(past_analytics_value))) + { + int past_posts = atoi(past_analytics_value); + int current_posts = atoi(furi_string_get_cstr(analytics_value)); + new_posts = current_posts - past_posts; + snprintf(analytics_text, sizeof(analytics_text), "%d new posts", new_posts); + } + else + { + snprintf(analytics_text, sizeof(analytics_text), "%s feed posts", furi_string_get_cstr(analytics_value)); + } + save_char("analytics_value", furi_string_get_cstr(analytics_value)); + } + else + { + snprintf(analytics_text, sizeof(analytics_text), "%s feed posts", furi_string_get_cstr(analytics_value)); + } + + easy_flipper_dialog("Notifications", analytics_text); + } + + // Free allocated resources + furi_string_free(announcement_value); + furi_string_free(announcement_time); + furi_string_free(analytics_value); + furi_string_free(analytics_time); + furi_string_free(announcement_json); + furi_string_free(analytics_json); + furi_string_free(notification); + + return true; +} + +// home notification +bool flip_social_home_notification() +{ + return flipper_http_process_response_async(flip_social_get_home_notification, flip_social_parse_home_notification); +} \ No newline at end of file diff --git a/flip_social/callback/flip_social_callback.h b/flip_social/callback/flip_social_callback.h index 722dee083..455146dea 100644 --- a/flip_social/callback/flip_social_callback.h +++ b/flip_social/callback/flip_social_callback.h @@ -7,6 +7,8 @@ #include #include #include +#include +#include /** * @brief Navigation callback to go back to the submenu Logged out. @@ -252,6 +254,7 @@ void flip_social_logged_in_messages_new_message_updated(void *context); * @return void */ void flip_social_text_input_logged_out_register_item_selected(void *context, uint32_t index); +void flip_social_logged_in_user_settings_item_selected(void *context, uint32_t index); void flip_social_logged_in_explore_updated(void *context); void flip_social_logged_in_message_users_updated(void *context); @@ -296,9 +299,9 @@ void flip_social_loader_draw_callback(Canvas *canvas, void *model); void flip_social_loader_init(View *view); void flip_social_loader_free_model(View *view); - bool flip_social_custom_event_callback(void *context, uint32_t index); - -bool messages_dialog_alloc(bool free_first); -bool feed_dialog_alloc(); +void messages_dialog_callback(DialogExResult result, void *context); +void feed_dialog_callback(DialogExResult result, void *context); +// +bool flip_social_home_notification(); #endif \ No newline at end of file diff --git a/flip_social/easy_flipper/easy_flipper.c b/flip_social/easy_flipper/easy_flipper.c index f61b7d231..c2ce949e4 100644 --- a/flip_social/easy_flipper/easy_flipper.c +++ b/flip_social/easy_flipper/easy_flipper.c @@ -1,5 +1,25 @@ #include +void easy_flipper_dialog( + char *header, + char *text) +{ + DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage *message = dialog_message_alloc(); + dialog_message_set_header( + message, header, 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + message, + text, + 0, + 63, + AlignLeft, + AlignBottom); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); +} + /** * @brief Navigation callback for exiting the application * @param context The context - unused diff --git a/flip_social/easy_flipper/easy_flipper.h b/flip_social/easy_flipper/easy_flipper.h index 3ccfe66b8..8c71106c7 100644 --- a/flip_social/easy_flipper/easy_flipper.h +++ b/flip_social/easy_flipper/easy_flipper.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -15,14 +16,21 @@ #include #include #include +#include +#include #include #include #include #include #include +#include #define EASY_TAG "EasyFlipper" +void easy_flipper_dialog( + char *header, + char *text); + /** * @brief Navigation callback for exiting the application * @param context The context - unused diff --git a/flip_social/explore/flip_social_explore.c b/flip_social/explore/flip_social_explore.c index ee18bbc90..36108a66c 100644 --- a/flip_social/explore/flip_social_explore.c +++ b/flip_social/explore/flip_social_explore.c @@ -2,19 +2,6 @@ FlipSocialModel *flip_social_explore_alloc(void) { - if (!app_instance) - { - return NULL; - } - - if (!app_instance->submenu_explore) - { - if (!easy_flipper_set_submenu(&app_instance->submenu_explore, FlipSocialViewLoggedInExploreSubmenu, "Explore", flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher)) - { - return NULL; - } - } - // Allocate memory for each username only if not already allocated FlipSocialModel *explore = malloc(sizeof(FlipSocialModel)); if (explore == NULL) @@ -22,55 +9,38 @@ FlipSocialModel *flip_social_explore_alloc(void) FURI_LOG_E(TAG, "Failed to allocate memory for explore model."); return NULL; } - for (size_t i = 0; i < MAX_EXPLORE_USERS; i++) - { - if (explore->usernames[i] == NULL) - { - explore->usernames[i] = malloc(MAX_USER_LENGTH); - if (explore->usernames[i] == NULL) - { - FURI_LOG_E(TAG, "Failed to allocate memory for username %zu", i); - return NULL; // Return false on memory allocation failure - } - } - } return explore; } void flip_social_free_explore(void) { - if (!app_instance) - { - return; - } - if (app_instance->submenu_explore) - { - submenu_free(app_instance->submenu_explore); - app_instance->submenu_explore = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoggedInExploreSubmenu); - } - if (!flip_social_explore) + if (flip_social_explore) { - return; + free(flip_social_explore); + flip_social_explore = NULL; } - free(flip_social_explore); - flip_social_explore = NULL; } // for now we're just listing the current users // as the feed is upgraded, then we can port more to the explore view bool flip_social_get_explore(void) { - if (fhttp.state == INACTIVE) + if (!flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "HTTP state is INACTIVE"); + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); return false; } + char directory[128]; + snprintf(directory, sizeof(directory), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/explore"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory); char *keyword = !app_instance->explore_logged_in || strlen(app_instance->explore_logged_in) == 0 ? "a" : app_instance->explore_logged_in; snprintf( fhttp.file_path, sizeof(fhttp.file_path), - STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/%s.json", + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/explore/%s.json", keyword); fhttp.save_received_data = true; @@ -88,30 +58,33 @@ bool flip_social_get_explore(void) } bool flip_social_get_explore_2(void) { - if (fhttp.state == INACTIVE) + if (!app_instance) + { + return false; + } + if (!flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "HTTP state is INACTIVE"); + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); return false; } + char directory[128]; + snprintf(directory, sizeof(directory), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/explore"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory); char *keyword = !app_instance->message_users_logged_in || strlen(app_instance->message_users_logged_in) == 0 ? "a" : app_instance->message_users_logged_in; snprintf( fhttp.file_path, sizeof(fhttp.file_path), - STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/%s.json", + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/explore/%s.json", keyword); fhttp.save_received_data = true; auth_headers_alloc(); char url[256]; snprintf(url, sizeof(url), "https://www.flipsocial.net/api/user/explore/%s/%d/", keyword, MAX_EXPLORE_USERS); - if (!flipper_http_get_request_with_headers(url, auth_headers)) - { - FURI_LOG_E(TAG, "Failed to send HTTP request for explore"); - fhttp.state = ISSUE; - return false; - } - fhttp.state = RECEIVING; - return true; + return flipper_http_get_request_with_headers(url, auth_headers); } bool flip_social_parse_json_explore() @@ -121,15 +94,11 @@ bool flip_social_parse_json_explore() if (user_data == NULL) { FURI_LOG_E(TAG, "Failed to load received data from file."); + flipper_http_deinit(); return false; } - char *data_cstr = (char *)furi_string_get_cstr(user_data); - if (data_cstr == NULL) - { - FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); - furi_string_free(user_data); - return false; - } + + flipper_http_deinit(); // Allocate memory for each username only if not already allocated flip_social_explore = flip_social_explore_alloc(); @@ -137,7 +106,6 @@ bool flip_social_parse_json_explore() { FURI_LOG_E(TAG, "Failed to allocate memory for explore usernames."); furi_string_free(user_data); - free(data_cstr); return false; } @@ -145,22 +113,21 @@ bool flip_social_parse_json_explore() flip_social_explore->count = 0; // set submenu - submenu_reset(app_instance->submenu_explore); - submenu_set_header(app_instance->submenu_explore, "Explore"); + submenu_reset(app_instance->submenu); + submenu_set_header(app_instance->submenu, "Explore"); // Parse the JSON array of usernames for (size_t i = 0; i < MAX_EXPLORE_USERS; i++) { - char *username = get_json_array_value("users", i, data_cstr, 64); // currently its 53 tokens (with max explore users at 50) + FuriString *username = get_json_array_value_furi("users", i, user_data); // currently its 53 tokens (with max explore users at 50) if (username == NULL) { break; } - snprintf(flip_social_explore->usernames[i], MAX_USER_LENGTH, "%s", username); - submenu_add_item(app_instance->submenu_explore, flip_social_explore->usernames[i], FlipSocialSubmenuExploreIndexStartIndex + i, flip_social_callback_submenu_choices, app_instance); + snprintf(flip_social_explore->usernames[i], MAX_USER_LENGTH, "%s", furi_string_get_cstr(username)); + submenu_add_item(app_instance->submenu, flip_social_explore->usernames[i], FlipSocialSubmenuExploreIndexStartIndex + i, flip_social_callback_submenu_choices, app_instance); flip_social_explore->count++; - free(username); + furi_string_free(username); } - free(data_cstr); furi_string_free(user_data); return flip_social_explore->count > 0; } diff --git a/flip_social/feed/flip_social_feed.c b/flip_social/feed/flip_social_feed.c index 379c44167..f355ecdcf 100644 --- a/flip_social/feed/flip_social_feed.c +++ b/flip_social/feed/flip_social_feed.c @@ -1,15 +1,15 @@ #include "flip_social_feed.h" -bool flip_social_get_feed() +bool flip_social_get_feed(bool alloc_http, int series_index) { if (!app_instance) { FURI_LOG_E(TAG, "FlipSocialApp is NULL"); return false; } - if (fhttp.state == INACTIVE) + if (alloc_http && !flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "HTTP state is INACTIVE"); + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); return false; } // Get the feed from the server @@ -26,7 +26,14 @@ bool flip_social_get_feed() fhttp.save_received_data = true; auth_headers_alloc(); char command[96]; - snprintf(command, 96, "https://www.flipsocial.net/api/feed/50/%s/extended/", app_instance->login_username_logged_out); + if (strstr(flip_social_feed_type[flip_social_feed_type_index], "Global")) + { + snprintf(command, 96, "https://www.flipsocial.net/api/feed/%d/%s/%d/max/series/", MAX_FEED_ITEMS, app_instance->login_username_logged_out, series_index); + } + else + { + snprintf(command, 96, "https://www.flipsocial.net/api/feed/%d/%s/%d/max/friends/series/", MAX_FEED_ITEMS, app_instance->login_username_logged_out, series_index); + } if (!flipper_http_get_request_with_headers(command, auth_headers)) { FURI_LOG_E(TAG, "Failed to send HTTP request for feed"); @@ -44,15 +51,10 @@ FlipSocialFeedMini *flip_social_parse_json_feed() if (feed_data == NULL) { FURI_LOG_E(TAG, "Failed to load received data from file."); + flipper_http_deinit(); return NULL; } - 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 NULL; - } + flipper_http_deinit(); FlipSocialFeedMini *feed_info = (FlipSocialFeedMini *)malloc(sizeof(FlipSocialFeedMini)); if (!feed_info) @@ -61,65 +63,40 @@ FlipSocialFeedMini *flip_social_parse_json_feed() return NULL; } - // Remove newlines - char *pos = data_cstr; - while ((pos = strchr(pos, '\n')) != NULL) - { - *pos = ' '; - } - int feed_count = 0; // Iterate through the feed array for (int i = 0; i < MAX_FEED_ITEMS; i++) { // Parse each item in the array - char *item = get_json_array_value("feed", i, data_cstr, MAX_TOKENS); + FuriString *item = get_json_array_value_furi("feed", i, feed_data); if (item == NULL) { break; } // Extract individual fields from the JSON object - char *username = get_json_value("username", item, 40); - char *message = get_json_value("message", item, 40); - char *flipped = get_json_value("flipped", item, 40); - char *flips = get_json_value("flip_count", item, 40); - char *id = get_json_value("id", item, 40); - - if (username == NULL || message == NULL || flipped == NULL || id == NULL) + FuriString *id = get_json_value_furi("id", item); + if (id == NULL) { FURI_LOG_E(TAG, "Failed to parse item fields."); - free(item); - free(username); - free(message); - free(flipped); - free(flips); - free(id); + furi_string_free(item); + furi_string_free(id); continue; } - - if (!flip_social_save_post(id, item)) + if (!flip_social_save_post(furi_string_get_cstr(id), furi_string_get_cstr(item))) { FURI_LOG_E(TAG, "Failed to save post."); - free(item); - free(username); - free(message); - free(flipped); - free(flips); - free(id); + furi_string_free(item); + furi_string_free(id); continue; } feed_count++; - feed_info->ids[i] = atoi(id); + feed_info->ids[i] = atoi(furi_string_get_cstr(id)); - // Free allocated memory - free(item); - free(username); - free(message); - free(flipped); - free(flips); - free(id); + // Furi_string_free allocated memory + furi_string_free(item); + furi_string_free(id); } // Store the number of feed items @@ -127,14 +104,13 @@ FlipSocialFeedMini *flip_social_parse_json_feed() feed_info->index = 0; furi_string_free(feed_data); - free(data_cstr); return feed_info; } bool flip_social_load_feed_post(int post_id) { char file_path[128]; - snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/feed_post_%d.json", post_id); + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/feed/feed_post_%d.json", post_id); // load the received data from the saved file FuriString *feed_data = flipper_http_load_from_file(file_path); @@ -143,85 +119,82 @@ bool flip_social_load_feed_post(int post_id) 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) + + // Parse the feed post + if (flip_feed_item) + { + free(flip_feed_item); + } + else { - FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); - furi_string_free(feed_data); - return false; + // first time + save_char("series_index", "1"); } - // Parse the feed post - if (!flip_feed_item) + flip_feed_item = (FlipSocialFeedItem *)malloc(sizeof(FlipSocialFeedItem)); + if (flip_feed_item == NULL) { - flip_feed_item = (FlipSocialFeedItem *)malloc(sizeof(FlipSocialFeedItem)); - if (flip_feed_item == NULL) - { - FURI_LOG_E(TAG, "Failed to allocate memory for feed post."); - furi_string_free(feed_data); - free(data_cstr); - return false; - } - flip_feed_item->username = malloc(MAX_USER_LENGTH); - flip_feed_item->message = malloc(MAX_MESSAGE_LENGTH); + FURI_LOG_E(TAG, "Failed to allocate memory for feed post."); + furi_string_free(feed_data); + return false; } // Extract individual fields from the JSON object - char *username = get_json_value("username", data_cstr, 16); - char *message = get_json_value("message", data_cstr, 16); - char *flipped = get_json_value("flipped", data_cstr, 16); - char *flips = get_json_value("flip_count", data_cstr, 16); - char *id = get_json_value("id", data_cstr, 16); - - if (username == NULL || message == NULL || flipped == NULL || id == NULL) + FuriString *username = get_json_value_furi("username", feed_data); + FuriString *message = get_json_value_furi("message", feed_data); + FuriString *flipped = get_json_value_furi("flipped", feed_data); + FuriString *flips = get_json_value_furi("flip_count", feed_data); + FuriString *date_created = get_json_value_furi("date_created", feed_data); + if (username == NULL || message == NULL || flipped == NULL || flips == NULL || date_created == NULL) { FURI_LOG_E(TAG, "Failed to parse item fields."); - free(username); - free(message); - free(flipped); - free(flips); - free(id); - free(data_cstr); + furi_string_free(username); + furi_string_free(message); + furi_string_free(flipped); + furi_string_free(flips); + furi_string_free(date_created); furi_string_free(feed_data); return false; } // Safely copy strings with bounds checking - snprintf(flip_feed_item->username, MAX_USER_LENGTH, "%s", username); - snprintf(flip_feed_item->message, MAX_MESSAGE_LENGTH, "%s", message); + snprintf(flip_feed_item->username, MAX_USER_LENGTH, "%s", furi_string_get_cstr(username)); + snprintf(flip_feed_item->message, MAX_MESSAGE_LENGTH, "%s", furi_string_get_cstr(message)); + snprintf(flip_feed_item->date_created, MAX_LINE_LENGTH, "%s", furi_string_get_cstr(date_created)); // Store boolean and integer values - flip_feed_item->is_flipped = strstr(flipped, "true") != NULL; - flip_feed_item->id = atoi(id); - flip_feed_item->flips = atoi(flips); + flip_feed_item->is_flipped = strstr(furi_string_get_cstr(flipped), "true") ? true : false; + flip_feed_item->id = post_id; + flip_feed_item->flips = atoi(furi_string_get_cstr(flips)); // Free allocated memory - free(username); - free(message); - free(flipped); - free(flips); - free(id); - + furi_string_free(username); + furi_string_free(message); + furi_string_free(flipped); + furi_string_free(flips); + furi_string_free(date_created); furi_string_free(feed_data); - free(data_cstr); - return true; } -bool flip_social_load_initial_feed() +bool flip_social_load_initial_feed(bool alloc_http, int series_index) { if (fhttp.state == INACTIVE) { FURI_LOG_E(TAG, "HTTP state is INACTIVE"); return false; } - if (!easy_flipper_set_loading(&app_instance->loading, FlipSocialViewLoading, flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher)) + Loading *loading; + int32_t loading_view_id = 987654321; // Random ID + loading = loading_alloc(); + if (!loading) { FURI_LOG_E(TAG, "Failed to set loading screen"); return false; } - view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoading); - if (flip_social_get_feed()) // start the async request + view_dispatcher_add_view(app_instance->view_dispatcher, loading_view_id, loading_get_view(loading)); + view_dispatcher_switch_to_view(app_instance->view_dispatcher, loading_view_id); + if (flip_social_get_feed(alloc_http, series_index)) // start the async request { furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); fhttp.state = RECEIVING; @@ -231,8 +204,8 @@ bool flip_social_load_initial_feed() FURI_LOG_E(HTTP_TAG, "Failed to send request"); fhttp.state = ISSUE; view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoggedInSubmenu); - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoading); - loading_free(app_instance->loading); + view_dispatcher_remove_view(app_instance->view_dispatcher, loading_view_id); + loading_free(loading); return false; } while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) @@ -247,10 +220,9 @@ bool flip_social_load_initial_feed() if (!flip_feed_info || flip_feed_info->count < 1) { FURI_LOG_E(TAG, "Failed to parse JSON feed"); - fhttp.state = ISSUE; view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoggedInSubmenu); - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoading); - loading_free(app_instance->loading); + view_dispatcher_remove_view(app_instance->view_dispatcher, loading_view_id); + loading_free(loading); return false; } @@ -258,24 +230,22 @@ bool flip_social_load_initial_feed() if (!flip_social_load_feed_post(flip_feed_info->ids[flip_feed_info->index])) { FURI_LOG_E(TAG, "Failed to load feed post"); - fhttp.state = ISSUE; view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoggedInSubmenu); - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoading); - loading_free(app_instance->loading); + view_dispatcher_remove_view(app_instance->view_dispatcher, loading_view_id); + loading_free(loading); return false; } - if (!feed_dialog_alloc()) + if (!feed_view_alloc()) { FURI_LOG_E(TAG, "Failed to allocate feed dialog"); - fhttp.state = ISSUE; view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoggedInSubmenu); - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoading); - loading_free(app_instance->loading); + view_dispatcher_remove_view(app_instance->view_dispatcher, loading_view_id); + loading_free(loading); return false; } - view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewFeedDialog); - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoading); - loading_free(app_instance->loading); + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipSocialViewLoggedInFeed); + view_dispatcher_remove_view(app_instance->view_dispatcher, loading_view_id); + loading_free(loading); return true; } \ No newline at end of file diff --git a/flip_social/feed/flip_social_feed.h b/flip_social/feed/flip_social_feed.h index a4b92ca6c..e346ecc72 100644 --- a/flip_social/feed/flip_social_feed.h +++ b/flip_social/feed/flip_social_feed.h @@ -4,8 +4,8 @@ #include #include -bool flip_social_get_feed(); +bool flip_social_get_feed(bool alloc_http, int series_index); bool flip_social_load_feed_post(int post_id); -bool flip_social_load_initial_feed(); +bool flip_social_load_initial_feed(bool alloc_http, int series_index); FlipSocialFeedMini *flip_social_parse_json_feed(); #endif diff --git a/flip_social/flip_social.c b/flip_social/flip_social.c index 4b18ccc0a..c58b17f00 100644 --- a/flip_social/flip_social.c +++ b/flip_social/flip_social.c @@ -18,6 +18,10 @@ bool flip_social_send_message = false; char *selected_message = NULL; char auth_headers[256] = {0}; +char *flip_social_feed_type[] = {"Global", "Friends"}; +uint8_t flip_social_feed_type_index = 0; +char *flip_social_notification_type[] = {"OFF", "ON"}; +uint8_t flip_social_notification_type_index = 0; void flip_social_loader_free_model(View *view); @@ -52,125 +56,6 @@ void flip_social_app_free(FlipSocialApp *app) submenu_free(app->submenu_logged_in); } // - if (app->submenu_messages_user_choices) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInMessagesUserChoices); - submenu_free(app->submenu_messages_user_choices); - } - - // Free Variable Item List(s) - if (app->variable_item_list_logged_out_wifi_settings) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedOutWifiSettings); - variable_item_list_free(app->variable_item_list_logged_out_wifi_settings); - } - if (app->variable_item_list_logged_out_login) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedOutLogin); - variable_item_list_free(app->variable_item_list_logged_out_login); - } - if (app->variable_item_list_logged_out_register) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedOutRegister); - variable_item_list_free(app->variable_item_list_logged_out_register); - } - if (app->variable_item_list_logged_in_profile) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInProfile); - variable_item_list_free(app->variable_item_list_logged_in_profile); - } - if (app->variable_item_list_logged_in_settings) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInSettings); - variable_item_list_free(app->variable_item_list_logged_in_settings); - } - if (app->variable_item_list_logged_in_settings_wifi) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInSettingsWifi); - variable_item_list_free(app->variable_item_list_logged_in_settings_wifi); - } - - // Free Text Input(s) - if (app->text_input_logged_out_wifi_settings_ssid) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedOutWifiSettingsSSIDInput); - text_input_free(app->text_input_logged_out_wifi_settings_ssid); - } - if (app->text_input_logged_out_wifi_settings_password) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedOutWifiSettingsPasswordInput); - text_input_free(app->text_input_logged_out_wifi_settings_password); - } - if (app->text_input_logged_out_login_username) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedOutLoginUsernameInput); - text_input_free(app->text_input_logged_out_login_username); - } - if (app->text_input_logged_out_login_password) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedOutLoginPasswordInput); - text_input_free(app->text_input_logged_out_login_password); - } - if (app->text_input_logged_out_register_username) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedOutRegisterUsernameInput); - text_input_free(app->text_input_logged_out_register_username); - } - if (app->text_input_logged_out_register_password) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedOutRegisterPasswordInput); - text_input_free(app->text_input_logged_out_register_password); - } - if (app->text_input_logged_out_register_password_2) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedOutRegisterPassword2Input); - text_input_free(app->text_input_logged_out_register_password_2); - } - if (app->text_input_logged_in_change_password) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInChangePasswordInput); - text_input_free(app->text_input_logged_in_change_password); - } - if (app->text_input_logged_in_change_bio) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInChangeBioInput); - text_input_free(app->text_input_logged_in_change_bio); - } - if (app->text_input_logged_in_compose_pre_save_input) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInComposeAddPreSaveInput); - text_input_free(app->text_input_logged_in_compose_pre_save_input); - } - if (app->text_input_logged_in_wifi_settings_ssid) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInWifiSettingsSSIDInput); - text_input_free(app->text_input_logged_in_wifi_settings_ssid); - } - if (app->text_input_logged_in_wifi_settings_password) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInWifiSettingsPasswordInput); - text_input_free(app->text_input_logged_in_wifi_settings_password); - } - if (app->text_input_logged_in_messages_new_message) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInMessagesNewMessageInput); - text_input_free(app->text_input_logged_in_messages_new_message); - } - if (app->text_input_logged_in_messages_new_message_user_choices) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInMessagesNewMessageUserChoicesInput); - text_input_free(app->text_input_logged_in_messages_new_message_user_choices); - } - if (app->text_input_logged_in_explore) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInExploreInput); - text_input_free(app->text_input_logged_in_explore); - } - if (app->text_input_logged_in_message_users) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipSocialViewLoggedInMessageUsersInput); - text_input_free(app->text_input_logged_in_message_users); - } // Free Widget(s) if (app->widget_result) @@ -264,55 +149,7 @@ void flip_social_app_free(FlipSocialApp *app) if (app->explore_user_bio) free(app->explore_user_bio); - // DeInit UART - flipper_http_deinit(); - // Free the app structure if (app_instance) free(app_instance); -} - -void auth_headers_alloc(void) -{ - if (!app_instance) - { - snprintf(auth_headers, sizeof(auth_headers), "{\"Content-Type\":\"application/json\"}"); - return; - } - - if (app_instance->login_username_logged_out && app_instance->login_password_logged_out && strlen(app_instance->login_username_logged_out) > 0 && strlen(app_instance->login_password_logged_out) > 0) - { - 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 && strlen(app_instance->login_username_logged_in) > 0 && strlen(app_instance->change_password_logged_in) > 0) - { - 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\"}"); - } -} - -FlipSocialFeedMini *flip_feed_info_alloc(void) -{ - FlipSocialFeedMini *feed_info = (FlipSocialFeedMini *)malloc(sizeof(FlipSocialFeedMini)); - if (!feed_info) - { - FURI_LOG_E(TAG, "Failed to allocate memory for feed_info"); - return NULL; - } - feed_info->count = 0; - feed_info->index = 0; - return feed_info; -} - -void flip_feed_info_free(void) -{ - if (!flip_feed_info) - { - return; - } - free(flip_feed_info); - flip_feed_info = NULL; -} +} \ No newline at end of file diff --git a/flip_social/flip_social.h b/flip_social/flip_social.h index 10f29f62b..4e2ec168d 100644 --- a/flip_social/flip_social.h +++ b/flip_social/flip_social.h @@ -7,16 +7,18 @@ #include #include #include +#include + #define TAG "FlipSocial" +#define VERSION_TAG TAG " v1.0.3" #define MAX_PRE_SAVED_MESSAGES 20 // Maximum number of pre-saved messages #define MAX_MESSAGE_LENGTH 100 // Maximum length of a message in the feed #define MAX_EXPLORE_USERS 50 // Maximum number of users to explore #define MAX_USER_LENGTH 32 // Maximum length of a username #define MAX_FRIENDS 50 // Maximum number of friends -#define MAX_TOKENS 576 // Adjust based on expected JSON tokens -#define MAX_FEED_ITEMS 50 // Maximum number of feed items -#define MAX_LINE_LENGTH 30 +#define MAX_FEED_ITEMS 20 // Maximum number of feed items +#define MAX_LINE_LENGTH 27 #define MAX_MESSAGE_USERS 40 // Maximum number of users to display in the submenu #define MAX_MESSAGES 20 // Maximum number of meesages between each user @@ -26,10 +28,15 @@ // Define the submenu items for our Hello World application typedef enum { - FlipSocialSubmenuLoggedOutIndexLogin, // click to go to the login screen - FlipSocialSubmenuLoggedOutIndexRegister, // click to go to the register screen + FlipSocialSubmenuLoggedOutIndexLogin, // click to go to the login screen + FlipSocialSubmenuLoggedOutIndexRegister, // click to go to the register screen + // FlipSocialSubmenuLoggedOutIndexAbout, // click to go to the about screen FlipSocialSubmenuLoggedOutIndexWifiSettings, // click to go to the wifi settings screen + FlipSocialSubmenuLoggedInIndexUserSettings, // click to go to the user settings screen + // + FlipSocialSubmenuLoggedInIndexAbout, // click to go to the about screen + FlipSocialSubmenuLoggedInIndexWifiSettings, // click to go to the wifi settings screen // FlipSocialSubmenuLoggedInIndexProfile, // click to go to the profile screen FlipSocialSubmenuExploreIndex, // click to go to the explore @@ -56,7 +63,7 @@ typedef enum // Define the ScriptPlaylist structure typedef struct { - char *messages[MAX_PRE_SAVED_MESSAGES]; + char messages[MAX_PRE_SAVED_MESSAGES][MAX_MESSAGE_LENGTH]; size_t count; size_t index; } PreSavedPlaylist; @@ -64,8 +71,9 @@ typedef struct // Define a FlipSocialFeed individual item typedef struct { - char *username; - char *message; + char username[MAX_USER_LENGTH]; + char message[MAX_MESSAGE_LENGTH]; + char date_created[MAX_LINE_LENGTH]; bool is_flipped; int id; int flips; @@ -76,26 +84,27 @@ typedef struct int ids[MAX_FEED_ITEMS]; size_t count; size_t index; + int series_index; } FlipSocialFeedMini; typedef struct { - char *usernames[MAX_EXPLORE_USERS]; + char usernames[MAX_EXPLORE_USERS][MAX_USER_LENGTH]; int count; int index; } FlipSocialModel; typedef struct { - char *usernames[MAX_MESSAGE_USERS]; + char usernames[MAX_MESSAGE_USERS][MAX_USER_LENGTH]; int count; int index; } FlipSocialModel2; typedef struct { - char *usernames[MAX_MESSAGES]; - char *messages[MAX_MESSAGES]; + char usernames[MAX_MESSAGES][MAX_USER_LENGTH]; + char messages[MAX_MESSAGES][MAX_MESSAGE_LENGTH]; int count; int index; } FlipSocialMessage; @@ -137,6 +146,7 @@ typedef enum // FlipSocialViewLoggedInSettingsAbout, // The about screen FlipSocialViewLoggedInSettingsWifi, // The wifi settings screen + FlipSocialViewLoggedInSettingsUser, // The user settings screen FlipSocialViewLoggedInWifiSettingsSSIDInput, // Text input screen for SSID input on wifi screen FlipSocialViewLoggedInWifiSettingsPasswordInput, // Text input screen for Password input on wifi screen // @@ -155,7 +165,11 @@ typedef enum FlipSocialViewFriendsDialog, // The dialog for the friends screen FlipSocialViewMessagesDialog, // The dialog for the messages screen FlipSocialViewComposeDialog, // The dialog for the compose screen - FlipSocialViewFeedDialog, // The dialog for the feed screen + // + FlipSocialViewTextInput, // The text input screen + FlipSocialViewVariableItemList, + // + FlipSocialViewSubmenu, } FlipSocialView; // Define the application structure @@ -164,44 +178,18 @@ typedef struct View *view_loader; Widget *widget_result; // - ViewDispatcher *view_dispatcher; // Switches between our views - Submenu *submenu_logged_out; // The application submenu (logged out) - Submenu *submenu_logged_in; // The application submenu (logged in) - Submenu *submenu_compose; // The application submenu (compose) - Submenu *submenu_explore; // The application submenu (explore) - Submenu *submenu_friends; // The application submenu (friends) - Submenu *submenu_messages; // The application submenu (messages) - Submenu *submenu_messages_user_choices; // The application submenu (messages user choices) - Widget *widget_logged_out_about; // The about screen (logged out) - Widget *widget_logged_in_about; // The about screen (logged in) - - VariableItemList *variable_item_list_logged_out_wifi_settings; // The wifi settings menu - VariableItemList *variable_item_list_logged_out_login; // The login menu - VariableItemList *variable_item_list_logged_out_register; // The register menu - // - VariableItemList *variable_item_list_logged_in_profile; // The profile menu - VariableItemList *variable_item_list_logged_in_settings; // The settings menu - VariableItemList *variable_item_list_logged_in_settings_wifi; // The wifi settings menu - - TextInput *text_input_logged_out_wifi_settings_ssid; // Text input for ssid input on wifi settings screen - TextInput *text_input_logged_out_wifi_settings_password; // Text input for password input on wifi settings screen - TextInput *text_input_logged_out_login_username; // Text input for username input on login screen - TextInput *text_input_logged_out_login_password; // Text input for password input on login screen - TextInput *text_input_logged_out_register_username; // Text input for username input on register screen - TextInput *text_input_logged_out_register_password; // Text input for password input on register screen - TextInput *text_input_logged_out_register_password_2; // Text input for password 2 input on register screen - // - TextInput *text_input_logged_in_change_password; // Text input for password input on change password screen - TextInput *text_input_logged_in_change_bio; // Text input for bio input on profile screen - TextInput *text_input_logged_in_compose_pre_save_input; // Text input for pre save input on compose screen - TextInput *text_input_logged_in_wifi_settings_ssid; // Text input for ssid input on wifi settings screen - TextInput *text_input_logged_in_wifi_settings_password; // Text input for password input on wifi settings screen - // - TextInput *text_input_logged_in_messages_new_message; // Text input for new message input on messages screen - TextInput *text_input_logged_in_messages_new_message_user_choices; // - // - TextInput *text_input_logged_in_explore; // Text input for explore input on explore screen - TextInput *text_input_logged_in_message_users; + ViewDispatcher *view_dispatcher; // Switches between our views + Submenu *submenu_logged_out; // The application submenu (logged out) + Submenu *submenu_logged_in; // The application submenu (logged in) + // + Submenu *submenu; + // + Widget *widget_logged_out_about; // The about screen (logged out) + Widget *widget_logged_in_about; // The about screen (logged in) + + VariableItemList *variable_item_list; // The main menu + + TextInput *text_input; // The text input VariableItem *variable_item_logged_out_wifi_settings_ssid; // Reference to the ssid configuration item VariableItem *variable_item_logged_out_wifi_settings_password; // Reference to the password configuration item @@ -217,10 +205,13 @@ typedef struct VariableItem *variable_item_logged_in_profile_change_password; // Reference to the change password configuration item VariableItem *variable_item_logged_in_profile_change_bio; // Reference to the change bio configuration item // - VariableItem *variable_item_logged_in_settings_about; // Reference to the about configuration item - VariableItem *variable_item_logged_in_settings_wifi; // Reference to the wifi settings configuration item - VariableItem *variable_item_logged_in_wifi_settings_ssid; // Reference to the ssid configuration item - VariableItem *variable_item_logged_in_wifi_settings_password; // Reference to the password configuration item + VariableItem *variable_item_logged_in_settings_about; // Reference to the about configuration item + VariableItem *variable_item_logged_in_settings_wifi; // Reference to the wifi settings configuration item + VariableItem *variable_item_logged_in_settings_user; // Reference to the user settings configuration item + VariableItem *variable_item_logged_in_user_settings_feed_type; // Reference to the feed type configuration item + VariableItem *variable_item_logged_in_user_settings_notifications; // Reference to the notifications configuration item + VariableItem *variable_item_logged_in_wifi_settings_ssid; // Reference to the ssid configuration item + VariableItem *variable_item_logged_in_wifi_settings_password; // Reference to the password configuration item // VariableItem *variable_item_logged_in_profile_friends; // Reference to the friends configuration item // @@ -299,12 +290,12 @@ typedef struct char *message_users_logged_in_temp_buffer; // Temporary buffer for message users text input uint32_t message_users_logged_in_temp_buffer_size; // Size of the message users temporary buffer - Loading *loading; // The loading screen DialogEx *dialog_explore; DialogEx *dialog_friends; DialogEx *dialog_messages; DialogEx *dialog_compose; - DialogEx *dialog_feed; + + View *view_feed; char *explore_user_bio; // Store the bio of the selected user } FlipSocialApp; @@ -328,8 +319,10 @@ extern bool flip_social_dialog_stop; extern bool flip_social_send_message; extern char *selected_message; extern char auth_headers[256]; - -void auth_headers_alloc(void); -FlipSocialFeedMini *flip_feed_info_alloc(void); -void flip_feed_info_free(void); +// +extern char *flip_social_feed_type[]; +extern uint8_t flip_social_feed_type_index; +// +extern char *flip_social_notification_type[]; +extern uint8_t flip_social_notification_type_index; #endif \ No newline at end of file diff --git a/flip_social/flip_storage/flip_social_storage.c b/flip_social/flip_storage/flip_social_storage.c index dea5b2a71..8edfdc49e 100644 --- a/flip_social/flip_storage/flip_social_storage.c +++ b/flip_social/flip_storage/flip_social_storage.c @@ -8,6 +8,7 @@ void save_playlist(const PreSavedPlaylist *playlist) FURI_LOG_E(TAG, "Playlist is NULL"); return; } + // Create the directory for saving settings char directory_path[128]; snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social"); @@ -29,7 +30,7 @@ void save_playlist(const PreSavedPlaylist *playlist) // Write each playlist message on a separate line for (size_t i = 0; i < playlist->count; ++i) { - // Add a newline character after each message + // Write the message if (storage_file_write(file, playlist->messages[i], strlen(playlist->messages[i])) != strlen(playlist->messages[i])) { FURI_LOG_E(TAG, "Failed to write playlist message %zu", i); @@ -42,34 +43,23 @@ void save_playlist(const PreSavedPlaylist *playlist) } } + // Close the file and storage storage_file_close(file); storage_file_free(file); furi_record_close(RECORD_STORAGE); } -// Function to load the playlist bool load_playlist(PreSavedPlaylist *playlist) { - // Ensure playlist is not NULL if (!playlist) { FURI_LOG_E(TAG, "Playlist is NULL"); return false; } - // Ensure playlist->messages is not NULL and allocate memory for each message - for (size_t i = 0; i < MAX_PRE_SAVED_MESSAGES; ++i) - { - if (!playlist->messages[i]) // Check if memory is already allocated - { - playlist->messages[i] = (char *)malloc(MAX_MESSAGE_LENGTH * sizeof(char)); - if (!playlist->messages[i]) - { - FURI_LOG_E(TAG, "Failed to allocate memory for message %zu", i); - return false; // Return false on memory allocation failure - } - } - } + // Clear existing data in the playlist + memset(playlist->messages, 0, sizeof(playlist->messages)); + playlist->count = 0; // Open the storage Storage *storage = furi_record_open(RECORD_STORAGE); @@ -80,29 +70,24 @@ bool load_playlist(PreSavedPlaylist *playlist) FURI_LOG_E(TAG, "Failed to open pre-saved messages file for reading: %s", PRE_SAVED_MESSAGES_PATH); storage_file_free(file); furi_record_close(RECORD_STORAGE); - return false; // Return false if the file does not exist + return false; } - // Initialize the playlist count - playlist->count = 0; - - // Read the file byte by byte to simulate reading lines + // Read the file line by line + char line[MAX_MESSAGE_LENGTH] = {0}; + size_t line_pos = 0; char ch; - size_t message_pos = 0; - bool message_started = false; - while (storage_file_read(file, &ch, 1) == 1) // Read one character at a time + while (storage_file_read(file, &ch, 1) == 1) { - message_started = true; - - if (ch == '\n' || message_pos >= (MAX_MESSAGE_LENGTH - 1)) // End of line or message is too long + if (ch == '\n' || line_pos >= (MAX_MESSAGE_LENGTH - 1)) // End of line or max length reached { - playlist->messages[playlist->count][message_pos] = '\0'; // Null-terminate the message - playlist->count++; // Move to the next message - message_pos = 0; // Reset for the next message - message_started = false; + line[line_pos] = '\0'; // Null-terminate the line + strncpy(playlist->messages[playlist->count], line, MAX_MESSAGE_LENGTH); + playlist->count++; + line_pos = 0; - // Ensure the playlist count does not exceed the maximum + // Ensure playlist count does not exceed maximum allowed if (playlist->count >= MAX_PRE_SAVED_MESSAGES) { FURI_LOG_W(TAG, "Reached maximum playlist messages"); @@ -111,21 +96,16 @@ bool load_playlist(PreSavedPlaylist *playlist) } else { - playlist->messages[playlist->count][message_pos++] = ch; // Add character to current message + line[line_pos++] = ch; } } - // Handle the case where the last message does not end with a newline - if (message_started && message_pos > 0) + // Handle the last line if it does not end with a newline + if (line_pos > 0) { - playlist->messages[playlist->count][message_pos] = '\0'; // Null-terminate the last message - playlist->count++; // Increment the count for the last message - - // Ensure the playlist count does not exceed the maximum - if (playlist->count >= MAX_PRE_SAVED_MESSAGES) - { - FURI_LOG_W(TAG, "Reached maximum playlist messages"); - } + line[line_pos] = '\0'; // Null-terminate the last line + strncpy(playlist->messages[playlist->count], line, MAX_MESSAGE_LENGTH); + playlist->count++; } // Close the file and storage @@ -135,6 +115,7 @@ bool load_playlist(PreSavedPlaylist *playlist) return true; } + void save_settings( const char *ssid, const char *password, @@ -396,13 +377,25 @@ bool load_settings( return true; } -bool flip_social_save_post(char *post_id, char *json_feed_data) +bool flip_social_save_post(const char *post_id, const char *json_feed_data) { + if (!post_id || !json_feed_data) + { + FURI_LOG_E(TAG, "Post ID or JSON feed data is NULL"); + return false; + } Storage *storage = furi_record_open(RECORD_STORAGE); File *file = storage_file_alloc(storage); + // Create the directory for saving the feed + char directory_path[128]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social"); + storage_common_mkdir(storage, directory_path); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/feed"); + storage_common_mkdir(storage, directory_path); + char file_path[128]; - snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/feed_post_%s.json", post_id); + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/feed/feed_post_%s.json", post_id); if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { @@ -425,4 +418,97 @@ bool flip_social_save_post(char *post_id, char *json_feed_data) storage_file_free(file); furi_record_close(RECORD_STORAGE); return true; +} +// +bool save_char( + const char *path_name, const char *value) +{ + if (!value) + { + return false; + } + // Create the directory for saving settings + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/data"); + storage_common_mkdir(storage, directory_path); + + // Open the settings file + File *file = storage_file_alloc(storage); + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/data/%s.txt", path_name); + + // 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; + } + + // Write the data to the file + size_t data_size = strlen(value) + 1; // Include null terminator + if (storage_file_write(file, value, 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; +} + +bool load_char( + const char *path_name, + char *value, + size_t value_size) +{ + if (!value) + { + return false; + } + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); + + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/data/%s.txt", path_name); + + // 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 + } + + // Read data into the buffer + size_t read_count = storage_file_read(file, value, value_size); + if (storage_file_get_error(file) != FSE_OK) + { + FURI_LOG_E(HTTP_TAG, "Error reading from file."); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + + // Ensure null-termination + value[read_count - 1] = '\0'; + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + return strlen(value) > 0; } \ No newline at end of file diff --git a/flip_social/flip_storage/flip_social_storage.h b/flip_social/flip_storage/flip_social_storage.h index 94a1fbd76..108c3d997 100644 --- a/flip_social/flip_storage/flip_social_storage.h +++ b/flip_social/flip_storage/flip_social_storage.h @@ -37,5 +37,13 @@ bool load_settings( char *is_logged_in, size_t is_logged_in_size); -bool flip_social_save_post(char *post_id, char *json_feed_data); +bool flip_social_save_post(const char *post_id, const char *json_feed_data); +// + +bool save_char( + const char *path_name, const char *value); +bool load_char( + const char *path_name, + char *value, + size_t value_size); #endif \ No newline at end of file diff --git a/flip_social/flipper_http/flipper_http.c b/flip_social/flipper_http/flipper_http.c index 0d5a518e3..f3d7743cc 100644 --- a/flip_social/flipper_http/flipper_http.c +++ b/flip_social/flipper_http/flipper_http.c @@ -1,8 +1,5 @@ #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; +FlipperHTTP fhttp = {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( @@ -187,19 +184,19 @@ int32_t flipper_http_worker(void *context) if (fhttp.save_bytes) { // Add byte to the buffer - file_buffer[file_buffer_len++] = c; + fhttp.file_buffer[fhttp.file_buffer_len++] = c; // Write to file if buffer is full - if (file_buffer_len >= FILE_BUFFER_SIZE) + if (fhttp.file_buffer_len >= FILE_BUFFER_SIZE) { if (!flipper_http_append_to_file( - file_buffer, - file_buffer_len, + fhttp.file_buffer, + fhttp.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.file_buffer_len = 0; fhttp.just_started_bytes = false; } } @@ -210,17 +207,17 @@ int32_t flipper_http_worker(void *context) // 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 + fhttp.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); + fhttp.handle_rx_line_cb(fhttp.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 + fhttp.rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer } } } @@ -382,6 +379,7 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void *context) } // FURI_LOG_I(HTTP_TAG, "UART initialized successfully."); + fhttp.state = IDLE; // set idle for easy use return true; } @@ -1114,27 +1112,27 @@ void flipper_http_rx_callback(const char *line, void *context) 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++) + for (size_t i = 0; i <= fhttp.file_buffer_len - marker_len; i++) { // Check if the marker is found - if (memcmp(&file_buffer[i], marker, marker_len) == 0) + if (memcmp(&fhttp.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; + size_t remaining_len = fhttp.file_buffer_len - (i + marker_len); + memmove(&fhttp.file_buffer[i], &fhttp.file_buffer[i + marker_len], remaining_len); + fhttp.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 (fhttp.file_buffer_len > 0) { - if (!flipper_http_append_to_file(file_buffer, file_buffer_len, false, fhttp.file_path)) + if (!flipper_http_append_to_file(fhttp.file_buffer, fhttp.file_buffer_len, false, fhttp.file_path)) { FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); } - file_buffer_len = 0; + fhttp.file_buffer_len = 0; } } @@ -1184,27 +1182,27 @@ void flipper_http_rx_callback(const char *line, void *context) 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++) + for (size_t i = 0; i <= fhttp.file_buffer_len - marker_len; i++) { // Check if the marker is found - if (memcmp(&file_buffer[i], marker, marker_len) == 0) + if (memcmp(&fhttp.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; + size_t remaining_len = fhttp.file_buffer_len - (i + marker_len); + memmove(&fhttp.file_buffer[i], &fhttp.file_buffer[i + marker_len], remaining_len); + fhttp.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 (fhttp.file_buffer_len > 0) { - if (!flipper_http_append_to_file(file_buffer, file_buffer_len, false, fhttp.file_path)) + if (!flipper_http_append_to_file(fhttp.file_buffer, fhttp.file_buffer_len, false, fhttp.file_path)) { FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); } - file_buffer_len = 0; + fhttp.file_buffer_len = 0; } } @@ -1332,7 +1330,7 @@ void flipper_http_rx_callback(const char *line, void *context) // 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; + fhttp.file_buffer_len = 0; return; } else if (strstr(line, "[POST/SUCCESS]") != NULL) @@ -1344,7 +1342,7 @@ void flipper_http_rx_callback(const char *line, void *context) // 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; + fhttp.file_buffer_len = 0; return; } else if (strstr(line, "[PUT/SUCCESS]") != NULL) @@ -1593,5 +1591,5 @@ void flipper_http_loading_task_2(bool (*http_request)(void), // 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); + // loading_free(loading); } \ No newline at end of file diff --git a/flip_social/flipper_http/flipper_http.h b/flip_social/flipper_http/flipper_http.h index 1b4bbb30f..e6bb821d3 100644 --- a/flip_social/flipper_http/flipper_http.h +++ b/flip_social/flipper_http/flipper_http.h @@ -20,7 +20,7 @@ #define UART_CH (momentum_settings.uart_esp_channel) // UART channel #define TIMEOUT_DURATION_TICKS (8 * 1000) // 8 seconds (increased for Pico W) #define BAUDRATE (115200) // UART baudrate -#define RX_BUF_SIZE 128 // UART RX buffer size +#define RX_BUF_SIZE 2048 // UART RX buffer size #define RX_LINE_BUFFER_SIZE 7000 // UART RX line buffer size (increase for large responses) #define MAX_FILE_SHOW 7000 // Maximum data from file to show #define FILE_BUFFER_SIZE 512 // File buffer size @@ -83,13 +83,13 @@ typedef struct 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 + + char rx_line_buffer[RX_LINE_BUFFER_SIZE]; + uint8_t file_buffer[FILE_BUFFER_SIZE]; + size_t file_buffer_len; } 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/font/font.c b/flip_social/font/font.c new file mode 100644 index 000000000..8f19c501e --- /dev/null +++ b/flip_social/font/font.c @@ -0,0 +1,316 @@ +#include + +static const uint8_t u8g2_font_4x6_tf[]; +static const uint8_t u8g2_font_6x10_tf[]; +static const uint8_t u8g2_font_5x8_tf[]; +static const uint8_t u8g2_font_9x15_tf[]; + +bool canvas_set_font_custom(Canvas *canvas, FontSize font_size) +{ + if (!canvas) + { + return false; + } + switch (font_size) + { + case FONT_SIZE_SMALL: + canvas_set_custom_u8g2_font(canvas, u8g2_font_4x6_tf); + break; + case FONT_SIZE_MEDIUM: + canvas_set_custom_u8g2_font(canvas, u8g2_font_5x8_tf); + break; + case FONT_SIZE_LARGE: + canvas_set_custom_u8g2_font(canvas, u8g2_font_6x10_tf); + break; + case FONT_SIZE_XLARGE: + canvas_set_custom_u8g2_font(canvas, u8g2_font_9x15_tf); + break; + default: + return false; + } + return true; +} + +void canvas_draw_str_multi(Canvas *canvas, uint8_t x, uint8_t y, const char *str) +{ + if (!canvas || !str) + { + return; + } + elements_multiline_text(canvas, x, y, str); +} + +/* + Fontname: -Misc-Fixed-Medium-R-Normal--6-60-75-75-C-40-ISO10646-1 + Copyright: Public domain font. Share and enjoy. + Glyphs: 191/919 + BBX Build Mode: 0 +*/ +static const uint8_t u8g2_font_4x6_tf[] = + "\277\0\2\2\3\3\2\4\4\4\6\0\377\5\377\5\377\0\351\1\323\5\216 \5\200\315\0!\6\351\310" + "\254\0\42\6\223\313$\25#\12\254\310\244\64T\32*\1$\11\263\307\245\241GJ\0%\10\253\310d" + "\324F\1&\11\254\310\305\24\253\230\2'\5\321\313\10(\7\362\307\251f\0)\10\262\307\304T)\0" + "*\7\253\310\244j\65+\10\253\310\305\264b\2,\6\222\307)\0-\5\213\312\14.\5\311\310\4/" + "\7\253\310Ve\4\60\10\253\310UCU\0\61\7\253\310%Y\15\62\7\253\310\65S\32\63\10\253\310" + "\314\224\27\0\64\10\253\310$\65b\1\65\10\253\310\214\250\27\0\66\7\253\310M\325\2\67\10\253\310\314" + "TF\0\70\7\253\310\255\326\2\71\7\253\310\265\344\2:\6\341\310\304\0;\7\252\307e\250\0<\7" + "\253\310\246\272\0=\6\233\311\354\1>\7\253\310\344\252\4\77\10\253\310\350\224a\2@\6\253\310-[" + "A\10\253\310UC\251\0B\10\253\310\250\264\322\2C\10\253\310U\62U\0D\10\253\310\250d-\0" + "E\10\253\310\214\250\342\0F\10\253\310\214\250b\4G\10\253\310\315\244\222\0H\10\253\310$\65\224\12" + "I\7\253\310\254X\15J\7\253\310\226\252\2K\10\253\310$\265\222\12L\7\253\310\304\346\0M\10\253" + "\310\244\61\224\12N\10\253\310\252\241$\0O\7\253\310UV\5P\10\253\310\250\264b\4Q\7\263\307" + "UV\35R\10\253\310\250\264\222\12S\7\253\310\355\274\0T\7\253\310\254\330\2U\7\253\310$\327\10" + "V\10\253\310$k\244\4W\10\253\310$\65\206\12X\10\253\310$\325R\1Y\10\253\310$UV\0" + "Z\7\253\310\314T\16[\6\352\310\254J\134\7\253\310\304\134\6]\6\252\310\250j^\5\223\313\65_" + "\5\213\307\14`\6\322\313\304\0a\7\243\310-\225\4b\10\253\310D\225\324\2c\6\243\310\315,d" + "\10\253\310\246\245\222\0e\6\243\310USf\10\253\310\246\264b\2g\10\253\307\255$\27\0h\10\253" + "\310D\225\254\0i\10\253\310e$\323\0j\10\263\307fX.\0k\10\253\310\304\264\222\12l\7\253" + "\310\310\326\0m\10\243\310\244\241T\0n\7\243\310\250d\5o\7\243\310U\252\2p\10\253\307\250\264" + "b\4q\10\253\307-\225d\0r\10\243\310\244\25#\0s\7\243\310\215\274\0t\10\253\310\245\25s" + "\0u\7\243\310$+\11v\7\243\310$\253\2w\10\243\310$\65T\0x\7\243\310\244\62\25y\10" + "\253\307$\225\344\2z\7\243\310\314\224\6{\10\263\307\246$\353\0|\6\351\310\14\1}\11\263\307\344" + "\250b\212\0~\7\224\313%\225\0\240\5\200\315\0\241\6\351\310\244\1\242\10\253\310\245\21W\2\243\7" + "\253\310\246\250\32\244\10\244\310\304$U\14\245\10\253\310\244j\305\4\246\6\351\310(\1\247\10\263\307\215" + "T\311\5\250\6\213\314\244\0\251\11\264\307\251\270\226L\12\252\7\253\310\255\244\7\253\10\234\311%\25S" + "\0\254\6\223\311\314\0\255\5\213\312\14\256\10\244\311\251\261\222\2\257\5\213\314\14\260\6\233\312u\1\261" + "\10\253\310\245\225\321\0\262\6\242\311(\3\263\7\252\310(\251\0\264\6\322\313)\0\265\10\253\307$k" + "E\0\266\7\254\310\15\265z\267\5\311\312\4\270\6\322\310)\0\271\6\242\311\255\2\272\7\253\310u\243" + "\1\273\10\234\311\244\230T\2\274\10\264\307\344\32U;\275\10\264\307\344J\307,\276\11\264\307\350\230Q" + "\262\3\277\10\253\310e\230\262\0\300\10\253\310\344\224\206\12\301\10\253\310\246j\250\0\302\10\253\310\310\224" + "\206\12\303\10\253\310\215\224\206\12\304\10\253\310\244\326P\1\305\10\253\310\305\224\206\12\306\11\254\310\215\224" + "\206\252\4\307\10\263\307U\62\65\1\310\10\253\310\304\241\342\0\311\7\253\310\216\25\7\312\10\253\310\215\221" + "\342\0\313\10\253\310\244\261\342\0\314\10\253\310\304\25\323\0\315\10\253\310\216\24\323\0\316\10\253\310\245\25" + "\323\0\317\7\253\310\244\262\32\320\11\254\310\314\264\252\221\0\321\10\254\310%\225\256\12\322\10\253\310\344\224" + "T\5\323\10\253\310\246JU\0\324\10\253\310\305\224T\5\325\10\254\310\215\325\31\1\326\10\253\310\244\226" + "\252\0\327\6\233\311\244\16\330\7\253\310\35j\1\331\10\253\310\344\224\324\10\332\10\253\310\246J\215\0\333" + "\10\253\310e\224\324\10\334\10\253\310\244\234\324\10\335\10\253\310\346T&\0\336\10\253\310D\225V\4\337" + "\10\263\307U+\15\11\340\10\253\310\344\270\222\0\341\10\253\310\246\270\222\0\342\10\253\310i\264\222\0\343" + "\11\254\310%\25U\251\0\344\10\253\310\244\214V\22\345\10\253\310e\270\222\0\346\10\244\310\215\264\342\0" + "\347\10\253\307UZ%\0\350\10\253\310\344\224\246\0\351\7\253\310\246j\12\352\10\253\310\310\224\246\0\353" + "\7\253\310\244\326\24\354\7\253\310\344X\15\355\6\253\310\316j\356\7\253\310u\246\1\357\10\253\310\244," + "\323\0\360\10\253\310\244rU\0\361\11\254\310%\225dj\1\362\10\253\310\344\230Z\0\363\10\253\310\246" + "\230Z\0\364\10\253\310e\230Z\0\365\7\253\310l\324\5\366\10\253\310\244\214\272\0\367\10\253\310e\264" + "Q\2\370\7\243\310-\265\0\371\10\253\310\344\224T\22\372\10\253\310\246J%\1\373\10\253\310e\224T" + "\22\374\10\253\310\244\234T\22\375\10\263\307\246j\304\5\376\11\263\307\304\250\322\212\0\377\11\263\307\244\234" + "F\134\0\0\0\0\4\377\377\0"; +/* + Fontname: -Misc-Fixed-Medium-R-Normal--8-80-75-75-C-50-ISO10646-1 + Copyright: Public domain font. Share and enjoy. + Glyphs: 191/1426 + BBX Build Mode: 0 +*/ +static const uint8_t u8g2_font_5x8_tf[] = + "\277\0\2\2\3\4\3\4\4\5\10\0\377\6\377\6\0\1\32\2\61\6\226 \5\0~\3!\7\61c" + "\63R\0\42\7\233n\223\254\0#\15=bW\246\64T\65T\231\22\0$\12=b\233W\275S\332" + "\21%\10\253f\23Sg\0&\12\10\263b\223\313T\2\77\11\263b" + "\327L\31&\0@\14E^+\243\134I%YC\5A\11\64b\247\242\34S\6B\12\64b\263\342" + "HQ\216\4C\11\64b\247\242.\223\2D\11\64b\263\242s$\0E\11\64b\63\364\312y\4F" + "\11\64b\63\364\312\65\0G\12\64b\247\242N\63)\0H\11\64b\23\345\230f\0I\7\263b\263" + "bkJ\11\64b\67sUF\0K\11\64b\23U\222\251\63L\10\64b\223\273G\0M\11\64b" + "\23\307\21\315\0N\11\64b\23\327Xg\0O\11\64b\247\242\63)\0P\12\64b\263\242\34)g" + "\0Q\11<^\247\242\134n\24R\12\64b\263\242\34)\312\0S\12\64b\247b\312\250L\12T\10" + "\263b\263b\27\0U\10\64b\23=\223\2V\11\64b\23\235I*\0W\11\64b\23\315q\304\0" + "X\12\64b\23e\222*\312\0Y\13\65b\223u\252\63\312(\2Z\11\64b\63rl\217\0[\7" + "\263b\63bs\134\12\64b\223\63\312(\243\34]\7\263b\63\233#^\6\223r\327\0_\6\14^" + "\63\2`\6\222r\23\3a\10$b\67\242L\3b\12\64b\223\363\212r$\0c\7\243b\67\263" + "\0d\11\64b_\215(\323\0e\10$b\247\322\310\12f\11\64b[\225\63G\0g\11,^\247" + "b\332I\1h\11\64b\223\363\212f\0i\10\263b\227\221\254\6j\11\273^\233a\251*\0k\11" + "\64b\223\313\221\242\14l\7\263b#\273\6m\11%b\243Z*\251\2n\7$b\263\242\31o\10" + "$b\247\242L\12p\11,^\263\342H\71\3q\10,^\67b\332\5r\10$b\223\222\235\1s" + "\7\243b\67\362\2t\12\64b\227\343\314)&\0u\7$b\23\315\64v\7\243b\223\254\12w\11" + "%b\223UR]\0x\10$b\23\223T\61y\12,^\23e\32\61)\0z\10$b\63b\71" + "\2{\13O\225\0-\6\15Ng\10.\7\233>/\255\4/\13=B\37e\224\273QF" + "\0\60\12=B\67\247\332Nu\4\61\14=B\67\313\224QF\31\305!\62\14=Bo\345\214\242\314" + "\31\15\1\63\14=Bgh\224\263\206:-\0\64\14=B\77\313T\246\241\63J\0\65\13=B\347" + "F\311\314H\247\5\66\13=BW\346\214\222\251\323\2\67\14=Bgh\224\63\312\65\312\0\70\13=" + "Boe\235V\326i\1\71\14=Boe\251TF\71J\0:\12\273>/\255\14\323J\0;\11" + "\273>/\255\14U\11<\12\274B\77\266QF\31\5=\10\35Jgh\70\4>\13\274B'\243\214" + "\62\212m\0\77\12=Bo\345\66\312t\4@\13=Boe\271\222\225\341\2A\13=B\67\247Z" + "\217\221u\0B\14=Bg\304*\246Y\305\241\0C\14=Boe\215\62\312(\247\5D\15=B" + "g\304*\246\230b\212C\1E\14=B\347F\31\215\224QFCF\15=B\347F\31\215\224QF" + "\31\1G\14=Boe\215\62\212;-\0H\11=B'\333cd;I\10\273Bg\305\256\1J" + "\14=Bwg\224QFe\224\0K\13=B'\313T\352\24\253\34L\16=B'\243\214\62\312(" + "\243\214\206\0M\12=B'\353\265\222\266\3N\12=B'\353\251\222\334:O\11=Boe\357\264" + "\0P\15=Bg\244\254\207\312(\243\214\0Q\12E>oe\257j\303\0R\13=Bg\244\254\207" + "*\253\34S\13=Boe\15\67\324i\1T\16=Bg\310\214\62\312(\243\214\42\0U\10=B" + "'\373N\13V\13=B'\333\251L\61\345\10W\12=B'\273\222Jw\0X\12=B'\353T" + "W\265\16Y\14=B'\353Tg\224QF\21Z\12=Bgh\224\273\321\20[\10\273Bg\304\316" + "\1\134\15=B'\243\14\63\314\60\303\214\2]\10\273Bgv\216\0^\7\35R\67\247:_\6\15" + ">g\10`\6\22['\6a\12-Bo\303\64t\32\1b\14=B'\243\214\222\251\247R\0c" + "\12-Boe\215rZ\0d\13=B\37e\224\314-\225\12e\12-Bo\345\61\62\134\0f\14" + "=BWVy\304\214\62\312\0g\14=:oh\235FF:-\0h\13=B'\243\214\222\251\355" + "\0i\10\273B/#\331\32j\13\314:\77c]K\231\24\0k\14=B'\243\214\262L\263\312\1" + "l\7\273BG\366\32m\12-BG\265TRI\7n\10-B'\231\332\16o\11-Boe;" + "-\0p\14=:'\231z*\225QF\0q\13=:\317\334R\251\214\62\12r\13-B'\231\32" + "e\224\21\0s\12-Boe\270\341P\0t\15=B/\243\67\17\25SLy\304\10\243\13=BWVyg\24\225\2\244\12-B'" + "\247\231\342\312\1\245\15E>'\353T\307!\63\312(\2\246\6\71Cg\15\247\13E>oe\64;" + "\67J\13\250\6\213^'\5\251\14=Boe\225\246J:-\0\252\12\264FoD\245j\64\2\253" + "\13.B\267\212)\346\230c\0\254\7\224Jg\344\0\255\6\214Ng\4\256\13=Bo\345\221\346\324" + "i\1\257\6\15^g\10\260\6\233R\257\13\261\13\65B\67\243\70dFq\10\262\10\254NO\305\346" + "\10\263\12\254Ng\243\244\321H\0\264\6\22[O\1\265\12\65>'\333S\251\214\0\266\16=Bo" + "\214\64RR\61\305\24S\0\267\5\11O'\270\6\22;O\1\271\7\253N/\311j\272\12\264FO" + "E\231\64\34\1\273\14.B'\346\230c\212)F\0\274\20N>/#\15\63\314hf\244S\34\31" + "\6\275\20N>/#\15\63\314heT\303\214\62\32\276\16M>G\303\234a\224Y\246\64\62\12\277" + "\12=B\67\323\31\345\326\2\300\14EB/\303\274\262\36#\353\0\301\13EB\277^Y\217\221u\0" + "\302\14EB\67\247\270\262\36#\353\0\303\14EB/*\271\262\36#\353\0\304\13EB\257\246V\326" + "cd\35\305\14EB\67\247\270\262\36#\353\0\306\13>Bw\244\262\71Fl\16\307\15M:oe" + "\215\62\312(\247]\3\310\16EB/\217\215\62\32)\243\214\206\0\311\16EB\77\215\215\62\32)\243" + "\214\206\0\312\16EB\67\216\215\62\32)\243\214\206\0\313\16EB\257\32\33e\64RF\31\15\1\314" + "\11\303B'\247\25[\3\315\11\303B\67\245\25[\3\316\11\303B\257\32)\266\6\317\11\303B'\345" + "\25[\3\320\15=Bg\304*\216T\246\70\24\0\321\13EB\67uO\225\344\326\1\322\13EB/" + "\303\274\262;-\0\323\12EB\277^\331\235\26\0\324\13EB\67\247\270\262;-\0\325\12EB\67" + "\65Wv\247\5\326\12EB\257\246Vv\247\5\327\11-B'\247\272\252\3\330\13=Bo\305+\315" + "\231\26\0\331\12EB/\303\332;-\0\332\11EB\277\314\336i\1\333\13EB\67\247\214\263;-" + "\0\334\12EB\257\306\331;-\0\335\14EB\277\314:\325\31e\24\1\336\16=B'\243\221\362P" + "\31e\224\21\0\337\13=Boe\231\312*+\5\340\14EB/\303Ln\230\206N#\341\13EB" + "\277&\67LC\247\21\342\14EB\67\247Lm\230\206N#\343\14EB\67\265\251\15\323\320i\4\344" + "\13=B\257\246\66LC\247\21\345\14EB\67\247\234\67LC\247\21\346\13.BodT\215\231\207" + "\0\347\13=:oe\215r\332\65\0\350\14EB/\303L\256\14" + "\1>\12\245\12\63\322\216YG\0\77\16\247\11s\6%U\343\264\71\207\63\0@\21\247\11s\6%" + "\65\15J\246D\223\42\347\203\2A\15\247\11\363\322$\253\244\326\341j\15B\22\247\11\63\6)LR" + "\61\31\244\60I\215\311 \1C\15\247\11s\6%\225\373\232\14\12\0D\15\247\11\63\6)LR\77" + "&\203\4E\15\247\11\63\16ry\220\342\346a\10F\14\247\11\63\16ry\220\342\316\0G\17\247\11" + "s\6%\225\333\206\324\232\14\12\0H\12\247\11\63R\327\341\352\65I\12\245\12\63\6)\354\247AJ" + "\15\250\11\363\6\65\357S\230\15\31\0K\21\247\11\63R\61\311\242\332\230\204QV\12\223\64L\12\247" + "\11\63\342\376<\14\1M\20\247\11\63Ru[*JE\212\244H\265\6N\17\247\11\63RuT\62" + ")\322\22q\265\6O\14\247\11s\6%\365\327dP\0P\15\247\11\63.\251u\30\222\270\63\0Q" + "\16\307\351r\6%\365K&U\6\65\27R\20\247\11\63.\251u\30\222(+\205I\252\6S\16\247" + "\11s\6%\265\357V\65\31\24\0T\12\247\11\63\16Y\334\337\0U\13\247\11\63R\377\232\14\12\0" + "V\21\247\11\63Rk\222EY\224U\302$L\322\14W\20\247\11\63RO\221\24I\221\24)\335\22" + "\0X\20\247\11\63R\65\311*i\234&Y%U\3Y\15\247\11\63R\65\311*i\334\33\0Z\14" + "\247\11\63\16q\332\347x\30\2[\12\304\373\62\6\255\177\33\2\134\13\247\11\63\362\70/\347\345<]" + "\12\304\372\62\206\254\177\33\4^\12Gi\363\322$\253\244\1_\7\30\370\62\16\2`\7\63\213\63\262" + "\2a\16w\11s\6=N\206!\25\225!\11b\17\247\11\63\342\226!\21U\353\250\14\11\0c\14" + "w\11s\6%\225[\223A\1d\15\247\11\263[\206D\134\35\225!\11e\15w\11s\6%U\207" + "s>(\0f\16\247\11\363\266R\26\305\341 \306\215\0g\23\247\331r\206DL\302$\214\206(\37" + "\224TM\6\5h\14\247\11\63\342\226!\21U\257\1i\12\245\12stt\354i\20j\15\326\331\62" + "u\312\332U\64&C\2k\17\247\11\63\342VMI\64\65\321\62%\15l\11\245\12\63\306\376\64\10" + "m\20w\11\63\26%\212\244H\212\244H\212\324\0n\13w\11\63\222!\21U\257\1o\14w\11s" + "\6%\365\232\14\12\0p\17\247\331\62\222!\21U\353\250\14I\134\6q\15\247\331r\206D\134\35\225" + "!\211\33r\14w\11\63\242IK\302$n\5s\15w\11s\6%\325\7]M\6\5t\14\227\11" + "\263\342p\330\342n\331\2u\20w\11\63\302$L\302$L\302$\214\206$v\16w\11\63R\65\311" + "\242\254\22&i\6w\16w\11\63RS$ER\244tK\0x\15w\11\63\322$\253\244\225\254\222" + "\6y\15\246\331\62B\337\224%\25\223!\1z\12w\11\63\16i\257\303\20{\15\305\373\262\226\260\32" + "ij\26V\7|\6\301\374\62>}\16\305\371\62\326\260\226jR\32V&\0~\12\67ys\64)" + "\322\24\0\240\5\0\310\63\241\10\261\14\63\244a\10\242\21\206\11\63\243!\211\22\251\222%Q\62D!" + "\0\243\21\247\11\363\266R\34\16b\234\212I\226$\13\0\244\16g\71\63\322d\220\262(\213\6%\15" + "\245\20\247\11\63R\65\311*\331 \206\203\30\327\0\246\10\261\374\62\6e\20\247\16\264\372r\224HT" + "\42S\42J\211\2\250\7%\232\63\62-\251\25\230\30\263\206,L\42K\224(\241\22%\222\224\204\331" + "\20\1\252\14u\71s\244\322\22EC:\10\253\15\207\31\363\242~\213\302(\214\302(\254\7F)\63" + "\256\15\255\7\25J\63\6\1\256\25\230\30\263\206,L\222I\211\22eRJJ\224\24\263!\2\257\6" + "\26\231\63\16\260\12Dks\224HJ\24\0\261\16\227\31\363\342\332\60dq\35\33\206\0\262\13dI" + "s\224(K\224l\10\263\13dIs\224\250(%\12\0\264\10\63\213\263\222\22\0\265\14\227\351\62R" + "\257\333\262\310\61\0\266\26\247\11s\206!K\264DK\222!\11\223\60\11\223\60\11\223\0\267\7\42L" + "\63\206\0\270\10\64\332\262\246D\1\271\10cIs\22\251e\272\12eIs\226LK\346A\273\16\207" + "\31\63\242\60\12\243\60\312\242~\3\274\17\247\11sR\271q\210\304$\213\62%\25\275\16\247\11sR" + "\271I\31\242\70\24\343!\276\21\247\11s\304(\315\263\250\42\211I\26eJ*\277\15\247\11\363r\270" + "\332\234\252\311\240\0\300\16\307\11s\362:\272URu\270Z\3\301\15\307\11s\333\321\255\222\252\303\325" + "\32\302\17\307\11\363\322$\253c[%U\207\253\65\303\17\267\11s\64i\307\266J\252\16Wk\0\304" + "\17\267\11s\262(\313\261\255\222\252\303\325\32\305\17\267\11\263\266\332\230d\225T\35\256\326\0\306\25\247" + "\11s\224!\312\242,\312\242lX\242,\312\242,\32\2\307\20\327\331r\6%\225\373\232\14\242\26\205" + "\32\0\310\21\307\11s\362:\66\14I\34\17Y\134\35\206\0\311\20\307\11s\333\261aH\342x\310\342" + "\352\60\4\312\22\307\11\363\322$\253#\303\220\304\361\220\305\325a\10\313\22\267\11s\262(\313\221aH" + "\342x\310\342\352\60\4\314\14\305\12\63\322\372 \205=\15\2\315\14\305\12\63\263\372 \205=\15\2\316" + "\15\305\12\263\262\244\226\16R\330\323 \317\14\265\12\63\62-\35\244\260\247A\320\25\250\10s\6-\214" + "\322$\35\302$M\322$M\302h\220\0\321\22\267\11s\64iG\322Q\311\244H\212\264D\134\3\322" + "\16\307\11s\362:\70(\251\257\311\240\0\323\15\307\11s\333\301AI}M\6\5\324\20\307\11\363\322" + "$\253C\203\222\372\232\14\12\0\325\17\267\11s\64i\207\6%\365\65\31\24\0\326\17\267\11s\262(" + "\313\241AI}M\6\5\327\15w\31\63\322$\253\244\225\254\222\6\330\26\307\371\262\223A\11\267DK" + "\244H\212\224L\311\306dPb\0\331\15\307\11s\362:\226\372\65\31\24\0\332\14\307\11s\333\261\324" + "\257\311\240\0\333\16\307\11\363\322$\253#\251_\223A\1\334\16\267\11s\262(\313\221\324\257\311\240\0" + "\335\16\307\11s\333\261TM\262J\32\267\1\336\16\247\11\63\342xXR\353\60$q\31\337\24\246\11" + "\263\246,\311\222(Q\262\250\226dI\226$\12\0\340\21\267\11\263\362:\66\350q\62\14\251\250\14I" + "\0\341\20\267\11s\333\301A\217\223aHEeH\2\342\22\267\11\363\322$\253C\203\36'\303\220\212" + "\312\220\4\343\22\247\11\263\244$\322\241A\217\223aHEeH\2\344\22\247\11s\262(\313\241A\217" + "\223aHEeH\2\345\22\267\11\363\304(\324\261A\217\223aHEeH\2\346\17w\11s,Q" + "-J\6%\312\242\212\62\347\17\247\331r\6%\225[\223A\324\242P\3\350\17\267\11s\362:\70(" + "\251:\234\363A\1\351\17\267\11s\333\301AI\325\341\234\17\12\0\352\21\267\11\363\322$\253C\203\222" + "\252\303\71\37\24\0\353\21\247\11s\262(\313\241AI\325\341\234\17\12\0\354\12\265\12\63\322\372\330\323" + " \355\12\265\12\363\332\221\261\247A\356\15\266\11\263\302$\312rd\355m\20\357\14\245\12\63\242$\212" + "\307\236\6\1\360\20\267\11s\242PL\362lPR\257\311\240\0\361\16\247\11s\64iG\222!\21U" + "\257\1\362\16\267\11s\362:\70(\251\327dP\0\363\15\267\11s\333\301AI\275&\203\2\364\17\267" + "\11\363\322$\253C\203\222zM\6\5\365\17\247\11s\64i\207\6%\365\232\14\12\0\366\17\247\11s" + "\262(\313\241AI\275&\203\2\367\16\227\11\363\322\65\307\206!\307\322\65\3\370\23\227\371\262\223A\311" + "\22-\221\42%S\262dPb\0\371\23\267\11s\362:\26&a\22&a\22&a\64$\1\372\22" + "\267\11s\333\261\60\11\223\60\11\223\60\11\243!\11\373\24\267\11\363\322$\253#a\22&a\22&a" + "\22FC\22\374\24\247\11s\242,\312\241\60\11\223\60\11\223\60\11\243!\11\375\17\346\331\62\333\241\320" + "\67eI\305dH\0\376\20\307\331\62\342\226!\21UuT\206$.\3\377\17\326\331r\242\366\320\67" + "eI\305dH\0\0\0\0\4\377\377\0"; diff --git a/flip_social/font/font.h b/flip_social/font/font.h new file mode 100644 index 000000000..f70711c3c --- /dev/null +++ b/flip_social/font/font.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include +typedef enum +{ + FONT_SIZE_SMALL = 1, + FONT_SIZE_MEDIUM = 2, + FONT_SIZE_LARGE = 3, + FONT_SIZE_XLARGE = 4 +} FontSize; +extern bool canvas_set_font_custom(Canvas *canvas, FontSize font_size); +extern void canvas_draw_str_multi(Canvas *canvas, uint8_t x, uint8_t y, const char *str); \ No newline at end of file diff --git a/flip_social/friends/flip_social_friends.c b/flip_social/friends/flip_social_friends.c index e564afab0..bb5108ef3 100644 --- a/flip_social/friends/flip_social_friends.c +++ b/flip_social/friends/flip_social_friends.c @@ -4,17 +4,10 @@ FlipSocialModel *flip_social_friends_alloc() { // Allocate memory for each username only if not already allocated FlipSocialModel *friends = malloc(sizeof(FlipSocialModel)); - for (size_t i = 0; i < MAX_FRIENDS; i++) + if (friends == NULL) { - if (friends->usernames[i] == NULL) - { - friends->usernames[i] = malloc(MAX_USER_LENGTH); - if (friends->usernames[i] == NULL) - { - FURI_LOG_E(TAG, "Failed to allocate memory for username %zu", i); - return NULL; // Return false on memory allocation failure - } - } + FURI_LOG_E(TAG, "Failed to allocate memory for friends usernames."); + return NULL; } return friends; } @@ -28,11 +21,6 @@ bool flip_social_get_friends() FURI_LOG_E(TAG, "App instance is NULL"); return false; } - if (fhttp.state == INACTIVE) - { - FURI_LOG_E(TAG, "HTTP state is INACTIVE"); - return false; - } // will return true unless the devboard is not connected char url[100]; snprintf( @@ -55,7 +43,7 @@ bool flip_social_get_friends() bool flip_social_update_friends() { - if (!app_instance->submenu_friends) + if (!app_instance->submenu) { FURI_LOG_E(TAG, "Friends submenu is NULL"); return false; @@ -66,11 +54,11 @@ bool flip_social_update_friends() return false; } // Add submenu items for the users - submenu_reset(app_instance->submenu_friends); - submenu_set_header(app_instance->submenu_friends, "Friends"); + submenu_reset(app_instance->submenu); + submenu_set_header(app_instance->submenu, "Friends"); for (int i = 0; i < flip_social_friends->count; i++) { - submenu_add_item(app_instance->submenu_friends, flip_social_friends->usernames[i], FlipSocialSubmenuLoggedInIndexFriendsStart + i, flip_social_callback_submenu_choices, app_instance); + submenu_add_item(app_instance->submenu, flip_social_friends->usernames[i], FlipSocialSubmenuLoggedInIndexFriendsStart + i, flip_social_callback_submenu_choices, app_instance); } return true; } @@ -82,89 +70,42 @@ bool flip_social_parse_json_friends() if (friend_data == NULL) { FURI_LOG_E(TAG, "Failed to load received data from file."); - return false; - } - char *data_cstr = (char *)furi_string_get_cstr(friend_data); - if (data_cstr == NULL) - { - FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); - furi_string_free(friend_data); + flipper_http_deinit(); return false; } - // Allocate memory for each username only if not already allocated + // Allocate memory for each username only if not already allocated flip_social_friends = flip_social_friends_alloc(); if (flip_social_friends == NULL) { FURI_LOG_E(TAG, "Failed to allocate memory for friends usernames."); furi_string_free(friend_data); - free(data_cstr); return false; } - // Remove newlines - char *pos = data_cstr; - while ((pos = strchr(pos, '\n')) != NULL) - { - *pos = ' '; - } - // Initialize friends count flip_social_friends->count = 0; // Reset the friends submenu - submenu_reset(app_instance->submenu_friends); - submenu_set_header(app_instance->submenu_friends, "Friends"); + submenu_reset(app_instance->submenu); + submenu_set_header(app_instance->submenu, "Friends"); // Extract the users array from the JSON - char *json_users = get_json_value("friends", data_cstr, 128); - if (json_users == NULL) + for (int i = 0; i < MAX_FRIENDS; i++) { - FURI_LOG_E(TAG, "Failed to parse friends array."); - furi_string_free(friend_data); - free(data_cstr); - return false; - } - - // Manual tokenization for comma-separated values - char *start = json_users + 1; // Skip the opening bracket - char *end; - while ((end = strchr(start, ',')) != NULL && flip_social_friends->count < MAX_FRIENDS) - { - *end = '\0'; // Null-terminate the current token - - // Remove quotes - if (*start == '"') - start++; - if (*(end - 1) == '"') - *(end - 1) = '\0'; - - // Copy username to pre-allocated memory - snprintf(flip_social_friends->usernames[flip_social_friends->count], MAX_USER_LENGTH, "%s", start); - submenu_add_item(app_instance->submenu_friends, flip_social_friends->usernames[flip_social_friends->count], FlipSocialSubmenuLoggedInIndexFriendsStart + flip_social_friends->count, flip_social_callback_submenu_choices, app_instance); - flip_social_friends->count++; - start = end + 1; - } - - // Handle the last token - if (*start != '\0' && flip_social_friends->count < MAX_FRIENDS) - { - if (*start == '"') - start++; - if (*(start + strlen(start) - 1) == ']') - *(start + strlen(start) - 1) = '\0'; - if (*(start + strlen(start) - 1) == '"') - *(start + strlen(start) - 1) = '\0'; - - snprintf(flip_social_friends->usernames[flip_social_friends->count], MAX_USER_LENGTH, "%s", start); - submenu_add_item(app_instance->submenu_friends, flip_social_friends->usernames[flip_social_friends->count], FlipSocialSubmenuLoggedInIndexFriendsStart + flip_social_friends->count, flip_social_callback_submenu_choices, app_instance); + FuriString *friend = get_json_array_value_furi("friends", i, friend_data); + if (friend == NULL) + { + FURI_LOG_E(TAG, "Failed to parse friend %d.", i); + furi_string_free(friend_data); + break; + } + snprintf(flip_social_friends->usernames[i], MAX_USER_LENGTH, "%s", furi_string_get_cstr(friend)); + submenu_add_item(app_instance->submenu, flip_social_friends->usernames[i], FlipSocialSubmenuLoggedInIndexFriendsStart + i, flip_social_callback_submenu_choices, app_instance); flip_social_friends->count++; + furi_string_free(friend); } - furi_string_free(friend_data); - free(data_cstr); - free(json_users); - free(start); - free(end); + // flipper_http_deinit(); return true; } diff --git a/flip_social/jsmn/jsmn.c b/flip_social/jsmn/jsmn.c index b85ab83b5..d101530c6 100644 --- a/flip_social/jsmn/jsmn.c +++ b/flip_social/jsmn/jsmn.c @@ -7,8 +7,6 @@ */ #include -#include -#include /** * Allocates a fresh unused token from the token pool. @@ -424,7 +422,7 @@ int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, } // Helper function to create a JSON object -char *jsmn(const char *key, const char *value) +char *get_json(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 @@ -448,14 +446,19 @@ 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) +char *get_json_value(char *key, const char *json_data) { // Parse the JSON feed if (json_data != NULL) { jsmn_parser parser; jsmn_init(&parser); - + uint32_t max_tokens = json_token_count(json_data); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + return NULL; + } // Allocate tokens array on the heap jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens); if (tokens == NULL) @@ -510,26 +513,79 @@ char *get_json_value(char *key, char *json_data, uint32_t max_tokens) { FURI_LOG_E("JSMM.H", "JSON data is NULL"); } - FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON."); + char warning[128]; + snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key); + FURI_LOG_E("JSMM.H", warning); 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) +// Helper function to skip a token and all its descendants. +// Returns the index of the next token after skipping this one. +// On error or out of bounds, returns -1. +static int skip_token(const jsmntok_t *tokens, int start, int total) { - // Retrieve the array string for the given key - char *array_str = get_json_value(key, json_data, max_tokens); + if (start < 0 || start >= total) + return -1; + + int i = start; + if (tokens[i].type == JSMN_OBJECT) + { + // For an object: size is number of key-value pairs + int pairs = tokens[i].size; + i++; // move to first key-value pair + for (int p = 0; p < pairs; p++) + { + // skip key (primitive/string) + i++; + if (i >= total) + return -1; + // skip value (which could be object/array and must be skipped recursively) + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; // i is now just past the object + } + else if (tokens[i].type == JSMN_ARRAY) + { + // For an array: size is number of elements + int elems = tokens[i].size; + i++; // move to first element + for (int e = 0; e < elems; e++) + { + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; // i is now just past the array + } + else + { + // Primitive or string token, just skip it + return i + 1; + } +} + +// Revised get_json_array_value +char *get_json_array_value(char *key, uint32_t index, const char *json_data) +{ + // Always extract the full array each time from the original json_data + char *array_str = get_json_value(key, json_data); if (array_str == NULL) { FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); return NULL; } + uint32_t max_tokens = json_token_count(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + free(array_str); + 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) { @@ -538,7 +594,6 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t return NULL; } - // Parse the JSON array int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); if (ret < 0) { @@ -548,7 +603,6 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t 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); @@ -557,50 +611,33 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t 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); + // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", 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 + // Find the index-th element: start from token[1], which is the first element + int elem_token = 1; 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 + elem_token = skip_token(tokens, elem_token, ret); + if (elem_token == -1 || elem_token >= ret) { - // 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."); + FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i); free(tokens); free(array_str); return NULL; } } - // Extract the array element - jsmntok_t element = tokens[current_token]; + // Now elem_token should point to the token of the requested element + jsmntok_t element = tokens[elem_token]; int length = element.end - element.start; char *value = malloc(length + 1); - if (value == NULL) + if (!value) { FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element."); free(tokens); @@ -608,11 +645,9 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t return NULL; } - // Copy the element value to a new string strncpy(value, array_str + element.start, length); - value[length] = '\0'; // Null-terminate the string + value[length] = '\0'; - // Clean up free(tokens); free(array_str); @@ -620,16 +655,22 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t } // 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) +char **get_json_array_values(char *key, char *json_data, int *num_values) { // Retrieve the array string for the given key - char *array_str = get_json_value(key, json_data, max_tokens); + char *array_str = get_json_value(key, json_data); if (array_str == NULL) { FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); return NULL; } - + uint32_t max_tokens = json_token_count(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + free(array_str); + return NULL; + } // Initialize the JSON parser jsmn_parser parser; jsmn_init(&parser); @@ -745,3 +786,18 @@ char **get_json_array_values(char *key, char *json_data, uint32_t max_tokens, in free(array_str); return values; } + +int json_token_count(const char *json) +{ + if (json == NULL) + { + return JSMN_ERROR_INVAL; + } + + jsmn_parser parser; + jsmn_init(&parser); + + // Pass NULL for tokens and 0 for num_tokens to get the token count only + int ret = jsmn_parse(&parser, json, strlen(json), NULL, 0); + return ret; // If ret >= 0, it represents the number of tokens needed. +} diff --git a/flip_social/jsmn/jsmn.h b/flip_social/jsmn/jsmn.h index 74cdccf95..5f96e5596 100644 --- a/flip_social/jsmn/jsmn.h +++ b/flip_social/jsmn/jsmn.h @@ -17,6 +17,7 @@ #define JSMN_H #include +#include #ifdef __cplusplus extern "C" @@ -28,61 +29,6 @@ extern "C" #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 */ @@ -110,23 +56,19 @@ extern "C" #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); +char *get_json(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); +char *get_json_value(char *key, const char *json_data); // Revised get_json_array_value function -char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t max_tokens); +char *get_json_array_value(char *key, uint32_t index, const char *json_data); // 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); +char **get_json_array_values(char *key, char *json_data, int *num_values); + +int json_token_count(const char *json); #endif /* JB_JSMN_EDIT */ diff --git a/flip_social/jsmn/jsmn_furi.c b/flip_social/jsmn/jsmn_furi.c new file mode 100644 index 000000000..d3bde9365 --- /dev/null +++ b/flip_social/jsmn/jsmn_furi.c @@ -0,0 +1,736 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * [License text continues...] + */ + +#include + +// Forward declarations of helper functions +static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s); +static int skip_token(const jsmntok_t *tokens, int start, int total); + +/** + * 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) +{ + if (parser->toknext >= num_tokens) + { + return NULL; + } + jsmntok_t *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. + * Now uses FuriString to access characters. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const size_t num_tokens) +{ + size_t len = furi_string_size(js); + int start = parser->pos; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + switch (c) + { +#ifndef JSMN_STRICT + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + break; + } + if (c < 32 || c >= 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; + } + jsmntok_t *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. + * Now uses FuriString to access characters. + */ +static int jsmn_parse_string(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const size_t num_tokens) +{ + size_t len = furi_string_size(js); + int start = parser->pos; + parser->pos++; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + if (c == '\"') + { + if (tokens == NULL) + { + return 0; + } + jsmntok_t *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; + } + + if (c == '\\' && (parser->pos + 1) < len) + { + parser->pos++; + char esc = furi_string_get_char(js, parser->pos); + switch (esc) + { + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + case 'u': + { + parser->pos++; + for (int i = 0; i < 4 && parser->pos < len; i++) + { + char hex = furi_string_get_char(js, parser->pos); + if (!((hex >= '0' && hex <= '9') || + (hex >= 'A' && hex <= 'F') || + (hex >= 'a' && hex <= 'f'))) + { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + } + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Create JSON parser + */ +void jsmn_init_furi(jsmn_parser *parser) +{ + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +/** + * Parse JSON string and fill tokens. + * Now uses FuriString for the input JSON. + */ +int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const unsigned int num_tokens) +{ + size_t len = furi_string_size(js); + int r; + int i; + int count = parser->toknext; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + jsmntype_t type; + + switch (c) + { + case '{': + case '[': + { + count++; + if (tokens == NULL) + { + break; + } + jsmntok_t *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 + 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; + } + { + jsmntok_t *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 + { + jsmntok_t *token; + 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; + } + } + 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, 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 ' ': + // Whitespace - ignore + 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 + 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': + 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 + default: +#endif + r = jsmn_parse_primitive(parser, js, tokens, num_tokens); + if (r < 0) + return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + { + tokens[parser->toksuper].size++; + } + break; +#ifdef JSMN_STRICT + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) + { + for (i = parser->toknext - 1; i >= 0; i--) + { + if (tokens[i].start != -1 && tokens[i].end == -1) + { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +// Helper function to create a JSON object: {"key":"value"} +FuriString *get_json_furi(const FuriString *key, const FuriString *value) +{ + FuriString *result = furi_string_alloc(); + furi_string_printf(result, "{\"%s\":\"%s\"}", + furi_string_get_cstr(key), + furi_string_get_cstr(value)); + return result; // Caller responsible for furi_string_free +} + +// Helper function to compare JSON keys +static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s) +{ + size_t s_len = furi_string_size(s); + size_t tok_len = tok->end - tok->start; + + if (tok->type != JSMN_STRING) + return -1; + if (s_len != tok_len) + return -1; + + FuriString *sub = furi_string_alloc_set(json); + furi_string_mid(sub, tok->start, tok_len); + + int res = furi_string_cmp(sub, s); + furi_string_free(sub); + + return (res == 0) ? 0 : -1; +} + +// Skip a token and its descendants +static int skip_token(const jsmntok_t *tokens, int start, int total) +{ + if (start < 0 || start >= total) + return -1; + + int i = start; + if (tokens[i].type == JSMN_OBJECT) + { + int pairs = tokens[i].size; + i++; + for (int p = 0; p < pairs; p++) + { + i++; // skip key + if (i >= total) + return -1; + i = skip_token(tokens, i, total); // skip value + if (i == -1) + return -1; + } + return i; + } + else if (tokens[i].type == JSMN_ARRAY) + { + int elems = tokens[i].size; + i++; + for (int e = 0; e < elems; e++) + { + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; + } + else + { + return i + 1; + } +} + +/** + * Parse JSON and return the value associated with a given char* key. + */ +FuriString *get_json_value_furi(const char *key, const FuriString *json_data) +{ + if (json_data == NULL) + { + FURI_LOG_E("JSMM.H", "JSON data is NULL"); + return NULL; + } + uint32_t max_tokens = json_token_count_furi(json_data); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + return NULL; + } + // Create a temporary FuriString from key + FuriString *key_str = furi_string_alloc(); + furi_string_cat_str(key_str, key); + + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(key_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, json_data, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); + free(tokens); + furi_string_free(key_str); + return NULL; + } + + if (ret < 1 || tokens[0].type != JSMN_OBJECT) + { + FURI_LOG_E("JSMM.H", "Root element is not an object."); + free(tokens); + furi_string_free(key_str); + return NULL; + } + + for (int i = 1; i < ret; i++) + { + if (jsoneq_furi(json_data, &tokens[i], key_str) == 0) + { + int length = tokens[i + 1].end - tokens[i + 1].start; + FuriString *value = furi_string_alloc_set(json_data); + furi_string_mid(value, tokens[i + 1].start, length); + free(tokens); + furi_string_free(key_str); + return value; + } + } + + free(tokens); + furi_string_free(key_str); + char warning[128]; + snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key); + FURI_LOG_E("JSMM.H", warning); + return NULL; +} + +/** + * Return the value at a given index in a JSON array for a given char* key. + */ +FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data) +{ + FuriString *array_str = get_json_value_furi(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key"); + return NULL; + } + uint32_t max_tokens = json_token_count_furi(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (ret < 1 || tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key is not an array."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (index >= (uint32_t)tokens[0].size) + { + // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int elem_token = 1; + for (uint32_t i = 0; i < index; i++) + { + elem_token = skip_token(tokens, elem_token, ret); + if (elem_token == -1 || elem_token >= ret) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i); + free(tokens); + furi_string_free(array_str); + return NULL; + } + } + + jsmntok_t element = tokens[elem_token]; + int length = element.end - element.start; + + FuriString *value = furi_string_alloc_set(array_str); + furi_string_mid(value, element.start, length); + + free(tokens); + furi_string_free(array_str); + + return value; +} + +/** + * Extract all object values from a JSON array associated with a given char* key. + */ +FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values) +{ + *num_values = 0; + // Convert key to FuriString and call get_json_value_furi + FuriString *array_str = get_json_value_furi(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key"); + return NULL; + } + + uint32_t max_tokens = json_token_count_furi(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key is not an array."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int array_size = tokens[0].size; + FuriString **values = (FuriString **)malloc(array_size * sizeof(FuriString *)); + if (values == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int actual_num_values = 0; + int current_token = 1; + 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]; + + int length = element.end - element.start; + FuriString *value = furi_string_alloc_set(array_str); + furi_string_mid(value, element.start, length); + + values[actual_num_values] = value; + actual_num_values++; + + // Skip this element and its descendants + current_token = skip_token(tokens, current_token, ret); + if (current_token == -1) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens after element %d.", i); + break; + } + } + + *num_values = actual_num_values; + if (actual_num_values < array_size) + { + FuriString **reduced_values = (FuriString **)realloc(values, actual_num_values * sizeof(FuriString *)); + if (reduced_values != NULL) + { + values = reduced_values; + } + } + + free(tokens); + furi_string_free(array_str); + return values; +} + +uint32_t json_token_count_furi(const FuriString *json) +{ + if (json == NULL) + { + return JSMN_ERROR_INVAL; + } + + jsmn_parser parser; + jsmn_init_furi(&parser); + + // Pass NULL for tokens and 0 for num_tokens to get the token count only + int ret = jsmn_parse_furi(&parser, json, NULL, 0); + return ret; // If ret >= 0, it represents the number of tokens needed. +} diff --git a/flip_social/jsmn/jsmn_furi.h b/flip_social/jsmn/jsmn_furi.h new file mode 100644 index 000000000..cb01f38ca --- /dev/null +++ b/flip_social/jsmn/jsmn_furi.h @@ -0,0 +1,74 @@ +/* + * 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_FURI_H +#define JSMN_FURI_H + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + + JSMN_API void jsmn_init_furi(jsmn_parser *parser); + JSMN_API int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/* Implementation in jsmn_furi.c */ +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_FURI_H */ + +#ifndef JB_JSMN_FURI_EDIT +#define JB_JSMN_FURI_EDIT + +// Helper function to create a JSON object +FuriString *get_json_furi(const FuriString *key, const FuriString *value); + +// Updated signatures to accept const char* key +FuriString *get_json_value_furi(const char *key, const FuriString *json_data); +FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data); +FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values); + +uint32_t json_token_count_furi(const FuriString *json); +/* Example usage: +char *json = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; +FuriString *json_data = char_to_furi_string(json); +if (!json_data) +{ + FURI_LOG_E(TAG, "Failed to allocate FuriString"); + return -1; +} +FuriString *value = get_json_value_furi("key1", json_data, json_token_count_furi(json_data)); +if (value) +{ + FURI_LOG_I(TAG, "Value: %s", furi_string_get_cstr(value)); + furi_string_free(value); +} +furi_string_free(json_data); +*/ +#endif /* JB_JSMN_EDIT */ diff --git a/flip_social/jsmn/jsmn_h.c b/flip_social/jsmn/jsmn_h.c new file mode 100644 index 000000000..59480e6e6 --- /dev/null +++ b/flip_social/jsmn/jsmn_h.c @@ -0,0 +1,15 @@ +#include +FuriString *char_to_furi_string(const char *str) +{ + FuriString *furi_str = furi_string_alloc(); + if (!furi_str) + { + return NULL; + } + for (size_t i = 0; i < strlen(str); i++) + { + furi_string_push_back(furi_str, str[i]); + } + return furi_str; +} +bool jsmn_memory_check(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); } \ No newline at end of file diff --git a/flip_social/jsmn/jsmn_h.h b/flip_social/jsmn/jsmn_h.h new file mode 100644 index 000000000..97d53e7ff --- /dev/null +++ b/flip_social/jsmn/jsmn_h.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include +#include +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 +{ + JSMN_ERROR_NOMEM = -1, + JSMN_ERROR_INVAL = -2, + JSMN_ERROR_PART = -3 +}; + +typedef struct +{ + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +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; + +typedef struct +{ + char *key; + char *value; +} JSON; + +typedef struct +{ + FuriString *key; + FuriString *value; +} FuriJSON; + +FuriString *char_to_furi_string(const char *str); + +// check memory +bool jsmn_memory_check(size_t heap_size); diff --git a/flip_social/messages/flip_social_messages.c b/flip_social/messages/flip_social_messages.c index c1ba402cb..9db309129 100644 --- a/flip_social/messages/flip_social_messages.c +++ b/flip_social/messages/flip_social_messages.c @@ -2,13 +2,6 @@ FlipSocialModel2 *flip_social_messages_alloc() { - if (!app_instance->submenu_messages) - { - if (!easy_flipper_set_submenu(&app_instance->submenu_messages, FlipSocialViewLoggedInMessagesSubmenu, "Messages", flip_social_callback_to_submenu_logged_in, &app_instance->view_dispatcher)) - { - return NULL; - } - } // Allocate memory for each username only if not already allocated FlipSocialModel2 *users = malloc(sizeof(FlipSocialModel2)); if (users == NULL) @@ -16,18 +9,6 @@ FlipSocialModel2 *flip_social_messages_alloc() FURI_LOG_E(TAG, "Failed to allocate memory for message users"); return NULL; } - for (size_t i = 0; i < MAX_MESSAGE_USERS; i++) - { - if (users->usernames[i] == NULL) - { - users->usernames[i] = malloc(MAX_USER_LENGTH); - if (users->usernames[i] == NULL) - { - FURI_LOG_E(TAG, "Failed to allocate memory for username %zu", i); - return NULL; // Return false on memory allocation failure - } - } - } return users; } @@ -40,27 +21,6 @@ FlipSocialMessage *flip_social_user_messages_alloc() FURI_LOG_E(TAG, "Failed to allocate memory for messages"); return NULL; } - for (size_t i = 0; i < MAX_MESSAGES; i++) - { - if (messages->usernames[i] == NULL) - { - messages->usernames[i] = malloc(MAX_USER_LENGTH); - if (messages->usernames[i] == NULL) - { - FURI_LOG_E(TAG, "Failed to allocate memory for username %zu", i); - return NULL; // Return false on memory allocation failure - } - } - if (messages->messages[i] == NULL) - { - messages->messages[i] = malloc(MAX_MESSAGE_LENGTH); - if (messages->messages[i] == NULL) - { - FURI_LOG_E(TAG, "Failed to allocate memory for message %zu", i); - return NULL; // Return false on memory allocation failure - } - } - } return messages; } @@ -76,12 +36,6 @@ void flip_social_free_message_users() void flip_social_free_messages() { - if (app_instance->submenu_messages) - { - submenu_free(app_instance->submenu_messages); - app_instance->submenu_messages = NULL; - view_dispatcher_remove_view(app_instance->view_dispatcher, FlipSocialViewLoggedInMessagesSubmenu); - } if (flip_social_messages == NULL) { return; @@ -92,7 +46,12 @@ void flip_social_free_messages() bool flip_social_update_messages_submenu() { - if (app_instance->submenu_messages == NULL) + if (!app_instance) + { + FURI_LOG_E(TAG, "App instance is NULL"); + return false; + } + if (app_instance->submenu == NULL) { FURI_LOG_E(TAG, "Submenu is NULL"); return false; @@ -102,19 +61,24 @@ bool flip_social_update_messages_submenu() FURI_LOG_E(TAG, "Message users model is NULL"); return false; } - submenu_reset(app_instance->submenu_messages); - submenu_set_header(app_instance->submenu_messages, "Messages"); - submenu_add_item(app_instance->submenu_messages, "[New Message]", FlipSocialSubmenuLoggedInIndexMessagesNewMessage, flip_social_callback_submenu_choices, app_instance); + submenu_reset(app_instance->submenu); + submenu_set_header(app_instance->submenu, "Messages"); + submenu_add_item(app_instance->submenu, "[New Message]", FlipSocialSubmenuLoggedInIndexMessagesNewMessage, flip_social_callback_submenu_choices, app_instance); for (int i = 0; i < flip_social_message_users->count; i++) { - submenu_add_item(app_instance->submenu_messages, flip_social_message_users->usernames[i], FlipSocialSubmenuLoggedInIndexMessagesUsersStart + i, flip_social_callback_submenu_choices, app_instance); + submenu_add_item(app_instance->submenu, flip_social_message_users->usernames[i], FlipSocialSubmenuLoggedInIndexMessagesUsersStart + i, flip_social_callback_submenu_choices, app_instance); } return true; } bool flip_social_update_submenu_user_choices() { - if (app_instance->submenu_messages_user_choices == NULL) + if (app_instance == NULL) + { + FURI_LOG_E(TAG, "App instance is NULL"); + return false; + } + if (app_instance->submenu == NULL) { FURI_LOG_E(TAG, "Submenu is NULL"); return false; @@ -124,11 +88,11 @@ bool flip_social_update_submenu_user_choices() FURI_LOG_E(TAG, "Explore model is NULL"); return false; } - submenu_reset(app_instance->submenu_messages_user_choices); - submenu_set_header(app_instance->submenu_messages_user_choices, "Users"); + submenu_reset(app_instance->submenu); + submenu_set_header(app_instance->submenu, "Users"); for (int i = 0; i < flip_social_explore->count; i++) { - submenu_add_item(app_instance->submenu_messages_user_choices, flip_social_explore->usernames[i], FlipSocialSubmenuLoggedInIndexMessagesUserChoicesIndexStart + i, flip_social_callback_submenu_choices, app_instance); + submenu_add_item(app_instance->submenu, flip_social_explore->usernames[i], FlipSocialSubmenuLoggedInIndexMessagesUserChoicesIndexStart + i, flip_social_callback_submenu_choices, app_instance); } return true; } @@ -141,16 +105,22 @@ bool flip_social_get_message_users() FURI_LOG_E(TAG, "Username is NULL"); return false; } - if (fhttp.state == INACTIVE) + if (!flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "HTTP state is INACTIVE"); + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); return false; } + char directory[128]; + snprintf(directory, sizeof(directory), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/messages"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory); char command[128]; snprintf( fhttp.file_path, sizeof(fhttp.file_path), - STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/message_users.json"); + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/messages/message_users.json"); fhttp.save_received_data = true; auth_headers_alloc(); @@ -159,6 +129,7 @@ bool flip_social_get_message_users() { FURI_LOG_E(TAG, "Failed to send HTTP request for messages"); fhttp.state = ISSUE; + flipper_http_deinit(); return false; } fhttp.state = RECEIVING; @@ -168,9 +139,9 @@ bool flip_social_get_message_users() // Get all the messages between the logged in user and the selected user bool flip_social_get_messages_with_user() { - if (fhttp.state == INACTIVE) + if (!flipper_http_init(flipper_http_rx_callback, app_instance)) { - FURI_LOG_E(TAG, "HTTP state is INACTIVE"); + FURI_LOG_E(TAG, "Failed to initialize FlipperHTTP"); return false; } if (app_instance->login_username_logged_out == NULL) @@ -178,16 +149,23 @@ 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) + if (strlen(flip_social_message_users->usernames[flip_social_message_users->index]) == 0) { FURI_LOG_E(TAG, "Username is NULL"); return false; } + char directory[128]; + snprintf(directory, sizeof(directory), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/messages"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory); + char command[256]; snprintf( fhttp.file_path, sizeof(fhttp.file_path), - STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/%s_messages.json", + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/messages/%s_messages.json", flip_social_message_users->usernames[flip_social_message_users->index]); fhttp.save_received_data = true; @@ -211,15 +189,10 @@ bool flip_social_parse_json_message_users() if (message_data == NULL) { FURI_LOG_E(TAG, "Failed to load received data from file."); + flipper_http_deinit(); return false; } - char *data_cstr = (char *)furi_string_get_cstr(message_data); - if (data_cstr == NULL) - { - FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); - furi_string_free(message_data); - return false; - } + flipper_http_deinit(); // Allocate memory for each username only if not already allocated flip_social_message_users = flip_social_messages_alloc(); @@ -227,65 +200,29 @@ bool flip_social_parse_json_message_users() { FURI_LOG_E(TAG, "Failed to allocate memory for message users."); furi_string_free(message_data); - free(data_cstr); return false; } // Initialize message users count flip_social_message_users->count = 0; - // Extract the users array from the JSON - char *json_users = get_json_value("users", data_cstr, 64); - if (json_users == NULL) - { - FURI_LOG_E(TAG, "Failed to parse users array."); - furi_string_free(message_data); - free(data_cstr); - return false; - } - - // Manual tokenization for comma-separated values - char *start = json_users + 1; // Skip the opening bracket - char *end; - while ((end = strchr(start, ',')) != NULL && flip_social_message_users->count < MAX_MESSAGE_USERS) + for (int i = 0; i < MAX_MESSAGE_USERS; i++) { - *end = '\0'; // Null-terminate the current token - - // Remove quotes - if (*start == '"') - start++; - if (*(end - 1) == '"') - *(end - 1) = '\0'; - - // Copy username to pre-allocated memory - snprintf(flip_social_message_users->usernames[flip_social_message_users->count], MAX_USER_LENGTH, "%s", start); - flip_social_message_users->count++; - start = end + 1; - } - - // Handle the last token - if (*start != '\0' && flip_social_message_users->count < MAX_MESSAGE_USERS) - { - if (*start == '"') - start++; - if (*(start + strlen(start) - 1) == ']') - *(start + strlen(start) - 1) = '\0'; - if (*(start + strlen(start) - 1) == '"') - *(start + strlen(start) - 1) = '\0'; - - snprintf(flip_social_message_users->usernames[flip_social_message_users->count], MAX_USER_LENGTH, "%s", start); + FuriString *user = get_json_array_value_furi("users", i, message_data); + if (user == NULL) + { + break; + } + snprintf(flip_social_message_users->usernames[i], MAX_USER_LENGTH, "%s", furi_string_get_cstr(user)); flip_social_message_users->count++; + furi_string_free(user); } // Add submenu items for the users flip_social_update_messages_submenu(); // Free the JSON data - free(json_users); - free(start); - free(end); furi_string_free(message_data); - free(data_cstr); return true; } @@ -297,82 +234,42 @@ bool flip_social_parse_json_message_user_choices() if (user_data == NULL) { FURI_LOG_E(TAG, "Failed to load received data from file."); - return false; - } - char *data_cstr = (char *)furi_string_get_cstr(user_data); - if (data_cstr == NULL) - { - FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); - furi_string_free(user_data); + flipper_http_deinit(); return false; } + flipper_http_deinit(); + // Allocate memory for each username only if not already allocated flip_social_explore = flip_social_explore_alloc(); if (flip_social_explore == NULL) { FURI_LOG_E(TAG, "Failed to allocate memory for explore usernames."); furi_string_free(user_data); - free(data_cstr); return false; } // Initialize explore count flip_social_explore->count = 0; - // Extract the users array from the JSON - char *json_users = get_json_value("users", data_cstr, 64); - if (json_users == NULL) + for (int i = 0; i < MAX_MESSAGE_USERS; i++) { - FURI_LOG_E(TAG, "Failed to parse users array."); - furi_string_free(user_data); - free(data_cstr); - return false; - } - - // Manual tokenization for comma-separated values - char *start = json_users + 1; // Skip the opening bracket - char *end; - while ((end = strchr(start, ',')) != NULL && flip_social_explore->count < MAX_EXPLORE_USERS) - { - *end = '\0'; // Null-terminate the current token - - // Remove quotes - if (*start == '"') - start++; - if (*(end - 1) == '"') - *(end - 1) = '\0'; - - // Copy username to pre-allocated memory - snprintf(flip_social_explore->usernames[flip_social_explore->count], MAX_USER_LENGTH, "%s", start); - flip_social_explore->count++; - start = end + 1; - } - - // Handle the last token - if (*start != '\0' && flip_social_explore->count < MAX_EXPLORE_USERS) - { - if (*start == '"') - start++; - if (*(start + strlen(start) - 1) == ']') - *(start + strlen(start) - 1) = '\0'; - if (*(start + strlen(start) - 1) == '"') - *(start + strlen(start) - 1) = '\0'; - - snprintf(flip_social_explore->usernames[flip_social_explore->count], MAX_USER_LENGTH, "%s", start); + FuriString *user = get_json_array_value_furi("users", i, user_data); + if (user == NULL) + { + break; + } + snprintf(flip_social_explore->usernames[i], MAX_USER_LENGTH, "%s", furi_string_get_cstr(user)); flip_social_explore->count++; + furi_string_free(user); } // Add submenu items for the users flip_social_update_submenu_user_choices(); // Free the JSON data - free(json_users); - free(start); - free(end); furi_string_free(user_data); - free(data_cstr); - return true; + return flip_social_explore->count > 0; } // parse messages between the logged in user and the selected user @@ -383,15 +280,10 @@ bool flip_social_parse_json_messages() if (message_data == NULL) { FURI_LOG_E(TAG, "Failed to load received data from file."); + flipper_http_deinit(); return false; } - char *data_cstr = (char *)furi_string_get_cstr(message_data); - if (data_cstr == NULL) - { - FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); - furi_string_free(message_data); - return false; - } + flipper_http_deinit(); // Allocate memory for each message only if not already allocated flip_social_messages = flip_social_user_messages_alloc(); @@ -399,7 +291,6 @@ bool flip_social_parse_json_messages() { FURI_LOG_E(TAG, "Failed to allocate memory for messages."); furi_string_free(message_data); - free(data_cstr); return false; } @@ -410,40 +301,38 @@ bool flip_social_parse_json_messages() for (int i = 0; i < MAX_MESSAGES; i++) { // Parse each item in the array - char *item = get_json_array_value("conversations", i, data_cstr, 64); + FuriString *item = get_json_array_value_furi("conversations", i, message_data); if (item == NULL) { break; } // Extract individual fields from the JSON object - char *sender = get_json_value("sender", item, 8); - char *content = get_json_value("content", item, 8); + FuriString *sender = get_json_value_furi("sender", item); + FuriString *content = get_json_value_furi("content", item); if (sender == NULL || content == NULL) { FURI_LOG_E(TAG, "Failed to parse item fields."); - free(item); + furi_string_free(item); continue; } // Store parsed values in pre-allocated memory - snprintf(flip_social_messages->usernames[i], MAX_USER_LENGTH, "%s", sender); - snprintf(flip_social_messages->messages[i], MAX_MESSAGE_LENGTH, "%s", content); + snprintf(flip_social_messages->usernames[i], MAX_USER_LENGTH, "%s", furi_string_get_cstr(sender)); + snprintf(flip_social_messages->messages[i], MAX_MESSAGE_LENGTH, "%s", furi_string_get_cstr(content)); flip_social_messages->count++; - free(item); - free(sender); - free(content); + furi_string_free(item); + furi_string_free(sender); + furi_string_free(content); } if (!messages_dialog_alloc(true)) { FURI_LOG_E(TAG, "Failed to allocate and set messages dialog."); furi_string_free(message_data); - free(data_cstr); return false; } furi_string_free(message_data); - free(data_cstr); return true; } \ No newline at end of file diff --git a/flip_store/.gitignore b/flip_store/.gitignore deleted file mode 100644 index 9bea4330f..000000000 --- a/flip_store/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ - -.DS_Store diff --git a/flip_store/README.md b/flip_store/README.md index c53c6bdec..9db991dc4 100644 --- a/flip_store/README.md +++ b/flip_store/README.md @@ -1,12 +1,13 @@ # FlipStore -Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no longer need another device to install apps. FlipStore uses the FlipperHTTP flash for the WiFi Devboard, first introduced in the WebCrawler app: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP +Download Flipper Zero apps directly to your Flipper Zero using WiFi. ## Features - App Catalog - Install Apps - Delete Apps -- Install Developer Board Flashes -- Install Custom Apps (coming soon) +- Install WiFi Developer Board Firmware +- Install Video Game Module Firmware +- Install GitHub Repositories (Beta) - Install Official Firmware (coming soon) ## Installation @@ -38,18 +39,7 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long - UX Improvements **v0.8** -- Download custom apps/assets from a GitHub URL +- Download GitHub repositories **1.0** -- Download Official Firmware/Firmware Updates - -## Contribution -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 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. +- Download Official Flipper Zero Firmware \ No newline at end of file diff --git a/flip_store/alloc/flip_store_alloc.c b/flip_store/alloc/flip_store_alloc.c index 953f08246..25ea48a69 100644 --- a/flip_store/alloc/flip_store_alloc.c +++ b/flip_store/alloc/flip_store_alloc.c @@ -6,133 +6,27 @@ FlipStoreApp *flip_store_app_alloc() Gui *gui = furi_record_open(RECORD_GUI); - if (!flipper_http_init(flipper_http_rx_callback, app)) - { - FURI_LOG_E(TAG, "Failed to initialize flipper http"); - return NULL; - } - - // Allocate the text input buffer - app->uart_text_input_buffer_size_ssid = 64; - app->uart_text_input_buffer_size_pass = 64; - if (!easy_flipper_set_buffer(&app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid)) - { - return NULL; - } - if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid)) - { - return NULL; - } - if (!easy_flipper_set_buffer(&app->uart_text_input_buffer_pass, app->uart_text_input_buffer_size_pass)) - { - return NULL; - } - if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer_pass, app->uart_text_input_buffer_size_pass)) - { - 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_store_custom_event_callback); + // Main view if (!easy_flipper_set_view(&app->view_loader, FlipStoreViewLoader, flip_store_loader_draw_callback, NULL, callback_to_submenu_options, &app->view_dispatcher, app)) { return NULL; } flip_store_loader_init(app->view_loader); - if (!easy_flipper_set_widget(&app->widget_result, FlipStoreViewWidgetResult, "Error, try again.", callback_to_submenu_options, &app->view_dispatcher)) - { - return NULL; - } - - // Main view - if (!easy_flipper_set_view(&app->view_app_info, FlipStoreViewAppInfo, flip_store_view_draw_callback_app_list, flip_store_input_callback, callback_to_app_list, &app->view_dispatcher, app)) - { - return NULL; - } - - // Widget - if (!easy_flipper_set_widget( - &app->widget, - FlipStoreViewAbout, - "Welcome to the FlipStore!\n------\nDownload apps via WiFi and\nrun them on your Flipper!\n------\nwww.github.com/jblanked", - callback_to_submenu, - &app->view_dispatcher)) - { - return NULL; - } - - // Popup - if (!easy_flipper_set_popup(&app->popup, FlipStoreViewPopup, "Failed", 0, 0, "You are not connected to Wifi.\n\nIf you have the FlipperHTTP\nflash installed, then update\nyour WiFi credentials.", 0, 10, popup_callback, callback_to_submenu, &app->view_dispatcher, app)) - { - FURI_LOG_E(TAG, "Failed to create popup"); - } - // Dialog - if (!easy_flipper_set_dialog_ex( - &app->dialog_delete, - FlipStoreViewAppDelete, - "Delete App", - 0, - 0, - "Are you sure you want to delete this app?", - 0, - 10, - "No", - "Yes", - NULL, - 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, FlipStoreViewTextInputSSID, "Enter SSID", app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid, flip_store_text_updated_ssid, callback_to_submenu, &app->view_dispatcher, app)) - { - return NULL; - } - if (!easy_flipper_set_uart_text_input(&app->uart_text_input_pass, FlipStoreViewTextInputPass, "Enter Password", app->uart_text_input_temp_buffer_pass, app->uart_text_input_buffer_size_pass, flip_store_text_updated_pass, callback_to_submenu, &app->view_dispatcher, app)) - { - return NULL; - } - - // Variable Item List - if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipStoreViewSettings, settings_item_selected, callback_to_submenu, &app->view_dispatcher, app)) + if (!easy_flipper_set_widget(&app->widget_result, FlipStoreViewWidgetResult, "Error, try again.", callback_to_submenu_options, &app->view_dispatcher)) { return NULL; } - app->variable_item_ssid = variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL); - app->variable_item_pass = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL); // Submenu - if (!easy_flipper_set_submenu(&app->submenu_main, FlipStoreViewSubmenu, "FlipStore v0.7", callback_exit_app, &app->view_dispatcher)) + if (!easy_flipper_set_submenu(&app->submenu_main, FlipStoreViewSubmenu, VERSION_TAG, callback_exit_app, &app->view_dispatcher)) { return NULL; } @@ -144,7 +38,11 @@ FlipStoreApp *flip_store_app_alloc() { return NULL; } - if (!easy_flipper_set_submenu(&app->submenu_firmwares, FlipStoreViewFirmwares, "ESP32 Firmwares", callback_to_submenu_options, &app->view_dispatcher)) + if (!easy_flipper_set_submenu(&app->submenu_firmwares, FlipStoreViewFirmwares, "ESP32 Firmware", callback_to_submenu_options, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_vgm_firmwares, FlipStoreViewVGMFirmwares, "VGM Firmware", callback_to_submenu_options, &app->view_dispatcher)) { return NULL; } @@ -155,8 +53,9 @@ FlipStoreApp *flip_store_app_alloc() submenu_add_item(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_options, "ESP32 Firmware", FlipStoreSubmenuIndexFirmwares, callback_submenu_choices, app); + submenu_add_item(app->submenu_options, "VGM Firmware", FlipStoreSubmenuIndexVGMFirmwares, callback_submenu_choices, app); + submenu_add_item(app->submenu_options, "GitHub Repository", FlipStoreSubmenuIndexGitHub, callback_submenu_choices, app); // submenu_add_item(app->submenu_app_list, "Bluetooth", FlipStoreSubmenuIndexAppListBluetooth, callback_submenu_choices, app); submenu_add_item(app->submenu_app_list, "Games", FlipStoreSubmenuIndexAppListGames, callback_submenu_choices, app); @@ -170,28 +69,6 @@ FlipStoreApp *flip_store_app_alloc() submenu_add_item(app->submenu_app_list, "Tools", FlipStoreSubmenuIndexAppListTools, callback_submenu_choices, app); submenu_add_item(app->submenu_app_list, "USB", FlipStoreSubmenuIndexAppListUSB, callback_submenu_choices, app); // - // dont add any items to the app list submenu of each category yet - - // load settings - if (load_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid, app->uart_text_input_buffer_pass, app->uart_text_input_buffer_size_pass)) - { - // Update variable items - if (app->variable_item_ssid) - variable_item_set_current_value_text(app->variable_item_ssid, app->uart_text_input_buffer_ssid); - // do not display the password - - // Copy items into their temp buffers with safety checks - if (app->uart_text_input_buffer_ssid && app->uart_text_input_temp_buffer_ssid) - { - strncpy(app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid - 1); - app->uart_text_input_temp_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; - } - if (app->uart_text_input_buffer_pass && app->uart_text_input_temp_buffer_pass) - { - strncpy(app->uart_text_input_temp_buffer_pass, app->uart_text_input_buffer_pass, app->uart_text_input_buffer_size_pass - 1); - app->uart_text_input_temp_buffer_pass[app->uart_text_input_buffer_size_pass - 1] = '\0'; - } - } // Switch to the main view view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenu); diff --git a/flip_store/app.c b/flip_store/app.c index 4252d18f1..5c036aba9 100644 --- a/flip_store/app.c +++ b/flip_store/app.c @@ -1,6 +1,5 @@ #include #include -#include // Entry point for the Hello World application int32_t main_flip_store(void *p) @@ -9,25 +8,47 @@ int32_t main_flip_store(void *p) UNUSED(p); // Initialize the Hello World application - app_instance = flip_store_app_alloc(); - if (!app_instance) + FlipStoreApp *app = flip_store_app_alloc(); + if (!app) { FURI_LOG_E(TAG, "Failed to allocate FlipStoreApp"); return -1; } - if (!flipper_http_ping()) + // check if board is connected (Derek Jamison) + FlipperHTTP *fhttp = flipper_http_alloc(); + if (!fhttp) + { + easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero."); + return -1; + } + + if (!flipper_http_ping(fhttp)) { FURI_LOG_E(TAG, "Failed to ping the device"); + flipper_http_free(fhttp); return -1; } + // Try to wait for pong response. + uint32_t counter = 10; + while (fhttp->state == INACTIVE && --counter > 0) + { + FURI_LOG_D(TAG, "Waiting for PONG"); + furi_delay_ms(100); + } + + flipper_http_free(fhttp); + if (counter == 0) + { + easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed."); + } + // Run the view dispatcher - view_dispatcher_run(app_instance->view_dispatcher); + view_dispatcher_run(app->view_dispatcher); // Free the resources used by the Hello World application - flip_store_app_free(app_instance); - flip_catalog_free(); + flip_store_app_free(app); // Return 0 to indicate success return 0; diff --git a/flip_store/application.fam b/flip_store/application.fam index 8f8df947d..abd9f3d24 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.7", + fap_version="0.8", ) diff --git a/flip_store/apps/flip_store_apps.c b/flip_store/apps/flip_store_apps.c index d2f3dd9d8..8202fd120 100644 --- a/flip_store/apps/flip_store_apps.c +++ b/flip_store/apps/flip_store_apps.c @@ -3,80 +3,72 @@ 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; +int catalog_iteration = 0; // define the list of categories +char *category_ids[] = { + "64a69817effe1f448a4053b4", // "Bluetooth", + "64971d11be1a76c06747de2f", // "Games", + "64971d106617ba37a4bc79b9", // "GPIO", + "64971d106617ba37a4bc79b6", // "Infrared", + "64971d11be1a76c06747de29", // "iButton", + "64971d116617ba37a4bc79bc", // "Media", + "64971d10be1a76c06747de26", // "NFC", + "64971d10577d519190ede5c2", // "RFID", + "64971d0f6617ba37a4bc79b3", // "Sub-GHz", + "64971d11577d519190ede5c5", // "Tools", + "64971d11be1a76c06747de2c", // "USB", +}; + char *categories[] = { - "Bluetooth", - "Games", - "GPIO", - "Infrared", - "iButton", - "Media", - "NFC", - "RFID", - "Sub-GHz", - "Tools", - "USB", + "Bluetooth", // "64a69817effe1f448a4053b4" + "Games", // "64971d11be1a76c06747de2f" + "GPIO", // "64971d106617ba37a4bc79b9" + "Infrared", // "64971d106617ba37a4bc79b6" + "iButton", // "64971d11be1a76c06747de29" + "Media", // "64971d116617ba37a4bc79bc" + "NFC", // "64971d10be1a76c06747de26" + "RFID", // "64971d10577d519190ede5c2" + "Sub-GHz", // "64971d0f6617ba37a4bc79b3" + "Tools", // "64971d11577d519190ede5c5" + "USB", // "64971d11be1a76c06747de2c" }; FlipStoreAppInfo *flip_catalog_alloc() { - FlipStoreAppInfo *app_catalog = (FlipStoreAppInfo *)malloc(MAX_APP_COUNT * sizeof(FlipStoreAppInfo)); - if (!app_catalog) + if (memmgr_get_free_heap() < MAX_APP_COUNT * sizeof(FlipStoreAppInfo)) { - FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog."); + FURI_LOG_E(TAG, "Not enough memory to allocate flip_catalog."); return NULL; } - for (int i = 0; i < MAX_APP_COUNT; i++) + FlipStoreAppInfo *app_catalog = malloc(MAX_APP_COUNT * sizeof(FlipStoreAppInfo)); + if (!app_catalog) { - 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; - } + FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog."); + return NULL; } + app_catalog->count = 0; + app_catalog->iteration = catalog_iteration; return app_catalog; } + void flip_catalog_free() { if (flip_catalog) { free(flip_catalog); + flip_catalog = NULL; } } -bool flip_store_process_app_list() +bool flip_store_process_app_list(FlipperHTTP *fhttp) { + if (!fhttp) + { + FURI_LOG_E(TAG, "FlipperHTTP is NULL."); + return false; + } // Initialize the flip_catalog flip_catalog = flip_catalog_alloc(); if (!flip_catalog) @@ -85,244 +77,137 @@ bool flip_store_process_app_list() return false; } - FuriString *feed_data = flipper_http_load_from_file(fhttp.file_path); + 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) + FuriString *json_data_str = furi_string_alloc(); + if (!json_data_str) + { + FURI_LOG_E("Game", "Failed to allocate json_data string"); + return NULL; + } + furi_string_cat_str(json_data_str, "{\"json_data\":"); + if (memmgr_get_free_heap() < furi_string_size(feed_data) + furi_string_size(json_data_str) + 2) { - FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); + FURI_LOG_E(TAG, "Not enough memory to allocate json_data_str."); furi_string_free(feed_data); + furi_string_free(json_data_str); return false; } + furi_string_cat(json_data_str, feed_data); + furi_string_free(feed_data); + furi_string_cat_str(json_data_str, "}"); - // 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; + flip_catalog->count = 0; - // Iterate through the data - for (size_t i = 0; data_cstr[i] != '\0' && state != STATE_DONE; ++i) + // parse the JSON data + for (int i = 0; i < MAX_APP_COUNT; i++) { - char c = data_cstr[i]; - - if (is_escaped) + FuriString *json_data_array = get_json_array_value_furi("json_data", i, json_data_str); + if (!json_data_array) { - 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; + break; } - if (c == '\\') + FuriString *app_id = get_json_value_furi("alias", json_data_array); + if (!app_id) { - is_escaped = true; - continue; + FURI_LOG_E(TAG, "Failed to get app_id."); + furi_string_free(json_data_array); + break; } + snprintf(flip_catalog[i].app_id, MAX_ID_LENGTH, "%s", furi_string_get_cstr(app_id)); + furi_string_free(app_id); - if (c == '\"') + FuriString *current_version = get_json_value_furi("current_version", json_data_array); + if (!current_version) { - 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; - } + FURI_LOG_E(TAG, "Failed to get current_version."); + furi_string_free(json_data_array); + break; + } - object_state = OBJECT_EXPECT_COMMA_OR_END; - } - } - } - continue; + FuriString *app_name = get_json_value_furi("name", current_version); + if (!app_name) + { + FURI_LOG_E(TAG, "Failed to get app_name."); + furi_string_free(json_data_array); + furi_string_free(current_version); + break; } + snprintf(flip_catalog[i].app_name, MAX_APP_NAME_LENGTH, "%s", furi_string_get_cstr(app_name)); + furi_string_free(app_name); - if (in_string) + FuriString *app_description = get_json_value_furi("short_description", current_version); + if (!app_description) { - 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; + FURI_LOG_E(TAG, "Failed to get app_description."); + furi_string_free(json_data_array); + furi_string_free(current_version); + break; } + snprintf(flip_catalog[i].app_description, MAX_APP_DESCRIPTION_LENGTH, "%s", furi_string_get_cstr(app_description)); + furi_string_free(app_description); - if (state == STATE_SEARCH_ARRAY_START && c == '[') + FuriString *app_version = get_json_value_furi("version", current_version); + if (!app_version) { - state = STATE_READ_ARRAY_ELEMENTS; - continue; + FURI_LOG_E(TAG, "Failed to get app_version."); + furi_string_free(json_data_array); + furi_string_free(current_version); + break; } + snprintf(flip_catalog[i].app_version, MAX_APP_VERSION_LENGTH, "%s", furi_string_get_cstr(app_version)); + furi_string_free(app_version); - if (state == STATE_READ_ARRAY_ELEMENTS) + FuriString *_id = get_json_value_furi("_id", current_version); + if (!_id) { - 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; - } + FURI_LOG_E(TAG, "Failed to get _id."); + furi_string_free(json_data_array); + furi_string_free(current_version); + break; } + snprintf(flip_catalog[i].app_build_id, MAX_ID_LENGTH, "%s", furi_string_get_cstr(_id)); + furi_string_free(_id); + + flip_catalog->count++; + furi_string_free(json_data_array); + furi_string_free(current_version); } - // Clean up - furi_string_free(feed_data); - free(data_cstr); - return app_count > 0; + furi_string_free(json_data_str); + return flip_catalog->count > 0; } -bool flip_store_get_fap_file(char *build_id, uint8_t target, uint16_t api_major, uint16_t api_minor) +static bool flip_store_get_fap_file(FlipperHTTP *fhttp, 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; + if (!fhttp || !build_id) + { + FURI_LOG_E(TAG, "FlipperHTTP or build_id is NULL."); + return false; + } + char url[256]; + 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; + return flipper_http_get_request_bytes(fhttp, url, "{\"Content-Type\": \"application/octet-stream\"}"); } -bool flip_store_install_app(char *category) +bool flip_store_install_app(FlipperHTTP *fhttp, 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); - - snprintf(fhttp.file_path, sizeof(fhttp.file_path), STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", category, flip_catalog[app_selected_index].app_id); - - 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)) - { - fhttp.state = RECEIVING; - return true; - } - else + if (!fhttp || !category) { - FURI_LOG_E(TAG, "Failed to send the request"); - flip_store_success = false; + FURI_LOG_E(TAG, "FlipperHTTP or category is NULL."); return false; } + snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", category, flip_catalog[app_selected_index].app_id); + 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); + return flip_store_get_fap_file(fhttp, flip_catalog[app_selected_index].app_build_id, target, api_major, api_minor); } \ No newline at end of file diff --git a/flip_store/apps/flip_store_apps.h b/flip_store/apps/flip_store_apps.h index 4b5de73e7..e9d66c9d0 100644 --- a/flip_store/apps/flip_store_apps.h +++ b/flip_store/apps/flip_store_apps.h @@ -8,29 +8,28 @@ // Define maximum limits #define MAX_APP_NAME_LENGTH 32 #define MAX_ID_LENGTH 32 -#define MAX_APP_COUNT 100 +#define MAX_APP_COUNT 50 #define MAX_APP_DESCRIPTION_LENGTH 100 #define MAX_APP_VERSION_LENGTH 5 // define the list of categories +extern char *category_ids[]; extern char *categories[]; +extern int catalog_iteration; typedef struct { - char *app_name; - char *app_id; - char *app_build_id; - char *app_version; - char *app_description; + char app_name[MAX_APP_NAME_LENGTH]; + char app_id[MAX_APP_NAME_LENGTH]; + char app_build_id[MAX_ID_LENGTH]; + char app_version[MAX_APP_VERSION_LENGTH]; + char app_description[MAX_APP_DESCRIPTION_LENGTH]; + size_t count; + int iteration; } 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 @@ -46,10 +45,8 @@ 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); +bool flip_store_process_app_list(FlipperHTTP *fhttp); // function to handle the entire installation process "asynchronously" -bool flip_store_install_app(char *category); +bool flip_store_install_app(FlipperHTTP *fhttp, char *category); #endif // FLIP_STORE_APPS_H \ No newline at end of file diff --git a/flip_store/assets/01-main-menu.png b/flip_store/assets/01-main-menu.png index 30367ebc7..f5a589c69 100644 Binary files a/flip_store/assets/01-main-menu.png and b/flip_store/assets/01-main-menu.png differ diff --git a/flip_store/assets/CHANGELOG.md b/flip_store/assets/CHANGELOG.md index adb0a3ad6..a17451f74 100644 --- a/flip_store/assets/CHANGELOG.md +++ b/flip_store/assets/CHANGELOG.md @@ -1,3 +1,17 @@ +## v0.8 +- Updated FlipperHTTP to the latest library. +- Switched to use Flipper catalog API. +- Added an option to download Video Game Module firmware (FlipperHTTP) +- Added an option to download Github repositories. +- Updated Marauder to the version 1.2 + +## v0.7.2 +- Final memory allocation improvements + +## v0.7.1 +- Improved memory allocation +- Fixed a crash when re-entering the same app list + ## v0.7 - Improved memory allocation - Added updates from Derek Jamison diff --git a/flip_store/assets/README.md b/flip_store/assets/README.md index 7e3e7e4de..00ac695c5 100644 --- a/flip_store/assets/README.md +++ b/flip_store/assets/README.md @@ -1,12 +1,12 @@ -# FlipStore -Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no longer need another device to install apps. FlipStore uses the FlipperHTTP flash for the WiFi Devboard, first introduced in the WebCrawler app: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP +Download Flipper Zero apps directly to your Flipper Zero using WiFi. -## Features +## Features - App Catalog - Install Apps - Delete Apps -- Install Developer Board Flashes -- Install Custom Apps (coming soon) +- Install WiFi Developer Board Firmware +- Install Video Game Module Firmware +- Install GitHub Repositories (Beta) - Install Official Firmware (coming soon) ## Installation @@ -38,18 +38,7 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long - UX Improvements **v0.8** -- Download custom apps from a GitHub URL +- Download GitHub repositories **1.0** -- Download Official Firmware/Firmware Updates - -## Contribution -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 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. +- Download Official Flipper Zero Firmware \ No newline at end of file diff --git a/flip_store/callback/flip_store_callback.c b/flip_store/callback/flip_store_callback.c index a44ef370f..6550b3220 100644 --- a/flip_store/callback/flip_store_callback.c +++ b/flip_store/callback/flip_store_callback.c @@ -1,4 +1,5 @@ #include +#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. @@ -12,138 +13,39 @@ bool flip_store_app_does_exist = false; uint32_t selected_firmware_index = 0; +static uint32_t callback_to_app_category_list(void *context); static bool flip_store_dl_app_fetch(DataLoaderModel *model) { - UNUSED(model); - return flip_store_install_app(categories[flip_store_category_index]); + if (!model->fhttp) + { + FURI_LOG_E(TAG, "FlipperHTTP is NULL"); + return false; + } + return flip_store_install_app(model->fhttp, categories[flip_store_category_index]); } static char *flip_store_dl_app_parse(DataLoaderModel *model) { - UNUSED(model); - if (fhttp.state != IDLE) + if (!model->fhttp || model->fhttp->state != IDLE) { - return NULL; + FURI_LOG_E(TAG, "FlipperHTTP is NULL or not IDLE"); + return "Failed to install app."; } return "App installed successfully."; } static void flip_store_dl_app_switch_to_view(FlipStoreApp *app) { - flip_store_generic_switch_to_view(app, flip_catalog[app_selected_index].app_name, flip_store_dl_app_fetch, flip_store_dl_app_parse, 1, callback_to_app_list, FlipStoreViewLoader); + flip_store_generic_switch_to_view(app, flip_catalog[app_selected_index].app_name, flip_store_dl_app_fetch, flip_store_dl_app_parse, 1, callback_to_app_category_list, FlipStoreViewLoader); } // -static bool flip_store_fetch_app_list(DataLoaderModel *model) -{ - UNUSED(model); - flip_catalog_free(); - snprintf( - fhttp.file_path, - sizeof(fhttp.file_path), - STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s.json", categories[flip_store_category_index]); - 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/", categories[flip_store_category_index]); - return fhttp.state != INACTIVE && flipper_http_get_request_with_headers(url, "{\"Content-Type\":\"application/json\"}"); -} -static char *flip_store_parse_app_list(DataLoaderModel *model) +static bool flip_store_fetch_firmware(DataLoaderModel *model) { - UNUSED(model); - if (!app_instance) - { - FURI_LOG_E(TAG, "FlipStoreApp is NULL"); - return "Failed to fetch app list."; - } - Submenu **submenu = NULL; - uint32_t view_id = 0; - switch (flip_store_category_index) + if (!model->fhttp) { - case 0: - submenu = &app_instance->submenu_app_list_bluetooth; - view_id = FlipStoreViewAppListBluetooth; - break; - case 1: - submenu = &app_instance->submenu_app_list_games; - view_id = FlipStoreViewAppListGames; - break; - case 2: - submenu = &app_instance->submenu_app_list_gpio; - view_id = FlipStoreViewAppListGPIO; - break; - case 3: - submenu = &app_instance->submenu_app_list_infrared; - view_id = FlipStoreViewAppListInfrared; - break; - case 4: - submenu = &app_instance->submenu_app_list_ibutton; - view_id = FlipStoreViewAppListiButton; - break; - case 5: - submenu = &app_instance->submenu_app_list_media; - view_id = FlipStoreViewAppListMedia; - break; - case 6: - submenu = &app_instance->submenu_app_list_nfc; - view_id = FlipStoreViewAppListNFC; - break; - case 7: - submenu = &app_instance->submenu_app_list_rfid; - view_id = FlipStoreViewAppListRFID; - break; - case 8: - submenu = &app_instance->submenu_app_list_subghz; - view_id = FlipStoreViewAppListSubGHz; - break; - case 9: - submenu = &app_instance->submenu_app_list_tools; - view_id = FlipStoreViewAppListTools; - break; - case 10: - submenu = &app_instance->submenu_app_list_usb; - view_id = FlipStoreViewAppListUSB; - break; - } - if (!submenu) - { - FURI_LOG_E(TAG, "Submenu is NULL"); - return "Failed to fetch app list."; - } - if (!easy_flipper_set_submenu(submenu, view_id, categories[flip_store_category_index], callback_to_app_list, &app_instance->view_dispatcher)) - { - return NULL; - } - - 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_instance); - } - else - { - break; - } - } - view_dispatcher_switch_to_view(app_instance->view_dispatcher, view_id); - return "Fetched app list successfully."; - } - else - { - FURI_LOG_E(TAG, "Failed to process the app list"); - return "Failed to fetch app list."; + FURI_LOG_E(TAG, "FlipperHTTP is NULL"); + return false; } -} -static void flip_store_switch_to_app_list(FlipStoreApp *app) -{ - flip_store_generic_switch_to_view(app, categories[flip_store_category_index], flip_store_fetch_app_list, flip_store_parse_app_list, 1, callback_to_submenu, FlipStoreViewLoader); -} -// -static bool flip_store_fetch_firmware(DataLoaderModel *model) -{ + model->fhttp->state = IDLE; if (model->request_index == 0) { firmware_free(); @@ -152,27 +54,27 @@ static bool flip_store_fetch_firmware(DataLoaderModel *model) { return false; } - firmware_request_success = flip_store_get_firmware_file( + return flip_store_get_firmware_file( + model->fhttp, firmwares[selected_firmware_index].links[0], firmwares[selected_firmware_index].name, strrchr(firmwares[selected_firmware_index].links[0], '/') + 1); - return firmware_request_success; } else if (model->request_index == 1) { - firmware_request_success_2 = flip_store_get_firmware_file( + return flip_store_get_firmware_file( + model->fhttp, firmwares[selected_firmware_index].links[1], firmwares[selected_firmware_index].name, strrchr(firmwares[selected_firmware_index].links[1], '/') + 1); - return firmware_request_success_2; } else if (model->request_index == 2) { - firmware_request_success_3 = flip_store_get_firmware_file( + return flip_store_get_firmware_file( + model->fhttp, firmwares[selected_firmware_index].links[2], firmwares[selected_firmware_index].name, strrchr(firmwares[selected_firmware_index].links[2], '/') + 1); - return firmware_request_success_3; } return false; } @@ -180,34 +82,59 @@ static char *flip_store_parse_firmware(DataLoaderModel *model) { if (model->request_index == 0) { - if (firmware_request_success) - { - return "File 1 installed."; - } + return "File 1 installed."; } else if (model->request_index == 1) { - if (firmware_request_success_2) - { - return "File 2 installed."; - } + return "File 2 installed."; } else if (model->request_index == 2) { - if (firmware_request_success_3) + return "Firmware downloaded successfully"; + } + return "Failed to download firmware."; +} +static void flip_store_switch_to_firmware_list(FlipStoreApp *app) +{ + flip_store_generic_switch_to_view(app, firmwares[selected_firmware_index].name, flip_store_fetch_firmware, flip_store_parse_firmware, FIRMWARE_LINKS, callback_to_firmware_list, FlipStoreViewLoader); +} +// +static bool flip_store_fetch_vgm_firmware(DataLoaderModel *model) +{ + if (!model->fhttp) + { + FURI_LOG_E(TAG, "FlipperHTTP is NULL"); + return false; + } + model->fhttp->state = IDLE; + if (model->request_index == 0) + { + vgm_firmware_free(); + vgm_firmwares = vgm_firmware_alloc(); + if (!vgm_firmwares) { - return "Firmware downloaded successfully"; + return false; } + return flip_store_get_firmware_file( + model->fhttp, + vgm_firmwares[selected_firmware_index].link, + vgm_firmwares[selected_firmware_index].name, + strrchr(vgm_firmwares[selected_firmware_index].link, '/') + 1); } - return NULL; + return false; } -static void flip_store_switch_to_firmware_list(FlipStoreApp *app) +static char *flip_store_parse_vgm_firmware(DataLoaderModel *model) { - flip_store_generic_switch_to_view(app, firmwares[selected_firmware_index].name, flip_store_fetch_firmware, flip_store_parse_firmware, FIRMWARE_LINKS, callback_to_submenu, FlipStoreViewLoader); + UNUSED(model); + return "Firmware downloaded successfully"; +} +static void flip_store_switch_to_vgm_firmware_list(FlipStoreApp *app) +{ + flip_store_generic_switch_to_view(app, vgm_firmwares[selected_firmware_index].name, flip_store_fetch_vgm_firmware, flip_store_parse_vgm_firmware, 1, callback_to_vgm_firmware_list, FlipStoreViewLoader); } // Function to draw the message on the canvas with word wrapping -void draw_description(Canvas *canvas, const char *description, int x, int y) +static void draw_description(Canvas *canvas, const char *description, int x, int y) { if (description == NULL || strlen(description) == 0) { @@ -267,13 +194,13 @@ void draw_description(Canvas *canvas, const char *description, int x, int y) } } -void flip_store_view_draw_callback_app_list(Canvas *canvas, void *model) +static 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); + char title[64]; + snprintf(title, 64, "%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); @@ -293,7 +220,7 @@ void flip_store_view_draw_callback_app_list(Canvas *canvas, void *model) canvas_draw_str_aligned(canvas, 97, 54, AlignLeft, AlignTop, "Install"); } -bool flip_store_input_callback(InputEvent *event, void *context) +static bool flip_store_input_callback(InputEvent *event, void *context) { FlipStoreApp *app = (FlipStoreApp *)context; if (!app) @@ -320,244 +247,752 @@ bool flip_store_input_callback(InputEvent *event, void *context) { if (event->key == InputKeyBack) { - // Back button clicked, switch to the previous view. - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); - return true; + // Back button clicked, switch to the previous view. + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppListCategory); + return true; + } + } + + return false; +} +static void free_text_input_view(FlipStoreApp *app); +static bool alloc_text_input_view(void *context, char *title); +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, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size); + + // Ensure null-termination + app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0'; + + // save the setting + save_char("WiFi-SSID", app->uart_text_input_buffer); + + // 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); + + // get value of password + char pass[64]; + if (load_char("WiFi-Password", pass, sizeof(pass))) + { + if (strlen(pass) > 0 && strlen(app->uart_text_input_buffer) > 0) + { + // save the settings + save_settings(app->uart_text_input_buffer, pass); + + // initialize the http + FlipperHTTP *fhttp = flipper_http_alloc(); + if (fhttp) + { + // save the wifi if the device is connected + if (!flipper_http_save_wifi(fhttp, app->uart_text_input_buffer, pass)) + { + easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed."); + } + + // free the resources + flipper_http_free(fhttp); + } + else + { + easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero."); + } + } + } + } + + // 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, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size); + + // Ensure null-termination + app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0'; + + // save the setting + save_char("WiFi-Password", app->uart_text_input_buffer); + + // 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); + } + + // get value of ssid + char ssid[64]; + if (load_char("WiFi-SSID", ssid, sizeof(ssid))) + { + if (strlen(ssid) > 0 && strlen(app->uart_text_input_buffer) > 0) + { + // save the settings + save_settings(ssid, app->uart_text_input_buffer); + + // initialize the http + FlipperHTTP *fhttp = flipper_http_alloc(); + if (fhttp) + { + // save the wifi if the device is connected + if (!flipper_http_save_wifi(fhttp, ssid, app->uart_text_input_buffer)) + { + easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed."); + } + + // free the resources + flipper_http_free(fhttp); + } + else + { + easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero."); + } + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); +} +static void flip_store_text_updated_repo(void *context); +static void flip_store_text_updated_author(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, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size); + + // Ensure null-termination + app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0'; + + // save the setting + save_char("Github-Author", app->uart_text_input_buffer); + + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenu); + text_input_reset(app->uart_text_input); + text_input_set_header_text(app->uart_text_input, "Repository"); + app->uart_text_input_buffer_size = 64; + free(app->uart_text_input_buffer); + free(app->uart_text_input_temp_buffer); + easy_flipper_set_buffer(&app->uart_text_input_buffer, app->uart_text_input_buffer_size); + easy_flipper_set_buffer(&app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size); + text_input_set_result_callback(app->uart_text_input, flip_store_text_updated_repo, app, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, false); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewTextInput); +} + +static bool flip_store_fetch_github(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(TAG, "FlipperHTTP is NULL"); + return false; + } + char author[64]; + char repo[64]; + if (!load_char("Github-Author", author, sizeof(author)) || !load_char("Github-Repo", repo, sizeof(repo))) + { + FURI_LOG_E(TAG, "Failed to load Github author or repo"); + return false; + } + return flip_store_get_github_contents(fhttp, author, repo); +} + +static bool flip_store_parse_github(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(TAG, "FlipperHTTP is NULL"); + return false; + } + char author[64]; + char repo[64]; + if (!load_char("Github-Author", author, sizeof(author)) || !load_char("Github-Repo", repo, sizeof(repo))) + { + FURI_LOG_E(TAG, "Failed to load Github author or repo"); + return false; + } + if (!flip_store_parse_github_contents(fhttp->file_path, author, repo)) + { + return false; + } + return flip_store_install_all_github_files(fhttp, author, repo); +} +static bool github_success = false; +static void flip_store_get_github_repository(FlipStoreApp *app) +{ + FlipperHTTP *fhttp = flipper_http_alloc(); + if (!fhttp) + { + FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP"); + return; + } + if (!app) + { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + bool http_request() + { + github_success = flip_store_fetch_github(fhttp); + return github_success; + } + bool http_parse() + { + github_success = flip_store_parse_github(fhttp); + return github_success; + } + flipper_http_loading_task(fhttp, http_request, http_parse, FlipStoreViewSubmenuOptions, FlipStoreViewWidgetResult, &app->view_dispatcher); + flipper_http_free(fhttp); + if (github_success) + { + easy_flipper_dialog("Success", "Repository downloaded\nsuccessfully."); + } + else + { + easy_flipper_dialog("Failure", "Failed to download\nrepository."); + } +} +static void flip_store_text_updated_repo(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, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size); + + // Ensure null-termination + app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0'; + + // save the setting + save_char("Github-Repo", app->uart_text_input_buffer); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenuOptions); + flip_store_get_github_repository(app); +} +static void free_category_submenu(FlipStoreApp *app) +{ + if (!app) + { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + if (app->submenu_app_list_category) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListCategory); + submenu_free(app->submenu_app_list_category); + app->submenu_app_list_category = NULL; + } +} +static void free_variable_item_list(FlipStoreApp *app); + +uint32_t callback_to_submenu(void *context) +{ + UNUSED(context); + firmware_free(); + return FlipStoreViewSubmenu; +} + +uint32_t callback_to_firmware_list(void *context) +{ + UNUSED(context); + return FlipStoreViewFirmwares; +} +uint32_t callback_to_vgm_firmware_list(void *context) +{ + UNUSED(context); + return FlipStoreViewVGMFirmwares; +} +static uint32_t callback_to_app_category_list(void *context) +{ + UNUSED(context); + return FlipStoreViewAppListCategory; +} +uint32_t callback_to_app_list(void *context) +{ + UNUSED(context); + return FlipStoreViewAppList; +} + +static uint32_t callback_to_wifi_settings(void *context) +{ + UNUSED(context); + return FlipStoreViewSettings; +} +static void dialog_firmware_callback(DialogExResult result, void *context) +{ + FlipStoreApp *app = (FlipStoreApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + if (result == DialogExResultLeft) // No + { + // switch to the firmware list + if (is_esp32_firmware) + { + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwares); + } + else + { + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewVGMFirmwares); + } + } + else if (result == DialogExResultRight) + { + // download the firmware then return to the firmware list + if (is_esp32_firmware) + { + flip_store_switch_to_firmware_list(app); + } + else + { + flip_store_switch_to_vgm_firmware_list(app); + } + } +} + +static bool alloc_about_view(FlipStoreApp *app) +{ + if (!app) + { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return false; + } + if (!app->widget_about) + { + if (!easy_flipper_set_widget( + &app->widget_about, + FlipStoreViewAbout, + "Welcome to the FlipStore!\n------\nDownload apps via WiFi and\nrun them on your Flipper!\n------\nwww.github.com/jblanked", + callback_to_submenu, + &app->view_dispatcher)) + { + return false; + } + if (!app->widget_about) + { + return false; + } + } + return true; +} + +static void free_about_view(FlipStoreApp *app) +{ + if (app && app->widget_about != NULL) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAbout); + widget_free(app->widget_about); + app->widget_about = NULL; + } +} +static bool alloc_text_input_view(void *context, char *title) +{ + FlipStoreApp *app = (FlipStoreApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return false; + } + if (!title) + { + FURI_LOG_E(TAG, "Title is NULL"); + return false; + } + app->uart_text_input_buffer_size = 64; + if (!app->uart_text_input_buffer) + { + if (!easy_flipper_set_buffer(&app->uart_text_input_buffer, app->uart_text_input_buffer_size)) + { + return false; + } + } + if (!app->uart_text_input_temp_buffer) + { + if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size)) + { + return false; + } + } + if (!app->uart_text_input) + { + if (strcmp(title, "SSID") != 0 && strcmp(title, "Password") != 0) + { + // Github repository download + if (!easy_flipper_set_uart_text_input( + &app->uart_text_input, + FlipStoreViewTextInput, + title, + app->uart_text_input_temp_buffer, + app->uart_text_input_buffer_size, + strcmp(title, "Author") == 0 ? flip_store_text_updated_author : flip_store_text_updated_repo, + callback_to_submenu_options, + &app->view_dispatcher, + app)) + { + return false; + } + if (!app->uart_text_input) + { + return false; + } + } + else + { + if (!easy_flipper_set_uart_text_input( + &app->uart_text_input, + FlipStoreViewTextInput, + title, + app->uart_text_input_temp_buffer, + app->uart_text_input_buffer_size, + strcmp(title, "SSID") == 0 ? flip_store_text_updated_ssid : flip_store_text_updated_pass, + callback_to_wifi_settings, + &app->view_dispatcher, + app)) + { + return false; + } + if (!app->uart_text_input) + { + return false; + } + char ssid[64]; + char pass[64]; + if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass))) + { + if (strcmp(title, "SSID") == 0) + { + strncpy(app->uart_text_input_temp_buffer, ssid, app->uart_text_input_buffer_size); + } + else + { + strncpy(app->uart_text_input_temp_buffer, pass, app->uart_text_input_buffer_size); + } + } + } + } + return true; +} +static void free_text_input_view(FlipStoreApp *app) +{ + if (!app) + { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + if (app->uart_text_input) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewTextInput); + text_input_free(app->uart_text_input); + app->uart_text_input = NULL; + } + if (app->uart_text_input_buffer) + { + free(app->uart_text_input_buffer); + app->uart_text_input_buffer = NULL; + } + if (app->uart_text_input_temp_buffer) + { + free(app->uart_text_input_temp_buffer); + app->uart_text_input_temp_buffer = NULL; + } +} +static void settings_item_selected(void *context, uint32_t index); +static bool alloc_variable_item_list(void *context) +{ + FlipStoreApp *app = (FlipStoreApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return false; + } + if (!app->variable_item_list) + { + if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipStoreViewSettings, settings_item_selected, callback_to_submenu, &app->view_dispatcher, app)) + return false; + + if (!app->variable_item_list) + return false; + + if (!app->variable_item_ssid) + { + app->variable_item_ssid = variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL); + variable_item_set_current_value_text(app->variable_item_ssid, ""); + } + if (!app->variable_item_pass) + { + app->variable_item_pass = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL); + variable_item_set_current_value_text(app->variable_item_pass, ""); + } + char ssid[64]; + char pass[64]; + if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass))) + { + variable_item_set_current_value_text(app->variable_item_ssid, ssid); + // variable_item_set_current_value_text(app->variable_item_pass, pass); + save_char("WiFi-SSID", ssid); + save_char("WiFi-Password", pass); } } - - return false; + return true; } - -void flip_store_text_updated_ssid(void *context) +static void free_variable_item_list(FlipStoreApp *app) { - 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_list) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSettings); + variable_item_list_free(app->variable_item_list); + app->variable_item_list = NULL; + } if (app->variable_item_ssid) { - variable_item_set_current_value_text(app->variable_item_ssid, app->uart_text_input_buffer_ssid); + free(app->variable_item_ssid); + app->variable_item_ssid = NULL; } - - // 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) + if (app->variable_item_pass) { - // 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"); - } + free(app->variable_item_pass); + app->variable_item_pass = NULL; } - - // switch to the settings view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); } -void flip_store_text_updated_pass(void *context) +static bool alloc_dialog_firmware(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); + return false; } - // 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)) + if (!app->dialog_firmware) + { + if (!easy_flipper_set_dialog_ex( + &app->dialog_firmware, + FlipStoreViewFirmwareDialog, + is_esp32_firmware ? "Download ESP32 Firmware" : "Download VGM 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 false; + } + if (!app->dialog_firmware) + { + return false; + } + if (is_esp32_firmware) { - FURI_LOG_E(TAG, "Failed to save WiFi settings"); + dialog_ex_set_header(app->dialog_firmware, firmwares[selected_firmware_index].name, 0, 0, AlignLeft, AlignTop); + } + else + { + dialog_ex_set_header(app->dialog_firmware, vgm_firmwares[selected_firmware_index].name, 0, 0, AlignLeft, AlignTop); } } - - // switch to the settings view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); + return true; } - -uint32_t callback_to_submenu(void *context) +static void free_dialog_firmware(FlipStoreApp *app) { - if (!context) + if (app && app->dialog_firmware) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewFirmwareDialog); + dialog_ex_free(app->dialog_firmware); + app->dialog_firmware = NULL; } - UNUSED(context); - firmware_free(); - return FlipStoreViewSubmenu; } - -uint32_t callback_to_submenu_options(void *context) +static bool alloc_app_info_view(FlipStoreApp *app) { - if (!context) + if (!app) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return false; } - UNUSED(context); - firmware_free(); - return FlipStoreViewSubmenuOptions; -} - -uint32_t callback_to_firmware_list(void *context) -{ - if (!context) + if (!app->view_app_info) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; + if (!easy_flipper_set_view( + &app->view_app_info, + FlipStoreViewAppInfo, + flip_store_view_draw_callback_app_list, + flip_store_input_callback, + callback_to_app_category_list, + &app->view_dispatcher, + app)) + { + return false; + } + if (!app->view_app_info) + { + return false; + } } - 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; + return true; } - -uint32_t callback_to_app_list(void *context) +static void free_app_info_view(FlipStoreApp *app) { - if (!context) + if (!app) + { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + if (app->view_app_info) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppInfo); + view_free(app->view_app_info); + app->view_app_info = NULL; } - 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) +uint32_t callback_to_submenu_options(void *context) { 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; + return FlipStoreViewSubmenuOptions; } + firmware_free(); + vgm_firmware_free(); + flip_catalog_free(); + free_category_submenu(app); + return FlipStoreViewSubmenuOptions; } - -void dialog_delete_callback(DialogExResult result, void *context) +void free_all_views(FlipStoreApp *app, bool should_free_variable_item_list) { - furi_assert(context); - FlipStoreApp *app = (FlipStoreApp *)context; - if (result == DialogExResultLeft) // No + if (!app) { - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; } - else if (result == DialogExResultRight) + free_about_view(app); + flip_catalog_free(); + if (should_free_variable_item_list) { - // 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); - } + free_variable_item_list(app); } + free_category_submenu(app); + free_text_input_view(app); + free_dialog_firmware(app); + free_app_info_view(app); + firmware_free(); + vgm_firmware_free(); +} +uint32_t callback_exit_app(void *context) +{ + UNUSED(context); + return VIEW_NONE; // Return VIEW_NONE to exit the app } -void dialog_firmware_callback(DialogExResult result, void *context) +static bool set_appropriate_list(FlipperHTTP *fhttp, FlipStoreApp *app) { - furi_assert(context); - FlipStoreApp *app = (FlipStoreApp *)context; - if (result == DialogExResultLeft) // No + if (!fhttp || !app) { - // switch to the firmware list - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwares); + FURI_LOG_E(TAG, "FlipperHTTP oor app is NULL"); + return false; } - else if (result == DialogExResultRight) + + if (!easy_flipper_set_submenu(&app->submenu_app_list_category, FlipStoreViewAppListCategory, categories[flip_store_category_index], callback_to_app_list, &app->view_dispatcher)) { - // download the firmware then return to the firmware list - flip_store_switch_to_firmware_list(app); + FURI_LOG_E(TAG, "Failed to set submenu"); + return false; } -} -void popup_callback(void *context) -{ - FlipStoreApp *app = (FlipStoreApp *)context; - if (!app) + if (flip_store_process_app_list(fhttp) && app->submenu_app_list_category && flip_catalog) { - FURI_LOG_E(TAG, "FlipStoreApp is NULL"); - return; + submenu_reset(app->submenu_app_list_category); + submenu_set_header(app->submenu_app_list_category, categories[flip_store_category_index]); + // add each app name to submenu + for (size_t i = 0; i < flip_catalog->count; i++) + { + if (strlen(flip_catalog[i].app_name) > 0) + { + submenu_add_item(app->submenu_app_list_category, flip_catalog[i].app_name, FlipStoreSubmenuIndexStartAppList + i, callback_submenu_choices, app); + } + else + { + break; + } + } + // add [LOAD NEXT] to submenu + submenu_add_item(app->submenu_app_list_category, "[LOAD NEXT]", FlipStoreSubmenuIndexStartAppList + flip_catalog->count, callback_submenu_choices, app); + return true; } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenu); + FURI_LOG_E(TAG, "Failed to process the app list"); + return false; } -uint32_t callback_exit_app(void *context) +// we'll have to loop 8 at a time due to memory constraints +static void fetch_appropiate_app_list(FlipStoreApp *app, int iteration) { - // Exit the application - if (!context) + FlipperHTTP *fhttp = flipper_http_alloc(); + if (!fhttp) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; + FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP"); + return; } - UNUSED(context); - return VIEW_NONE; // Return VIEW_NONE to exit the app + bool fetch_app_list() + { + // ensure /apps_data/flip_store/data exists + Storage *storage = furi_record_open(RECORD_STORAGE); + char dir[256]; + snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data"); + storage_common_mkdir(storage, dir); + furi_record_close(RECORD_STORAGE); + fhttp->state = IDLE; + flip_catalog_free(); + snprintf( + fhttp->file_path, + sizeof(fhttp->file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s.json", categories[flip_store_category_index]); + fhttp->save_received_data = true; + fhttp->is_bytes_request = false; + char url[256]; + // load 8 at a time + snprintf(url, sizeof(url), "https://catalog.flipperzero.one/api/v0/0/application?limit=8&is_latest_release_version=true&offset=%d&sort_by=updated_at&sort_order=-1&category_id=%s", iteration, category_ids[flip_store_category_index]); + return flipper_http_get_request_with_headers(fhttp, url, "{\"Content-Type\":\"application/json\"}"); + } + bool parse_app_list() + { + return set_appropriate_list(fhttp, app); + } + flipper_http_loading_task(fhttp, fetch_app_list, parse_app_list, FlipStoreViewAppListCategory, FlipStoreViewSubmenuOptions, &app->view_dispatcher); + flipper_http_free(fhttp); } void callback_submenu_choices(void *context, uint32_t index) @@ -568,15 +1003,29 @@ void callback_submenu_choices(void *context, uint32_t index) FURI_LOG_E(TAG, "FlipStoreApp is NULL"); return; } + switch (index) { case FlipStoreSubmenuIndexAbout: + free_all_views(app, true); + if (!alloc_about_view(app)) + { + FURI_LOG_E(TAG, "Failed to set about view"); + return; + } view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAbout); break; case FlipStoreSubmenuIndexSettings: + free_all_views(app, true); + if (!alloc_variable_item_list(app)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); break; case FlipStoreSubmenuIndexOptions: + free_all_views(app, true); view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenuOptions); break; case FlipStoreSubmenuIndexAppList: @@ -584,12 +1033,7 @@ void callback_submenu_choices(void *context, uint32_t index) 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; - } + case FlipStoreSubmenuIndexFirmwares: // esp32 firmwares firmwares = firmware_alloc(); if (firmwares == NULL) { @@ -604,64 +1048,99 @@ void callback_submenu_choices(void *context, uint32_t index) } view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwares); break; + case FlipStoreSubmenuIndexVGMFirmwares: // vgm firmwares + vgm_firmwares = vgm_firmware_alloc(); + if (vgm_firmwares == NULL) + { + FURI_LOG_E(TAG, "Failed to allocate memory for vgm firmwares"); + return; + } + submenu_reset(app->submenu_vgm_firmwares); + submenu_set_header(app->submenu_vgm_firmwares, "VGM Firmwares"); + for (int i = 0; i < VGM_FIRMWARE_COUNT; i++) + { + submenu_add_item(app->submenu_vgm_firmwares, vgm_firmwares[i].name, FlipStoreSubmenuIndexStartVGMFirmwares + i, callback_submenu_choices, app); + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewVGMFirmwares); + break; + case FlipStoreSubmenuIndexGitHub: // github + free_all_views(app, true); + if (!alloc_text_input_view(app, "Author")) + { + FURI_LOG_E(TAG, "Failed to allocate text input view"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewTextInput); + break; case FlipStoreSubmenuIndexAppListBluetooth: + free_all_views(app, true); flip_store_category_index = 0; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; case FlipStoreSubmenuIndexAppListGames: + free_all_views(app, true); flip_store_category_index = 1; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; case FlipStoreSubmenuIndexAppListGPIO: + free_all_views(app, true); flip_store_category_index = 2; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; case FlipStoreSubmenuIndexAppListInfrared: + free_all_views(app, true); flip_store_category_index = 3; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; case FlipStoreSubmenuIndexAppListiButton: + free_all_views(app, true); flip_store_category_index = 4; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; case FlipStoreSubmenuIndexAppListMedia: + free_all_views(app, true); flip_store_category_index = 5; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; case FlipStoreSubmenuIndexAppListNFC: + free_all_views(app, true); flip_store_category_index = 6; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; case FlipStoreSubmenuIndexAppListRFID: + free_all_views(app, true); flip_store_category_index = 7; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; case FlipStoreSubmenuIndexAppListSubGHz: + free_all_views(app, true); flip_store_category_index = 8; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; case FlipStoreSubmenuIndexAppListTools: + free_all_views(app, true); flip_store_category_index = 9; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; case FlipStoreSubmenuIndexAppListUSB: + free_all_views(app, true); flip_store_category_index = 10; flip_store_app_does_exist = false; - flip_store_switch_to_app_list(app); + fetch_appropiate_app_list(app, 0); break; default: - // Check if the index is within the firmwares list range - if (index >= FlipStoreSubmenuIndexStartFirmwares && index < FlipStoreSubmenuIndexStartFirmwares + 3) + // Check if the index is within the ESP32 firmwares list range + if (index >= FlipStoreSubmenuIndexStartFirmwares && index < FlipStoreSubmenuIndexStartFirmwares + FIRMWARE_COUNT) { // Get the firmware index uint32_t firmware_index = index - FlipStoreSubmenuIndexStartFirmwares; @@ -671,27 +1150,59 @@ void callback_submenu_choices(void *context, uint32_t index) { // Get the firmware name selected_firmware_index = firmware_index; + is_esp32_firmware = true; + + // Switch to the firmware download view + free_dialog_firmware(app); + if (!alloc_dialog_firmware(app)) + { + FURI_LOG_E(TAG, "Failed to allocate dialog firmware"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwareDialog); + } + else + { + FURI_LOG_E(TAG, "Invalid firmware index"); + easy_flipper_dialog("Error", "Issue parsing firmware."); + } + } + // Check if the index is within the VGM firmwares list range + else if (index >= FlipStoreSubmenuIndexStartVGMFirmwares && index < FlipStoreSubmenuIndexStartVGMFirmwares + VGM_FIRMWARE_COUNT) + { + // Get the firmware index + uint32_t firmware_index = index - FlipStoreSubmenuIndexStartVGMFirmwares; + + // Check if the firmware index is valid + if ((int)firmware_index >= 0 && firmware_index < VGM_FIRMWARE_COUNT) + { + // Get the firmware name + selected_firmware_index = firmware_index; + is_esp32_firmware = false; // Switch to the firmware download view - dialog_ex_set_header(app->dialog_firmware, firmwares[firmware_index].name, 0, 0, AlignLeft, AlignTop); + free_dialog_firmware(app); + if (!alloc_dialog_firmware(app)) + { + FURI_LOG_E(TAG, "Failed to allocate dialog firmware"); + return; + } 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); + easy_flipper_dialog("Error", "Issue parsing firmware."); } } // Check if the index is within the app list range - else if (index >= FlipStoreSubmenuIndexStartAppList && index < FlipStoreSubmenuIndexStartAppList + MAX_APP_COUNT) + else if (index >= FlipStoreSubmenuIndexStartAppList && index < FlipStoreSubmenuIndexStartAppList + flip_catalog->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) + if ((int)app_index >= 0 && app_index < flip_catalog->count) { // Get the app name char *app_name = flip_catalog[app_index].app_name; @@ -701,6 +1212,12 @@ void callback_submenu_choices(void *context, uint32_t index) { app_selected_index = app_index; flip_store_app_does_exist = app_exists(flip_catalog[app_selected_index].app_id, categories[flip_store_category_index]); + free_app_info_view(app); + if (!alloc_app_info_view(app)) + { + FURI_LOG_E(TAG, "Failed to allocate app info view"); + return; + } view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppInfo); } else @@ -713,6 +1230,15 @@ void callback_submenu_choices(void *context, uint32_t index) FURI_LOG_E(TAG, "Invalid app index"); } } + // Check if the index is for loading the next set of apps + else if (index == FlipStoreSubmenuIndexStartAppList + flip_catalog->count) + { + catalog_iteration = flip_catalog->iteration + 8; + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewWidgetResult); + free_category_submenu(app); + flip_catalog_free(); + fetch_appropiate_app_list(app, catalog_iteration); + } else { FURI_LOG_E(TAG, "Unknown submenu index"); @@ -721,6 +1247,55 @@ void callback_submenu_choices(void *context, uint32_t index) } } +static void settings_item_selected(void *context, uint32_t index) +{ + FlipStoreApp *app = (FlipStoreApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return; + } + char ssid[64]; + char pass[64]; + switch (index) + { + case 0: // Input SSID + // Text Input + free_all_views(app, false); + if (!alloc_text_input_view(app, "SSID")) + { + FURI_LOG_E(TAG, "Failed to allocate text input view"); + return; + } + // load SSID + if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass))) + { + strncpy(app->uart_text_input_temp_buffer, ssid, app->uart_text_input_buffer_size - 1); + app->uart_text_input_temp_buffer[app->uart_text_input_buffer_size - 1] = '\0'; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewTextInput); + break; + case 1: // Input Password + free_all_views(app, false); + if (!alloc_text_input_view(app, "Password")) + { + FURI_LOG_E(TAG, "Failed to allocate text input view"); + return; + } + // load password + if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass))) + { + strncpy(app->uart_text_input_temp_buffer, pass, app->uart_text_input_buffer_size - 1); + app->uart_text_input_temp_buffer[app->uart_text_input_buffer_size - 1] = '\0'; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewTextInput); + break; + default: + FURI_LOG_E(TAG, "Unknown configuration item index"); + break; + } +} + static void flip_store_widget_set_text(char *message, Widget **widget) { if (widget == NULL) @@ -823,6 +1398,50 @@ static void flip_store_widget_set_text(char *message, Widget **widget) // Add the formatted message to the widget widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message); } +static void flip_store_request_error(Canvas *canvas, FlipperHTTP *fhttp) +{ + if (!canvas) + { + FURI_LOG_E(TAG, "Canvas is NULL"); + return; + } + if (!fhttp) + { + FURI_LOG_E(TAG, "FlipperHTTP is NULL"); + 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 + { + 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."); + } +} void flip_store_loader_draw_callback(Canvas *canvas, void *model) { @@ -832,8 +1451,8 @@ void flip_store_loader_draw_callback(Canvas *canvas, void *model) return; } - SerialState http_state = fhttp.state; DataLoaderModel *data_loader_model = (DataLoaderModel *)model; + SerialState http_state = data_loader_model->fhttp->state; DataState data_state = data_loader_model->data_state; char *title = data_loader_model->title; @@ -841,7 +1460,7 @@ void flip_store_loader_draw_callback(Canvas *canvas, void *model) if (http_state == INACTIVE) { - canvas_draw_str(canvas, 0, 7, "WiFi Dev Board disconnected."); + 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"); @@ -852,7 +1471,7 @@ void flip_store_loader_draw_callback(Canvas *canvas, void *model) if (data_state == DataStateError || data_state == DataStateParseError) { - flip_store_request_error(canvas); + flip_store_request_error(canvas, data_loader_model->fhttp); return; } @@ -902,7 +1521,14 @@ static void flip_store_loader_process_callback(void *context) View *view = app->view_loader; DataState current_data_state; - with_view_model(view, DataLoaderModel * model, { current_data_state = model->data_state; }, false); + DataLoaderModel *loader_model = NULL; + with_view_model(view, DataLoaderModel * model, { current_data_state = model->data_state; loader_model = model; }, false); + if (!loader_model || !loader_model->fhttp) + { + FURI_LOG_E(TAG, "Model or fhttp is NULL"); + DEV_CRASH(); + return; + } if (current_data_state == DataStateInitial) { @@ -920,7 +1546,7 @@ static void flip_store_loader_process_callback(void *context) } // Clear any previous responses - strncpy(fhttp.last_response, "", 1); + strncpy(model->fhttp->last_response, "", 1); bool request_status = fetch(model); if (!request_status) { @@ -931,21 +1557,21 @@ static void flip_store_loader_process_callback(void *context) } else if (current_data_state == DataStateRequested || current_data_state == DataStateError) { - if (fhttp.state == IDLE && fhttp.last_response != NULL) + if (loader_model->fhttp->state == IDLE && loader_model->fhttp->last_response != NULL) { - if (strstr(fhttp.last_response, "[PONG]") != NULL) + if (strstr(loader_model->fhttp->last_response, "[PONG]") != NULL) { FURI_LOG_DEV(TAG, "PONG received."); } - else if (strncmp(fhttp.last_response, "[SUCCESS]", 9) == 0) + else if (strncmp(loader_model->fhttp->last_response, "[SUCCESS]", 9) == 0) { - FURI_LOG_DEV(TAG, "SUCCESS received. %s", fhttp.last_response ? fhttp.last_response : "NULL"); + FURI_LOG_DEV(TAG, "SUCCESS received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL"); } - else if (strncmp(fhttp.last_response, "[ERROR]", 9) == 0) + else if (strncmp(loader_model->fhttp->last_response, "[ERROR]", 9) == 0) { - FURI_LOG_DEV(TAG, "ERROR received. %s", fhttp.last_response ? fhttp.last_response : "NULL"); + FURI_LOG_DEV(TAG, "ERROR received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL"); } - else if (strlen(fhttp.last_response) == 0) + else if (strlen(loader_model->fhttp->last_response) == 0) { // Still waiting on response } @@ -954,21 +1580,21 @@ static void flip_store_loader_process_callback(void *context) with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateReceived; }, true); } } - else if (fhttp.state == SENDING || fhttp.state == RECEIVING) + else if (loader_model->fhttp->state == SENDING || loader_model->fhttp->state == RECEIVING) { // continue waiting } - else if (fhttp.state == INACTIVE) + else if (loader_model->fhttp->state == INACTIVE) { // inactive. try again } - else if (fhttp.state == ISSUE) + else if (loader_model->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"); + FURI_LOG_DEV(TAG, "Unexpected state: %d lastresp: %s", loader_model->fhttp->state, loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL"); DEV_CRASH(); } } @@ -989,7 +1615,7 @@ static void flip_store_loader_process_callback(void *context) { 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"); + FURI_LOG_DEV(TAG, "Parsed data: %s\r\ntext: %s", model->fhttp->last_response ? model->fhttp->last_response : "NULL", data_text ? data_text : "NULL"); model->data_text = data_text; if (data_text == NULL) { @@ -1014,7 +1640,7 @@ static void flip_store_loader_process_callback(void *context) } else { - flip_store_widget_set_text(model->data_text != NULL ? model->data_text : "Empty result", &app->widget_result); + flip_store_widget_set_text(model->data_text != NULL ? model->data_text : "", &app->widget_result); if (model->data_text != NULL) { free(model->data_text); @@ -1118,8 +1744,14 @@ void flip_store_loader_free_model(View *view) } if (model->parser_context) { - free(model->parser_context); - model->parser_context = NULL; + // do not free the context here, it is the app context + // free(model->parser_context); + // model->parser_context = NULL; + } + if (model->fhttp) + { + flipper_http_free(model->fhttp); + model->fhttp = NULL; } }, false); @@ -1174,6 +1806,12 @@ void flip_store_generic_switch_to_view(FlipStoreApp *app, char *title, DataLoade model->back_callback = back; model->data_state = DataStateInitial; model->data_text = NULL; + // + model->parser_context = app; + if (!model->fhttp) + { + model->fhttp = flipper_http_alloc(); + } }, true); diff --git a/flip_store/callback/flip_store_callback.h b/flip_store/callback/flip_store_callback.h index 68b2d0d41..1d40d6b2d 100644 --- a/flip_store/callback/flip_store_callback.h +++ b/flip_store/callback/flip_store_callback.h @@ -15,35 +15,20 @@ extern bool flip_store_app_does_exist; extern uint32_t selected_firmware_index; -// 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_vgm_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); +void free_all_views(FlipStoreApp *app, bool should_free_variable_item_list); + // Add edits by Derek Jamison typedef enum DataState DataState; enum DataState @@ -77,6 +62,7 @@ struct DataLoaderModel size_t request_count; ViewNavigationCallback back_callback; FuriTimer *timer; + FlipperHTTP *fhttp; }; void flip_store_generic_switch_to_view(FlipStoreApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id); diff --git a/flip_store/easy_flipper/easy_flipper.c b/flip_store/easy_flipper/easy_flipper.c index f61b7d231..c2ce949e4 100644 --- a/flip_store/easy_flipper/easy_flipper.c +++ b/flip_store/easy_flipper/easy_flipper.c @@ -1,5 +1,25 @@ #include +void easy_flipper_dialog( + char *header, + char *text) +{ + DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage *message = dialog_message_alloc(); + dialog_message_set_header( + message, header, 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + message, + text, + 0, + 63, + AlignLeft, + AlignBottom); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); +} + /** * @brief Navigation callback for exiting the application * @param context The context - unused diff --git a/flip_store/easy_flipper/easy_flipper.h b/flip_store/easy_flipper/easy_flipper.h index 3ccfe66b8..875cca896 100644 --- a/flip_store/easy_flipper/easy_flipper.h +++ b/flip_store/easy_flipper/easy_flipper.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,8 @@ #include #include #include +#include +#include #include #include #include @@ -23,6 +26,10 @@ #define EASY_TAG "EasyFlipper" +void easy_flipper_dialog( + char *header, + char *text); + /** * @brief Navigation callback for exiting the application * @param context The context - unused diff --git a/flip_store/firmwares/flip_store_firmwares.c b/flip_store/firmwares/flip_store_firmwares.c index 5977d2a52..1b089414b 100644 --- a/flip_store/firmwares/flip_store_firmwares.c +++ b/flip_store/firmwares/flip_store_firmwares.c @@ -1,17 +1,8 @@ #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; +VGMFirmware *vgm_firmwares = NULL; +bool is_esp32_firmware = true; Firmware *firmware_alloc() { @@ -21,93 +12,95 @@ Firmware *firmware_alloc() 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"; + snprintf(fw[0].name, sizeof(fw[0].name), "%s", "Black Magic"); + snprintf(fw[0].links[0], sizeof(fw[0].links[0]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/bootloader.bin"); + snprintf(fw[0].links[1], sizeof(fw[0].links[1]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/partition-table.bin"); + snprintf(fw[0].links[2], sizeof(fw[0].links[2]), "%s", "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"; + snprintf(fw[1].name, sizeof(fw[1].name), "%s", "FlipperHTTP"); + snprintf(fw[1].links[0], sizeof(fw[1].links[0]), "%s", "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_bootloader.bin"); + snprintf(fw[1].links[1], sizeof(fw[1].links[1]), "%s", "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_firmware_a.bin"); + snprintf(fw[1].links[2], sizeof(fw[1].links[2]), "%s", "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_1_0_20241128_flipper.bin"; + snprintf(fw[2].name, sizeof(fw[2].name), "%s", "Marauder"); + snprintf(fw[2].links[0], sizeof(fw[2].links[0]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.bootloader.bin"); + snprintf(fw[2].links[1], sizeof(fw[2].links[1]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.partitions.bin"); + snprintf(fw[2].links[2], sizeof(fw[2].links[2]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/CURRENT/esp32_marauder_v1_2_0_12192024_flipper.bin"); + + return fw; +} - // 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 +VGMFirmware *vgm_firmware_alloc() +{ + VGMFirmware *fw = (VGMFirmware *)malloc(VGM_FIRMWARE_COUNT * sizeof(VGMFirmware)); + if (!fw) + { + FURI_LOG_E(TAG, "Failed to allocate memory for VGM Firmware"); + return NULL; + } - // 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 + // FlipperHTTP + snprintf(fw[0].name, sizeof(fw[0].name), "%s", "FlipperHTTP"); + snprintf(fw[0].link, sizeof(fw[0].link), "%s", "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/Video%20Game%20Module/MicroPython/flipper_http_vgm_micro_python.uf2"); - // 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); + firmwares = NULL; + } +} +void vgm_firmware_free() +{ + if (vgm_firmwares) + { + free(vgm_firmwares); + vgm_firmwares = NULL; } } -bool flip_store_get_firmware_file(char *link, char *name, char *filename) +bool flip_store_get_firmware_file(FlipperHTTP *fhttp, char *link, char *name, char *filename) { - if (fhttp.state == INACTIVE) + if (!fhttp) { + FURI_LOG_E(TAG, "FlipperHTTP is NULL"); return false; } + 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) + // save in ESP32 flasher directory + if (is_esp32_firmware) + { + 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); + } + else // install in app_data directory { - fhttp.state = RECEIVING; - return true; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/vgm"); + storage_common_mkdir(storage, directory_path); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/vgm/%s", name); + storage_common_mkdir(storage, directory_path); + snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/vgm/%s/%s", name, filename); } - fhttp.state = ISSUE; - return false; + furi_record_close(RECORD_STORAGE); + fhttp->save_received_data = false; + fhttp->is_bytes_request = true; + return flipper_http_get_request_bytes(fhttp, link, "{\"Content-Type\":\"application/octet-stream\"}"); } diff --git a/flip_store/firmwares/flip_store_firmwares.h b/flip_store/firmwares/flip_store_firmwares.h index 923c07c21..6b5770fb5 100644 --- a/flip_store/firmwares/flip_store_firmwares.h +++ b/flip_store/firmwares/flip_store_firmwares.h @@ -7,25 +7,25 @@ typedef struct { - char *name; - char *links[FIRMWARE_LINKS]; + char name[16]; + char links[FIRMWARE_LINKS][256]; } Firmware; +typedef struct +{ + char name[16]; + char link[256]; +} VGMFirmware; + extern Firmware *firmwares; +extern VGMFirmware *vgm_firmwares; Firmware *firmware_alloc(); +VGMFirmware *vgm_firmware_alloc(); void firmware_free(); +void vgm_firmware_free(); +extern bool is_esp32_firmware; // 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; +bool flip_store_get_firmware_file(FlipperHTTP *fhttp, char *link, char *name, char *filename); #endif // FLIP_STORE_FIRMWARES_H \ No newline at end of file diff --git a/flip_store/flip_storage/flip_store_storage.c b/flip_store/flip_storage/flip_store_storage.c index 4e382b786..1db29732b 100644 --- a/flip_store/flip_storage/flip_store_storage.c +++ b/flip_store/flip_storage/flip_store_storage.c @@ -315,4 +315,132 @@ bool parse_json_incrementally(const char *file_path, const char *target_key, cha furi_record_close(RECORD_STORAGE); } return false; +} + +bool save_char( + const char *path_name, const char *value) +{ + if (!value) + { + return false; + } + // Create the directory for saving settings + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + + // Open the settings file + File *file = storage_file_alloc(storage); + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s.txt", path_name); + + // 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; + } + + // Write the data to the file + size_t data_size = strlen(value) + 1; // Include null terminator + if (storage_file_write(file, value, 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; +} + +bool load_char( + const char *path_name, + char *value, + size_t value_size) +{ + if (!value) + { + return false; + } + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); + + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s.txt", path_name); + + // 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 + } + + // Read data into the buffer + size_t read_count = storage_file_read(file, value, value_size); + if (storage_file_get_error(file) != FSE_OK) + { + FURI_LOG_E(HTTP_TAG, "Error reading from file."); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + + // Ensure null-termination + value[read_count - 1] = '\0'; + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + return true; +} + +bool save_char_with_path(const char *full_path, const char *value) +{ + if (!value) + { + return false; + } + + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); + + // Open the file in write mode + if (!storage_file_open(file, full_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) + { + FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", full_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + + // Write the data to the file + size_t data_size = strlen(value) + 1; // Include null terminator + if (storage_file_write(file, value, 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; } \ No newline at end of file diff --git a/flip_store/flip_storage/flip_store_storage.h b/flip_store/flip_storage/flip_store_storage.h index d62443507..bf27ae374 100644 --- a/flip_store/flip_storage/flip_store_storage.h +++ b/flip_store/flip_storage/flip_store_storage.h @@ -27,4 +27,14 @@ 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); +bool save_char( + const char *path_name, const char *value); + +bool load_char( + const char *path_name, + char *value, + size_t value_size); + +bool save_char_with_path(const char *full_path, const char *value); + #endif \ No newline at end of file diff --git a/flip_store/flip_store.c b/flip_store/flip_store.c index 37cc03e26..17728615d 100644 --- a/flip_store/flip_store.c +++ b/flip_store/flip_store.c @@ -1,7 +1,5 @@ #include - -void flip_store_loader_free_model(View *view); -FlipStoreApp *app_instance = NULL; +#include // Function to free the resources used by FlipStoreApp void flip_store_app_free(FlipStoreApp *app) @@ -27,12 +25,6 @@ void flip_store_app_free(FlipStoreApp *app) view_free(app->view_loader); } - if (app->view_app_info) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppInfo); - view_free(app->view_app_info); - } - // Free Submenu(s) if (app->submenu_main) { @@ -54,151 +46,22 @@ void flip_store_app_free(FlipStoreApp *app) 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); - } - if (app->submenu_app_list_games) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListGames); - submenu_free(app->submenu_app_list_games); - } - if (app->submenu_app_list_gpio) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListGPIO); - submenu_free(app->submenu_app_list_gpio); - } - if (app->submenu_app_list_infrared) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListInfrared); - submenu_free(app->submenu_app_list_infrared); - } - if (app->submenu_app_list_ibutton) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListiButton); - submenu_free(app->submenu_app_list_ibutton); - } - if (app->submenu_app_list_media) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListMedia); - submenu_free(app->submenu_app_list_media); - } - if (app->submenu_app_list_nfc) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListNFC); - submenu_free(app->submenu_app_list_nfc); - } - if (app->submenu_app_list_rfid) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListRFID); - submenu_free(app->submenu_app_list_rfid); - } - if (app->submenu_app_list_subghz) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListSubGHz); - submenu_free(app->submenu_app_list_subghz); - } - if (app->submenu_app_list_tools) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListTools); - submenu_free(app->submenu_app_list_tools); - } - if (app->submenu_app_list_usb) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListUSB); - submenu_free(app->submenu_app_list_usb); - } - - // Free Widget(s) - if (app->widget) + if (app->submenu_vgm_firmwares) { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAbout); - widget_free(app->widget); + view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewVGMFirmwares); + submenu_free(app->submenu_vgm_firmwares); } - // Free Variable Item List(s) - if (app->variable_item_list) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSettings); - variable_item_list_free(app->variable_item_list); - } - - // Free Text Input(s) - if (app->uart_text_input_ssid) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewTextInputSSID); - text_input_free(app->uart_text_input_ssid); - } - if (app->uart_text_input_pass) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewTextInputPass); - text_input_free(app->uart_text_input_pass); - } - - // Free popup - if (app->popup) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewPopup); - popup_free(app->popup); - } - - // Free dialog - if (app->dialog_delete) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppDelete); - dialog_ex_free(app->dialog_delete); - } - if (app->dialog_firmware) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewFirmwareDialog); - dialog_ex_free(app->dialog_firmware); - } - - // deinitalize flipper http - flipper_http_deinit(); + free_all_views(app, true); // free the 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); // free the app - free(app); -} - -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."); - } + if (app) + free(app); } \ No newline at end of file diff --git a/flip_store/flip_store.h b/flip_store/flip_store.h index 6e73fc443..84ce56c2f 100644 --- a/flip_store/flip_store.h +++ b/flip_store/flip_store.h @@ -11,11 +11,20 @@ #include #include #include +#include #include + #define TAG "FlipStore" +#define VERSION_TAG "FlipStore v0.8" + #define FIRMWARE_COUNT 3 #define FIRMWARE_LINKS 3 +#define VGM_FIRMWARE_COUNT 1 +#define VGM_FIRMWARE_LINKS 1 + +#define MAX_GITHUB_FILES 30 + // Define the submenu items for our FlipStore application typedef enum { @@ -25,8 +34,10 @@ typedef enum // FlipStoreSubmenuIndexOptions, // Click to view the options // - FlipStoreSubmenuIndexAppList, - FlipStoreSubmenuIndexFirmwares, + FlipStoreSubmenuIndexAppList, // Click to view the app list + FlipStoreSubmenuIndexFirmwares, // Click to view the ESP32 firmwares + FlipStoreSubmenuIndexVGMFirmwares, // Click to view the VGM firmwares + FlipStoreSubmenuIndexGitHub, // Click to view the GitHub repository view // FlipStoreSubmenuIndexAppListBluetooth, FlipStoreSubmenuIndexAppListGames, @@ -40,7 +51,8 @@ typedef enum FlipStoreSubmenuIndexAppListTools, FlipStoreSubmenuIndexAppListUSB, // - FlipStoreSubmenuIndexStartFirmwares, + FlipStoreSubmenuIndexStartFirmwares = 50, + FlipStoreSubmenuIndexStartVGMFirmwares = 60, // FlipStoreSubmenuIndexStartAppList = 100, } FlipStoreSubmenuIndex; @@ -49,39 +61,29 @@ typedef enum typedef enum { // - FlipStoreViewSubmenu, // The submenu - FlipStoreViewSubmenuOptions, // The submenu options - // - FlipStoreViewAbout, // The about screen - FlipStoreViewSettings, // The settings screen - FlipStoreViewTextInputSSID, // The text input screen for SSID - FlipStoreViewTextInputPass, // The text input screen for password - // - FlipStoreViewPopup, // The popup screen - // - FlipStoreViewAppList, // The app list screen - FlipStoreViewFirmwares, // The firmwares screen (submenu) - FlipStoreViewFirmwareDialog, // The firmware view (DialogEx) of the selected firmware - // - 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 - // - FlipStoreViewAppListBluetooth, // the app list screen for Bluetooth - FlipStoreViewAppListGames, // the app list screen for Games - FlipStoreViewAppListGPIO, // the app list screen for GPIO - FlipStoreViewAppListInfrared, // the app list screen for Infrared - FlipStoreViewAppListiButton, // the app list screen for iButton - FlipStoreViewAppListMedia, // the app list screen for Media - FlipStoreViewAppListNFC, // the app list screen for NFC - FlipStoreViewAppListRFID, // the app list screen for RFID - FlipStoreViewAppListSubGHz, // the app list screen for Sub-GHz - FlipStoreViewAppListTools, // the app list screen for Tools - FlipStoreViewAppListUSB, // the app list screen for USB - // - // - FlipStoreViewWidgetResult, // The text box that displays the random fact - FlipStoreViewLoader, // The loader screen retrieves data from the internet + FlipStoreViewSubmenu, // The submenu + FlipStoreViewSubmenuOptions, // The submenu options + // + FlipStoreViewAbout, // The about screen + FlipStoreViewSettings, // The settings screen + FlipStoreViewTextInput, // The text input screen + // + FlipStoreViewAppList, // The app list screen + FlipStoreViewFirmwares, // The firmwares screen (submenu) + FlipStoreViewVGMFirmwares, // The VGM firmwares screen (submenu) + FlipStoreViewAGithub, // The GitHub repository screen + // + FlipStoreViewFirmwareDialog, // The firmware view (DialogEx) of the selected firmware + // + 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 + // + FlipStoreViewAppListCategory, // the app list screen for each category + // + // + FlipStoreViewWidgetResult, // The text box that displays the random fact + FlipStoreViewLoader, // The loader screen retrieves data from the internet } FlipStoreView; // Each screen will have its own view @@ -93,48 +95,26 @@ typedef struct ViewDispatcher *view_dispatcher; // Switches between our views View *view_app_info; // The app info screen (view) of the selected app // - DialogEx *dialog_firmware; // The dialog for installing a firmware - View *view_firmware_download; // The firmware download screen (view) of the selected firmware + DialogEx *dialog_firmware; // The dialog for installing a 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 - Submenu *submenu_app_list_gpio; // The submenu (app list) for GPIO - Submenu *submenu_app_list_infrared; // The submenu (app list) for Infrared - Submenu *submenu_app_list_ibutton; // The submenu (app list) for iButton - Submenu *submenu_app_list_media; // The submenu (app list) for Media - Submenu *submenu_app_list_nfc; // The submenu (app list) for NFC - Submenu *submenu_app_list_rfid; // The submenu (app list) for RFID - Submenu *submenu_app_list_subghz; // The submenu (app list) for Sub-GHz - Submenu *submenu_app_list_tools; // The submenu (app list) for Tools - Submenu *submenu_app_list_usb; // The submenu (app list) for USB - // - Widget *widget; // The widget - Popup *popup; // The popup - DialogEx *dialog_delete; // The dialog for deleting an app + 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_vgm_firmwares; // The submenu (VGM firmwares) + Submenu *submenu_app_list_category; // The submenu (app list) for each category + Widget *widget_about; // The widget VariableItemList *variable_item_list; // The variable item list (settngs) VariableItem *variable_item_ssid; // The variable item VariableItem *variable_item_pass; // The variable item - TextInput *uart_text_input_ssid; // The text input - TextInput *uart_text_input_pass; // The text input - - char *uart_text_input_buffer_ssid; // Buffer for the text input - char *uart_text_input_temp_buffer_ssid; // Temporary buffer for the text input - uint32_t uart_text_input_buffer_size_ssid; // Size of the text input buffer - - char *uart_text_input_buffer_pass; // Buffer for the text input - char *uart_text_input_temp_buffer_pass; // Temporary buffer for the text input - uint32_t uart_text_input_buffer_size_pass; // Size of the text input buffer + // + TextInput *uart_text_input; // The text input + char *uart_text_input_buffer; // Buffer for the text input + char *uart_text_input_temp_buffer; // Temporary buffer for the text input + uint32_t uart_text_input_buffer_size; // Size of the text input buffer } FlipStoreApp; void flip_store_app_free(FlipStoreApp *app); -void flip_store_request_error(Canvas *canvas); -extern FlipStoreApp *app_instance; - #endif // FLIP_STORE_E_H \ No newline at end of file diff --git a/flip_store/flipper_http/flipper_http.c b/flip_store/flipper_http/flipper_http.c index 361dc535c..e2ec2eaa6 100644 --- a/flip_store/flipper_http/flipper_http.c +++ b/flip_store/flipper_http/flipper_http.c @@ -1,8 +1,9 @@ -#include -FlipperHTTP fhttp; -char rx_line_buffer[RX_LINE_BUFFER_SIZE]; -uint8_t file_buffer[FILE_BUFFER_SIZE]; -size_t file_buffer_len = 0; +// Description: Flipper HTTP API (For use with Flipper Zero and the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP) +// License: MIT +// Author: JBlanked +// File: flipper_http.c +#include // change this to where flipper_http.h is located + // Function to append received data to file // make sure to initialize the file path before calling this function bool flipper_http_append_to_file( @@ -88,7 +89,8 @@ FuriString *flipper_http_load_from_file(char *file_path) { storage_file_free(file); furi_record_close(RECORD_STORAGE); - return NULL; // Return false if the file does not exist + FURI_LOG_E(HTTP_TAG, "Failed to open file for reading: %s", file_path); + return NULL; } // Allocate a FuriString to hold the received data @@ -105,6 +107,16 @@ FuriString *flipper_http_load_from_file(char *file_path) // Reset the FuriString to ensure it's empty before reading furi_string_reset(str_result); + if (memmgr_get_free_heap() < MAX_FILE_SHOW) + { + FURI_LOG_E(HTTP_TAG, "Not enough heap to read file."); + furi_string_free(str_result); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + // Define a buffer to hold the read data uint8_t *buffer = (uint8_t *)malloc(MAX_FILE_SHOW); if (!buffer) @@ -135,9 +147,100 @@ FuriString *flipper_http_load_from_file(char *file_path) 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; +} + +FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit) +{ + // 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); + FURI_LOG_E(HTTP_TAG, "Failed to open file for reading: %s", file_path); + return NULL; + } + + if (memmgr_get_free_heap() < limit) + { + FURI_LOG_E(HTTP_TAG, "Not enough heap to read file."); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Allocate a buffer to hold the read data + uint8_t *buffer = (uint8_t *)malloc(limit); + if (!buffer) + { + FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Allocate a FuriString with preallocated capacity + FuriString *str_result = furi_string_alloc(); + if (!str_result) + { + FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString"); + free(buffer); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + furi_string_reserve(str_result, limit); + + // Read data into the buffer + size_t read_count = storage_file_read(file, buffer, limit); + if (storage_file_get_error(file) != FSE_OK) + { + FURI_LOG_E(HTTP_TAG, "Error reading from file."); + furi_string_free(str_result); + free(buffer); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + if (read_count == 0) + { + FURI_LOG_E(HTTP_TAG, "No data read from file."); + furi_string_free(str_result); + free(buffer); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Append the entire buffer to FuriString in one operation + furi_string_cat_str(str_result, (char *)buffer); // Clean up storage_file_close(file); @@ -151,13 +254,23 @@ FuriString *flipper_http_load_from_file(char *file_path) /** * @brief Worker thread to handle UART data asynchronously. * @return 0 - * @param context The context to pass to the callback. + * @param context The FlipperHTTP context. * @note This function will handle received data asynchronously via the callback. */ // UART worker thread int32_t flipper_http_worker(void *context) { - UNUSED(context); + if (!context) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return -1; + } + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return -1; + } size_t rx_line_pos = 0; while (1) @@ -171,11 +284,11 @@ int32_t flipper_http_worker(void *context) if (events & WorkerEvtRxDone) { // Continuously read from the stream buffer until it's empty - while (!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) + 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); + size_t received = furi_stream_buffer_receive(fhttp->flipper_http_stream, &c, 1, 0); if (received == 0) { @@ -184,43 +297,43 @@ int32_t flipper_http_worker(void *context) } // Append the received byte to the file if saving is enabled - if (fhttp.save_bytes) + if (fhttp->save_bytes) { // Add byte to the buffer - file_buffer[file_buffer_len++] = c; + fhttp->file_buffer[fhttp->file_buffer_len++] = c; // Write to file if buffer is full - if (file_buffer_len >= FILE_BUFFER_SIZE) + if (fhttp->file_buffer_len >= FILE_BUFFER_SIZE) { if (!flipper_http_append_to_file( - file_buffer, - file_buffer_len, - fhttp.just_started_bytes, - fhttp.file_path)) + fhttp->file_buffer, + fhttp->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; + fhttp->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) + 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 + fhttp->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); + fhttp->handle_rx_line_cb(fhttp->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 + fhttp->rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer } } } @@ -233,22 +346,27 @@ int32_t flipper_http_worker(void *context) /** * @brief Callback function for the GET timeout timer. * @return 0 - * @param context The context to pass to the callback. + * @param context The FlipperHTTP context. * @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."); + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + FURI_LOG_E(HTTP_TAG, "Timeout reached 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; + 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; + fhttp->state = ISSUE; } // UART RX Handler Callback (Interrupt Context) @@ -257,7 +375,7 @@ void get_timeout_timer_callback(void *context) * @return void * @param handle The UART handle. * @param event The event type. - * @param context The context to pass to the callback. + * @param context The FlipperHTTP context. * @note This function will handle received data asynchronously via the callback. */ void _flipper_http_rx_callback( @@ -265,171 +383,208 @@ void _flipper_http_rx_callback( FuriHalSerialRxEvent event, void *context) { - UNUSED(context); + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } 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); + 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. + * @return FlipperHTTP context if the UART was initialized successfully, NULL otherwise. * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_init(FlipperHTTP_Callback callback, void *context) +FlipperHTTP *flipper_http_alloc() { - if (!context) - { - FURI_LOG_E(HTTP_TAG, "Invalid context provided to flipper_http_init."); - return false; - } - if (!callback) + FlipperHTTP *fhttp = (FlipperHTTP *)malloc(sizeof(FlipperHTTP)); + if (!fhttp) { - FURI_LOG_E(HTTP_TAG, "Invalid callback provided to flipper_http_init."); - return false; + FURI_LOG_E(HTTP_TAG, "Failed to allocate FlipperHTTP."); + return NULL; } - fhttp.flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1); - if (!fhttp.flipper_http_stream) + memset(fhttp, 0, sizeof(FlipperHTTP)); // Initialize allocated memory to zero + + 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; + free(fhttp); + return NULL; } - fhttp.rx_thread = furi_thread_alloc(); - if (!fhttp.rx_thread) + 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_stream_buffer_free(fhttp->flipper_http_stream); + free(fhttp); + return NULL; } - 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); + 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); // Corrected context + furi_thread_set_callback(fhttp->rx_thread, flipper_http_worker); - fhttp.handle_rx_line_cb = callback; - fhttp.callback_context = context; + fhttp->handle_rx_line_cb = flipper_http_rx_callback; + fhttp->callback_context = fhttp; - furi_thread_start(fhttp.rx_thread); - fhttp.rx_thread_id = furi_thread_get_id(fhttp.rx_thread); + 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 + // 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; + // Cleanup resources + 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); + free(fhttp); + return NULL; } - fhttp.serial_handle = furi_hal_serial_control_acquire(UART_CH); - if (!fhttp.serial_handle) + 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; + 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); + free(fhttp); + return NULL; } // Initialize UART with acquired handle - furi_hal_serial_init(fhttp.serial_handle, BAUDRATE); + furi_hal_serial_init(fhttp->serial_handle, BAUDRATE); // Enable RX direction - furi_hal_serial_enable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); + 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); + // Start asynchronous RX with the corrected callback and context + furi_hal_serial_async_rx_start(fhttp->serial_handle, _flipper_http_rx_callback, fhttp, false); // Corrected context // Wait for the TX to complete to ensure UART is ready - furi_hal_serial_tx_wait_complete(fhttp.serial_handle); + furi_hal_serial_tx_wait_complete(fhttp->serial_handle); // Allocate the timer for handling timeouts - fhttp.get_timeout_timer = furi_timer_alloc( + fhttp->get_timeout_timer = furi_timer_alloc( get_timeout_timer_callback, // Callback function FuriTimerTypeOnce, // One-shot timer - &fhttp // Context passed to callback + fhttp // Corrected context ); - if (!fhttp.get_timeout_timer) + 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; + 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); + free(fhttp); + return NULL; } // 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) + 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; + // Cleanup resources + furi_timer_free(fhttp->get_timeout_timer); + 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); + free(fhttp); + return NULL; } + memset(fhttp->last_response, 0, RX_BUF_SIZE); // Initialize last_response + + fhttp->state = IDLE; // FURI_LOG_I(HTTP_TAG, "UART initialized successfully."); - return true; + return fhttp; } // Deinitialize UART /** * @brief Deinitialize UART. * @return void + * @param fhttp The FlipperHTTP context * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. */ -void flipper_http_deinit() +void flipper_http_free(FlipperHTTP *fhttp) { - if (fhttp.serial_handle == NULL) + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + 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); + 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); + 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); + furi_thread_flags_set(fhttp->rx_thread_id, WorkerEvtStop); // Wait for the thread to finish - furi_thread_join(fhttp.rx_thread); + furi_thread_join(fhttp->rx_thread); // Free the thread resources - furi_thread_free(fhttp.rx_thread); + furi_thread_free(fhttp->rx_thread); // Free the stream buffer - furi_stream_buffer_free(fhttp.flipper_http_stream); + furi_stream_buffer_free(fhttp->flipper_http_stream); // Free the timer - if (fhttp.get_timeout_timer) + if (fhttp->get_timeout_timer) { - furi_timer_free(fhttp.get_timeout_timer); - fhttp.get_timeout_timer = NULL; + furi_timer_free(fhttp->get_timeout_timer); + fhttp->get_timeout_timer = NULL; } // Free the last response - if (fhttp.last_response) + if (fhttp->last_response) { - free(fhttp.last_response); - fhttp.last_response = NULL; + free(fhttp->last_response); + fhttp->last_response = NULL; } + // Free the FlipperHTTP context + free(fhttp); + fhttp = NULL; + // FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully."); } @@ -437,11 +592,17 @@ void flipper_http_deinit() /** * @brief Send data over UART with newline termination. * @return true if the data was sent successfully, false otherwise. + * @param fhttp The FlipperHTTP context * @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) +bool flipper_http_send_data(FlipperHTTP *fhttp, const char *data) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } size_t data_length = strlen(data); if (data_length == 0) { @@ -453,7 +614,7 @@ bool flipper_http_send_data(const char *data) 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."); + FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP->"); return false; } @@ -462,20 +623,20 @@ bool flipper_http_send_data(const char *data) 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))) + 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."; + 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); + 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; + fhttp->state = IDLE; return true; } @@ -483,20 +644,26 @@ bool flipper_http_send_data(const char *data) /** * @brief Send a PING request to check if the Wifi Dev Board is connected. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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() +bool flipper_http_ping(FlipperHTTP *fhttp) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } const char *command = "[PING]"; - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, 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; + fhttp->state = INACTIVE; // The response will be handled asynchronously via the callback return true; } @@ -505,12 +672,18 @@ bool flipper_http_ping() /** * @brief Send a command to list available commands. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_list_commands() +bool flipper_http_list_commands(FlipperHTTP *fhttp) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } const char *command = "[LIST]"; - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send LIST command."); return false; @@ -524,12 +697,18 @@ bool flipper_http_list_commands() /** * @brief Allow the LED to display while processing. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_led_on() +bool flipper_http_led_on(FlipperHTTP *fhttp) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } const char *command = "[LED/ON]"; - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send LED ON command."); return false; @@ -543,12 +722,18 @@ bool flipper_http_led_on() /** * @brief Disable the LED from displaying while processing. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_led_off() +bool flipper_http_led_off(FlipperHTTP *fhttp) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } const char *command = "[LED/OFF]"; - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send LED OFF command."); return false; @@ -562,12 +747,18 @@ bool flipper_http_led_off() /** * @brief Parse JSON data. * @return true if the JSON data was parsed successfully, false otherwise. + * @param fhttp The FlipperHTTP context * @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) +bool flipper_http_parse_json(FlipperHTTP *fhttp, const char *key, const char *json_data) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (!key || !json_data) { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json."); @@ -583,7 +774,7 @@ bool flipper_http_parse_json(const char *key, const char *json_data) return false; } - if (!flipper_http_send_data(buffer)) + if (!flipper_http_send_data(fhttp, buffer)) { FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse command."); return false; @@ -597,13 +788,19 @@ bool flipper_http_parse_json(const char *key, const char *json_data) /** * @brief Parse JSON array data. * @return true if the JSON array data was parsed successfully, false otherwise. + * @param fhttp The FlipperHTTP context * @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) +bool flipper_http_parse_json_array(FlipperHTTP *fhttp, const char *key, int index, const char *json_data) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (!key || !json_data) { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json_array."); @@ -624,7 +821,7 @@ bool flipper_http_parse_json_array(const char *key, int index, const char *json_ return false; } - if (!flipper_http_send_data(buffer)) + if (!flipper_http_send_data(fhttp, buffer)) { FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse array command."); return false; @@ -638,12 +835,18 @@ bool flipper_http_parse_json_array(const char *key, int index, const char *json_ /** * @brief Send a command to scan for WiFi networks. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_scan_wifi() +bool flipper_http_scan_wifi(FlipperHTTP *fhttp) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } const char *command = "[WIFI/SCAN]"; - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi scan command."); return false; @@ -657,10 +860,16 @@ bool flipper_http_scan_wifi() /** * @brief Send a command to save WiFi settings. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_save_wifi(const char *ssid, const char *password) +bool flipper_http_save_wifi(FlipperHTTP *fhttp, const char *ssid, const char *password) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (!ssid || !password) { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_save_wifi."); @@ -675,7 +884,7 @@ bool flipper_http_save_wifi(const char *ssid, const char *password) return false; } - if (!flipper_http_send_data(buffer)) + if (!flipper_http_send_data(fhttp, buffer)) { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command."); return false; @@ -689,12 +898,18 @@ bool flipper_http_save_wifi(const char *ssid, const char *password) /** * @brief Send a command to get the IP address of the WiFi Devboard * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_ip_address() +bool flipper_http_ip_address(FlipperHTTP *fhttp) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } const char *command = "[IP/ADDRESS]"; - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send IP address command."); return false; @@ -708,12 +923,18 @@ bool flipper_http_ip_address() /** * @brief Send a command to get the IP address of the connected WiFi network. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_ip_wifi() +bool flipper_http_ip_wifi(FlipperHTTP *fhttp) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } const char *command = "[WIFI/IP]"; - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi IP command."); return false; @@ -727,12 +948,18 @@ bool flipper_http_ip_wifi() /** * @brief Send a command to disconnect from WiFi. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_disconnect_wifi() +bool flipper_http_disconnect_wifi(FlipperHTTP *fhttp) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } const char *command = "[WIFI/DISCONNECT]"; - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command."); return false; @@ -746,12 +973,18 @@ bool flipper_http_disconnect_wifi() /** * @brief Send a command to connect to WiFi. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_connect_wifi() +bool flipper_http_connect_wifi(FlipperHTTP *fhttp) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } const char *command = "[WIFI/CONNECT]"; - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command."); return false; @@ -765,11 +998,17 @@ bool flipper_http_connect_wifi() /** * @brief Send a GET request to the specified URL. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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) +bool flipper_http_get_request(FlipperHTTP *fhttp, const char *url) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (!url) { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request."); @@ -786,7 +1025,7 @@ bool flipper_http_get_request(const char *url) } // Send GET request via UART - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command."); return false; @@ -795,16 +1034,23 @@ bool flipper_http_get_request(const char *url) // 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 fhttp The FlipperHTTP context * @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) +bool flipper_http_get_request_with_headers(FlipperHTTP *fhttp, const char *url, const char *headers) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (!url || !headers) { FURI_LOG_E( @@ -823,7 +1069,7 @@ bool flipper_http_get_request_with_headers(const char *url, const char *headers) } // Send GET request via UART - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); return false; @@ -832,16 +1078,23 @@ bool flipper_http_get_request_with_headers(const char *url, const char *headers) // 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 fhttp The FlipperHTTP context * @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) +bool flipper_http_get_request_bytes(FlipperHTTP *fhttp, const char *url, const char *headers) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (!url || !headers) { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_bytes."); @@ -859,7 +1112,7 @@ bool flipper_http_get_request_bytes(const char *url, const char *headers) } // Send GET request via UART - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); return false; @@ -868,20 +1121,28 @@ bool flipper_http_get_request_bytes(const char *url, const char *headers) // 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 fhttp The FlipperHTTP context * @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( + FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (!url || !headers || !payload) { FURI_LOG_E( @@ -906,7 +1167,7 @@ bool flipper_http_post_request_with_headers( } // Send POST request via UART - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); return false; @@ -915,17 +1176,24 @@ bool flipper_http_post_request_with_headers( // 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 fhttp The FlipperHTTP context * @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) +bool flipper_http_post_request_bytes(FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (!url || !headers || !payload) { FURI_LOG_E( @@ -949,7 +1217,7 @@ bool flipper_http_post_request_bytes(const char *url, const char *headers, const } // Send POST request via UART - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); return false; @@ -958,20 +1226,28 @@ bool flipper_http_post_request_bytes(const char *url, const char *headers, const // 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 fhttp The FlipperHTTP context * @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( + FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (!url || !headers || !payload) { FURI_LOG_E( @@ -995,7 +1271,7 @@ bool flipper_http_put_request_with_headers( } // Send PUT request via UART - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data."); return false; @@ -1004,20 +1280,28 @@ bool flipper_http_put_request_with_headers( // 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 fhttp The FlipperHTTP context * @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( + FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (!url || !headers || !payload) { FURI_LOG_E( @@ -1043,7 +1327,7 @@ bool flipper_http_delete_request_with_headers( } // Send DELETE request via UART - if (!flipper_http_send_data(command)) + if (!flipper_http_send_data(fhttp, command)) { FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data."); return false; @@ -1052,17 +1336,60 @@ bool flipper_http_delete_request_with_headers( // The response will be handled asynchronously via the callback return true; } +// Function to trim leading and trailing spaces and newlines from a constant string +static 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; +} + // 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. + * @param context The FlipperHTTP context. * @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) + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + if (!line) { FURI_LOG_E(HTTP_TAG, "Invalid arguments provided to flipper_http_rx_callback."); return; @@ -1078,233 +1405,233 @@ void flipper_http_rx_callback(const char *line, void *context) strstr(trimmed_line, "[PUT/END]") == NULL && strstr(trimmed_line, "[DELETE/END]") == NULL) { - strncpy(fhttp.last_response, trimmed_line, RX_BUF_SIZE); + 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) + if (fhttp->state != INACTIVE && fhttp->state != ISSUE) { - fhttp.state = RECEIVING; + 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) + 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); + 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) + 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++) + for (size_t i = 0; i <= fhttp->file_buffer_len - marker_len; i++) { // Check if the marker is found - if (memcmp(&file_buffer[i], marker, marker_len) == 0) + if (memcmp(&fhttp->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; + size_t remaining_len = fhttp->file_buffer_len - (i + marker_len); + memmove(&fhttp->file_buffer[i], &fhttp->file_buffer[i + marker_len], remaining_len); + fhttp->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 (fhttp->file_buffer_len > 0) { - if (!flipper_http_append_to_file(file_buffer, file_buffer_len, false, fhttp.file_path)) + if (!flipper_http_append_to_file(fhttp->file_buffer, fhttp->file_buffer_len, false, fhttp->file_path)) { FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); } - file_buffer_len = 0; + fhttp->file_buffer_len = 0; } } - fhttp.is_bytes_request = false; + fhttp->is_bytes_request = false; return; } // Append the new line to the existing data - if (fhttp.save_received_data && + if (fhttp->save_received_data && !flipper_http_append_to_file( - line, strlen(line), !fhttp.just_started_get, fhttp.file_path)) + 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; + fhttp->started_receiving_get = false; + fhttp->just_started_get = false; + fhttp->state = IDLE; return; } - if (!fhttp.just_started_get) + if (!fhttp->just_started_get) { - fhttp.just_started_get = true; + fhttp->just_started_get = true; } return; } // Check if we've started receiving data from a POST request - else if (fhttp.started_receiving_post) + 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); + 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) + 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++) + for (size_t i = 0; i <= fhttp->file_buffer_len - marker_len; i++) { // Check if the marker is found - if (memcmp(&file_buffer[i], marker, marker_len) == 0) + if (memcmp(&fhttp->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; + size_t remaining_len = fhttp->file_buffer_len - (i + marker_len); + memmove(&fhttp->file_buffer[i], &fhttp->file_buffer[i + marker_len], remaining_len); + fhttp->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 (fhttp->file_buffer_len > 0) { - if (!flipper_http_append_to_file(file_buffer, file_buffer_len, false, fhttp.file_path)) + if (!flipper_http_append_to_file(fhttp->file_buffer, fhttp->file_buffer_len, false, fhttp->file_path)) { FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); } - file_buffer_len = 0; + fhttp->file_buffer_len = 0; } } - fhttp.is_bytes_request = false; + fhttp->is_bytes_request = false; return; } // Append the new line to the existing data - if (fhttp.save_received_data && + if (fhttp->save_received_data && !flipper_http_append_to_file( - line, strlen(line), !fhttp.just_started_post, fhttp.file_path)) + 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; + fhttp->started_receiving_post = false; + fhttp->just_started_post = false; + fhttp->state = IDLE; return; } - if (!fhttp.just_started_post) + if (!fhttp->just_started_post) { - fhttp.just_started_post = true; + fhttp->just_started_post = true; } return; } // Check if we've started receiving data from a PUT request - else if (fhttp.started_receiving_put) + 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); + 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; + 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 && + if (fhttp->save_received_data && !flipper_http_append_to_file( - line, strlen(line), !fhttp.just_started_put, fhttp.file_path)) + 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; + fhttp->started_receiving_put = false; + fhttp->just_started_put = false; + fhttp->state = IDLE; return; } - if (!fhttp.just_started_put) + if (!fhttp->just_started_put) { - fhttp.just_started_put = true; + fhttp->just_started_put = true; } return; } // Check if we've started receiving data from a DELETE request - else if (fhttp.started_receiving_delete) + 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); + 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; + 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 && + if (fhttp->save_received_data && !flipper_http_append_to_file( - line, strlen(line), !fhttp.just_started_delete, fhttp.file_path)) + 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; + fhttp->started_receiving_delete = false; + fhttp->just_started_delete = false; + fhttp->state = IDLE; return; } - if (!fhttp.just_started_delete) + if (!fhttp->just_started_delete) { - fhttp.just_started_delete = true; + fhttp->just_started_delete = true; } return; } @@ -1318,49 +1645,49 @@ void flipper_http_rx_callback(const char *line, void *context) { FURI_LOG_I(HTTP_TAG, "Received info: %s", line); - if (fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL) + if (fhttp->state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL) { - fhttp.state = IDLE; + 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->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; + fhttp->save_bytes = fhttp->is_bytes_request; + fhttp->just_started_bytes = true; + fhttp->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; + 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; + fhttp->save_bytes = fhttp->is_bytes_request; + fhttp->just_started_bytes = true; + fhttp->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; + 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; + 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) @@ -1370,7 +1697,7 @@ void flipper_http_rx_callback(const char *line, void *context) else if (strstr(line, "[ERROR]") != NULL) { FURI_LOG_E(HTTP_TAG, "Received error: %s", line); - fhttp.state = ISSUE; + fhttp->state = ISSUE; return; } else if (strstr(line, "[PONG]") != NULL) @@ -1378,88 +1705,57 @@ void flipper_http_rx_callback(const char *line, void *context) FURI_LOG_I(HTTP_TAG, "Received PONG response: Wifi Dev Board is still alive."); // send command to connect to WiFi - if (fhttp.state == INACTIVE) + if (fhttp->state == INACTIVE) { - fhttp.state = IDLE; + fhttp->state = IDLE; return; } } - if (fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) + if (fhttp->state == INACTIVE && strstr(line, "[PONG]") != NULL) { - fhttp.state = IDLE; + fhttp->state = IDLE; } - else if (fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) + else if (fhttp->state == INACTIVE && strstr(line, "[PONG]") == NULL) { - fhttp.state = INACTIVE; + fhttp->state = INACTIVE; } else { - fhttp.state = IDLE; + 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 fhttp The FlipperHTTP context * @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)) +bool flipper_http_process_response_async(FlipperHTTP *fhttp, bool (*http_request)(void), bool (*parse_json)(void)) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } if (http_request()) // start the async request { - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; + 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) + 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); + 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..."); @@ -1470,6 +1766,7 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars /** * @brief Perform a task while displaying a loading screen + * @param fhttp The FlipperHTTP context * @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 @@ -1477,12 +1774,23 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars * @param view_dispatcher The view dispatcher to use * @return */ -void flipper_http_loading_task(bool (*http_request)(void), +void flipper_http_loading_task(FlipperHTTP *fhttp, + bool (*http_request)(void), bool (*parse_response)(void), uint32_t success_view_id, uint32_t failure_view_id, ViewDispatcher **view_dispatcher) { + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + if (fhttp->state == INACTIVE) + { + view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); + return; + } Loading *loading; int32_t loading_view_id = 987654321; // Random ID @@ -1501,7 +1809,7 @@ void flipper_http_loading_task(bool (*http_request)(void), view_dispatcher_switch_to_view(*view_dispatcher, loading_view_id); // Make the request - if (!flipper_http_process_response_async(http_request, parse_response)) + if (!flipper_http_process_response_async(fhttp, http_request, parse_response)) { FURI_LOG_E(HTTP_TAG, "Failed to make request"); view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); @@ -1514,5 +1822,5 @@ void flipper_http_loading_task(bool (*http_request)(void), // 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); + loading_free(loading); // comment this out if you experience a freeze } \ No newline at end of file diff --git a/flip_store/flipper_http/flipper_http.h b/flip_store/flipper_http/flipper_http.h index 02ba35961..6c778c3f2 100644 --- a/flip_store/flipper_http/flipper_http.h +++ b/flip_store/flipper_http/flipper_http.h @@ -1,6 +1,8 @@ -// flipper_http.h -#ifndef FLIPPER_HTTP_H -#define FLIPPER_HTTP_H +// Description: Flipper HTTP API (For use with Flipper Zero and the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP) +// License: MIT +// Author: JBlanked +// File: flipper_http.h +#pragma once #include #include @@ -20,9 +22,9 @@ #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 8192 // UART RX line buffer size (increase for large responses) -#define MAX_FILE_SHOW 8192 // Maximum data from file to show +#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 12000 // Maximum data from file to show #define FILE_BUFFER_SIZE 512 // File buffer size // Forward declaration for callback @@ -83,13 +85,11 @@ typedef struct 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; + char rx_line_buffer[RX_LINE_BUFFER_SIZE]; + uint8_t file_buffer[FILE_BUFFER_SIZE]; + size_t file_buffer_len; +} FlipperHTTP; // fhttp.last_response holds the last received data from the UART @@ -102,6 +102,7 @@ bool flipper_http_append_to_file( char *file_path); FuriString *flipper_http_load_from_file(char *file_path); +FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit); // UART worker thread /** @@ -139,172 +140,189 @@ void _flipper_http_rx_callback( // 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. + * @return FlipperHTTP context if the UART was initialized successfully, NULL otherwise. * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_init(FlipperHTTP_Callback callback, void *context); +FlipperHTTP *flipper_http_alloc(); // Deinitialize UART /** * @brief Deinitialize UART. * @return void + * @param fhttp The FlipperHTTP context * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. */ -void flipper_http_deinit(); +void flipper_http_free(FlipperHTTP *fhttp); // 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_send_data(FlipperHTTP *fhttp, 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. + * @param fhttp The FlipperHTTP context * @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(); +bool flipper_http_ping(FlipperHTTP *fhttp); // Function to list available commands /** * @brief Send a command to list available commands. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_list_commands(); +bool flipper_http_list_commands(FlipperHTTP *fhttp); // Function to turn on the LED /** * @brief Allow the LED to display while processing. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_led_on(); +bool flipper_http_led_on(FlipperHTTP *fhttp); // Function to turn off the LED /** * @brief Disable the LED from displaying while processing. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_led_off(); +bool flipper_http_led_off(FlipperHTTP *fhttp); // Function to parse JSON data /** * @brief Parse JSON data. * @return true if the JSON data was parsed successfully, false otherwise. + * @param fhttp The FlipperHTTP context * @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); +bool flipper_http_parse_json(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_parse_json_array(FlipperHTTP *fhttp, 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_scan_wifi(); +bool flipper_http_scan_wifi(FlipperHTTP *fhttp); // 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_save_wifi(const char *ssid, const char *password); +bool flipper_http_save_wifi(FlipperHTTP *fhttp, 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_ip_address(); +bool flipper_http_ip_address(FlipperHTTP *fhttp); // 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_ip_wifi(); +bool flipper_http_ip_wifi(FlipperHTTP *fhttp); // 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_disconnect_wifi(); +bool flipper_http_disconnect_wifi(FlipperHTTP *fhttp); // 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_connect_wifi(); +bool flipper_http_connect_wifi(FlipperHTTP *fhttp); // 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_get_request(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_get_request_with_headers(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_get_request_bytes(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context * @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( + FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload); @@ -313,23 +331,26 @@ bool flipper_http_post_request_with_headers( /** * @brief Send a POST request to the specified URL. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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); +bool flipper_http_post_request_bytes(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context * @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( + FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload); @@ -338,12 +359,14 @@ bool flipper_http_put_request_with_headers( /** * @brief Send a DELETE request to the specified URL. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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( + FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload); @@ -353,23 +376,23 @@ bool flipper_http_delete_request_with_headers( * @brief Callback function to handle received data asynchronously. * @return void * @param line The received line. - * @param context The context passed to the callback. + * @param context The FlipperHTTP context. * @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 fhttp The FlipperHTTP context * @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)); +bool flipper_http_process_response_async(FlipperHTTP *fhttp, bool (*http_request)(void), bool (*parse_json)(void)); /** * @brief Perform a task while displaying a loading screen + * @param fhttp The FlipperHTTP context * @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 @@ -377,10 +400,9 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars * @param view_dispatcher The view dispatcher to use * @return */ -void flipper_http_loading_task(bool (*http_request)(void), +void flipper_http_loading_task(FlipperHTTP *fhttp, + 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_store/github/flip_store_github.c b/flip_store/github/flip_store_github.c new file mode 100644 index 000000000..d445a9b40 --- /dev/null +++ b/flip_store/github/flip_store_github.c @@ -0,0 +1,378 @@ +#include +#include + +// Helper to download a file from Github and save it to the storage +bool flip_store_download_github_file( + FlipperHTTP *fhttp, + const char *filename, + const char *author, + const char *repo, + const char *link) +{ + if (!fhttp || !filename || !author || !repo || !link) + { + FURI_LOG_E(TAG, "Invalid arguments."); + return false; + } + + snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s.txt", author, repo, filename); + fhttp->state = IDLE; + fhttp->save_received_data = false; + fhttp->is_bytes_request = true; + + return flipper_http_get_request_bytes(fhttp, link, "{\"Content-Type\":\"application/octet-stream\"}"); +} + +bool flip_store_get_github_contents(FlipperHTTP *fhttp, const char *author, const char *repo) +{ + // Create Initial directory + Storage *storage = furi_record_open(RECORD_STORAGE); + + char dir[256]; + + // create a data directory: /ext/apps_data/flip_store/data + snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data"); + storage_common_mkdir(storage, dir); + + // create a data directory for the author: /ext/apps_data/flip_store/data/author + snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s", author); + storage_common_mkdir(storage, dir); + + // example path: /ext/apps_data/flip_store/data/author/info.json + snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/info.json", author); + + // create a data directory for the repo: /ext/apps_data/flip_store/data/author/repo + snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s", author, repo); + storage_common_mkdir(storage, dir); + + // example path: /ext/apps_data/flip_store/author + snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s", author); + storage_common_mkdir(storage, dir); + + // example path: /ext/apps_data/flip_store/author/repo + snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s", author, repo); + storage_common_mkdir(storage, dir); + + furi_record_close(RECORD_STORAGE); + + // get the contents of the repo + char link[256]; + snprintf(link, sizeof(link), "https://api.github.com/repos/%s/%s/contents", author, repo); + fhttp->save_received_data = true; + return flipper_http_get_request_with_headers(fhttp, link, "{\"Content-Type\":\"application/json\"}"); +} + +#include +#include +#include + +// Assuming necessary headers and definitions for FuriString, FURI_LOG, etc., are included. + +bool flip_store_parse_github_contents(char *file_path, const char *author, const char *repo) +{ + FURI_LOG_I(TAG, "Parsing Github contents from %s - %s.", author, repo); + if (!file_path || !author || !repo) + { + FURI_LOG_E(TAG, "Invalid arguments."); + return false; + } + + // Load JSON data from file + FuriString *git_data = flipper_http_load_from_file(file_path); + if (git_data == NULL) + { + FURI_LOG_E(TAG, "Failed to load received data from file."); + return false; + } + + // Allocate a new FuriString to hold the entire JSON structure + FuriString *git_data_str = furi_string_alloc(); + if (!git_data_str) + { + FURI_LOG_E(TAG, "Failed to allocate git_data_str."); + furi_string_free(git_data); + return false; + } + + // Construct the full JSON string + furi_string_cat_str(git_data_str, "{\"json_data\":"); + furi_string_cat(git_data_str, git_data); + furi_string_cat_str(git_data_str, "}"); + furi_string_free(git_data); // Free the original git_data as it's now part of git_data_str + + // Check available memory + const size_t additional_bytes = strlen("{\"json_data\":") + 1; // +1 for the closing "}" + if (memmgr_get_free_heap() < furi_string_size(git_data_str) + additional_bytes) + { + FURI_LOG_E(TAG, "Not enough memory to allocate git_data_str."); + furi_string_free(git_data_str); + return false; + } + + int file_count = 0; + char dir[512]; // Increased size to accommodate longer paths if necessary + FURI_LOG_I(TAG, "Looping through Github files."); + FURI_LOG_I(TAG, "Available memory: %d bytes", memmgr_get_free_heap()); + + // Get the C-string and its length for processing + char *data = (char *)furi_string_get_cstr(git_data_str); + size_t data_len = furi_string_size(git_data_str); + + size_t pos = 0; // Current position in the data string + // Locate the start of the JSON array + char *array_start = strchr(data, '['); + if (!array_start) + { + FURI_LOG_E(TAG, "Invalid JSON format: '[' not found."); + furi_string_free(git_data_str); + return false; + } + pos = array_start - data; // Update position to the start of the array + size_t brace_count = 0; + size_t obj_start = 0; + bool in_string = false; // To handle braces inside strings + + while (pos < data_len && file_count < MAX_GITHUB_FILES) + { + char current = data[pos]; + + // Toggle in_string flag if a quote is found (handling escaped quotes) + if (current == '"' && (pos == 0 || data[pos - 1] != '\\')) + { + in_string = !in_string; + } + + if (!in_string) + { + if (current == '{') + { + if (brace_count == 0) + { + obj_start = pos; // Potential start of a JSON object + } + brace_count++; + } + else if (current == '}') + { + brace_count--; + if (brace_count == 0) + { + size_t obj_end = pos; + size_t obj_length = obj_end - obj_start + 1; + // Extract the JSON object substring + char *obj_str = malloc(obj_length + 1); + if (!obj_str) + { + FURI_LOG_E(TAG, "Memory allocation failed for obj_str."); + break; + } + strncpy(obj_str, data + obj_start, obj_length); + obj_str[obj_length] = '\0'; // Null-terminate + + FuriString *json_data_array = furi_string_alloc(); + furi_string_set(json_data_array, obj_str); // Set the string to the allocated memory + free(obj_str); // Free the temporary C-string + + if (!json_data_array) + { + FURI_LOG_E(TAG, "Failed to initialize json_data_array."); + break; + } + + FURI_LOG_I(TAG, "Loaded json data array value %d. Available memory: %d bytes", file_count, memmgr_get_free_heap()); + + // Extract "type" field + FuriString *type = get_json_value_furi("type", json_data_array); + if (!type) + { + FURI_LOG_E(TAG, "Failed to get type."); + furi_string_free(json_data_array); + break; + } + + // Skip non-file types (e.g., directories) + if (strcmp(furi_string_get_cstr(type), "file") != 0) + { + furi_string_free(type); + furi_string_free(json_data_array); + pos = obj_end + 1; // Move past this object + continue; + } + furi_string_free(type); + + // Extract "download_url" and "name" + FuriString *download_url = get_json_value_furi("download_url", json_data_array); + if (!download_url) + { + FURI_LOG_E(TAG, "Failed to get download_url."); + furi_string_free(json_data_array); + break; + } + + FuriString *name = get_json_value_furi("name", json_data_array); + if (!name) + { + FURI_LOG_E(TAG, "Failed to get name."); + furi_string_free(json_data_array); + furi_string_free(download_url); + break; + } + + furi_string_free(json_data_array); + FURI_LOG_I(TAG, "Received name and download_url. Available memory: %d bytes", memmgr_get_free_heap()); + + // Create JSON to save + FuriString *json = furi_string_alloc(); + if (!json) + { + FURI_LOG_E(TAG, "Failed to allocate json."); + furi_string_free(download_url); + furi_string_free(name); + break; + } + + furi_string_cat_str(json, "{\"name\":\""); + furi_string_cat(json, name); + furi_string_cat_str(json, "\",\"link\":\""); + furi_string_cat(json, download_url); + furi_string_cat_str(json, "\"}"); + + FURI_LOG_I(TAG, "Created json. Available memory: %d bytes", memmgr_get_free_heap()); + + // Save the JSON to the data folder: /ext/apps_data/flip_store/data/author/repo/fileX.json + snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/file%d.json", author, repo, file_count); + if (!save_char_with_path(dir, furi_string_get_cstr(json))) + { + FURI_LOG_E(TAG, "Failed to save json."); + } + + FURI_LOG_I(TAG, "Saved file %s.", furi_string_get_cstr(name)); + + // Free allocated resources + furi_string_free(name); + furi_string_free(download_url); + furi_string_free(json); + + file_count++; + + // This can be expensive for large strings; consider memory constraints + size_t remaining_length = data_len - (obj_end + 1); + memmove(data + obj_start, data + obj_end + 1, remaining_length + 1); // +1 to include null terminator + data_len -= (obj_end + 1 - obj_start); + pos = obj_start; // Reset position to the start of the modified string + continue; + } + } + } + + pos++; + } + + furi_string_free(git_data_str); + + // Save file count + char file_count_str[16]; + snprintf(file_count_str, sizeof(file_count_str), "%d", file_count); + char file_count_dir[512]; // Increased size for longer paths + snprintf(file_count_dir, sizeof(file_count_dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/file_count.txt", author); + + FURI_LOG_I(TAG, "Successfully parsed %d files.", file_count); + return save_char_with_path(file_count_dir, file_count_str); +} + +bool flip_store_install_all_github_files(FlipperHTTP *fhttp, const char *author, const char *repo) +{ + FURI_LOG_I(TAG, "Installing all Github files."); + if (!fhttp || !author || !repo) + { + FURI_LOG_E(TAG, "Invalid arguments."); + return false; + } + fhttp->state = RECEIVING; + // get the file count + char file_count_dir[256]; // /ext/apps_data/flip_store/data/author/file_count.txt + snprintf(file_count_dir, sizeof(file_count_dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/file_count.txt", author); + FuriString *file_count = flipper_http_load_from_file(file_count_dir); + if (file_count == NULL) + { + FURI_LOG_E(TAG, "Failed to load file count."); + return false; + } + int count = atoi(furi_string_get_cstr(file_count)); + furi_string_free(file_count); + + // install all files + char file_dir[256]; // /ext/apps_data/flip_store/data/author/repo/file.json + FURI_LOG_I(TAG, "Installing %d files.", count); + for (int i = 0; i < count; i++) + { + snprintf(file_dir, sizeof(file_dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/file%d.json", author, repo, i); + FURI_LOG_I(TAG, "Loading file %s. Available memory: %d bytes", file_dir, memmgr_get_free_heap()); + FuriString *file = flipper_http_load_from_file_with_limit(file_dir, 512); + if (!file) + { + FURI_LOG_E(TAG, "Failed to load file."); + return false; + } + FURI_LOG_I(TAG, "Loaded file %s.", file_dir); + FuriString *name = get_json_value_furi("name", file); + if (!name) + { + FURI_LOG_E(TAG, "Failed to get name."); + furi_string_free(file); + return false; + } + FuriString *link = get_json_value_furi("link", file); + if (!link) + { + FURI_LOG_E(TAG, "Failed to get link."); + furi_string_free(file); + furi_string_free(name); + return false; + } + furi_string_free(file); + + bool fetch_file() + { + return flip_store_download_github_file(fhttp, furi_string_get_cstr(name), author, repo, furi_string_get_cstr(link)); + } + + bool parse() + { + // remove .txt from the filename + char current_file_path[512]; + char new_file_path[512]; + snprintf(current_file_path, sizeof(current_file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s.txt", author, repo, furi_string_get_cstr(name)); + snprintf(new_file_path, sizeof(new_file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s", author, repo, furi_string_get_cstr(name)); + Storage *storage = furi_record_open(RECORD_STORAGE); + if (!storage_file_exists(storage, current_file_path)) + { + FURI_LOG_E(TAG, "Failed to download file."); + furi_record_close(RECORD_STORAGE); + return false; + } + if (storage_common_rename(storage, current_file_path, new_file_path) != FSE_OK) + { + FURI_LOG_E(TAG, "Failed to rename file."); + furi_record_close(RECORD_STORAGE); + return false; + } + furi_record_close(RECORD_STORAGE); + return true; + } + // download the file and wait until it is downloaded + FURI_LOG_I(TAG, "Downloading file %s", furi_string_get_cstr(name)); + if (!flipper_http_process_response_async(fhttp, fetch_file, parse)) + { + FURI_LOG_E(TAG, "Failed to download file."); + furi_string_free(name); + furi_string_free(link); + return false; + } + FURI_LOG_I(TAG, "Downloaded file %s", furi_string_get_cstr(name)); + furi_string_free(name); + furi_string_free(link); + } + fhttp->state = IDLE; + return true; +} \ No newline at end of file diff --git a/flip_store/github/flip_store_github.h b/flip_store/github/flip_store_github.h new file mode 100644 index 000000000..7f9d4564d --- /dev/null +++ b/flip_store/github/flip_store_github.h @@ -0,0 +1,26 @@ +#pragma once +#include + +/* +I did try downloading a zip file from Github but a few issues +- unsure how to unzip the file +- either Github redirected us to a loading page, or their was no response on if using an IoT device +- any contributions to this would be appreciated +*/ + +// Helper to download a file from Github and save it to the storage +bool flip_store_download_github_file( + FlipperHTTP *fhttp, + const char *filename, + const char *author, + const char *repo, + const char *link); + +// Helper to get the contents of a Github repo (from https://api.github.com/repos/author/repo/contents) +bool flip_store_get_github_contents(FlipperHTTP *fhttp, const char *author, const char *repo); + +// Helper to parse the contents of a Github repo (takes parts of the data and saves it for easy access later) +bool flip_store_parse_github_contents(char *file_path, const char *author, const char *repo); + +// Helper to install all files from the parsed Github repo contents in flip_store_parse_github_contents +bool flip_store_install_all_github_files(FlipperHTTP *fhttp, const char *author, const char *repo); \ No newline at end of file diff --git a/flip_store/jsmn/jsmn.c b/flip_store/jsmn/jsmn.c index b85ab83b5..d101530c6 100644 --- a/flip_store/jsmn/jsmn.c +++ b/flip_store/jsmn/jsmn.c @@ -7,8 +7,6 @@ */ #include -#include -#include /** * Allocates a fresh unused token from the token pool. @@ -424,7 +422,7 @@ int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, } // Helper function to create a JSON object -char *jsmn(const char *key, const char *value) +char *get_json(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 @@ -448,14 +446,19 @@ 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) +char *get_json_value(char *key, const char *json_data) { // Parse the JSON feed if (json_data != NULL) { jsmn_parser parser; jsmn_init(&parser); - + uint32_t max_tokens = json_token_count(json_data); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + return NULL; + } // Allocate tokens array on the heap jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens); if (tokens == NULL) @@ -510,26 +513,79 @@ char *get_json_value(char *key, char *json_data, uint32_t max_tokens) { FURI_LOG_E("JSMM.H", "JSON data is NULL"); } - FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON."); + char warning[128]; + snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key); + FURI_LOG_E("JSMM.H", warning); 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) +// Helper function to skip a token and all its descendants. +// Returns the index of the next token after skipping this one. +// On error or out of bounds, returns -1. +static int skip_token(const jsmntok_t *tokens, int start, int total) { - // Retrieve the array string for the given key - char *array_str = get_json_value(key, json_data, max_tokens); + if (start < 0 || start >= total) + return -1; + + int i = start; + if (tokens[i].type == JSMN_OBJECT) + { + // For an object: size is number of key-value pairs + int pairs = tokens[i].size; + i++; // move to first key-value pair + for (int p = 0; p < pairs; p++) + { + // skip key (primitive/string) + i++; + if (i >= total) + return -1; + // skip value (which could be object/array and must be skipped recursively) + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; // i is now just past the object + } + else if (tokens[i].type == JSMN_ARRAY) + { + // For an array: size is number of elements + int elems = tokens[i].size; + i++; // move to first element + for (int e = 0; e < elems; e++) + { + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; // i is now just past the array + } + else + { + // Primitive or string token, just skip it + return i + 1; + } +} + +// Revised get_json_array_value +char *get_json_array_value(char *key, uint32_t index, const char *json_data) +{ + // Always extract the full array each time from the original json_data + char *array_str = get_json_value(key, json_data); if (array_str == NULL) { FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); return NULL; } + uint32_t max_tokens = json_token_count(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + free(array_str); + 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) { @@ -538,7 +594,6 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t return NULL; } - // Parse the JSON array int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); if (ret < 0) { @@ -548,7 +603,6 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t 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); @@ -557,50 +611,33 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t 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); + // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", 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 + // Find the index-th element: start from token[1], which is the first element + int elem_token = 1; 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 + elem_token = skip_token(tokens, elem_token, ret); + if (elem_token == -1 || elem_token >= ret) { - // 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."); + FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i); free(tokens); free(array_str); return NULL; } } - // Extract the array element - jsmntok_t element = tokens[current_token]; + // Now elem_token should point to the token of the requested element + jsmntok_t element = tokens[elem_token]; int length = element.end - element.start; char *value = malloc(length + 1); - if (value == NULL) + if (!value) { FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element."); free(tokens); @@ -608,11 +645,9 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t return NULL; } - // Copy the element value to a new string strncpy(value, array_str + element.start, length); - value[length] = '\0'; // Null-terminate the string + value[length] = '\0'; - // Clean up free(tokens); free(array_str); @@ -620,16 +655,22 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t } // 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) +char **get_json_array_values(char *key, char *json_data, int *num_values) { // Retrieve the array string for the given key - char *array_str = get_json_value(key, json_data, max_tokens); + char *array_str = get_json_value(key, json_data); if (array_str == NULL) { FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); return NULL; } - + uint32_t max_tokens = json_token_count(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + free(array_str); + return NULL; + } // Initialize the JSON parser jsmn_parser parser; jsmn_init(&parser); @@ -745,3 +786,18 @@ char **get_json_array_values(char *key, char *json_data, uint32_t max_tokens, in free(array_str); return values; } + +int json_token_count(const char *json) +{ + if (json == NULL) + { + return JSMN_ERROR_INVAL; + } + + jsmn_parser parser; + jsmn_init(&parser); + + // Pass NULL for tokens and 0 for num_tokens to get the token count only + int ret = jsmn_parse(&parser, json, strlen(json), NULL, 0); + return ret; // If ret >= 0, it represents the number of tokens needed. +} diff --git a/flip_store/jsmn/jsmn.h b/flip_store/jsmn/jsmn.h index 74cdccf95..5f96e5596 100644 --- a/flip_store/jsmn/jsmn.h +++ b/flip_store/jsmn/jsmn.h @@ -17,6 +17,7 @@ #define JSMN_H #include +#include #ifdef __cplusplus extern "C" @@ -28,61 +29,6 @@ extern "C" #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 */ @@ -110,23 +56,19 @@ extern "C" #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); +char *get_json(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); +char *get_json_value(char *key, const char *json_data); // Revised get_json_array_value function -char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t max_tokens); +char *get_json_array_value(char *key, uint32_t index, const char *json_data); // 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); +char **get_json_array_values(char *key, char *json_data, int *num_values); + +int json_token_count(const char *json); #endif /* JB_JSMN_EDIT */ diff --git a/flip_store/jsmn/jsmn_furi.c b/flip_store/jsmn/jsmn_furi.c new file mode 100644 index 000000000..0eab40c99 --- /dev/null +++ b/flip_store/jsmn/jsmn_furi.c @@ -0,0 +1,736 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * [License text continues...] + */ + +#include + +// Forward declarations of helper functions +static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s); +static int skip_token(const jsmntok_t *tokens, int start, int total); + +/** + * 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) +{ + if (parser->toknext >= num_tokens) + { + return NULL; + } + jsmntok_t *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. + * Now uses FuriString to access characters. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const size_t num_tokens) +{ + size_t len = furi_string_size(js); + int start = parser->pos; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + switch (c) + { +#ifndef JSMN_STRICT + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + break; + } + if (c < 32 || c >= 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; + } + jsmntok_t *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. + * Now uses FuriString to access characters. + */ +static int jsmn_parse_string(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const size_t num_tokens) +{ + size_t len = furi_string_size(js); + int start = parser->pos; + parser->pos++; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + if (c == '\"') + { + if (tokens == NULL) + { + return 0; + } + jsmntok_t *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; + } + + if (c == '\\' && (parser->pos + 1) < len) + { + parser->pos++; + char esc = furi_string_get_char(js, parser->pos); + switch (esc) + { + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + case 'u': + { + parser->pos++; + for (int i = 0; i < 4 && parser->pos < len; i++) + { + char hex = furi_string_get_char(js, parser->pos); + if (!((hex >= '0' && hex <= '9') || + (hex >= 'A' && hex <= 'F') || + (hex >= 'a' && hex <= 'f'))) + { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + } + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Create JSON parser + */ +void jsmn_init_furi(jsmn_parser *parser) +{ + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +/** + * Parse JSON string and fill tokens. + * Now uses FuriString for the input JSON. + */ +int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const unsigned int num_tokens) +{ + size_t len = furi_string_size(js); + int r; + int i; + int count = parser->toknext; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + jsmntype_t type; + + switch (c) + { + case '{': + case '[': + { + count++; + if (tokens == NULL) + { + break; + } + jsmntok_t *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 + 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; + } + { + jsmntok_t *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 + { + jsmntok_t *token; + 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; + } + } + 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, 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 ' ': + // Whitespace - ignore + 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 + 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': + 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 + default: +#endif + r = jsmn_parse_primitive(parser, js, tokens, num_tokens); + if (r < 0) + return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + { + tokens[parser->toksuper].size++; + } + break; +#ifdef JSMN_STRICT + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) + { + for (i = parser->toknext - 1; i >= 0; i--) + { + if (tokens[i].start != -1 && tokens[i].end == -1) + { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +// Helper function to create a JSON object: {"key":"value"} +FuriString *get_json_furi(const FuriString *key, const FuriString *value) +{ + FuriString *result = furi_string_alloc(); + furi_string_printf(result, "{\"%s\":\"%s\"}", + furi_string_get_cstr(key), + furi_string_get_cstr(value)); + return result; // Caller responsible for furi_string_free +} + +// Helper function to compare JSON keys +static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s) +{ + size_t s_len = furi_string_size(s); + size_t tok_len = tok->end - tok->start; + + if (tok->type != JSMN_STRING) + return -1; + if (s_len != tok_len) + return -1; + + FuriString *sub = furi_string_alloc_set(json); + furi_string_mid(sub, tok->start, tok_len); + + int res = furi_string_cmp(sub, s); + furi_string_free(sub); + + return (res == 0) ? 0 : -1; +} + +// Skip a token and its descendants +static int skip_token(const jsmntok_t *tokens, int start, int total) +{ + if (start < 0 || start >= total) + return -1; + + int i = start; + if (tokens[i].type == JSMN_OBJECT) + { + int pairs = tokens[i].size; + i++; + for (int p = 0; p < pairs; p++) + { + i++; // skip key + if (i >= total) + return -1; + i = skip_token(tokens, i, total); // skip value + if (i == -1) + return -1; + } + return i; + } + else if (tokens[i].type == JSMN_ARRAY) + { + int elems = tokens[i].size; + i++; + for (int e = 0; e < elems; e++) + { + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; + } + else + { + return i + 1; + } +} + +/** + * Parse JSON and return the value associated with a given char* key. + */ +FuriString *get_json_value_furi(const char *key, const FuriString *json_data) +{ + if (json_data == NULL) + { + FURI_LOG_E("JSMM.H", "JSON data is NULL"); + return NULL; + } + uint32_t max_tokens = json_token_count_furi(json_data); + if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + return NULL; + } + // Create a temporary FuriString from key + FuriString *key_str = furi_string_alloc(); + furi_string_cat_str(key_str, key); + + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(key_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, json_data, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); + free(tokens); + furi_string_free(key_str); + return NULL; + } + + if (ret < 1 || tokens[0].type != JSMN_OBJECT) + { + FURI_LOG_E("JSMM.H", "Root element is not an object."); + free(tokens); + furi_string_free(key_str); + return NULL; + } + + for (int i = 1; i < ret; i++) + { + if (jsoneq_furi(json_data, &tokens[i], key_str) == 0) + { + int length = tokens[i + 1].end - tokens[i + 1].start; + FuriString *value = furi_string_alloc_set(json_data); + furi_string_mid(value, tokens[i + 1].start, length); + free(tokens); + furi_string_free(key_str); + return value; + } + } + + free(tokens); + furi_string_free(key_str); + char warning[128]; + snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key); + FURI_LOG_E("JSMM.H", warning); + return NULL; +} + +/** + * Return the value at a given index in a JSON array for a given char* key. + */ +FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data) +{ + FuriString *array_str = get_json_value_furi(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key"); + return NULL; + } + uint32_t max_tokens = json_token_count_furi(array_str); + if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (ret < 1 || tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key is not an array."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (index >= (uint32_t)tokens[0].size) + { + // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int elem_token = 1; + for (uint32_t i = 0; i < index; i++) + { + elem_token = skip_token(tokens, elem_token, ret); + if (elem_token == -1 || elem_token >= ret) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i); + free(tokens); + furi_string_free(array_str); + return NULL; + } + } + + jsmntok_t element = tokens[elem_token]; + int length = element.end - element.start; + + FuriString *value = furi_string_alloc_set(array_str); + furi_string_mid(value, element.start, length); + + free(tokens); + furi_string_free(array_str); + + return value; +} + +/** + * Extract all object values from a JSON array associated with a given char* key. + */ +FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values) +{ + *num_values = 0; + // Convert key to FuriString and call get_json_value_furi + FuriString *array_str = get_json_value_furi(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key"); + return NULL; + } + + uint32_t max_tokens = json_token_count_furi(array_str); + if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key is not an array."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int array_size = tokens[0].size; + FuriString **values = (FuriString **)malloc(array_size * sizeof(FuriString *)); + if (values == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int actual_num_values = 0; + int current_token = 1; + 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]; + + int length = element.end - element.start; + FuriString *value = furi_string_alloc_set(array_str); + furi_string_mid(value, element.start, length); + + values[actual_num_values] = value; + actual_num_values++; + + // Skip this element and its descendants + current_token = skip_token(tokens, current_token, ret); + if (current_token == -1) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens after element %d.", i); + break; + } + } + + *num_values = actual_num_values; + if (actual_num_values < array_size) + { + FuriString **reduced_values = (FuriString **)realloc(values, actual_num_values * sizeof(FuriString *)); + if (reduced_values != NULL) + { + values = reduced_values; + } + } + + free(tokens); + furi_string_free(array_str); + return values; +} + +uint32_t json_token_count_furi(const FuriString *json) +{ + if (json == NULL) + { + return JSMN_ERROR_INVAL; + } + + jsmn_parser parser; + jsmn_init_furi(&parser); + + // Pass NULL for tokens and 0 for num_tokens to get the token count only + int ret = jsmn_parse_furi(&parser, json, NULL, 0); + return ret; // If ret >= 0, it represents the number of tokens needed. +} diff --git a/flip_store/jsmn/jsmn_furi.h b/flip_store/jsmn/jsmn_furi.h new file mode 100644 index 000000000..cb01f38ca --- /dev/null +++ b/flip_store/jsmn/jsmn_furi.h @@ -0,0 +1,74 @@ +/* + * 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_FURI_H +#define JSMN_FURI_H + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + + JSMN_API void jsmn_init_furi(jsmn_parser *parser); + JSMN_API int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/* Implementation in jsmn_furi.c */ +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_FURI_H */ + +#ifndef JB_JSMN_FURI_EDIT +#define JB_JSMN_FURI_EDIT + +// Helper function to create a JSON object +FuriString *get_json_furi(const FuriString *key, const FuriString *value); + +// Updated signatures to accept const char* key +FuriString *get_json_value_furi(const char *key, const FuriString *json_data); +FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data); +FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values); + +uint32_t json_token_count_furi(const FuriString *json); +/* Example usage: +char *json = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; +FuriString *json_data = char_to_furi_string(json); +if (!json_data) +{ + FURI_LOG_E(TAG, "Failed to allocate FuriString"); + return -1; +} +FuriString *value = get_json_value_furi("key1", json_data, json_token_count_furi(json_data)); +if (value) +{ + FURI_LOG_I(TAG, "Value: %s", furi_string_get_cstr(value)); + furi_string_free(value); +} +furi_string_free(json_data); +*/ +#endif /* JB_JSMN_EDIT */ diff --git a/flip_store/jsmn/jsmn_h.c b/flip_store/jsmn/jsmn_h.c new file mode 100644 index 000000000..59480e6e6 --- /dev/null +++ b/flip_store/jsmn/jsmn_h.c @@ -0,0 +1,15 @@ +#include +FuriString *char_to_furi_string(const char *str) +{ + FuriString *furi_str = furi_string_alloc(); + if (!furi_str) + { + return NULL; + } + for (size_t i = 0; i < strlen(str); i++) + { + furi_string_push_back(furi_str, str[i]); + } + return furi_str; +} +bool jsmn_memory_check(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); } \ No newline at end of file diff --git a/flip_store/jsmn/jsmn_h.h b/flip_store/jsmn/jsmn_h.h new file mode 100644 index 000000000..97d53e7ff --- /dev/null +++ b/flip_store/jsmn/jsmn_h.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include +#include +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 +{ + JSMN_ERROR_NOMEM = -1, + JSMN_ERROR_INVAL = -2, + JSMN_ERROR_PART = -3 +}; + +typedef struct +{ + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +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; + +typedef struct +{ + char *key; + char *value; +} JSON; + +typedef struct +{ + FuriString *key; + FuriString *value; +} FuriJSON; + +FuriString *char_to_furi_string(const char *str); + +// check memory +bool jsmn_memory_check(size_t heap_size); diff --git a/flip_trader/alloc/flip_trader_alloc.c b/flip_trader/alloc/flip_trader_alloc.c index adfa6c02d..f919e898c 100644 --- a/flip_trader/alloc/flip_trader_alloc.c +++ b/flip_trader/alloc/flip_trader_alloc.c @@ -1,13 +1,15 @@ #include "alloc/flip_trader_alloc.h" // Function to allocate resources for the FlipTraderApp -FlipTraderApp* flip_trader_app_alloc() { - FlipTraderApp* app = (FlipTraderApp*)malloc(sizeof(FlipTraderApp)); +FlipTraderApp *flip_trader_app_alloc() +{ + FlipTraderApp *app = (FlipTraderApp *)malloc(sizeof(FlipTraderApp)); - Gui* gui = furi_record_open(RECORD_GUI); + Gui *gui = furi_record_open(RECORD_GUI); // initialize uart - if(!flipper_http_init(flipper_http_rx_callback, app)) { + if (!flipper_http_init(flipper_http_rx_callback, app)) + { FURI_LOG_E(TAG, "Failed to initialize flipper http"); return NULL; } @@ -15,170 +17,108 @@ FlipTraderApp* flip_trader_app_alloc() { // Allocate the text input buffer app->uart_text_input_buffer_size_ssid = 64; app->uart_text_input_buffer_size_password = 64; - if(!easy_flipper_set_buffer( - &app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid)) { + if (!easy_flipper_set_buffer(&app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid)) + { return NULL; } - if(!easy_flipper_set_buffer( - &app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid)) { + if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid)) + { return NULL; } - if(!easy_flipper_set_buffer( - &app->uart_text_input_buffer_password, app->uart_text_input_buffer_size_password)) { + if (!easy_flipper_set_buffer(&app->uart_text_input_buffer_password, app->uart_text_input_buffer_size_password)) + { return NULL; } - if(!easy_flipper_set_buffer( - &app->uart_text_input_temp_buffer_password, - app->uart_text_input_buffer_size_password)) { + if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer_password, app->uart_text_input_buffer_size_password)) + { return NULL; } asset_names = asset_names_alloc(); - if(!asset_names) { + if (!asset_names) + { return NULL; } // Allocate ViewDispatcher - if(!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) { + 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); + view_dispatcher_set_custom_event_callback(app->view_dispatcher, flip_trader_custom_event_callback); // Main view - if(!easy_flipper_set_view( - &app->view_loader, - FlipTraderViewLoader, - flip_trader_loader_draw_callback, - NULL, - callback_to_assets_submenu, - &app->view_dispatcher, - app)) { + if (!easy_flipper_set_view(&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_about, - FlipTraderViewAbout, - "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)) { + if (!easy_flipper_set_widget(&app->widget_about, FlipTraderViewAbout, "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)) { + 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( - &app->uart_text_input_ssid, - FlipTraderViewTextInputSSID, - "Enter SSID", - app->uart_text_input_temp_buffer_ssid, - app->uart_text_input_buffer_size_ssid, - text_updated_ssid, - callback_to_wifi_settings, - &app->view_dispatcher, - app)) { + if (!easy_flipper_set_uart_text_input(&app->uart_text_input_ssid, FlipTraderViewTextInputSSID, "Enter SSID", app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid, text_updated_ssid, callback_to_wifi_settings, &app->view_dispatcher, app)) + { return NULL; } - if(!easy_flipper_set_uart_text_input( - &app->uart_text_input_password, - FlipTraderViewTextInputPassword, - "Enter password", - app->uart_text_input_temp_buffer_password, - app->uart_text_input_buffer_size_password, - text_updated_password, - callback_to_wifi_settings, - &app->view_dispatcher, - app)) { + if (!easy_flipper_set_uart_text_input(&app->uart_text_input_password, FlipTraderViewTextInputPassword, "Enter password", app->uart_text_input_temp_buffer_password, app->uart_text_input_buffer_size_password, text_updated_password, callback_to_wifi_settings, &app->view_dispatcher, app)) + { return NULL; } // Variable Item List - if(!easy_flipper_set_variable_item_list( - &app->variable_item_list_wifi, - FlipTraderViewWiFiSettings, - settings_item_selected, - callback_to_submenu, - &app->view_dispatcher, - app)) { + if (!easy_flipper_set_variable_item_list(&app->variable_item_list_wifi, FlipTraderViewWiFiSettings, settings_item_selected, callback_to_submenu, &app->view_dispatcher, app)) + { return NULL; } - app->variable_item_ssid = - variable_item_list_add(app->variable_item_list_wifi, "SSID", 0, NULL, NULL); - app->variable_item_password = - variable_item_list_add(app->variable_item_list_wifi, "Password", 0, NULL, NULL); + app->variable_item_ssid = variable_item_list_add(app->variable_item_list_wifi, "SSID", 0, NULL, NULL); + app->variable_item_password = variable_item_list_add(app->variable_item_list_wifi, "Password", 0, NULL, NULL); variable_item_set_current_value_text(app->variable_item_ssid, ""); variable_item_set_current_value_text(app->variable_item_password, ""); // Submenu - if(!easy_flipper_set_submenu( - &app->submenu_main, - FlipTraderViewMainSubmenu, - "FlipTrader v1.2", - easy_flipper_callback_exit_app, - &app->view_dispatcher)) { + if (!easy_flipper_set_submenu(&app->submenu_main, FlipTraderViewMainSubmenu, "FlipTrader v1.2", easy_flipper_callback_exit_app, &app->view_dispatcher)) + { return NULL; } - if(!easy_flipper_set_submenu( - &app->submenu_assets, - FlipTraderViewAssetsSubmenu, - "Assets", - callback_to_submenu, - &app->view_dispatcher)) { + if (!easy_flipper_set_submenu(&app->submenu_assets, FlipTraderViewAssetsSubmenu, "Assets", callback_to_submenu, &app->view_dispatcher)) + { return NULL; } - submenu_add_item( - app->submenu_main, "Assets", FlipTradeSubmenuIndexAssets, callback_submenu_choices, app); - submenu_add_item( - app->submenu_main, "About", FlipTraderSubmenuIndexAbout, callback_submenu_choices, app); - submenu_add_item( - app->submenu_main, "WiFi", FlipTraderSubmenuIndexSettings, callback_submenu_choices, app); + submenu_add_item(app->submenu_main, "Assets", FlipTradeSubmenuIndexAssets, callback_submenu_choices, app); + submenu_add_item(app->submenu_main, "About", FlipTraderSubmenuIndexAbout, callback_submenu_choices, app); + submenu_add_item(app->submenu_main, "WiFi", FlipTraderSubmenuIndexSettings, callback_submenu_choices, app); // add the assets - for(uint32_t i = 0; i < ASSET_COUNT; i++) { - submenu_add_item( - app->submenu_assets, - asset_names[i], - FlipTraderSubmenuIndexAssetStartIndex + i, - callback_submenu_choices, - app); + for (uint32_t i = 0; i < ASSET_COUNT; i++) + { + submenu_add_item(app->submenu_assets, asset_names[i], FlipTraderSubmenuIndexAssetStartIndex + i, callback_submenu_choices, app); } // load settings - if(load_settings( - app->uart_text_input_buffer_ssid, - app->uart_text_input_buffer_size_ssid, - app->uart_text_input_buffer_password, - app->uart_text_input_buffer_size_password)) { + if (load_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid, app->uart_text_input_buffer_password, app->uart_text_input_buffer_size_password)) + { // Update variable items - if(app->variable_item_ssid) - variable_item_set_current_value_text( - app->variable_item_ssid, app->uart_text_input_buffer_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 - if(app->uart_text_input_buffer_ssid && app->uart_text_input_temp_buffer_ssid) { - strncpy( - app->uart_text_input_temp_buffer_ssid, - app->uart_text_input_buffer_ssid, - app->uart_text_input_buffer_size_ssid - 1); - app->uart_text_input_temp_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = - '\0'; + if (app->uart_text_input_buffer_ssid && app->uart_text_input_temp_buffer_ssid) + { + strncpy(app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid - 1); + app->uart_text_input_temp_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; } - if(app->uart_text_input_buffer_password && app->uart_text_input_temp_buffer_password) { - strncpy( - app->uart_text_input_temp_buffer_password, - app->uart_text_input_buffer_password, - app->uart_text_input_buffer_size_password - 1); - app->uart_text_input_temp_buffer_password[app->uart_text_input_buffer_size_password - 1] = - '\0'; + if (app->uart_text_input_buffer_password && app->uart_text_input_temp_buffer_password) + { + strncpy(app->uart_text_input_temp_buffer_password, app->uart_text_input_buffer_password, app->uart_text_input_buffer_size_password - 1); + app->uart_text_input_temp_buffer_password[app->uart_text_input_buffer_size_password - 1] = '\0'; } } @@ -186,4 +126,4 @@ FlipTraderApp* flip_trader_app_alloc() { view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewMainSubmenu); return app; -} +} \ No newline at end of file diff --git a/flip_trader/alloc/flip_trader_alloc.h b/flip_trader/alloc/flip_trader_alloc.h index 2108b6319..1381677a0 100644 --- a/flip_trader/alloc/flip_trader_alloc.h +++ b/flip_trader/alloc/flip_trader_alloc.h @@ -5,6 +5,6 @@ #include // Function to allocate resources for the FlipTraderApp -FlipTraderApp* flip_trader_app_alloc(); +FlipTraderApp *flip_trader_app_alloc(); -#endif // FLIP_TRADER_I_H +#endif // FLIP_TRADER_I_H \ No newline at end of file diff --git a/flip_trader/app.c b/flip_trader/app.c index ca631933d..e3d2d541a 100644 --- a/flip_trader/app.c +++ b/flip_trader/app.c @@ -2,34 +2,40 @@ #include // Entry point for the FlipTrader application -int32_t flip_trader_app(void* p) { +int32_t flip_trader_app(void *p) +{ // Suppress unused parameter warning UNUSED(p); // Initialize the FlipTrader application app_instance = flip_trader_app_alloc(); - if(!app_instance) { + if (!app_instance) + { FURI_LOG_E(TAG, "Failed to allocate FlipTraderApp"); return -1; } - if(!flipper_http_ping()) { + if (!flipper_http_ping()) + { FURI_LOG_E(TAG, "Failed to ping the device"); return -1; } - if(app_instance->uart_text_input_buffer_ssid != NULL && - app_instance->uart_text_input_buffer_password != NULL) { + 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) { + 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(); + 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( diff --git a/flip_trader/callback/flip_trader_callback.c b/flip_trader/callback/flip_trader_callback.c index 5b105d18c..28582f15f 100644 --- a/flip_trader/callback/flip_trader_callback.c +++ b/flip_trader/callback/flip_trader_callback.c @@ -3,8 +3,7 @@ // 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 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, ...) @@ -17,35 +16,46 @@ 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) { +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) { + 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) { + } + 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) { + } + else if (strstr(fhttp.last_response, "[PONG]") != NULL) + { canvas_clear(canvas); canvas_draw_str(canvas, 0, 10, "[STATUS]Connecting to AP..."); - } else { + } + 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 { + } + else + { canvas_clear(canvas); canvas_draw_str(canvas, 0, 10, "Failed to receive data."); canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); @@ -53,29 +63,29 @@ static void flip_trader_request_error_draw(Canvas* canvas) { } } -static bool send_price_request(AssetLoaderModel* model) { +static bool send_price_request(AssetLoaderModel* model) +{ UNUSED(model); - if(fhttp.state == INACTIVE) { + if (fhttp.state == INACTIVE) + { return false; } - if(!sent_get_request && fhttp.state == IDLE) { + if (!sent_get_request && fhttp.state == IDLE) + { sent_get_request = true; char url[128]; - char* headers = jsmn("Content-Type", "application/json"); + 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]); + 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) { + if (!get_request_success) + { FURI_LOG_E(TAG, "Failed to send GET request"); fhttp.state = ISSUE; return false; @@ -85,26 +95,31 @@ static bool send_price_request(AssetLoaderModel* model) { return true; } -static char* process_asset_price(AssetLoaderModel* model) { +static char *process_asset_price(AssetLoaderModel* model) +{ UNUSED(model); - if(!request_processed) { + 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) { + 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) { + 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) { + 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); @@ -112,8 +127,9 @@ static char* process_asset_price(AssetLoaderModel* model) { free(data_cstr); return NULL; } - char* price = get_json_value("05. price", global_quote, MAX_TOKENS); - if(price == 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); @@ -134,24 +150,21 @@ static char* process_asset_price(AssetLoaderModel* model) { 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); +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) { +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) { + switch (index) + { // view the assets submenu case FlipTradeSubmenuIndexAssets: view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewAssetsSubmenu); @@ -166,46 +179,48 @@ void callback_submenu_choices(void* context, uint32_t index) { break; default: // handle FlipTraderSubmenuIndexAssetStartIndex + index - if(index >= FlipTraderSubmenuIndexAssetStartIndex) { + if (index >= FlipTraderSubmenuIndexAssetStartIndex) + { asset_index = index - FlipTraderSubmenuIndexAssetStartIndex; flip_trader_asset_switch_to_view(app); - } else { + } + else + { FURI_LOG_E(TAG, "Unknown submenu index"); } break; } } -void text_updated_ssid(void* context) { - FlipTraderApp* app = (FlipTraderApp*)context; - if(!app) { +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); + 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); + 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)) { + 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"); } } @@ -214,36 +229,35 @@ void text_updated_ssid(void* context) { view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewWiFiSettings); } -void text_updated_password(void* context) { - FlipTraderApp* app = (FlipTraderApp*)context; - if(!app) { +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); + 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); + 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)) { + 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"); } } @@ -252,8 +266,10 @@ void text_updated_password(void* context) { view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewWiFiSettings); } -uint32_t callback_to_submenu(void* context) { - if(!context) { +uint32_t callback_to_submenu(void *context) +{ + if (!context) + { FURI_LOG_E(TAG, "Context is NULL"); return VIEW_NONE; } @@ -265,8 +281,10 @@ uint32_t callback_to_submenu(void* context) { return FlipTraderViewMainSubmenu; } -uint32_t callback_to_wifi_settings(void* context) { - if(!context) { +uint32_t callback_to_wifi_settings(void *context) +{ + if (!context) + { FURI_LOG_E(TAG, "Context is NULL"); return VIEW_NONE; } @@ -274,8 +292,10 @@ uint32_t callback_to_wifi_settings(void* context) { return FlipTraderViewWiFiSettings; } -uint32_t callback_to_assets_submenu(void* context) { - if(!context) { +uint32_t callback_to_assets_submenu(void *context) +{ + if (!context) + { FURI_LOG_E(TAG, "Context is NULL"); return VIEW_NONE; } @@ -287,13 +307,16 @@ uint32_t callback_to_assets_submenu(void* context) { return FlipTraderViewAssetsSubmenu; } -void settings_item_selected(void* context, uint32_t index) { - FlipTraderApp* app = (FlipTraderApp*)context; - if(!app) { +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) { + switch (index) + { case 0: // Input SSID view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewTextInputSSID); break; @@ -306,13 +329,16 @@ void settings_item_selected(void* context, uint32_t index) { } } -static void flip_trader_widget_set_text(char* message, Widget** widget) { - if(widget == NULL) { +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) { + if (message == NULL) + { FURI_LOG_E(TAG, "flip_trader_set_widget_text - message is NULL"); DEV_CRASH(); return; @@ -320,19 +346,20 @@ static void flip_trader_widget_set_text(char* message, Widget** widget) { 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)) { + 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) { + 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 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; + uint32_t line_length = (remaining_length < max_line_length) ? remaining_length : max_line_length; // Temporary buffer to hold the current line char line[30]; @@ -340,11 +367,12 @@ static void flip_trader_widget_set_text(char* message, Widget** widget) { 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] != ' ') { + 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) { + 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 @@ -352,7 +380,8 @@ static void flip_trader_widget_set_text(char* message, Widget** widget) { } // Manually copy the fixed line into the formatted_message buffer - for(uint32_t j = 0; j < line_length; j++) { + for (uint32_t j = 0; j < line_length; j++) + { formatted_message[formatted_index++] = line[j]; } @@ -363,7 +392,8 @@ static void flip_trader_widget_set_text(char* message, Widget** widget) { i += line_length; // Skip spaces at the beginning of the next line - while(message[i] == ' ') { + while (message[i] == ' ') + { i++; } } @@ -372,20 +402,23 @@ static void flip_trader_widget_set_text(char* message, Widget** 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) { +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; + AssetLoaderModel *asset_loader_model = (AssetLoaderModel *)model; AssetState asset_state = asset_loader_model->asset_state; - char* title = asset_loader_model->title; + char *title = asset_loader_model->title; canvas_set_font(canvas, FontSecondary); - if(http_state == INACTIVE) { + 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,"); @@ -395,7 +428,8 @@ void flip_trader_loader_draw_callback(Canvas* canvas, void* model) { return; } - if(asset_state == AssetStateError || asset_state == AssetStateParseError) { + if (asset_state == AssetStateError || asset_state == AssetStateParseError) + { flip_trader_request_error_draw(canvas); return; } @@ -403,53 +437,61 @@ void flip_trader_loader_draw_callback(Canvas* canvas, void* model) { canvas_draw_str(canvas, 0, 7, title); canvas_draw_str(canvas, 0, 17, "Loading..."); - if(asset_state == AssetStateInitial) { + if (asset_state == AssetStateInitial) + { return; } - if(http_state == SENDING) { + if (http_state == SENDING) + { canvas_draw_str(canvas, 0, 27, "Sending..."); return; } - if(http_state == RECEIVING || asset_state == AssetStateRequested) { + if (http_state == RECEIVING || asset_state == AssetStateRequested) + { canvas_draw_str(canvas, 0, 27, "Receiving..."); return; } - if(http_state == IDLE && asset_state == AssetStateReceived) { + if (http_state == IDLE && asset_state == AssetStateReceived) + { canvas_draw_str(canvas, 0, 27, "Processing..."); return; } - if(http_state == IDLE && asset_state == AssetStateParsed) { + 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) { +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; + 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); + with_view_model(view, AssetLoaderModel * model, { current_asset_state = model->asset_state; }, false); - if(current_asset_state == AssetStateInitial) { + if (current_asset_state == AssetStateInitial) + { with_view_model( view, AssetLoaderModel * model, { model->asset_state = AssetStateRequested; AssetLoaderFetch fetch = model->fetcher; - if(fetch == NULL) { + if (fetch == NULL) + { FURI_LOG_E(TAG, "Model doesn't have Fetch function assigned."); model->asset_state = AssetStateError; return; @@ -458,151 +500,174 @@ static void flip_trader_loader_process_callback(void* context) { // Clear any previous responses strncpy(fhttp.last_response, "", 1); bool request_status = fetch(model); - if(!request_status) { + 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) { + } + 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) { + } + 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) { + 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) { + } + 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"); + } + 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) { + } + else if (current_asset_state == AssetStateReceived) + { with_view_model( view, AssetLoaderModel * model, { - char* asset_text; - if(model->parser == NULL) { + char *asset_text; + if (model->parser == NULL) + { asset_text = NULL; FURI_LOG_DEV(TAG, "Parser is NULL"); DEV_CRASH(); - } else { + } + 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"); + 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) { + if (asset_text == NULL) + { model->asset_state = AssetStateParseError; - } else { + } + else + { model->asset_state = AssetStateParsed; } }, true); - } else if(current_asset_state == AssetStateParsed) { + } + else if (current_asset_state == AssetStateParsed) + { with_view_model( view, AssetLoaderModel * model, { - if(++model->request_index < model->request_count) { + 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) { + } + 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); + 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) { +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; + 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) { +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; + 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); + 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) { +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; + FlipTraderApp *app = (FlipTraderApp *)context; + View *view = app->view_loader; with_view_model( view, AssetLoaderModel * model, { - if(model->timer) { + if (model->timer) + { furi_timer_stop(model->timer); } }, false); } -void flip_trader_loader_init(View* view) { - if(view == NULL) { +void flip_trader_loader_init(View *view) +{ + if (view == NULL) + { FURI_LOG_E(TAG, "flip_trader_loader_init - view is NULL"); DEV_CRASH(); return; @@ -612,8 +677,10 @@ void flip_trader_loader_init(View* view) { view_set_exit_callback(view, flip_trader_loader_on_exit); } -void flip_trader_loader_free_model(View* view) { - if(view == NULL) { +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; @@ -622,11 +689,13 @@ void flip_trader_loader_free_model(View* view) { view, AssetLoaderModel * model, { - if(model->timer) { + if (model->timer) + { furi_timer_free(model->timer); model->timer = NULL; } - if(model->parser_context) { + if (model->parser_context) + { free(model->parser_context); model->parser_context = NULL; } @@ -634,14 +703,17 @@ void flip_trader_loader_free_model(View* view) { false); } -bool flip_trader_custom_event_callback(void* context, uint32_t index) { - if(context == NULL) { +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) { + switch (index) + { case FlipTraderCustomEventProcess: flip_trader_loader_process_callback(context); return true; @@ -651,22 +723,18 @@ bool flip_trader_custom_event_callback(void* context, uint32_t index) { } } -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) { +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) { + View *view = app->view_loader; + if (view == NULL) + { FURI_LOG_E(TAG, "flip_trader_generic_switch_to_view - view is NULL"); DEV_CRASH(); return; diff --git a/flip_trader/callback/flip_trader_callback.h b/flip_trader/callback/flip_trader_callback.h index b69ecdd94..8c74f5836 100644 --- a/flip_trader/callback/flip_trader_callback.h +++ b/flip_trader/callback/flip_trader_callback.h @@ -11,20 +11,21 @@ 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 callback_submenu_choices(void *context, uint32_t index); +void text_updated_ssid(void *context); -void text_updated_password(void* context); +void text_updated_password(void *context); -uint32_t callback_to_submenu(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); +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 { +enum AssetState +{ AssetStateInitial, AssetStateRequested, AssetStateReceived, @@ -34,40 +35,35 @@ enum AssetState { }; typedef enum FlipTraderCustomEvent FlipTraderCustomEvent; -enum 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; +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; + void *parser_context; size_t request_index; size_t request_count; ViewNavigationCallback back_callback; - FuriTimer* timer; + 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_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_draw_callback(Canvas *canvas, void *model); -void flip_trader_loader_init(View* view); +void flip_trader_loader_init(View *view); -void flip_trader_loader_free_model(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 +bool flip_trader_custom_event_callback(void *context, uint32_t index); +#endif // FLIP_TRADER_CALLBACK_H \ No newline at end of file diff --git a/flip_trader/easy_flipper/easy_flipper.c b/flip_trader/easy_flipper/easy_flipper.c index 8b98e1a1b..f61b7d231 100644 --- a/flip_trader/easy_flipper/easy_flipper.c +++ b/flip_trader/easy_flipper/easy_flipper.c @@ -5,9 +5,11 @@ * @param context The context - unused * @return next view id (VIEW_NONE to exit the app) */ -uint32_t easy_flipper_callback_exit_app(void* context) { +uint32_t easy_flipper_callback_exit_app(void *context) +{ // Exit the application - if(!context) { + if (!context) + { FURI_LOG_E(EASY_TAG, "Context is NULL"); return VIEW_NONE; } @@ -21,13 +23,16 @@ uint32_t easy_flipper_callback_exit_app(void* context) { * @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) { +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) { + *buffer = (char *)malloc(buffer_size); + if (!*buffer) + { FURI_LOG_E(EASY_TAG, "Failed to allocate buffer"); return false; } @@ -46,32 +51,39 @@ bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size) { * @return true if successful, false otherwise */ bool easy_flipper_set_view( - View** 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) { + 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) { + if (!*view) + { FURI_LOG_E(EASY_TAG, "Failed to allocate View"); return false; } - if(draw_callback) { + if (draw_callback) + { view_set_draw_callback(*view, draw_callback); } - if(input_callback) { + if (input_callback) + { view_set_input_callback(*view, input_callback); } - if(context) { + if (context) + { view_set_context(*view, context); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(*view, previous_callback); } view_dispatcher_add_view(*view_dispatcher, view_id, *view); @@ -85,18 +97,22 @@ bool easy_flipper_set_view( * @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) { +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) { + 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) { + if (context) + { view_dispatcher_set_event_callback_context(*view_dispatcher, context); } return true; @@ -113,24 +129,29 @@ bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui * @return true if successful, false otherwise */ bool easy_flipper_set_submenu( - Submenu** submenu, + Submenu **submenu, int32_t view_id, - char* title, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!submenu) { + 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) { + if (!*submenu) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Submenu"); return false; } - if(title) { + if (title) + { submenu_set_header(*submenu, title); } - if(previous_callback) { + 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)); @@ -147,20 +168,24 @@ bool easy_flipper_set_submenu( * @return true if successful, false otherwise */ bool easy_flipper_set_menu( - Menu** menu, + Menu **menu, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!menu) { + 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) { + if (!*menu) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Menu"); return false; } - if(previous_callback) { + 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)); @@ -177,24 +202,29 @@ bool easy_flipper_set_menu( * @return true if successful, false otherwise */ bool easy_flipper_set_widget( - Widget** widget, + Widget **widget, int32_t view_id, - char* text, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!widget) { + 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) { + if (!*widget) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Widget"); return false; } - if(text) { + if (text) + { widget_add_text_scroll_element(*widget, 0, 0, 128, 64, text); } - if(previous_callback) { + 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)); @@ -213,30 +243,33 @@ bool easy_flipper_set_widget( * @return true if successful, false otherwise */ bool easy_flipper_set_variable_item_list( - VariableItemList** 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) { + 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) { + if (!*variable_item_list) + { FURI_LOG_E(EASY_TAG, "Failed to allocate VariableItemList"); return false; } - if(enter_callback) { + 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); + 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)); + view_dispatcher_add_view(*view_dispatcher, view_id, variable_item_list_get_view(*variable_item_list)); return true; } @@ -249,38 +282,38 @@ bool easy_flipper_set_variable_item_list( * @return true if successful, false otherwise */ bool easy_flipper_set_text_input( - TextInput** text_input, + TextInput **text_input, int32_t view_id, - char* header_text, - char* text_input_temp_buffer, + 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) { + 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) { + if (!*text_input) + { FURI_LOG_E(EASY_TAG, "Failed to allocate TextInput"); return false; } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(text_input_get_view(*text_input), previous_callback); } - if(header_text) { + 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); + 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; @@ -295,38 +328,38 @@ bool easy_flipper_set_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_uart_text_input( - TextInput** uart_text_input, + TextInput **uart_text_input, int32_t view_id, - char* header_text, - char* uart_text_input_temp_buffer, + 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) { + 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) { + if (!*uart_text_input) + { FURI_LOG_E(EASY_TAG, "Failed to allocate UART_TextInput"); return false; } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(text_input_get_view(*uart_text_input), previous_callback); } - if(header_text) { + 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); + 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)); @@ -353,52 +386,63 @@ bool easy_flipper_set_uart_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_dialog_ex( - DialogEx** dialog_ex, + DialogEx **dialog_ex, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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) { + 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) { + if (!*dialog_ex) + { FURI_LOG_E(EASY_TAG, "Failed to allocate DialogEx"); return false; } - if(header) { + if (header) + { dialog_ex_set_header(*dialog_ex, header, header_x, header_y, AlignLeft, AlignTop); } - if(text) { + if (text) + { dialog_ex_set_text(*dialog_ex, text, text_x, text_y, AlignLeft, AlignTop); } - if(left_button_text) { + if (left_button_text) + { dialog_ex_set_left_button_text(*dialog_ex, left_button_text); } - if(right_button_text) { + if (right_button_text) + { dialog_ex_set_right_button_text(*dialog_ex, right_button_text); } - if(center_button_text) { + if (center_button_text) + { dialog_ex_set_center_button_text(*dialog_ex, center_button_text); } - if(result_callback) { + if (result_callback) + { dialog_ex_set_result_callback(*dialog_ex, result_callback); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(dialog_ex_get_view(*dialog_ex), previous_callback); } - if(context) { + if (context) + { dialog_ex_set_context(*dialog_ex, context); } view_dispatcher_add_view(*view_dispatcher, view_id, dialog_ex_get_view(*dialog_ex)); @@ -422,40 +466,48 @@ bool easy_flipper_set_dialog_ex( * @return true if successful, false otherwise */ bool easy_flipper_set_popup( - Popup** popup, + Popup **popup, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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) { + 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) { + if (!*popup) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Popup"); return false; } - if(header) { + if (header) + { popup_set_header(*popup, header, header_x, header_y, AlignLeft, AlignTop); } - if(text) { + if (text) + { popup_set_text(*popup, text, text_x, text_y, AlignLeft, AlignTop); } - if(result_callback) { + if (result_callback) + { popup_set_callback(*popup, result_callback); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(popup_get_view(*popup), previous_callback); } - if(context) { + if (context) + { popup_set_context(*popup, context); } view_dispatcher_add_view(*view_dispatcher, view_id, popup_get_view(*popup)); @@ -471,20 +523,24 @@ bool easy_flipper_set_popup( * @return true if successful, false otherwise */ bool easy_flipper_set_loading( - Loading** loading, + Loading **loading, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!loading) { + 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) { + if (!*loading) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Loading"); return false; } - if(previous_callback) { + 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)); @@ -497,16 +553,19 @@ bool easy_flipper_set_loading( * @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) { +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) { + if (!furi_string) + { FURI_LOG_E(EASY_TAG, "Failed to allocate FuriString"); return false; } furi_string_set_str(*furi_string, buffer); return true; -} +} \ No newline at end of file diff --git a/flip_trader/easy_flipper/easy_flipper.h b/flip_trader/easy_flipper/easy_flipper.h index 1d6dbe430..3ccfe66b8 100644 --- a/flip_trader/easy_flipper/easy_flipper.h +++ b/flip_trader/easy_flipper/easy_flipper.h @@ -28,14 +28,14 @@ * @param context The context - unused * @return next view id (VIEW_NONE to exit the app) */ -uint32_t easy_flipper_callback_exit_app(void* context); +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); +bool easy_flipper_set_buffer(char **buffer, uint32_t buffer_size); /** * @brief Initialize a View object * @param view The View object to initialize @@ -47,13 +47,13 @@ bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size); * @return true if successful, false otherwise */ bool easy_flipper_set_view( - View** 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); + 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 @@ -62,7 +62,7 @@ bool easy_flipper_set_view( * @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); +bool easy_flipper_set_view_dispatcher(ViewDispatcher **view_dispatcher, Gui *gui, void *context); /** * @brief Initialize a Submenu object @@ -75,11 +75,11 @@ bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui * @return true if successful, false otherwise */ bool easy_flipper_set_submenu( - Submenu** submenu, + Submenu **submenu, int32_t view_id, - char* title, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + char *title, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a Menu object @@ -92,10 +92,10 @@ bool easy_flipper_set_submenu( * @return true if successful, false otherwise */ bool easy_flipper_set_menu( - Menu** menu, + Menu **menu, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a Widget object @@ -107,11 +107,11 @@ bool easy_flipper_set_menu( * @return true if successful, false otherwise */ bool easy_flipper_set_widget( - Widget** widget, + Widget **widget, int32_t view_id, - char* text, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + char *text, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a VariableItemList object @@ -125,12 +125,12 @@ bool easy_flipper_set_widget( * @return true if successful, false otherwise */ bool easy_flipper_set_variable_item_list( - VariableItemList** 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); + void (*enter_callback)(void *, uint32_t), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a TextInput object @@ -141,15 +141,15 @@ bool easy_flipper_set_variable_item_list( * @return true if successful, false otherwise */ bool easy_flipper_set_text_input( - TextInput** text_input, + TextInput **text_input, int32_t view_id, - char* header_text, - char* text_input_temp_buffer, + 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); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a TextInput object with extra symbols @@ -160,15 +160,15 @@ bool easy_flipper_set_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_uart_text_input( - TextInput** uart_text_input, + TextInput **uart_text_input, int32_t view_id, - char* header_text, - char* uart_text_input_temp_buffer, + 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); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a DialogEx object @@ -190,21 +190,21 @@ bool easy_flipper_set_uart_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_dialog_ex( - DialogEx** dialog_ex, + DialogEx **dialog_ex, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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); + 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 @@ -223,18 +223,18 @@ bool easy_flipper_set_dialog_ex( * @return true if successful, false otherwise */ bool easy_flipper_set_popup( - Popup** popup, + Popup **popup, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + char *text, uint16_t text_x, uint16_t text_y, - void (*result_callback)(void*), - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher, - void* context); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a Loading object @@ -245,10 +245,10 @@ bool easy_flipper_set_popup( * @return true if successful, false otherwise */ bool easy_flipper_set_loading( - Loading** loading, + Loading **loading, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Set a char butter to a FuriString @@ -256,6 +256,6 @@ bool easy_flipper_set_loading( * @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); +bool easy_flipper_set_char_to_furi_string(FuriString **furi_string, char *buffer); -#endif +#endif \ No newline at end of file diff --git a/flip_trader/flip_storage/flip_trader_storage.c b/flip_trader/flip_storage/flip_trader_storage.c index ce863f84e..ed8a03a98 100644 --- a/flip_trader/flip_storage/flip_trader_storage.c +++ b/flip_trader/flip_storage/flip_trader_storage.c @@ -1,18 +1,21 @@ #include "flip_storage/flip_trader_storage.h" -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( - directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_trader"); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_trader"); // Create the directory - Storage* storage = furi_record_open(RECORD_STORAGE); + Storage *storage = furi_record_open(RECORD_STORAGE); storage_common_mkdir(storage, directory_path); // Open the settings file - File* file = storage_file_alloc(storage); - if(!storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + File *file = storage_file_alloc(storage); + if (!storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) + { FURI_LOG_E(TAG, "Failed to open settings file for writing: %s", SETTINGS_PATH); storage_file_free(file); furi_record_close(RECORD_STORAGE); @@ -21,15 +24,17 @@ void save_settings(const char* ssid, const char* password) { // Save the ssid length and data size_t ssid_length = strlen(ssid) + 1; // Include null terminator - if(storage_file_write(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, ssid, ssid_length) != ssid_length) { + if (storage_file_write(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, ssid, ssid_length) != ssid_length) + { FURI_LOG_E(TAG, "Failed to write SSID"); } // Save the password length and data size_t password_length = strlen(password) + 1; // Include null terminator - if(storage_file_write(file, &password_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, password, password_length) != password_length) { + if (storage_file_write(file, &password_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, password, password_length) != password_length) + { FURI_LOG_E(TAG, "Failed to write password"); } @@ -38,11 +43,17 @@ void save_settings(const char* ssid, const char* password) { furi_record_close(RECORD_STORAGE); } -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); +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); - if(!storage_file_open(file, SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) { + if (!storage_file_open(file, SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) + { FURI_LOG_E(TAG, "Failed to open settings file for reading: %s", SETTINGS_PATH); storage_file_free(file); furi_record_close(RECORD_STORAGE); @@ -51,8 +62,9 @@ bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password // Load the ssid size_t ssid_length; - if(storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || - ssid_length > ssid_size || storage_file_read(file, ssid, ssid_length) != ssid_length) { + if (storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || ssid_length > ssid_size || + storage_file_read(file, ssid, ssid_length) != ssid_length) + { FURI_LOG_E(TAG, "Failed to read SSID"); storage_file_close(file); storage_file_free(file); @@ -63,9 +75,9 @@ bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password // Load the password size_t password_length; - if(storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || - password_length > password_size || - storage_file_read(file, password, password_length) != password_length) { + if (storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || password_length > password_size || + storage_file_read(file, password, password_length) != password_length) + { FURI_LOG_E(TAG, "Failed to read password"); storage_file_close(file); storage_file_free(file); @@ -79,4 +91,4 @@ bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password furi_record_close(RECORD_STORAGE); return true; -} +} \ No newline at end of file diff --git a/flip_trader/flip_storage/flip_trader_storage.h b/flip_trader/flip_storage/flip_trader_storage.h index b2edac06a..3b147bb05 100644 --- a/flip_trader/flip_storage/flip_trader_storage.h +++ b/flip_trader/flip_storage/flip_trader_storage.h @@ -7,8 +7,14 @@ #define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_trader/settings.bin" -void save_settings(const char* ssid, const char* password); +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 load_settings( + char *ssid, + size_t ssid_size, + char *password, + size_t password_size); -#endif // FLIP_TRADER_STORAGE_H +#endif // FLIP_TRADER_STORAGE_H \ No newline at end of file diff --git a/flip_trader/flip_trader.c b/flip_trader/flip_trader.c index cc078af04..adfa83efb 100644 --- a/flip_trader/flip_trader.c +++ b/flip_trader/flip_trader.c @@ -1,19 +1,24 @@ #include -void flip_trader_loader_free_model(View* view); +void flip_trader_loader_free_model(View *view); -void asset_names_free(char** names) { - if(names) { - for(int i = 0; i < ASSET_COUNT; i++) { +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() { +char **asset_names_alloc() +{ // Allocate memory for an array of 42 string pointers - char** names = malloc(ASSET_COUNT * sizeof(char*)); - if(!names) { + char **names = malloc(ASSET_COUNT * sizeof(char *)); + if (!names) + { FURI_LOG_E(TAG, "Failed to allocate memory for asset names"); return NULL; } @@ -65,56 +70,66 @@ char** asset_names_alloc() { return names; } -char** asset_names = NULL; +char **asset_names = NULL; // index uint32_t asset_index = 0; -FlipTraderApp* app_instance = NULL; +FlipTraderApp *app_instance = NULL; // Function to free the resources used by FlipTraderApp -void flip_trader_app_free(FlipTraderApp* app) { - if(!app) { +void flip_trader_app_free(FlipTraderApp *app) +{ + if (!app) + { FURI_LOG_E(TAG, "FlipTraderApp is NULL"); return; } // Free View(s) - if(app->view_loader) { + 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) { + if (app->submenu_main) + { view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewMainSubmenu); submenu_free(app->submenu_main); } - if(app->submenu_assets) { + if (app->submenu_assets) + { view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewAssetsSubmenu); submenu_free(app->submenu_assets); } // Free Widget(s) - if(app->widget_about) { + if (app->widget_about) + { view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewAbout); widget_free(app->widget_about); } - if(app->widget_result) { + 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) { + 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) { + 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) { + if (app->uart_text_input_password) + { view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewTextInputPassword); text_input_free(app->uart_text_input_password); } @@ -132,4 +147,4 @@ void flip_trader_app_free(FlipTraderApp* app) { // free the app free(app); -} +} \ No newline at end of file diff --git a/flip_trader/flip_trader.h b/flip_trader/flip_trader.h index 35f28e828..f208790f6 100644 --- a/flip_trader/flip_trader.h +++ b/flip_trader/flip_trader.h @@ -16,62 +16,65 @@ #define TAG "FlipTrader" // Define the submenu items for our FlipTrader application -typedef enum { +typedef enum +{ // FlipTraderSubmenuIndexMain, // Click to run get the info of the selected pair - FlipTradeSubmenuIndexAssets, // Click to view the assets screen (ETHUSD, BTCUSD, etc.) - FlipTraderSubmenuIndexAbout, // Click to view the about screen - FlipTraderSubmenuIndexSettings, // Click to view the WiFi settings screen - // + FlipTradeSubmenuIndexAssets, // Click to view the assets screen (ETHUSD, BTCUSD, etc.) + FlipTraderSubmenuIndexAbout, // Click to view the about screen + FlipTraderSubmenuIndexSettings, // Click to view the WiFi settings screen + // FlipTraderSubmenuIndexAssetStartIndex, // Start of the submenu items for the assets } FlipTraderSubmenuIndex; // Define a single view for our FlipTrader application -typedef enum { - FlipTraderViewMain, // The screen that displays the info of the selected pair - FlipTraderViewMainSubmenu, // The main submenu of the FlipTrader app - FlipTraderViewAbout, // The about screen - FlipTraderViewWiFiSettings, // The WiFi settings screen - FlipTraderViewTextInputSSID, // The text input screen for the SSID +typedef enum +{ + FlipTraderViewMain, // The screen that displays the info of the selected pair + FlipTraderViewMainSubmenu, // The main submenu of the FlipTrader app + FlipTraderViewAbout, // The about screen + FlipTraderViewWiFiSettings, // The WiFi settings screen + FlipTraderViewTextInputSSID, // The text input screen for the SSID 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 + 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_loader; // The screen that loads data from internet - Submenu* submenu_main; // The submenu - Submenu* submenu_assets; // The submenu for the assets - 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 - TextInput* uart_text_input_ssid; // The text input for the SSID - TextInput* uart_text_input_password; // The text input for the password +typedef struct +{ + ViewDispatcher *view_dispatcher; // Switches between our views + 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_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 + TextInput *uart_text_input_ssid; // The text input for the SSID + TextInput *uart_text_input_password; // The text input for the password - 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) + 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) uint32_t uart_text_input_buffer_size_ssid; // Size of the text input buffer (SSID) - char* uart_text_input_buffer_password; // Buffer for the text input (password) - char* uart_text_input_temp_buffer_password; // Temporary buffer for the text input (password) + char *uart_text_input_buffer_password; // Buffer for the text input (password) + 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) } FlipTraderApp; #define ASSET_COUNT 42 -extern char** asset_names; +extern char **asset_names; // index 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 +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 \ No newline at end of file diff --git a/flip_trader/flipper_http/flipper_http.c b/flip_trader/flipper_http/flipper_http.c index ed3b216de..a13dc5135 100644 --- a/flip_trader/flipper_http/flipper_http.c +++ b/flip_trader/flipper_http/flipper_http.c @@ -5,17 +5,21 @@ 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( - const void* data, + 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); + char *file_path) +{ + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); - if(start_new_file) { + 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)) { + 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); @@ -23,15 +27,19 @@ bool flipper_http_append_to_file( } } // Open the file in write mode - if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + 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 { + } + else + { // Open the file in append mode - if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_OPEN_APPEND)) { + 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); @@ -40,7 +48,8 @@ bool flipper_http_append_to_file( } // Write the data to the file - if(storage_file_write(file, data, data_size) != data_size) { + 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); @@ -54,32 +63,37 @@ bool flipper_http_append_to_file( return true; } -FuriString* flipper_http_load_from_file(char* file_path) { +FuriString *flipper_http_load_from_file(char *file_path) +{ // Open the storage record - Storage* storage = furi_record_open(RECORD_STORAGE); - if(!storage) { + 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) { + 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)) { + 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) { + 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); @@ -91,8 +105,9 @@ FuriString* flipper_http_load_from_file(char* file_path) { furi_string_reset(str_result); // Define a buffer to hold the read data - uint8_t* buffer = (uint8_t*)malloc(MAX_FILE_SHOW); - if(!buffer) { + 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); @@ -103,7 +118,8 @@ FuriString* flipper_http_load_from_file(char* file_path) { // 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) { + 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); @@ -113,7 +129,8 @@ FuriString* flipper_http_load_from_file(char* file_path) { } // Append each byte to the FuriString - for(size_t i = 0; i < read_count; i++) { + for (size_t i = 0; i < read_count; i++) + { furi_string_push_back(str_result, buffer[i]); } @@ -137,38 +154,47 @@ FuriString* flipper_http_load_from_file(char* file_path) { * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -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) { + while (1) + { uint32_t events = furi_thread_flags_wait( WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever); - if(events & WorkerEvtStop) break; - if(events & WorkerEvtRxDone) { + 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)) { + 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) { + if (received == 0) + { // No more data to read break; } // Append the received byte to the file if saving is enabled - if(fhttp.save_bytes) { + 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)) { + 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; @@ -177,9 +203,11 @@ int32_t flipper_http_worker(void* context) { } // Handle line buffering only if callback is set (text data) - if(fhttp.handle_rx_line_cb) { + if (fhttp.handle_rx_line_cb) + { // Handle line buffering - if(c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1) { + 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 @@ -187,7 +215,9 @@ int32_t flipper_http_worker(void* context) { // Reset the line buffer position rx_line_pos = 0; - } else { + } + else + { rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer } } @@ -195,29 +225,38 @@ int32_t flipper_http_worker(void* context) { } } - if(fhttp.save_bytes) { + 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)) { + 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) { + 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) { + } + 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) { + if (fhttp.save_bytes) + { + char *end = NULL; + if ((end = strstr(fhttp.file_path, "\n")) != NULL) + { *end = '\0'; } } @@ -234,7 +273,8 @@ int32_t flipper_http_worker(void* context) { * @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) { +void get_timeout_timer_callback(void *context) +{ UNUSED(context); FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving the end."); @@ -258,11 +298,13 @@ void get_timeout_timer_callback(void* context) { * @note This function will handle received data asynchronously via the callback. */ void _flipper_http_rx_callback( - FuriHalSerialHandle* handle, + FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, - void* context) { + void *context) +{ UNUSED(context); - if(event == FuriHalSerialRxEventData) { + 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); @@ -277,23 +319,28 @@ void _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) { +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) { + 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) { + 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) { + if (!fhttp.rx_thread) + { FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread."); furi_stream_buffer_free(fhttp.flipper_http_stream); return false; @@ -311,13 +358,15 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { 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)) { + 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) { + 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); @@ -340,11 +389,12 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { // 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 + FuriTimerTypeOnce, // One-shot timer + &fhttp // Context passed to callback ); - if(!fhttp.get_timeout_timer) { + 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); @@ -361,8 +411,9 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { // 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) { + 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; } @@ -377,8 +428,10 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { * @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) { +void flipper_http_deinit() +{ + if (fhttp.serial_handle == NULL) + { FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?"); return; } @@ -401,13 +454,15 @@ void flipper_http_deinit() { furi_stream_buffer_free(fhttp.flipper_http_stream); // Free the timer - if(fhttp.get_timeout_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) { + if (fhttp.last_response) + { free(fhttp.last_response); fhttp.last_response = NULL; } @@ -422,34 +477,38 @@ void flipper_http_deinit() { * @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) { +bool flipper_http_send_data(const char *data) +{ size_t data_length = strlen(data); - if(data_length == 0) { + 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 + 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] = '\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))) { + 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); + 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); @@ -465,9 +524,11 @@ bool flipper_http_send_data(const char* data) { * @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)) { +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; } @@ -483,9 +544,11 @@ bool flipper_http_ping() { * @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)) { +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; } @@ -500,9 +563,11 @@ bool flipper_http_list_commands() { * @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)) { +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; } @@ -517,9 +582,11 @@ bool flipper_http_led_on() { * @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)) { +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; } @@ -536,8 +603,10 @@ bool flipper_http_led_off() { * @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) { +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; } @@ -545,12 +614,14 @@ bool flipper_http_parse_json(const char* key, const char* json_data) { char buffer[256]; int ret = snprintf(buffer, sizeof(buffer), "[PARSE]{\"key\":\"%s\",\"json\":%s}", key, json_data); - if(ret < 0 || ret >= (int)sizeof(buffer)) { + 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)) { + if (!flipper_http_send_data(buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse command."); return false; } @@ -568,8 +639,10 @@ bool flipper_http_parse_json(const char* key, const char* json_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) { +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; } @@ -582,12 +655,14 @@ bool flipper_http_parse_json_array(const char* key, int index, const char* json_ key, index, json_data); - if(ret < 0 || ret >= (int)sizeof(buffer)) { + 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)) { + if (!flipper_http_send_data(buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse array command."); return false; } @@ -602,9 +677,11 @@ bool flipper_http_parse_json_array(const char* key, int index, const char* json_ * @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)) { +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; } @@ -619,20 +696,24 @@ bool flipper_http_scan_wifi() { * @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) { +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)) { + 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)) { + if (!flipper_http_send_data(buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command."); return false; } @@ -647,9 +728,11 @@ bool flipper_http_save_wifi(const char* ssid, const char* password) { * @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)) { +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; } @@ -664,9 +747,11 @@ bool flipper_http_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_wifi() { - const char* command = "[WIFI/IP]"; - if(!flipper_http_send_data(command)) { +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; } @@ -681,9 +766,11 @@ bool flipper_http_ip_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)) { +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; } @@ -698,9 +785,11 @@ bool flipper_http_disconnect_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)) { +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; } @@ -716,8 +805,10 @@ bool flipper_http_connect_wifi() { * @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) { +bool flipper_http_get_request(const char *url) +{ + if (!url) + { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request."); return false; } @@ -725,13 +816,15 @@ bool flipper_http_get_request(const char* url) { // Prepare GET request command char command[256]; int ret = snprintf(command, sizeof(command), "[GET]%s", url); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command."); return false; } @@ -747,8 +840,10 @@ bool flipper_http_get_request(const char* url) { * @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) { +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; @@ -758,13 +853,15 @@ bool flipper_http_get_request_with_headers(const char* url, const char* 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)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); return false; } @@ -780,8 +877,10 @@ bool flipper_http_get_request_with_headers(const char* url, const char* headers) * @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) { +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; } @@ -790,13 +889,15 @@ bool flipper_http_get_request_bytes(const char* url, const char* 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)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); return false; } @@ -814,10 +915,12 @@ bool flipper_http_get_request_bytes(const char* url, const char* headers) { * @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) { + 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."); @@ -833,13 +936,15 @@ bool flipper_http_post_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); return false; } @@ -856,8 +961,10 @@ bool flipper_http_post_request_with_headers( * @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) { +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; @@ -872,13 +979,15 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); return false; } @@ -896,10 +1005,12 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const * @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) { + 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; @@ -914,13 +1025,15 @@ bool flipper_http_put_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data."); return false; } @@ -938,10 +1051,12 @@ bool flipper_http_put_request_with_headers( * @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) { + 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."); @@ -957,14 +1072,16 @@ bool flipper_http_delete_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data."); return false; } @@ -980,26 +1097,31 @@ bool flipper_http_delete_request_with_headers( * @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) { +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') { + 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) { + 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) { + if (fhttp.state != INACTIVE && fhttp.state != ISSUE) + { fhttp.state = RECEIVING; } @@ -1007,11 +1129,13 @@ void flipper_http_rx_callback(const char* line, void* context) { // 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) { + 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) { + 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); @@ -1025,9 +1149,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1035,18 +1160,21 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_get) { + 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) { + 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) { + 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); @@ -1060,9 +1188,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1070,18 +1199,21 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_post) { + 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) { + 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) { + 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); @@ -1095,9 +1227,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1105,18 +1238,21 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_put) { + 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) { + 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) { + 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); @@ -1130,9 +1266,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1140,22 +1277,29 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_delete) { + 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) { + if (strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL) + { FURI_LOG_I(HTTP_TAG, "Operation succeeded."); - } else if(strstr(line, "[INFO]") != NULL) { + } + 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) { + if (fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL) + { fhttp.state = IDLE; } - } else if(strstr(line, "[GET/SUCCESS]") != NULL) { + } + 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); @@ -1164,7 +1308,9 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.save_bytes = fhttp.is_bytes_request; fhttp.just_started_bytes = true; return; - } else if(strstr(line, "[POST/SUCCESS]") != NULL) { + } + 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); @@ -1173,67 +1319,86 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.save_bytes = fhttp.is_bytes_request; fhttp.just_started_bytes = true; return; - } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { + } + 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) { + } + 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) { + } + else if (strstr(line, "[DISCONNECTED]") != NULL) + { FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully."); - } else if(strstr(line, "[ERROR]") != NULL) { + } + 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) { + } + 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) { + if (fhttp.state == INACTIVE) + { fhttp.state = IDLE; return; } } - if(fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) { + if (fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) + { fhttp.state = IDLE; - } else if(fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) { + } + else if (fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) + { fhttp.state = INACTIVE; - } else { + } + 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; +char *trim(const char *str) +{ + const char *end; + char *trimmed_str; size_t len; // Trim leading space - while(isspace((unsigned char)*str)) + while (isspace((unsigned char)*str)) str++; // All spaces? - if(*str == 0) return strdup(""); // Return an empty string if 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)) + 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) { + trimmed_str = (char *)malloc(len + 1); + if (trimmed_str == NULL) + { return NULL; // Handle memory allocation failure } @@ -1250,24 +1415,28 @@ char* trim(const char* str) { * @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 +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 { + } + 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) { + 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) + 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; -} +} \ No newline at end of file diff --git a/flip_trader/flipper_http/flipper_http.h b/flip_trader/flipper_http/flipper_http.h index 6b8f9e068..d0e431107 100644 --- a/flip_trader/flipper_http/flipper_http.h +++ b/flip_trader/flipper_http/flipper_http.h @@ -11,69 +11,72 @@ // 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 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 +#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); +typedef void (*FlipperHTTP_Callback)(const char *line, void *context); // State variable to track the UART state -typedef enum { - INACTIVE, // Inactive state - IDLE, // Default state +typedef enum +{ + INACTIVE, // Inactive state + IDLE, // Default state RECEIVING, // Receiving data - SENDING, // Sending data - ISSUE, // Issue with connection + SENDING, // Sending data + ISSUE, // Issue with connection } SerialState; // Event Flags for UART Worker Thread -typedef enum { +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 +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 + 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 *last_response; char file_path[256]; // Path to save the received data // Timer-related members - FuriTimer* get_timeout_timer; // Timer for HTTP request timeout + 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 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 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 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 + 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; + 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 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; @@ -88,12 +91,12 @@ extern 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( - const void* data, + const void *data, size_t data_size, bool start_new_file, - char* file_path); + char *file_path); -FuriString* flipper_http_load_from_file(char* file_path); +FuriString *flipper_http_load_from_file(char *file_path); // UART worker thread /** @@ -103,7 +106,7 @@ FuriString* flipper_http_load_from_file(char* file_path); * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -int32_t flipper_http_worker(void* context); +int32_t flipper_http_worker(void *context); // Timer callback function /** @@ -112,7 +115,7 @@ int32_t flipper_http_worker(void* context); * @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); +void get_timeout_timer_callback(void *context); // UART RX Handler Callback (Interrupt Context) /** @@ -124,9 +127,9 @@ void get_timeout_timer_callback(void* context); * @note This function will handle received data asynchronously via the callback. */ void _flipper_http_rx_callback( - FuriHalSerialHandle* handle, + FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, - void* context); + void *context); // UART initialization function /** @@ -136,7 +139,7 @@ void _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); +bool flipper_http_init(FlipperHTTP_Callback callback, void *context); // Deinitialize UART /** @@ -153,7 +156,7 @@ void flipper_http_deinit(); * @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); +bool flipper_http_send_data(const char *data); // Function to send a PING request /** @@ -197,7 +200,7 @@ bool flipper_http_led_off(); * @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); +bool flipper_http_parse_json(const char *key, const char *json_data); // Function to parse JSON array data /** @@ -208,7 +211,7 @@ bool flipper_http_parse_json(const char* key, const char* json_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); +bool flipper_http_parse_json_array(const char *key, int index, const char *json_data); // Function to scan for WiFi networks /** @@ -224,7 +227,7 @@ bool flipper_http_scan_wifi(); * @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); +bool flipper_http_save_wifi(const char *ssid, const char *password); // Function to get IP address of WiFi Devboard /** @@ -265,7 +268,7 @@ bool flipper_http_connect_wifi(); * @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); +bool flipper_http_get_request(const char *url); // Function to send a GET request with headers /** @@ -275,7 +278,7 @@ bool flipper_http_get_request(const char* url); * @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); +bool flipper_http_get_request_with_headers(const char *url, const char *headers); // Function to send a GET request with headers and return bytes /** @@ -285,7 +288,7 @@ bool flipper_http_get_request_with_headers(const char* url, const char* headers) * @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); +bool flipper_http_get_request_bytes(const char *url, const char *headers); // Function to send a POST request with headers /** @@ -297,9 +300,9 @@ bool flipper_http_get_request_bytes(const char* url, const char* headers); * @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); + const char *url, + const char *headers, + const char *payload); // Function to send a POST request with headers and return bytes /** @@ -310,7 +313,7 @@ bool flipper_http_post_request_with_headers( * @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); +bool flipper_http_post_request_bytes(const char *url, const char *headers, const char *payload); // Function to send a PUT request with headers /** @@ -322,9 +325,9 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const * @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); + const char *url, + const char *headers, + const char *payload); // Function to send a DELETE request with headers /** @@ -336,9 +339,9 @@ bool flipper_http_put_request_with_headers( * @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); + const char *url, + const char *headers, + const char *payload); // Function to handle received data asynchronously /** @@ -348,10 +351,10 @@ bool flipper_http_delete_request_with_headers( * @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); +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); +char *trim(const char *str); /** * @brief Process requests and parse JSON data asynchronously * @param http_request The function to send the request diff --git a/flip_trader/jsmn/jsmn.c b/flip_trader/jsmn/jsmn.c index eb33b3cc7..b85ab83b5 100644 --- a/flip_trader/jsmn/jsmn.c +++ b/flip_trader/jsmn/jsmn.c @@ -13,11 +13,13 @@ /** * 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; +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) { + if (parser->toknext >= num_tokens) + { return NULL; } tok = &tokens[parser->toknext++]; @@ -32,8 +34,9 @@ static jsmntok_t* /** * Fills token type and boundaries. */ -static void - jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { +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; @@ -43,19 +46,19 @@ static void /** * 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; +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]) { + 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 ':': @@ -72,7 +75,8 @@ static int jsmn_parse_primitive( /* to quiet a warning from gcc*/ break; } - if(js[parser->pos] < 32 || js[parser->pos] >= 127) { + if (js[parser->pos] < 32 || js[parser->pos] >= 127) + { parser->pos = start; return JSMN_ERROR_INVAL; } @@ -84,12 +88,14 @@ static int jsmn_parse_primitive( #endif found: - if(tokens == NULL) { + if (tokens == NULL) + { parser->pos--; return 0; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { parser->pos = start; return JSMN_ERROR_NOMEM; } @@ -104,29 +110,31 @@ static int jsmn_parse_primitive( /** * 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; +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++) { + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { char c = js[parser->pos]; /* Quote: end of string */ - if(c == '\"') { - if(tokens == NULL) { + if (c == '\"') + { + if (tokens == NULL) + { return 0; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { parser->pos = start; return JSMN_ERROR_NOMEM; } @@ -138,10 +146,12 @@ static int jsmn_parse_string( } /* Backslash: Quoted symbol expected */ - if(c == '\\' && parser->pos + 1 < len) { + if (c == '\\' && parser->pos + 1 < len) + { int i; parser->pos++; - switch(js[parser->pos]) { + switch (js[parser->pos]) + { /* Allowed escaped symbols */ case '\"': case '/': @@ -155,11 +165,13 @@ static int jsmn_parse_string( /* Allows escaped symbol \uXXXX */ case 'u': parser->pos++; - for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + 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 */ + 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; } @@ -181,7 +193,8 @@ static int jsmn_parse_string( /** * Create JSON parser over an array of tokens */ -void jsmn_init(jsmn_parser* parser) { +void jsmn_init(jsmn_parser *parser) +{ parser->pos = 0; parser->toknext = 0; parser->toksuper = -1; @@ -190,38 +203,41 @@ void jsmn_init(jsmn_parser* parser) { /** * 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 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; + jsmntok_t *token; int count = parser->toknext; - for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { char c; jsmntype_t type; c = js[parser->pos]; - switch(c) { + switch (c) + { case '{': case '[': count++; - if(tokens == NULL) { + if (tokens == NULL) + { break; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { return JSMN_ERROR_NOMEM; } - if(parser->toksuper != -1) { - jsmntok_t* t = &tokens[parser->toksuper]; + 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) { + if (t->type == JSMN_OBJECT) + { return JSMN_ERROR_INVAL; } #endif @@ -236,26 +252,33 @@ int jsmn_parse( break; case '}': case ']': - if(tokens == NULL) { + if (tokens == NULL) + { break; } type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); #ifdef JSMN_PARENT_LINKS - if(parser->toknext < 1) { + 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) { + 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) { + if (token->parent == -1) + { + if (token->type != type || parser->toksuper == -1) + { return JSMN_ERROR_INVAL; } break; @@ -263,10 +286,13 @@ int jsmn_parse( token = &tokens[token->parent]; } #else - for(i = parser->toknext - 1; i >= 0; i--) { + for (i = parser->toknext - 1; i >= 0; i--) + { token = &tokens[i]; - if(token->start != -1 && token->end == -1) { - if(token->type != type) { + if (token->start != -1 && token->end == -1) + { + if (token->type != type) + { return JSMN_ERROR_INVAL; } parser->toksuper = -1; @@ -275,12 +301,15 @@ int jsmn_parse( } } /* Error if unmatched closing bracket */ - if(i == -1) { + if (i == -1) + { return JSMN_ERROR_INVAL; } - for(; i >= 0; i--) { + for (; i >= 0; i--) + { token = &tokens[i]; - if(token->start != -1 && token->end == -1) { + if (token->start != -1 && token->end == -1) + { parser->toksuper = i; break; } @@ -289,11 +318,13 @@ int jsmn_parse( break; case '\"': r = jsmn_parse_string(parser, js, len, tokens, num_tokens); - if(r < 0) { + if (r < 0) + { return r; } count++; - if(parser->toksuper != -1 && tokens != NULL) { + if (parser->toksuper != -1 && tokens != NULL) + { tokens[parser->toksuper].size++; } break; @@ -306,15 +337,19 @@ int jsmn_parse( 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) { + 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) { + 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; } @@ -340,9 +375,12 @@ int jsmn_parse( 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)) { + 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; } } @@ -351,11 +389,13 @@ int jsmn_parse( default: #endif r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); - if(r < 0) { + if (r < 0) + { return r; } count++; - if(parser->toksuper != -1 && tokens != NULL) { + if (parser->toksuper != -1 && tokens != NULL) + { tokens[parser->toksuper].size++; } break; @@ -368,10 +408,13 @@ int jsmn_parse( } } - if(tokens != NULL) { - for(i = parser->toknext - 1; i >= 0; i--) { + 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) { + if (tokens[i].start != -1 && tokens[i].end == -1) + { return JSMN_ERROR_PART; } } @@ -381,10 +424,12 @@ int jsmn_parse( } // 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) { +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); @@ -392,30 +437,36 @@ 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) { - if(tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && - strncmp(json + tok->start, s, tok->end - tok->start) == 0) { +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) { +char *get_json_value(char *key, char *json_data, uint32_t max_tokens) +{ // Parse the JSON feed - if(json_data != NULL) { + 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) { + 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) { + if (ret < 0) + { // Handle parsing errors FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); free(tokens); @@ -423,19 +474,23 @@ char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { } // Ensure that the root element is an object - if(ret < 1 || tokens[0].type != JSMN_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) { + 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) { + char *value = malloc(length + 1); + if (value == NULL) + { FURI_LOG_E("JSMM.H", "Failed to allocate memory for value."); free(tokens); return NULL; @@ -450,7 +505,9 @@ char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { // Free the token array if key was not found free(tokens); - } else { + } + else + { FURI_LOG_E("JSMM.H", "JSON data is NULL"); } FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON."); @@ -458,10 +515,12 @@ 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) { +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) { + 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; } @@ -471,8 +530,9 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t jsmn_init(&parser); // Allocate memory for JSON tokens - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); - if(tokens == NULL) { + 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; @@ -480,7 +540,8 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t // Parse the JSON array int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); - if(ret < 0) { + if (ret < 0) + { FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); free(tokens); free(array_str); @@ -488,7 +549,8 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // Ensure the root element is an array - if(ret < 1 || tokens[0].type != JSMN_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); @@ -496,12 +558,9 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // 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); + 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; @@ -509,20 +568,27 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t // 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 (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) { + } + else if (tokens[current_token].type == JSMN_ARRAY) + { // For nested arrays, skip all elements current_token += 1 + tokens[current_token].size; - } else { + } + else + { // For primitive types, simply move to the next token current_token += 1; } // Safety check to prevent out-of-bounds - if(current_token >= ret) { + if (current_token >= ret) + { FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array."); free(tokens); free(array_str); @@ -533,8 +599,9 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t // Extract the array element jsmntok_t element = tokens[current_token]; int length = element.end - element.start; - char* value = malloc(length + 1); - if(value == NULL) { + 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); @@ -553,10 +620,12 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // 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) { +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) { + 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; } @@ -566,8 +635,9 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in jsmn_init(&parser); // Allocate memory for JSON tokens - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); // Allocate on the heap - if(tokens == NULL) { + 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; @@ -575,7 +645,8 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // Parse the JSON array int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); - if(ret < 0) { + if (ret < 0) + { FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); free(tokens); free(array_str); @@ -583,7 +654,8 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in } // Ensure the root element is an array - if(tokens[0].type != JSMN_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); @@ -592,8 +664,9 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // 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) { + 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); @@ -604,15 +677,18 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // 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) { + 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) { + 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; @@ -622,10 +698,12 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in 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) { + 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++) { + for (int j = 0; j < actual_num_values; j++) + { free(values[j]); } free(values); @@ -647,14 +725,17 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in *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) { + 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++) { + for (int i = actual_num_values; i < array_size; i++) + { free(values[i]); } } diff --git a/flip_trader/jsmn/jsmn.h b/flip_trader/jsmn/jsmn.h index cd95a0e58..74cdccf95 100644 --- a/flip_trader/jsmn/jsmn.h +++ b/flip_trader/jsmn/jsmn.h @@ -19,7 +19,8 @@ #include #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif #ifdef JSMN_STATIC @@ -28,71 +29,71 @@ extern "C" { #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 -}; - -/** + 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; + typedef struct + { + jsmntype_t type; + int start; + int end; + int size; #ifdef JSMN_PARENT_LINKS - int parent; + int parent; #endif -} jsmntok_t; + } 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; - -/** + 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); + 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); + 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 */ @@ -116,16 +117,16 @@ JSMN_API int jsmn_parse( #include // Helper function to create a JSON object -char* jsmn(const char* key, const char* value); +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); +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); +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); +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); +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/alloc/flip_weather_alloc.c b/flip_weather/alloc/flip_weather_alloc.c index 8cebbc983..f18711531 100644 --- a/flip_weather/alloc/flip_weather_alloc.c +++ b/flip_weather/alloc/flip_weather_alloc.c @@ -1,13 +1,15 @@ #include // Function to allocate resources for the FlipWeatherApp -FlipWeatherApp* flip_weather_app_alloc() { - FlipWeatherApp* app = (FlipWeatherApp*)malloc(sizeof(FlipWeatherApp)); +FlipWeatherApp *flip_weather_app_alloc() +{ + FlipWeatherApp *app = (FlipWeatherApp *)malloc(sizeof(FlipWeatherApp)); - Gui* gui = furi_record_open(RECORD_GUI); + Gui *gui = furi_record_open(RECORD_GUI); // initialize uart - if(!flipper_http_init(flipper_http_rx_callback, app)) { + if (!flipper_http_init(flipper_http_rx_callback, app)) + { FURI_LOG_E(TAG, "Failed to initialize flipper http"); return NULL; } @@ -15,150 +17,94 @@ FlipWeatherApp* flip_weather_app_alloc() { // Allocate the text input buffer app->uart_text_input_buffer_size_ssid = 64; app->uart_text_input_buffer_size_password = 64; - if(!easy_flipper_set_buffer( - &app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid)) { + if (!easy_flipper_set_buffer(&app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid)) + { return NULL; } - if(!easy_flipper_set_buffer( - &app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid)) { + if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid)) + { return NULL; } - if(!easy_flipper_set_buffer( - &app->uart_text_input_buffer_password, app->uart_text_input_buffer_size_password)) { + if (!easy_flipper_set_buffer(&app->uart_text_input_buffer_password, app->uart_text_input_buffer_size_password)) + { return NULL; } - if(!easy_flipper_set_buffer( - &app->uart_text_input_temp_buffer_password, - app->uart_text_input_buffer_size_password)) { + if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer_password, app->uart_text_input_buffer_size_password)) + { return NULL; } // Allocate ViewDispatcher - if(!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) { + 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); + view_dispatcher_set_custom_event_callback(app->view_dispatcher, flip_weather_custom_event_callback); // Main view - if(!easy_flipper_set_view( - &app->view_loader, - FlipWeatherViewLoader, - flip_weather_loader_draw_callback, - NULL, - callback_to_submenu, - &app->view_dispatcher, - app)) { + if (!easy_flipper_set_view(&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.2\n-----\nUse WiFi to get GPS and \nWeather information.\n-----\nwww.github.com/jblanked", - callback_to_submenu, - &app->view_dispatcher)) { + if (!easy_flipper_set_widget(&app->widget, FlipWeatherViewAbout, "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)) { + if (!easy_flipper_set_widget(&app->widget_result, FlipWeatherViewWidgetResult, "Error, try again.", callback_to_submenu, &app->view_dispatcher)) + { return NULL; } // Text Input - if(!easy_flipper_set_uart_text_input( - &app->uart_text_input_ssid, - FlipWeatherViewTextInputSSID, - "Enter SSID", - app->uart_text_input_temp_buffer_ssid, - app->uart_text_input_buffer_size_ssid, - text_updated_ssid, - callback_to_wifi_settings, - &app->view_dispatcher, - app)) { + if (!easy_flipper_set_uart_text_input(&app->uart_text_input_ssid, FlipWeatherViewTextInputSSID, "Enter SSID", app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid, text_updated_ssid, callback_to_wifi_settings, &app->view_dispatcher, app)) + { return NULL; } - if(!easy_flipper_set_uart_text_input( - &app->uart_text_input_password, - FlipWeatherViewTextInputPassword, - "Enter Password", - app->uart_text_input_temp_buffer_password, - app->uart_text_input_buffer_size_password, - text_updated_password, - callback_to_wifi_settings, - &app->view_dispatcher, - app)) { + if (!easy_flipper_set_uart_text_input(&app->uart_text_input_password, FlipWeatherViewTextInputPassword, "Enter Password", app->uart_text_input_temp_buffer_password, app->uart_text_input_buffer_size_password, text_updated_password, callback_to_wifi_settings, &app->view_dispatcher, app)) + { return NULL; } // Variable Item List - if(!easy_flipper_set_variable_item_list( - &app->variable_item_list, - FlipWeatherViewSettings, - settings_item_selected, - callback_to_submenu, - &app->view_dispatcher, - app)) { + if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWeatherViewSettings, settings_item_selected, callback_to_submenu, &app->view_dispatcher, app)) + { return NULL; } - app->variable_item_ssid = - variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL); - app->variable_item_password = - variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL); + app->variable_item_ssid = variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL); + app->variable_item_password = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL); variable_item_set_current_value_text(app->variable_item_ssid, ""); variable_item_set_current_value_text(app->variable_item_password, ""); // Submenu - if(!easy_flipper_set_submenu( - &app->submenu, - FlipWeatherViewSubmenu, - "FlipWeather v1.2", - callback_exit_app, - &app->view_dispatcher)) { + if (!easy_flipper_set_submenu(&app->submenu, FlipWeatherViewSubmenu, "FlipWeather v1.2", callback_exit_app, &app->view_dispatcher)) + { return NULL; } - submenu_add_item( - app->submenu, "Weather", FlipWeatherSubmenuIndexWeather, callback_submenu_choices, app); - submenu_add_item( - app->submenu, "GPS", FlipWeatherSubmenuIndexGPS, callback_submenu_choices, app); - submenu_add_item( - app->submenu, "About", FlipWeatherSubmenuIndexAbout, callback_submenu_choices, app); - submenu_add_item( - app->submenu, "Settings", FlipWeatherSubmenuIndexSettings, callback_submenu_choices, app); + submenu_add_item(app->submenu, "Weather", FlipWeatherSubmenuIndexWeather, callback_submenu_choices, app); + submenu_add_item(app->submenu, "GPS", FlipWeatherSubmenuIndexGPS, callback_submenu_choices, app); + submenu_add_item(app->submenu, "About", FlipWeatherSubmenuIndexAbout, callback_submenu_choices, app); + submenu_add_item(app->submenu, "Settings", FlipWeatherSubmenuIndexSettings, callback_submenu_choices, app); // load settings - if(load_settings( - app->uart_text_input_buffer_ssid, - app->uart_text_input_buffer_size_ssid, - app->uart_text_input_buffer_password, - app->uart_text_input_buffer_size_password)) { + if (load_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid, app->uart_text_input_buffer_password, app->uart_text_input_buffer_size_password)) + { // Update variable items - if(app->variable_item_ssid) - variable_item_set_current_value_text( - app->variable_item_ssid, app->uart_text_input_buffer_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 - if(app->uart_text_input_buffer_ssid && app->uart_text_input_temp_buffer_ssid) { - strncpy( - app->uart_text_input_temp_buffer_ssid, - app->uart_text_input_buffer_ssid, - app->uart_text_input_buffer_size_ssid - 1); - app->uart_text_input_temp_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = - '\0'; + if (app->uart_text_input_buffer_ssid && app->uart_text_input_temp_buffer_ssid) + { + strncpy(app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid - 1); + app->uart_text_input_temp_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0'; } - if(app->uart_text_input_buffer_password && app->uart_text_input_temp_buffer_password) { - strncpy( - app->uart_text_input_temp_buffer_password, - app->uart_text_input_buffer_password, - app->uart_text_input_buffer_size_password - 1); - app->uart_text_input_temp_buffer_password[app->uart_text_input_buffer_size_password - 1] = - '\0'; + if (app->uart_text_input_buffer_password && app->uart_text_input_temp_buffer_password) + { + strncpy(app->uart_text_input_temp_buffer_password, app->uart_text_input_buffer_password, app->uart_text_input_buffer_size_password - 1); + app->uart_text_input_temp_buffer_password[app->uart_text_input_buffer_size_password - 1] = '\0'; } } @@ -166,4 +112,4 @@ FlipWeatherApp* flip_weather_app_alloc() { view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSubmenu); return app; -} +} \ No newline at end of file diff --git a/flip_weather/alloc/flip_weather_alloc.h b/flip_weather/alloc/flip_weather_alloc.h index 3fe0b5f66..25ec449aa 100644 --- a/flip_weather/alloc/flip_weather_alloc.h +++ b/flip_weather/alloc/flip_weather_alloc.h @@ -3,6 +3,6 @@ #include #include #include -FlipWeatherApp* flip_weather_app_alloc(); +FlipWeatherApp *flip_weather_app_alloc(); -#endif +#endif \ No newline at end of file diff --git a/flip_weather/app.c b/flip_weather/app.c index f827e3173..195130e20 100644 --- a/flip_weather/app.c +++ b/flip_weather/app.c @@ -2,35 +2,41 @@ #include // Entry point for the FlipWeather application -int32_t flip_weather_app(void* p) { +int32_t flip_weather_app(void *p) +{ // Suppress unused parameter warning UNUSED(p); // Initialize the FlipWeather application app_instance = flip_weather_app_alloc(); - if(!app_instance) { + if (!app_instance) + { FURI_LOG_E(TAG, "Failed to allocate FlipWeatherApp"); return -1; } - if(!flipper_http_ping()) { + if (!flipper_http_ping()) + { FURI_LOG_E(TAG, "Failed to ping the device"); 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) { + 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) { + 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(); + 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( diff --git a/flip_weather/callback/flip_weather_callback.c b/flip_weather/callback/flip_weather_callback.c index 77252ad5e..019e86b54 100644 --- a/flip_weather/callback/flip_weather_callback.c +++ b/flip_weather/callback/flip_weather_callback.c @@ -3,8 +3,7 @@ // 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 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, ...) @@ -15,83 +14,80 @@ 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) { +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) { + 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) { + } + 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) { + } + 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) { + } + else if (strstr(fhttp.last_response, "[PONG]") != NULL) + { canvas_clear(canvas); canvas_draw_str(canvas, 0, 10, "[STATUS]Connecting to AP..."); - } else { + } + 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 { + } + 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_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); +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) { +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) { + switch (index) + { case FlipWeatherSubmenuIndexWeather: - flipper_http_loading_task( - send_geo_location_request, - process_geo_location_2, - FlipWeatherViewSubmenu, - FlipWeatherViewSubmenu, - &app->view_dispatcher); + 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: @@ -108,36 +104,35 @@ void callback_submenu_choices(void* context, uint32_t index) { } } -void text_updated_ssid(void* context) { - FlipWeatherApp* app = (FlipWeatherApp*)context; - if(!app) { +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); + 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); + 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)) { + 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"); } } @@ -146,36 +141,35 @@ void text_updated_ssid(void* context) { view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSettings); } -void text_updated_password(void* context) { - FlipWeatherApp* app = (FlipWeatherApp*)context; - if(!app) { +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); + 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); + 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)) { + 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"); } } @@ -184,8 +178,10 @@ void text_updated_password(void* context) { view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSettings); } -uint32_t callback_to_submenu(void* context) { - if(!context) { +uint32_t callback_to_submenu(void *context) +{ + if (!context) + { FURI_LOG_E(TAG, "Context is NULL"); return VIEW_NONE; } @@ -198,24 +194,29 @@ uint32_t callback_to_submenu(void* context) { weather_information_processed = false; sent_weather_request = false; weather_request_success = false; - if(weather_data != NULL) { + if (weather_data != NULL) + { free(weather_data); weather_data = NULL; } - if(total_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) { +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) { + switch (index) + { case 0: // Input SSID view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewTextInputSSID); break; @@ -233,9 +234,11 @@ void settings_item_selected(void* context, uint32_t index) { * @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_exit_app(void *context) +{ // Exit the application - if(!context) { + if (!context) + { FURI_LOG_E(TAG, "Context is NULL"); return VIEW_NONE; } @@ -243,8 +246,10 @@ uint32_t callback_exit_app(void* context) { return VIEW_NONE; // Return VIEW_NONE to exit the app } -uint32_t callback_to_wifi_settings(void* context) { - if(!context) { +uint32_t callback_to_wifi_settings(void *context) +{ + if (!context) + { FURI_LOG_E(TAG, "Context is NULL"); return VIEW_NONE; } @@ -252,13 +257,16 @@ uint32_t callback_to_wifi_settings(void* context) { return FlipWeatherViewSettings; } -static void flip_weather_widget_set_text(char* message, Widget** widget) { - if(widget == NULL) { +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) { + if (message == NULL) + { FURI_LOG_E(TAG, "flip_weather_set_widget_text - message is NULL"); DEV_CRASH(); return; @@ -266,32 +274,36 @@ static void flip_weather_widget_set_text(char* message, Widget** widget) { 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 + 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)) { + 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 + 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; + 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') { + 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 (found_newline) + { // If newline found, set line_length up to the newline line_length = newline_pos - i; } @@ -302,15 +314,19 @@ static void flip_weather_widget_set_text(char* message, Widget** widget) { line[line_length] = '\0'; // If newline was found, skip it for the next iteration - if(found_newline) { + if (found_newline) + { i += line_length + 1; // +1 to skip the '\n' character - } else { + } + 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] != ' ') { + 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) { + 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 @@ -321,13 +337,15 @@ static void flip_weather_widget_set_text(char* message, Widget** widget) { i += line_length; // Skip any spaces at the beginning of the next line - while(i < message_length && message[i] == ' ') { + 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++) { + for (uint32_t j = 0; j < line_length; j++) + { formatted_message[formatted_index++] = line[j]; } @@ -342,20 +360,23 @@ static void flip_weather_widget_set_text(char* message, Widget** 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) { +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; + DataLoaderModel *data_loader_model = (DataLoaderModel *)model; DataState data_state = data_loader_model->data_state; - char* title = data_loader_model->title; + char *title = data_loader_model->title; canvas_set_font(canvas, FontSecondary); - if(http_state == INACTIVE) { + 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,"); @@ -365,7 +386,8 @@ void flip_weather_loader_draw_callback(Canvas* canvas, void* model) { return; } - if(data_state == DataStateError || data_state == DataStateParseError) { + if (data_state == DataStateError || data_state == DataStateParseError) + { flip_weather_request_error_draw(canvas); return; } @@ -373,53 +395,61 @@ void flip_weather_loader_draw_callback(Canvas* canvas, void* model) { canvas_draw_str(canvas, 0, 7, title); canvas_draw_str(canvas, 0, 17, "Loading..."); - if(data_state == DataStateInitial) { + if (data_state == DataStateInitial) + { return; } - if(http_state == SENDING) { + if (http_state == SENDING) + { canvas_draw_str(canvas, 0, 27, "Sending..."); return; } - if(http_state == RECEIVING || data_state == DataStateRequested) { + if (http_state == RECEIVING || data_state == DataStateRequested) + { canvas_draw_str(canvas, 0, 27, "Receiving..."); return; } - if(http_state == IDLE && data_state == DataStateReceived) { + if (http_state == IDLE && data_state == DataStateReceived) + { canvas_draw_str(canvas, 0, 27, "Processing..."); return; } - if(http_state == IDLE && data_state == DataStateParsed) { + 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) { +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; + 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); + with_view_model(view, DataLoaderModel * model, { current_data_state = model->data_state; }, false); - if(current_data_state == DataStateInitial) { + if (current_data_state == DataStateInitial) + { with_view_model( view, DataLoaderModel * model, { model->data_state = DataStateRequested; DataLoaderFetch fetch = model->fetcher; - if(fetch == NULL) { + if (fetch == NULL) + { FURI_LOG_E(TAG, "Model doesn't have Fetch function assigned."); model->data_state = DataStateError; return; @@ -428,151 +458,174 @@ static void flip_weather_loader_process_callback(void* context) { // Clear any previous responses strncpy(fhttp.last_response, "", 1); bool request_status = fetch(model); - if(!request_status) { + 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) { + } + 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) { + } + 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) { + 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) { + } + 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"); + } + 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) { + } + else if (current_data_state == DataStateReceived) + { with_view_model( view, DataLoaderModel * model, { - char* data_text; - if(model->parser == NULL) { + char *data_text; + if (model->parser == NULL) + { data_text = NULL; FURI_LOG_DEV(TAG, "Parser is NULL"); DEV_CRASH(); - } else { + } + 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"); + 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) { + if (data_text == NULL) + { model->data_state = DataStateParseError; - } else { + } + else + { model->data_state = DataStateParsed; } }, true); - } else if(current_data_state == DataStateParsed) { + } + else if (current_data_state == DataStateParsed) + { with_view_model( view, DataLoaderModel * model, { - if(++model->request_index < model->request_count) { + 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) { + } + 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); + 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) { +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; + 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) { +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; + 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); + 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) { +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; + FlipWeatherApp *app = (FlipWeatherApp *)context; + View *view = app->view_loader; with_view_model( view, DataLoaderModel * model, { - if(model->timer) { + if (model->timer) + { furi_timer_stop(model->timer); } }, false); } -void flip_weather_loader_init(View* view) { - if(view == NULL) { +void flip_weather_loader_init(View *view) +{ + if (view == NULL) + { FURI_LOG_E(TAG, "flip_weather_loader_init - view is NULL"); DEV_CRASH(); return; @@ -582,8 +635,10 @@ void flip_weather_loader_init(View* view) { view_set_exit_callback(view, flip_weather_loader_on_exit); } -void flip_weather_loader_free_model(View* view) { - if(view == NULL) { +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; @@ -592,11 +647,13 @@ void flip_weather_loader_free_model(View* view) { view, DataLoaderModel * model, { - if(model->timer) { + if (model->timer) + { furi_timer_free(model->timer); model->timer = NULL; } - if(model->parser_context) { + if (model->parser_context) + { free(model->parser_context); model->parser_context = NULL; } @@ -604,14 +661,17 @@ void flip_weather_loader_free_model(View* view) { false); } -bool flip_weather_custom_event_callback(void* context, uint32_t index) { - if(context == NULL) { +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) { + switch (index) + { case FlipWeatherCustomEventProcess: flip_weather_loader_process_callback(context); return true; @@ -621,22 +681,18 @@ bool flip_weather_custom_event_callback(void* context, uint32_t index) { } } -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) { +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) { + View *view = app->view_loader; + if (view == NULL) + { FURI_LOG_E(TAG, "flip_weather_generic_switch_to_view - view is NULL"); DEV_CRASH(); return; diff --git a/flip_weather/callback/flip_weather_callback.h b/flip_weather/callback/flip_weather_callback.h index 174c43b3f..9b12bdd3c 100644 --- a/flip_weather/callback/flip_weather_callback.h +++ b/flip_weather/callback/flip_weather_callback.h @@ -9,37 +9,30 @@ 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); +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); +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_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_draw_callback(Canvas *canvas, void *model); -void flip_weather_loader_init(View* view); +void flip_weather_loader_init(View *view); -void flip_weather_loader_free_model(View* view); +void flip_weather_loader_free_model(View *view); -bool flip_weather_custom_event_callback(void* context, uint32_t index); -#endif +bool flip_weather_custom_event_callback(void *context, uint32_t index); +#endif \ No newline at end of file diff --git a/flip_weather/easy_flipper/easy_flipper.c b/flip_weather/easy_flipper/easy_flipper.c index 8b98e1a1b..f61b7d231 100644 --- a/flip_weather/easy_flipper/easy_flipper.c +++ b/flip_weather/easy_flipper/easy_flipper.c @@ -5,9 +5,11 @@ * @param context The context - unused * @return next view id (VIEW_NONE to exit the app) */ -uint32_t easy_flipper_callback_exit_app(void* context) { +uint32_t easy_flipper_callback_exit_app(void *context) +{ // Exit the application - if(!context) { + if (!context) + { FURI_LOG_E(EASY_TAG, "Context is NULL"); return VIEW_NONE; } @@ -21,13 +23,16 @@ uint32_t easy_flipper_callback_exit_app(void* context) { * @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) { +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) { + *buffer = (char *)malloc(buffer_size); + if (!*buffer) + { FURI_LOG_E(EASY_TAG, "Failed to allocate buffer"); return false; } @@ -46,32 +51,39 @@ bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size) { * @return true if successful, false otherwise */ bool easy_flipper_set_view( - View** 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) { + 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) { + if (!*view) + { FURI_LOG_E(EASY_TAG, "Failed to allocate View"); return false; } - if(draw_callback) { + if (draw_callback) + { view_set_draw_callback(*view, draw_callback); } - if(input_callback) { + if (input_callback) + { view_set_input_callback(*view, input_callback); } - if(context) { + if (context) + { view_set_context(*view, context); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(*view, previous_callback); } view_dispatcher_add_view(*view_dispatcher, view_id, *view); @@ -85,18 +97,22 @@ bool easy_flipper_set_view( * @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) { +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) { + 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) { + if (context) + { view_dispatcher_set_event_callback_context(*view_dispatcher, context); } return true; @@ -113,24 +129,29 @@ bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui * @return true if successful, false otherwise */ bool easy_flipper_set_submenu( - Submenu** submenu, + Submenu **submenu, int32_t view_id, - char* title, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!submenu) { + 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) { + if (!*submenu) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Submenu"); return false; } - if(title) { + if (title) + { submenu_set_header(*submenu, title); } - if(previous_callback) { + 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)); @@ -147,20 +168,24 @@ bool easy_flipper_set_submenu( * @return true if successful, false otherwise */ bool easy_flipper_set_menu( - Menu** menu, + Menu **menu, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!menu) { + 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) { + if (!*menu) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Menu"); return false; } - if(previous_callback) { + 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)); @@ -177,24 +202,29 @@ bool easy_flipper_set_menu( * @return true if successful, false otherwise */ bool easy_flipper_set_widget( - Widget** widget, + Widget **widget, int32_t view_id, - char* text, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!widget) { + 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) { + if (!*widget) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Widget"); return false; } - if(text) { + if (text) + { widget_add_text_scroll_element(*widget, 0, 0, 128, 64, text); } - if(previous_callback) { + 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)); @@ -213,30 +243,33 @@ bool easy_flipper_set_widget( * @return true if successful, false otherwise */ bool easy_flipper_set_variable_item_list( - VariableItemList** 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) { + 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) { + if (!*variable_item_list) + { FURI_LOG_E(EASY_TAG, "Failed to allocate VariableItemList"); return false; } - if(enter_callback) { + 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); + 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)); + view_dispatcher_add_view(*view_dispatcher, view_id, variable_item_list_get_view(*variable_item_list)); return true; } @@ -249,38 +282,38 @@ bool easy_flipper_set_variable_item_list( * @return true if successful, false otherwise */ bool easy_flipper_set_text_input( - TextInput** text_input, + TextInput **text_input, int32_t view_id, - char* header_text, - char* text_input_temp_buffer, + 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) { + 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) { + if (!*text_input) + { FURI_LOG_E(EASY_TAG, "Failed to allocate TextInput"); return false; } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(text_input_get_view(*text_input), previous_callback); } - if(header_text) { + 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); + 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; @@ -295,38 +328,38 @@ bool easy_flipper_set_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_uart_text_input( - TextInput** uart_text_input, + TextInput **uart_text_input, int32_t view_id, - char* header_text, - char* uart_text_input_temp_buffer, + 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) { + 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) { + if (!*uart_text_input) + { FURI_LOG_E(EASY_TAG, "Failed to allocate UART_TextInput"); return false; } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(text_input_get_view(*uart_text_input), previous_callback); } - if(header_text) { + 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); + 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)); @@ -353,52 +386,63 @@ bool easy_flipper_set_uart_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_dialog_ex( - DialogEx** dialog_ex, + DialogEx **dialog_ex, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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) { + 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) { + if (!*dialog_ex) + { FURI_LOG_E(EASY_TAG, "Failed to allocate DialogEx"); return false; } - if(header) { + if (header) + { dialog_ex_set_header(*dialog_ex, header, header_x, header_y, AlignLeft, AlignTop); } - if(text) { + if (text) + { dialog_ex_set_text(*dialog_ex, text, text_x, text_y, AlignLeft, AlignTop); } - if(left_button_text) { + if (left_button_text) + { dialog_ex_set_left_button_text(*dialog_ex, left_button_text); } - if(right_button_text) { + if (right_button_text) + { dialog_ex_set_right_button_text(*dialog_ex, right_button_text); } - if(center_button_text) { + if (center_button_text) + { dialog_ex_set_center_button_text(*dialog_ex, center_button_text); } - if(result_callback) { + if (result_callback) + { dialog_ex_set_result_callback(*dialog_ex, result_callback); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(dialog_ex_get_view(*dialog_ex), previous_callback); } - if(context) { + if (context) + { dialog_ex_set_context(*dialog_ex, context); } view_dispatcher_add_view(*view_dispatcher, view_id, dialog_ex_get_view(*dialog_ex)); @@ -422,40 +466,48 @@ bool easy_flipper_set_dialog_ex( * @return true if successful, false otherwise */ bool easy_flipper_set_popup( - Popup** popup, + Popup **popup, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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) { + 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) { + if (!*popup) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Popup"); return false; } - if(header) { + if (header) + { popup_set_header(*popup, header, header_x, header_y, AlignLeft, AlignTop); } - if(text) { + if (text) + { popup_set_text(*popup, text, text_x, text_y, AlignLeft, AlignTop); } - if(result_callback) { + if (result_callback) + { popup_set_callback(*popup, result_callback); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(popup_get_view(*popup), previous_callback); } - if(context) { + if (context) + { popup_set_context(*popup, context); } view_dispatcher_add_view(*view_dispatcher, view_id, popup_get_view(*popup)); @@ -471,20 +523,24 @@ bool easy_flipper_set_popup( * @return true if successful, false otherwise */ bool easy_flipper_set_loading( - Loading** loading, + Loading **loading, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!loading) { + 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) { + if (!*loading) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Loading"); return false; } - if(previous_callback) { + 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)); @@ -497,16 +553,19 @@ bool easy_flipper_set_loading( * @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) { +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) { + if (!furi_string) + { FURI_LOG_E(EASY_TAG, "Failed to allocate FuriString"); return false; } furi_string_set_str(*furi_string, buffer); return true; -} +} \ No newline at end of file diff --git a/flip_weather/easy_flipper/easy_flipper.h b/flip_weather/easy_flipper/easy_flipper.h index 219e42c74..0e99e17fb 100644 --- a/flip_weather/easy_flipper/easy_flipper.h +++ b/flip_weather/easy_flipper/easy_flipper.h @@ -30,14 +30,14 @@ * @param context The context - unused * @return next view id (VIEW_NONE to exit the app) */ -uint32_t easy_flipper_callback_exit_app(void* context); +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); +bool easy_flipper_set_buffer(char **buffer, uint32_t buffer_size); /** * @brief Initialize a View object * @param view The View object to initialize @@ -49,13 +49,13 @@ bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size); * @return true if successful, false otherwise */ bool easy_flipper_set_view( - View** 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); + 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 @@ -64,7 +64,7 @@ bool easy_flipper_set_view( * @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); +bool easy_flipper_set_view_dispatcher(ViewDispatcher **view_dispatcher, Gui *gui, void *context); /** * @brief Initialize a Submenu object @@ -77,11 +77,11 @@ bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui * @return true if successful, false otherwise */ bool easy_flipper_set_submenu( - Submenu** submenu, + Submenu **submenu, int32_t view_id, - char* title, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + char *title, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a Menu object @@ -94,10 +94,10 @@ bool easy_flipper_set_submenu( * @return true if successful, false otherwise */ bool easy_flipper_set_menu( - Menu** menu, + Menu **menu, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a Widget object @@ -109,11 +109,11 @@ bool easy_flipper_set_menu( * @return true if successful, false otherwise */ bool easy_flipper_set_widget( - Widget** widget, + Widget **widget, int32_t view_id, - char* text, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + char *text, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a VariableItemList object @@ -127,12 +127,12 @@ bool easy_flipper_set_widget( * @return true if successful, false otherwise */ bool easy_flipper_set_variable_item_list( - VariableItemList** 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); + void (*enter_callback)(void *, uint32_t), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a TextInput object @@ -143,15 +143,15 @@ bool easy_flipper_set_variable_item_list( * @return true if successful, false otherwise */ bool easy_flipper_set_text_input( - TextInput** text_input, + TextInput **text_input, int32_t view_id, - char* header_text, - char* text_input_temp_buffer, + 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); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a TextInput object with extra symbols @@ -162,15 +162,15 @@ bool easy_flipper_set_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_uart_text_input( - TextInput** uart_text_input, + TextInput **uart_text_input, int32_t view_id, - char* header_text, - char* uart_text_input_temp_buffer, + 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); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a DialogEx object @@ -192,21 +192,21 @@ bool easy_flipper_set_uart_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_dialog_ex( - DialogEx** dialog_ex, + DialogEx **dialog_ex, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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); + 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 @@ -225,18 +225,18 @@ bool easy_flipper_set_dialog_ex( * @return true if successful, false otherwise */ bool easy_flipper_set_popup( - Popup** popup, + Popup **popup, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + char *text, uint16_t text_x, uint16_t text_y, - void (*result_callback)(void*), - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher, - void* context); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a Loading object @@ -247,10 +247,10 @@ bool easy_flipper_set_popup( * @return true if successful, false otherwise */ bool easy_flipper_set_loading( - Loading** loading, + Loading **loading, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Set a char butter to a FuriString @@ -258,6 +258,6 @@ bool easy_flipper_set_loading( * @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); +bool easy_flipper_set_char_to_furi_string(FuriString **furi_string, char *buffer); -#endif +#endif \ No newline at end of file diff --git a/flip_weather/flip_storage/flip_weather_storage.c b/flip_weather/flip_storage/flip_weather_storage.c index db50eb39a..e9b225cea 100644 --- a/flip_weather/flip_storage/flip_weather_storage.c +++ b/flip_weather/flip_storage/flip_weather_storage.c @@ -1,19 +1,22 @@ #include "flip_storage/flip_weather_storage.h" -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( - directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_weather"); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_weather"); // Create the directory - Storage* storage = furi_record_open(RECORD_STORAGE); + Storage *storage = furi_record_open(RECORD_STORAGE); storage_common_mkdir(storage, directory_path); // Open the settings file - File* file = storage_file_alloc(storage); - if(!storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + File *file = storage_file_alloc(storage); + if (!storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) + { FURI_LOG_E(TAG, "Failed to open settings file for writing: %s", SETTINGS_PATH); storage_file_free(file); furi_record_close(RECORD_STORAGE); @@ -22,15 +25,17 @@ void save_settings(const char* ssid, const char* password) { // Save the ssid length and data size_t ssid_length = strlen(ssid) + 1; // Include null terminator - if(storage_file_write(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, ssid, ssid_length) != ssid_length) { + if (storage_file_write(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, ssid, ssid_length) != ssid_length) + { FURI_LOG_E(TAG, "Failed to write SSID"); } // Save the password length and data size_t password_length = strlen(password) + 1; // Include null terminator - if(storage_file_write(file, &password_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, password, password_length) != password_length) { + if (storage_file_write(file, &password_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, password, password_length) != password_length) + { FURI_LOG_E(TAG, "Failed to write password"); } @@ -39,11 +44,17 @@ void save_settings(const char* ssid, const char* password) { furi_record_close(RECORD_STORAGE); } -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); +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); - if(!storage_file_open(file, SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) { + if (!storage_file_open(file, SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) + { FURI_LOG_E(TAG, "Failed to open settings file for reading: %s", SETTINGS_PATH); storage_file_free(file); furi_record_close(RECORD_STORAGE); @@ -52,8 +63,9 @@ bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password // Load the ssid size_t ssid_length; - if(storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || - ssid_length > ssid_size || storage_file_read(file, ssid, ssid_length) != ssid_length) { + if (storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || ssid_length > ssid_size || + storage_file_read(file, ssid, ssid_length) != ssid_length) + { FURI_LOG_E(TAG, "Failed to read SSID"); storage_file_close(file); storage_file_free(file); @@ -64,9 +76,9 @@ bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password // Load the password size_t password_length; - if(storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || - password_length > password_size || - storage_file_read(file, password, password_length) != password_length) { + if (storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || password_length > password_size || + storage_file_read(file, password, password_length) != password_length) + { FURI_LOG_E(TAG, "Failed to read password"); storage_file_close(file); storage_file_free(file); @@ -80,4 +92,4 @@ bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password furi_record_close(RECORD_STORAGE); return true; -} +} \ No newline at end of file diff --git a/flip_weather/flip_storage/flip_weather_storage.h b/flip_weather/flip_storage/flip_weather_storage.h index 1cd6ddcb5..f4d2b915f 100644 --- a/flip_weather/flip_storage/flip_weather_storage.h +++ b/flip_weather/flip_storage/flip_weather_storage.h @@ -7,7 +7,13 @@ #define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_weather/settings.bin" -void save_settings(const char* ssid, const char* password); +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 +bool load_settings( + char *ssid, + size_t ssid_size, + char *password, + size_t password_size); +#endif // FLIP_WEATHER_STORAGE_H \ No newline at end of file diff --git a/flip_weather/flip_weather.c b/flip_weather/flip_weather.c index b29d09bfd..afd2987ca 100644 --- a/flip_weather/flip_weather.c +++ b/flip_weather/flip_weather.c @@ -3,75 +3,91 @@ char lat_data[32]; char lon_data[32]; -char* total_data = NULL; -char* weather_data = NULL; +char *total_data = NULL; +char *weather_data = NULL; -FlipWeatherApp* app_instance = NULL; -void flip_weather_loader_free_model(View* view); +FlipWeatherApp *app_instance = NULL; +void flip_weather_loader_free_model(View *view); // Function to free the resources used by FlipWeatherApp -void flip_weather_app_free(FlipWeatherApp* app) { - if(!app) { +void flip_weather_app_free(FlipWeatherApp *app) +{ + if (!app) + { FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); return; } // Free View(s) - if(app->view_loader) { + 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) - if(app->submenu) { + if (app->submenu) + { view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewSubmenu); submenu_free(app->submenu); } // Free Widget(s) - if(app->widget) { + if (app->widget) + { view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewAbout); widget_free(app->widget); } - if(app->widget_result) { + 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) { + if (app->variable_item_list) + { view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewSettings); variable_item_list_free(app->variable_item_list); } // Free Text Input(s) - if(app->uart_text_input_ssid) { + if (app->uart_text_input_ssid) + { view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewTextInputSSID); text_input_free(app->uart_text_input_ssid); } - if(app->uart_text_input_password) { + if (app->uart_text_input_password) + { view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewTextInputPassword); text_input_free(app->uart_text_input_password); } // Free the text input buffer - if(app->uart_text_input_buffer_ssid) free(app->uart_text_input_buffer_ssid); - if(app->uart_text_input_temp_buffer_ssid) free(app->uart_text_input_temp_buffer_ssid); - if(app->uart_text_input_buffer_password) free(app->uart_text_input_buffer_password); - if(app->uart_text_input_temp_buffer_password) free(app->uart_text_input_temp_buffer_password); + if (app->uart_text_input_buffer_ssid) + free(app->uart_text_input_buffer_ssid); + if (app->uart_text_input_temp_buffer_ssid) + free(app->uart_text_input_temp_buffer_ssid); + if (app->uart_text_input_buffer_password) + free(app->uart_text_input_buffer_password); + if (app->uart_text_input_temp_buffer_password) + free(app->uart_text_input_temp_buffer_password); // 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); - if(total_data) free(total_data); + if (total_data) + free(total_data); // free the app - if(app) free(app); -} + if (app) + free(app); +} \ No newline at end of file diff --git a/flip_weather/flip_weather.h b/flip_weather/flip_weather.h index 656393681..1852caec5 100644 --- a/flip_weather/flip_weather.h +++ b/flip_weather/flip_weather.h @@ -5,61 +5,64 @@ #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 { - FlipWeatherSubmenuIndexWeather, // Click to view the weather - FlipWeatherSubmenuIndexGPS, // Click to view the GPS - FlipWeatherSubmenuIndexAbout, // Click to view the about screen +typedef enum +{ + FlipWeatherSubmenuIndexWeather, // Click to view the weather + FlipWeatherSubmenuIndexGPS, // Click to view the GPS + FlipWeatherSubmenuIndexAbout, // Click to view the about screen FlipWeatherSubmenuIndexSettings, // Click to view the settings screen } FlipWeatherSubmenuIndex; // Define a single view for our FlipWeather application -typedef enum { - FlipWeatherViewSubmenu, // The main submenu - FlipWeatherViewAbout, // The about screen - FlipWeatherViewSettings, // The wifi settings screen - FlipWeatherViewTextInputSSID, // The text input screen for SSID +typedef enum +{ + 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 + FlipWeatherViewPopupError, // The error popup screen FlipWeatherViewWidgetResult, // The text box that displays the random fact - FlipWeatherViewLoader, // The loader screen retrieves data from the internet + 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_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 - TextInput* uart_text_input_ssid; // The text input - TextInput* uart_text_input_password; // The text input +typedef struct +{ + ViewDispatcher *view_dispatcher; // Switches between our views + 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 + TextInput *uart_text_input_ssid; // The text input + TextInput *uart_text_input_password; // The text input - char* uart_text_input_buffer_ssid; // Buffer for the text input - char* uart_text_input_temp_buffer_ssid; // Temporary buffer for the text input + char *uart_text_input_buffer_ssid; // Buffer for the text input + char *uart_text_input_temp_buffer_ssid; // Temporary buffer for the text input uint32_t uart_text_input_buffer_size_ssid; // Size of the text input buffer - char* uart_text_input_buffer_password; // Buffer for the text input - char* uart_text_input_temp_buffer_password; // Temporary buffer for the text input + char *uart_text_input_buffer_password; // Buffer for the text input + char *uart_text_input_temp_buffer_password; // Temporary buffer for the text input uint32_t uart_text_input_buffer_size_password; // Size of the text input buffer } FlipWeatherApp; extern char lat_data[32]; extern char lon_data[32]; -extern char* total_data; -extern char* weather_data; +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; +void flip_weather_app_free(FlipWeatherApp *app); +extern FlipWeatherApp *app_instance; -#endif +#endif \ No newline at end of file diff --git a/flip_weather/flipper_http/flipper_http.c b/flip_weather/flipper_http/flipper_http.c index e7aafd857..177ee3bf6 100644 --- a/flip_weather/flipper_http/flipper_http.c +++ b/flip_weather/flipper_http/flipper_http.c @@ -6,17 +6,21 @@ 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, + 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); + char *file_path) +{ + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); - if(start_new_file) { + 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)) { + 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); @@ -24,15 +28,19 @@ bool flipper_http_append_to_file( } } // Open the file in write mode - if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + 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 { + } + else + { // Open the file in append mode - if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_OPEN_APPEND)) { + 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); @@ -41,7 +49,8 @@ bool flipper_http_append_to_file( } // Write the data to the file - if(storage_file_write(file, data, data_size) != data_size) { + 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); @@ -55,32 +64,37 @@ bool flipper_http_append_to_file( return true; } -FuriString* flipper_http_load_from_file(char* file_path) { +FuriString *flipper_http_load_from_file(char *file_path) +{ // Open the storage record - Storage* storage = furi_record_open(RECORD_STORAGE); - if(!storage) { + 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) { + 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)) { + 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) { + 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); @@ -92,8 +106,9 @@ FuriString* flipper_http_load_from_file(char* file_path) { furi_string_reset(str_result); // Define a buffer to hold the read data - uint8_t* buffer = (uint8_t*)malloc(MAX_FILE_SHOW); - if(!buffer) { + 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); @@ -104,7 +119,8 @@ FuriString* flipper_http_load_from_file(char* file_path) { // 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) { + 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); @@ -114,7 +130,8 @@ FuriString* flipper_http_load_from_file(char* file_path) { } // Append each byte to the FuriString - for(size_t i = 0; i < read_count; i++) { + for (size_t i = 0; i < read_count; i++) + { furi_string_push_back(str_result, buffer[i]); } @@ -138,39 +155,48 @@ FuriString* flipper_http_load_from_file(char* file_path) { * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -int32_t flipper_http_worker(void* context) { +int32_t flipper_http_worker(void *context) +{ UNUSED(context); size_t rx_line_pos = 0; - while(1) { + while (1) + { uint32_t events = furi_thread_flags_wait( WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever); - if(events & WorkerEvtStop) { + if (events & WorkerEvtStop) + { break; } - if(events & WorkerEvtRxDone) { + if (events & WorkerEvtRxDone) + { // Continuously read from the stream buffer until it's empty - while(!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) { + 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) { + if (received == 0) + { // No more data to read break; } // Append the received byte to the file if saving is enabled - if(fhttp.save_bytes) { + 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)) { + 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; @@ -179,9 +205,11 @@ int32_t flipper_http_worker(void* context) { } // Handle line buffering only if callback is set (text data) - if(fhttp.handle_rx_line_cb) { + if (fhttp.handle_rx_line_cb) + { // Handle line buffering - if(c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1) { + 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 @@ -189,7 +217,9 @@ int32_t flipper_http_worker(void* context) { // Reset the line buffer position rx_line_pos = 0; - } else { + } + else + { rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer } } @@ -206,7 +236,8 @@ int32_t flipper_http_worker(void* context) { * @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) { +void get_timeout_timer_callback(void *context) +{ UNUSED(context); FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving the end."); @@ -230,11 +261,13 @@ void get_timeout_timer_callback(void* context) { * @note This function will handle received data asynchronously via the callback. */ void _flipper_http_rx_callback( - FuriHalSerialHandle* handle, + FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, - void* context) { + void *context) +{ UNUSED(context); - if(event == FuriHalSerialRxEventData) { + 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); @@ -249,23 +282,28 @@ void _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) { +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) { + 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) { + 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) { + if (!fhttp.rx_thread) + { FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread."); furi_stream_buffer_free(fhttp.flipper_http_stream); return false; @@ -283,13 +321,15 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { 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)) { + 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) { + 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); @@ -312,11 +352,12 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { // 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 + FuriTimerTypeOnce, // One-shot timer + &fhttp // Context passed to callback ); - if(!fhttp.get_timeout_timer) { + 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); @@ -333,8 +374,9 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { // 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) { + 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; } @@ -349,8 +391,10 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { * @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) { +void flipper_http_deinit() +{ + if (fhttp.serial_handle == NULL) + { FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?"); return; } @@ -373,13 +417,15 @@ void flipper_http_deinit() { furi_stream_buffer_free(fhttp.flipper_http_stream); // Free the timer - if(fhttp.get_timeout_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) { + if (fhttp.last_response) + { free(fhttp.last_response); fhttp.last_response = NULL; } @@ -394,9 +440,11 @@ void flipper_http_deinit() { * @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) { +bool flipper_http_send_data(const char *data) +{ size_t data_length = strlen(data); - if(data_length == 0) { + if (data_length == 0) + { FURI_LOG_E("FlipperHTTP", "Attempted to send empty data."); fhttp.state = ISSUE; return false; @@ -404,25 +452,27 @@ 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 > 512) { // 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[513]; // 512 + 1 for safety strncpy(send_buffer, data, 512); - send_buffer[data_length] = '\n'; // Append newline + 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))) { + 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); + 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); @@ -438,9 +488,11 @@ bool flipper_http_send_data(const char* data) { * @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)) { +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; } @@ -456,9 +508,11 @@ bool flipper_http_ping() { * @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)) { +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; } @@ -473,9 +527,11 @@ bool flipper_http_list_commands() { * @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)) { +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; } @@ -490,9 +546,11 @@ bool flipper_http_led_on() { * @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)) { +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; } @@ -509,8 +567,10 @@ bool flipper_http_led_off() { * @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) { +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; } @@ -518,12 +578,14 @@ bool flipper_http_parse_json(const char* key, const char* json_data) { char buffer[256]; int ret = snprintf(buffer, sizeof(buffer), "[PARSE]{\"key\":\"%s\",\"json\":%s}", key, json_data); - if(ret < 0 || ret >= (int)sizeof(buffer)) { + 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)) { + if (!flipper_http_send_data(buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse command."); return false; } @@ -541,8 +603,10 @@ bool flipper_http_parse_json(const char* key, const char* json_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) { +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; } @@ -555,12 +619,14 @@ bool flipper_http_parse_json_array(const char* key, int index, const char* json_ key, index, json_data); - if(ret < 0 || ret >= (int)sizeof(buffer)) { + 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)) { + if (!flipper_http_send_data(buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse array command."); return false; } @@ -575,9 +641,11 @@ bool flipper_http_parse_json_array(const char* key, int index, const char* json_ * @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)) { +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; } @@ -592,20 +660,24 @@ bool flipper_http_scan_wifi() { * @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) { +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)) { + 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)) { + if (!flipper_http_send_data(buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command."); return false; } @@ -620,9 +692,11 @@ bool flipper_http_save_wifi(const char* ssid, const char* password) { * @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)) { +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; } @@ -637,9 +711,11 @@ bool flipper_http_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_wifi() { - const char* command = "[WIFI/IP]"; - if(!flipper_http_send_data(command)) { +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; } @@ -654,9 +730,11 @@ bool flipper_http_ip_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)) { +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; } @@ -671,9 +749,11 @@ bool flipper_http_disconnect_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)) { +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; } @@ -689,8 +769,10 @@ bool flipper_http_connect_wifi() { * @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) { +bool flipper_http_get_request(const char *url) +{ + if (!url) + { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request."); return false; } @@ -698,13 +780,15 @@ bool flipper_http_get_request(const char* url) { // Prepare GET request command char command[512]; int ret = snprintf(command, sizeof(command), "[GET]%s", url); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command."); return false; } @@ -720,8 +804,10 @@ bool flipper_http_get_request(const char* url) { * @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) { +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; @@ -731,13 +817,15 @@ bool flipper_http_get_request_with_headers(const char* url, const char* 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)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); return false; } @@ -753,8 +841,10 @@ bool flipper_http_get_request_with_headers(const char* url, const char* headers) * @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) { +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; } @@ -763,13 +853,15 @@ bool flipper_http_get_request_bytes(const char* url, const char* 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)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); return false; } @@ -787,10 +879,12 @@ bool flipper_http_get_request_bytes(const char* url, const char* headers) { * @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) { + 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."); @@ -806,13 +900,15 @@ bool flipper_http_post_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); return false; } @@ -829,8 +925,10 @@ bool flipper_http_post_request_with_headers( * @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) { +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; @@ -845,13 +943,15 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); return false; } @@ -869,10 +969,12 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const * @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) { + 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; @@ -887,13 +989,15 @@ bool flipper_http_put_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data."); return false; } @@ -911,10 +1015,12 @@ bool flipper_http_put_request_with_headers( * @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) { + 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."); @@ -930,14 +1036,16 @@ bool flipper_http_delete_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data."); return false; } @@ -953,26 +1061,31 @@ bool flipper_http_delete_request_with_headers( * @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) { +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') { + 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) { + 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) { + if (fhttp.state != INACTIVE && fhttp.state != ISSUE) + { fhttp.state = RECEIVING; } @@ -980,11 +1093,13 @@ void flipper_http_rx_callback(const char* line, void* context) { // 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) { + 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) { + 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); @@ -994,14 +1109,17 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.save_bytes = false; fhttp.save_received_data = false; - if(fhttp.is_bytes_request) { + 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++) { + 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) { + 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); @@ -1011,9 +1129,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1025,9 +1144,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1035,18 +1155,21 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_get) { + 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) { + 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) { + 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); @@ -1056,14 +1179,17 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.save_bytes = false; fhttp.save_received_data = false; - if(fhttp.is_bytes_request) { + 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++) { + 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) { + 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); @@ -1073,9 +1199,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1087,9 +1214,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1097,18 +1225,21 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_post) { + 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) { + 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) { + 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); @@ -1122,9 +1253,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1132,18 +1264,21 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_put) { + 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) { + 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) { + 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); @@ -1157,9 +1292,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1167,22 +1303,29 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_delete) { + 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) { + if (strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL) + { FURI_LOG_I(HTTP_TAG, "Operation succeeded."); - } else if(strstr(line, "[INFO]") != NULL) { + } + 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) { + if (fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL) + { fhttp.state = IDLE; } - } else if(strstr(line, "[GET/SUCCESS]") != NULL) { + } + 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); @@ -1192,7 +1335,9 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.just_started_bytes = true; file_buffer_len = 0; return; - } else if(strstr(line, "[POST/SUCCESS]") != NULL) { + } + 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); @@ -1202,67 +1347,86 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.just_started_bytes = true; file_buffer_len = 0; return; - } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { + } + 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) { + } + 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) { + } + else if (strstr(line, "[DISCONNECTED]") != NULL) + { FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully."); - } else if(strstr(line, "[ERROR]") != NULL) { + } + 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) { + } + 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) { + if (fhttp.state == INACTIVE) + { fhttp.state = IDLE; return; } } - if(fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) { + if (fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) + { fhttp.state = IDLE; - } else if(fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) { + } + else if (fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) + { fhttp.state = INACTIVE; - } else { + } + 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; +char *trim(const char *str) +{ + const char *end; + char *trimmed_str; size_t len; // Trim leading space - while(isspace((unsigned char)*str)) + while (isspace((unsigned char)*str)) str++; // All spaces? - if(*str == 0) return strdup(""); // Return an empty string if 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)) + 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) { + trimmed_str = (char *)malloc(len + 1); + if (trimmed_str == NULL) + { return NULL; // Handle memory allocation failure } @@ -1279,21 +1443,25 @@ char* trim(const char* str) { * @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 +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 { + } + 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) { + 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) + if (!parse_json()) // parse the JSON before switching to the view (synchonous) { FURI_LOG_E(HTTP_TAG, "Failed to parse the JSON..."); return false; @@ -1310,17 +1478,18 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars * @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; +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) { + if (!loading) + { FURI_LOG_E(HTTP_TAG, "Failed to allocate loading"); view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); @@ -1333,7 +1502,8 @@ void flipper_http_loading_task( view_dispatcher_switch_to_view(*view_dispatcher, loading_view_id); // Make the request - if(!flipper_http_process_response_async(http_request, parse_response)) { + 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); diff --git a/flip_weather/flipper_http/flipper_http.h b/flip_weather/flipper_http/flipper_http.h index 1eb1e1e70..764262f72 100644 --- a/flip_weather/flipper_http/flipper_http.h +++ b/flip_weather/flipper_http/flipper_http.h @@ -15,69 +15,72 @@ // 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 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 +#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); +typedef void (*FlipperHTTP_Callback)(const char *line, void *context); // State variable to track the UART state -typedef enum { - INACTIVE, // Inactive state - IDLE, // Default state +typedef enum +{ + INACTIVE, // Inactive state + IDLE, // Default state RECEIVING, // Receiving data - SENDING, // Sending data - ISSUE, // Issue with connection + SENDING, // Sending data + ISSUE, // Issue with connection } SerialState; // Event Flags for UART Worker Thread -typedef enum { +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 +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 + 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 *last_response; char file_path[256]; // Path to save the received data // Timer-related members - FuriTimer* get_timeout_timer; // Timer for HTTP request timeout + 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 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 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 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 + 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; + 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 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; @@ -93,12 +96,12 @@ extern size_t file_buffer_len; // 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, + const void *data, size_t data_size, bool start_new_file, - char* file_path); + char *file_path); -FuriString* flipper_http_load_from_file(char* file_path); +FuriString *flipper_http_load_from_file(char *file_path); // UART worker thread /** @@ -108,7 +111,7 @@ FuriString* flipper_http_load_from_file(char* file_path); * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -int32_t flipper_http_worker(void* context); +int32_t flipper_http_worker(void *context); // Timer callback function /** @@ -117,7 +120,7 @@ int32_t flipper_http_worker(void* context); * @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); +void get_timeout_timer_callback(void *context); // UART RX Handler Callback (Interrupt Context) /** @@ -129,9 +132,9 @@ void get_timeout_timer_callback(void* context); * @note This function will handle received data asynchronously via the callback. */ void _flipper_http_rx_callback( - FuriHalSerialHandle* handle, + FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, - void* context); + void *context); // UART initialization function /** @@ -141,7 +144,7 @@ void _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); +bool flipper_http_init(FlipperHTTP_Callback callback, void *context); // Deinitialize UART /** @@ -158,7 +161,7 @@ void flipper_http_deinit(); * @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); +bool flipper_http_send_data(const char *data); // Function to send a PING request /** @@ -202,7 +205,7 @@ bool flipper_http_led_off(); * @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); +bool flipper_http_parse_json(const char *key, const char *json_data); // Function to parse JSON array data /** @@ -213,7 +216,7 @@ bool flipper_http_parse_json(const char* key, const char* json_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); +bool flipper_http_parse_json_array(const char *key, int index, const char *json_data); // Function to scan for WiFi networks /** @@ -229,7 +232,7 @@ bool flipper_http_scan_wifi(); * @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); +bool flipper_http_save_wifi(const char *ssid, const char *password); // Function to get IP address of WiFi Devboard /** @@ -270,7 +273,7 @@ bool flipper_http_connect_wifi(); * @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); +bool flipper_http_get_request(const char *url); // Function to send a GET request with headers /** @@ -280,7 +283,7 @@ bool flipper_http_get_request(const char* url); * @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); +bool flipper_http_get_request_with_headers(const char *url, const char *headers); // Function to send a GET request with headers and return bytes /** @@ -290,7 +293,7 @@ bool flipper_http_get_request_with_headers(const char* url, const char* headers) * @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); +bool flipper_http_get_request_bytes(const char *url, const char *headers); // Function to send a POST request with headers /** @@ -302,9 +305,9 @@ bool flipper_http_get_request_bytes(const char* url, const char* headers); * @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); + const char *url, + const char *headers, + const char *payload); // Function to send a POST request with headers and return bytes /** @@ -315,7 +318,7 @@ bool flipper_http_post_request_with_headers( * @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); +bool flipper_http_post_request_bytes(const char *url, const char *headers, const char *payload); // Function to send a PUT request with headers /** @@ -327,9 +330,9 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const * @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); + const char *url, + const char *headers, + const char *payload); // Function to send a DELETE request with headers /** @@ -341,9 +344,9 @@ bool flipper_http_put_request_with_headers( * @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); + const char *url, + const char *headers, + const char *payload); // Function to handle received data asynchronously /** @@ -353,10 +356,10 @@ bool flipper_http_delete_request_with_headers( * @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); +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); +char *trim(const char *str); /** * @brief Process requests and parse JSON data asynchronously * @param http_request The function to send the request @@ -374,11 +377,10 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars * @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); +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_weather/jsmn/jsmn.c b/flip_weather/jsmn/jsmn.c index eb33b3cc7..b85ab83b5 100644 --- a/flip_weather/jsmn/jsmn.c +++ b/flip_weather/jsmn/jsmn.c @@ -13,11 +13,13 @@ /** * 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; +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) { + if (parser->toknext >= num_tokens) + { return NULL; } tok = &tokens[parser->toknext++]; @@ -32,8 +34,9 @@ static jsmntok_t* /** * Fills token type and boundaries. */ -static void - jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { +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; @@ -43,19 +46,19 @@ static void /** * 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; +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]) { + 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 ':': @@ -72,7 +75,8 @@ static int jsmn_parse_primitive( /* to quiet a warning from gcc*/ break; } - if(js[parser->pos] < 32 || js[parser->pos] >= 127) { + if (js[parser->pos] < 32 || js[parser->pos] >= 127) + { parser->pos = start; return JSMN_ERROR_INVAL; } @@ -84,12 +88,14 @@ static int jsmn_parse_primitive( #endif found: - if(tokens == NULL) { + if (tokens == NULL) + { parser->pos--; return 0; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { parser->pos = start; return JSMN_ERROR_NOMEM; } @@ -104,29 +110,31 @@ static int jsmn_parse_primitive( /** * 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; +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++) { + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { char c = js[parser->pos]; /* Quote: end of string */ - if(c == '\"') { - if(tokens == NULL) { + if (c == '\"') + { + if (tokens == NULL) + { return 0; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { parser->pos = start; return JSMN_ERROR_NOMEM; } @@ -138,10 +146,12 @@ static int jsmn_parse_string( } /* Backslash: Quoted symbol expected */ - if(c == '\\' && parser->pos + 1 < len) { + if (c == '\\' && parser->pos + 1 < len) + { int i; parser->pos++; - switch(js[parser->pos]) { + switch (js[parser->pos]) + { /* Allowed escaped symbols */ case '\"': case '/': @@ -155,11 +165,13 @@ static int jsmn_parse_string( /* Allows escaped symbol \uXXXX */ case 'u': parser->pos++; - for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + 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 */ + 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; } @@ -181,7 +193,8 @@ static int jsmn_parse_string( /** * Create JSON parser over an array of tokens */ -void jsmn_init(jsmn_parser* parser) { +void jsmn_init(jsmn_parser *parser) +{ parser->pos = 0; parser->toknext = 0; parser->toksuper = -1; @@ -190,38 +203,41 @@ void jsmn_init(jsmn_parser* parser) { /** * 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 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; + jsmntok_t *token; int count = parser->toknext; - for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { char c; jsmntype_t type; c = js[parser->pos]; - switch(c) { + switch (c) + { case '{': case '[': count++; - if(tokens == NULL) { + if (tokens == NULL) + { break; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { return JSMN_ERROR_NOMEM; } - if(parser->toksuper != -1) { - jsmntok_t* t = &tokens[parser->toksuper]; + 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) { + if (t->type == JSMN_OBJECT) + { return JSMN_ERROR_INVAL; } #endif @@ -236,26 +252,33 @@ int jsmn_parse( break; case '}': case ']': - if(tokens == NULL) { + if (tokens == NULL) + { break; } type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); #ifdef JSMN_PARENT_LINKS - if(parser->toknext < 1) { + 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) { + 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) { + if (token->parent == -1) + { + if (token->type != type || parser->toksuper == -1) + { return JSMN_ERROR_INVAL; } break; @@ -263,10 +286,13 @@ int jsmn_parse( token = &tokens[token->parent]; } #else - for(i = parser->toknext - 1; i >= 0; i--) { + for (i = parser->toknext - 1; i >= 0; i--) + { token = &tokens[i]; - if(token->start != -1 && token->end == -1) { - if(token->type != type) { + if (token->start != -1 && token->end == -1) + { + if (token->type != type) + { return JSMN_ERROR_INVAL; } parser->toksuper = -1; @@ -275,12 +301,15 @@ int jsmn_parse( } } /* Error if unmatched closing bracket */ - if(i == -1) { + if (i == -1) + { return JSMN_ERROR_INVAL; } - for(; i >= 0; i--) { + for (; i >= 0; i--) + { token = &tokens[i]; - if(token->start != -1 && token->end == -1) { + if (token->start != -1 && token->end == -1) + { parser->toksuper = i; break; } @@ -289,11 +318,13 @@ int jsmn_parse( break; case '\"': r = jsmn_parse_string(parser, js, len, tokens, num_tokens); - if(r < 0) { + if (r < 0) + { return r; } count++; - if(parser->toksuper != -1 && tokens != NULL) { + if (parser->toksuper != -1 && tokens != NULL) + { tokens[parser->toksuper].size++; } break; @@ -306,15 +337,19 @@ int jsmn_parse( 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) { + 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) { + 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; } @@ -340,9 +375,12 @@ int jsmn_parse( 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)) { + 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; } } @@ -351,11 +389,13 @@ int jsmn_parse( default: #endif r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); - if(r < 0) { + if (r < 0) + { return r; } count++; - if(parser->toksuper != -1 && tokens != NULL) { + if (parser->toksuper != -1 && tokens != NULL) + { tokens[parser->toksuper].size++; } break; @@ -368,10 +408,13 @@ int jsmn_parse( } } - if(tokens != NULL) { - for(i = parser->toknext - 1; i >= 0; i--) { + 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) { + if (tokens[i].start != -1 && tokens[i].end == -1) + { return JSMN_ERROR_PART; } } @@ -381,10 +424,12 @@ int jsmn_parse( } // 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) { +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); @@ -392,30 +437,36 @@ 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) { - if(tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && - strncmp(json + tok->start, s, tok->end - tok->start) == 0) { +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) { +char *get_json_value(char *key, char *json_data, uint32_t max_tokens) +{ // Parse the JSON feed - if(json_data != NULL) { + 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) { + 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) { + if (ret < 0) + { // Handle parsing errors FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); free(tokens); @@ -423,19 +474,23 @@ char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { } // Ensure that the root element is an object - if(ret < 1 || tokens[0].type != JSMN_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) { + 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) { + char *value = malloc(length + 1); + if (value == NULL) + { FURI_LOG_E("JSMM.H", "Failed to allocate memory for value."); free(tokens); return NULL; @@ -450,7 +505,9 @@ char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { // Free the token array if key was not found free(tokens); - } else { + } + else + { FURI_LOG_E("JSMM.H", "JSON data is NULL"); } FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON."); @@ -458,10 +515,12 @@ 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) { +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) { + 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; } @@ -471,8 +530,9 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t jsmn_init(&parser); // Allocate memory for JSON tokens - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); - if(tokens == NULL) { + 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; @@ -480,7 +540,8 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t // Parse the JSON array int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); - if(ret < 0) { + if (ret < 0) + { FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); free(tokens); free(array_str); @@ -488,7 +549,8 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // Ensure the root element is an array - if(ret < 1 || tokens[0].type != JSMN_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); @@ -496,12 +558,9 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // 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); + 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; @@ -509,20 +568,27 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t // 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 (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) { + } + else if (tokens[current_token].type == JSMN_ARRAY) + { // For nested arrays, skip all elements current_token += 1 + tokens[current_token].size; - } else { + } + else + { // For primitive types, simply move to the next token current_token += 1; } // Safety check to prevent out-of-bounds - if(current_token >= ret) { + if (current_token >= ret) + { FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array."); free(tokens); free(array_str); @@ -533,8 +599,9 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t // Extract the array element jsmntok_t element = tokens[current_token]; int length = element.end - element.start; - char* value = malloc(length + 1); - if(value == NULL) { + 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); @@ -553,10 +620,12 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // 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) { +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) { + 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; } @@ -566,8 +635,9 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in jsmn_init(&parser); // Allocate memory for JSON tokens - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); // Allocate on the heap - if(tokens == NULL) { + 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; @@ -575,7 +645,8 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // Parse the JSON array int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); - if(ret < 0) { + if (ret < 0) + { FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); free(tokens); free(array_str); @@ -583,7 +654,8 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in } // Ensure the root element is an array - if(tokens[0].type != JSMN_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); @@ -592,8 +664,9 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // 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) { + 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); @@ -604,15 +677,18 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // 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) { + 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) { + 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; @@ -622,10 +698,12 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in 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) { + 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++) { + for (int j = 0; j < actual_num_values; j++) + { free(values[j]); } free(values); @@ -647,14 +725,17 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in *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) { + 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++) { + for (int i = actual_num_values; i < array_size; i++) + { free(values[i]); } } diff --git a/flip_weather/jsmn/jsmn.h b/flip_weather/jsmn/jsmn.h index cd95a0e58..74cdccf95 100644 --- a/flip_weather/jsmn/jsmn.h +++ b/flip_weather/jsmn/jsmn.h @@ -19,7 +19,8 @@ #include #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif #ifdef JSMN_STATIC @@ -28,71 +29,71 @@ extern "C" { #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 -}; - -/** + 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; + typedef struct + { + jsmntype_t type; + int start; + int end; + int size; #ifdef JSMN_PARENT_LINKS - int parent; + int parent; #endif -} jsmntok_t; + } 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; - -/** + 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); + 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); + 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 */ @@ -116,16 +117,16 @@ JSMN_API int jsmn_parse( #include // Helper function to create a JSON object -char* jsmn(const char* key, const char* value); +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); +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); +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); +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); +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 index 26876fcca..a5032996b 100644 --- a/flip_weather/parse/flip_weather_parse.c +++ b/flip_weather/parse/flip_weather_parse.c @@ -6,15 +6,17 @@ bool got_ip_address = false; bool geo_information_processed = false; bool weather_information_processed = false; -bool send_geo_location_request() { - if(fhttp.state == INACTIVE) { +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\"}")) { + 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; @@ -23,18 +25,15 @@ bool send_geo_location_request() { return true; } -bool send_geo_weather_request(DataLoaderModel* model) { +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\"}")) { + 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; @@ -42,17 +41,19 @@ bool send_geo_weather_request(DataLoaderModel* model) { fhttp.state = RECEIVING; return true; } -char* process_geo_location(DataLoaderModel* model) { +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) { + 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; @@ -61,23 +62,17 @@ char* process_geo_location(DataLoaderModel* model) { 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) { + 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); + snprintf(total_data, 512, "You are in %s, %s, %s. \nLatitude: %s, Longitude: %s", city, region, country, latitude, longitude); fhttp.state = IDLE; free(city); @@ -89,16 +84,18 @@ char* process_geo_location(DataLoaderModel* model) { 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) { +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; @@ -118,48 +115,44 @@ bool process_geo_location_2() { return false; } -char* process_weather(DataLoaderModel* model) { +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) { + 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) { + char *ptr = strstr(time, "T"); + if (ptr != NULL) + { *ptr = ' '; } - if(!weather_data) { - weather_data = (char*)malloc(512); - if(!weather_data) { + 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); + 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); @@ -171,4 +164,4 @@ char* process_weather(DataLoaderModel* model) { free(time); } return weather_data; -} +} \ No newline at end of file diff --git a/flip_weather/parse/flip_weather_parse.h b/flip_weather/parse/flip_weather_parse.h index 68fcce62e..417556175 100644 --- a/flip_weather/parse/flip_weather_parse.h +++ b/flip_weather/parse/flip_weather_parse.h @@ -9,7 +9,8 @@ extern bool weather_information_processed; // Add edits by Derek Jamison typedef enum DataState DataState; -enum DataState { +enum DataState +{ DataStateInitial, DataStateRequested, DataStateReceived, @@ -19,30 +20,32 @@ enum DataState { }; typedef enum FlipWeatherCustomEvent FlipWeatherCustomEvent; -enum 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; +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; + void *parser_context; size_t request_index; size_t request_count; ViewNavigationCallback back_callback; - FuriTimer* timer; + FuriTimer *timer; }; bool send_geo_location_request(); -char* process_geo_location(DataLoaderModel* model); +char *process_geo_location(DataLoaderModel *model); bool process_geo_location_2(); -char* process_weather(DataLoaderModel* model); -bool send_geo_weather_request(DataLoaderModel* model); +char *process_weather(DataLoaderModel *model); +bool send_geo_weather_request(DataLoaderModel *model); -#endif +#endif \ No newline at end of file diff --git a/flip_wifi/CHANGELOG.md b/flip_wifi/CHANGELOG.md index 6ad3c2689..9ff81e19d 100644 --- a/flip_wifi/CHANGELOG.md +++ b/flip_wifi/CHANGELOG.md @@ -1,7 +1,23 @@ +## v1.3.2 +- Fixed a crash that occurred when deleting a network from the Saved APs list. +- Updated to connect to the network after clicking "Set" on the selected SSID in the Saved APs list. + +## v1.3.1 +- Fixed a loading error that occurred when scanning networks. +- Fixed a freeze that occurred when saving networks manually. + +## v1.3 +- Updated to save credentials for the FlipWorld game. +- Added fast commands: CUSTOM, PING, LIST, IP/ADDRESS, and WIFI/IP. +- Improved memory allocation. +- Increased the WiFi scan list capacity from 25 to 100. + +## v1.2.1 +- Fixed crash when saving networks manually. + ## v1.2 - Updated scan loading and parsing. - Added connectivity check on startup. (thanks to Derek Jamison) -- Fixed crash when saving networks manually. ## v1.1 - Fixed a freeze issue when configurations did not exist (thanks to WillyJL). diff --git a/flip_wifi/README.md b/flip_wifi/README.md index 4cc139d3f..50fb4cddf 100644 --- a/flip_wifi/README.md +++ b/flip_wifi/README.md @@ -1,11 +1,9 @@ -# FlipWiFi - FlipWiFi is the companion app for the popular FlipperHTTP flash, originally introduced in the https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP. It allows you to scan and save WiFi networks for use across all FlipperHTTP apps. ## Requirements - WiFi Developer Board, Raspberry Pi, or ESP32 Device with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP -- WiFi Access Point +- 2.4 GHz WiFi Access Point ## Features @@ -14,9 +12,9 @@ FlipWiFi is the companion app for the popular FlipperHTTP flash, originally intr ## Setup -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: +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/data" 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/FlipperHTTP -2. **Install the App**: Download FlipWiFi from the App Store. +1. **Flash the WiFi Developer 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 Flipper Lab. 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/alloc/flip_wifi_alloc.c b/flip_wifi/alloc/flip_wifi_alloc.c index cb398f5c0..188275c1a 100644 --- a/flip_wifi/alloc/flip_wifi_alloc.c +++ b/flip_wifi/alloc/flip_wifi_alloc.c @@ -1,205 +1,30 @@ -#include +#include // Function to allocate resources for the FlipWiFiApp -FlipWiFiApp* flip_wifi_app_alloc() { - FlipWiFiApp* app = (FlipWiFiApp*)malloc(sizeof(FlipWiFiApp)); +FlipWiFiApp *flip_wifi_app_alloc() +{ + FlipWiFiApp *app = (FlipWiFiApp *)malloc(sizeof(FlipWiFiApp)); - Gui* gui = furi_record_open(RECORD_GUI); - - // initialize uart - if(!flipper_http_init(flipper_http_rx_callback, app)) { - FURI_LOG_E(TAG, "Failed to initialize flipper http"); - return NULL; - } - - // Allocate the text input buffer - app->uart_text_input_buffer_size_password_scan = 64; - app->uart_text_input_buffer_size_password_saved = 64; - app->uart_text_input_buffer_size_add_ssid = 64; - app->uart_text_input_buffer_size_add_password = 64; - if(!easy_flipper_set_buffer( - &app->uart_text_input_buffer_password_scan, - app->uart_text_input_buffer_size_password_scan)) { - return NULL; - } - if(!easy_flipper_set_buffer( - &app->uart_text_input_temp_buffer_password_scan, - app->uart_text_input_buffer_size_password_scan)) { - return NULL; - } - if(!easy_flipper_set_buffer( - &app->uart_text_input_buffer_password_saved, - app->uart_text_input_buffer_size_password_saved)) { - return NULL; - } - if(!easy_flipper_set_buffer( - &app->uart_text_input_temp_buffer_password_saved, - app->uart_text_input_buffer_size_password_saved)) { - return NULL; - } - if(!easy_flipper_set_buffer( - &app->uart_text_input_buffer_add_ssid, app->uart_text_input_buffer_size_add_ssid)) { - return NULL; - } - if(!easy_flipper_set_buffer( - &app->uart_text_input_temp_buffer_add_ssid, - app->uart_text_input_buffer_size_add_ssid)) { - return NULL; - } - if(!easy_flipper_set_buffer( - &app->uart_text_input_buffer_add_password, - app->uart_text_input_buffer_size_add_password)) { - return NULL; - } - if(!easy_flipper_set_buffer( - &app->uart_text_input_temp_buffer_add_password, - app->uart_text_input_buffer_size_add_password)) { - return NULL; - } + Gui *gui = furi_record_open(RECORD_GUI); // Allocate ViewDispatcher - if(!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) { - return NULL; - } - - // View(s) - if(!easy_flipper_set_view( - &app->view_wifi_scan, - FlipWiFiViewWiFiScan, - flip_wifi_view_draw_callback_scan, - flip_wifi_view_input_callback_scan, - callback_to_submenu_scan, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_view( - &app->view_wifi_saved, - FlipWiFiViewWiFiSaved, - flip_wifi_view_draw_callback_saved, - flip_wifi_view_input_callback_saved, - callback_to_submenu_saved, - &app->view_dispatcher, - app)) { - return NULL; - } - - // Widget - if(!easy_flipper_set_widget( - &app->widget_info, - FlipWiFiViewAbout, - "FlipWiFi v1.2.1\n-----\nFlipperHTTP companion app.\nScan and save WiFi networks.\n-----\nwww.github.com/jblanked", - callback_to_submenu_main, - &app->view_dispatcher)) { - return NULL; - } - - // Text Input - if(!easy_flipper_set_uart_text_input( - &app->uart_text_input_password_scan, - FlipWiFiViewTextInputScan, - "Enter WiFi Password", - app->uart_text_input_temp_buffer_password_scan, - app->uart_text_input_buffer_size_password_scan, - flip_wifi_text_updated_password_scan, - callback_to_submenu_scan, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_uart_text_input( - &app->uart_text_input_password_saved, - FlipWiFiViewTextInputSaved, - "Enter WiFi Password", - app->uart_text_input_temp_buffer_password_saved, - app->uart_text_input_buffer_size_password_saved, - flip_wifi_text_updated_password_saved, - callback_to_submenu_saved, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_uart_text_input( - &app->uart_text_input_add_ssid, - FlipWiFiViewTextInputSavedAddSSID, - "Enter SSID", - app->uart_text_input_temp_buffer_add_ssid, - app->uart_text_input_buffer_size_add_ssid, - flip_wifi_text_updated_add_ssid, - callback_to_submenu_saved, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_uart_text_input( - &app->uart_text_input_add_password, - FlipWiFiViewTextInputSavedAddPassword, - "Enter Password", - app->uart_text_input_temp_buffer_add_password, - app->uart_text_input_buffer_size_add_password, - flip_wifi_text_updated_add_password, - callback_to_submenu_saved, - &app->view_dispatcher, - app)) { + if (!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) + { return NULL; } // Submenu - if(!easy_flipper_set_submenu( - &app->submenu_main, - FlipWiFiViewSubmenuMain, - "FlipWiFi v1.2.1", - easy_flipper_callback_exit_app, - &app->view_dispatcher)) { - return NULL; - } - if(!easy_flipper_set_submenu( - &app->submenu_wifi_scan, - FlipWiFiViewSubmenuScan, - "WiFi Scan", - callback_to_submenu_main, - &app->view_dispatcher)) { + if (!easy_flipper_set_submenu(&app->submenu_main, FlipWiFiViewSubmenuMain, "FlipWiFi v1.3.2", callback_exit_app, &app->view_dispatcher)) + { return NULL; } - if(!easy_flipper_set_submenu( - &app->submenu_wifi_saved, - FlipWiFiViewSubmenuSaved, - "Saved APs", - callback_to_submenu_main, - &app->view_dispatcher)) { - return NULL; - } - submenu_add_item( - app->submenu_main, "Scan", FlipWiFiSubmenuIndexWiFiScan, callback_submenu_choices, app); - submenu_add_item( - app->submenu_main, - "Saved APs", - FlipWiFiSubmenuIndexWiFiSaved, - callback_submenu_choices, - app); - submenu_add_item( - app->submenu_main, "Info", FlipWiFiSubmenuIndexAbout, callback_submenu_choices, app); - - // 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); - } + submenu_add_item(app->submenu_main, "Scan", FlipWiFiSubmenuIndexWiFiScan, callback_submenu_choices, app); + submenu_add_item(app->submenu_main, "Saved APs", FlipWiFiSubmenuIndexWiFiSaved, callback_submenu_choices, app); + submenu_add_item(app->submenu_main, "Commands", FlipWiFiSubmenuIndexCommands, callback_submenu_choices, app); + submenu_add_item(app->submenu_main, "Info", FlipWiFiSubmenuIndexAbout, callback_submenu_choices, app); // Switch to the main view view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuMain); return app; -} +} \ No newline at end of file diff --git a/flip_wifi/alloc/flip_wifi_alloc.h b/flip_wifi/alloc/flip_wifi_alloc.h index 630514e40..07885ecb1 100644 --- a/flip_wifi/alloc/flip_wifi_alloc.h +++ b/flip_wifi/alloc/flip_wifi_alloc.h @@ -1,11 +1,6 @@ -#ifndef FLIP_WIFI_I_H -#define FLIP_WIFI_I_H +#pragma once #include -#include -#include // Function to allocate resources for the FlipWiFiApp -FlipWiFiApp* flip_wifi_app_alloc(); - -#endif // FLIP_WIFI_I_H +FlipWiFiApp *flip_wifi_app_alloc(); diff --git a/flip_wifi/app.c b/flip_wifi/app.c index 96abe35a5..5aa219e5d 100644 --- a/flip_wifi/app.c +++ b/flip_wifi/app.c @@ -2,55 +2,51 @@ #include // Entry point for the FlipWiFi application -int32_t flip_wifi_main(void* p) { - // Suppress unused parameter warning +int32_t flip_wifi_main(void *p) +{ UNUSED(p); // Initialize the FlipWiFi application - app_instance = flip_wifi_app_alloc(); - if(!app_instance) { + FlipWiFiApp *app = flip_wifi_app_alloc(); + if (!app) + { FURI_LOG_E(TAG, "Failed to allocate FlipWiFiApp"); return -1; } - if(!flipper_http_ping()) { - FURI_LOG_E(TAG, "Failed to ping the device"); - return -1; - } + // check if board is connected (Derek Jamison) + // initialize the http + if (flipper_http_init(flipper_http_rx_callback, app)) + { + if (!flipper_http_ping()) + { + FURI_LOG_E(TAG, "Failed to ping the device"); + 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) { + 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); - } + if (counter == 0) + easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed."); + + flipper_http_deinit(); + } + else + { + easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero."); } // Run the view dispatcher - view_dispatcher_run(app_instance->view_dispatcher); + view_dispatcher_run(app->view_dispatcher); // Free the resources used by the FlipWiFi application - flip_wifi_app_free(app_instance); + flip_wifi_app_free(app); // Return 0 to indicate success return 0; diff --git a/flip_wifi/application.fam b/flip_wifi/application.fam index 715643a32..8e001f37d 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.2.1", + fap_version="1.3.2", fap_description="FlipperHTTP companion app.", ) diff --git a/flip_wifi/assets/01-home.png b/flip_wifi/assets/01-home.png index 3dbe35f24..ca3550102 100644 Binary files a/flip_wifi/assets/01-home.png and b/flip_wifi/assets/01-home.png differ diff --git a/flip_wifi/callback/flip_wifi_callback.c b/flip_wifi/callback/flip_wifi_callback.c index a31cf95c1..8bec32373 100644 --- a/flip_wifi/callback/flip_wifi_callback.c +++ b/flip_wifi/callback/flip_wifi_callback.c @@ -1,74 +1,493 @@ #include -char* ssid_list[64]; -uint32_t ssid_index = 0; +static char *ssid_list[64]; +static uint32_t ssid_index = 0; +static char current_ssid[64]; +static char current_password[64]; -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"); - submenu_add_item( - app->submenu_wifi_saved, - "[Add Network]", - FlipWiFiSubmenuIndexWiFiSavedAddSSID, - callback_submenu_choices, - app); - for(uint32_t i = 0; i < app->wifi_playlist.count; i++) { - submenu_add_item( - app->submenu_wifi_saved, - app->wifi_playlist.ssids[i], - FlipWiFiSubmenuIndexWiFiSavedStart + i, - callback_submenu_choices, - app); +static void flip_wifi_redraw_submenu_saved(void *context); +static void flip_wifi_view_draw_callback_scan(Canvas *canvas, void *model); +static void flip_wifi_view_draw_callback_saved(Canvas *canvas, void *model); +static bool flip_wifi_view_input_callback_scan(InputEvent *event, void *context); +static bool flip_wifi_view_input_callback_saved(InputEvent *event, void *context); +static uint32_t callback_to_submenu_saved(void *context); +static uint32_t callback_to_submenu_scan(void *context); +static uint32_t callback_to_submenu_main(void *context); + +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); + +static bool flip_wifi_alloc_playlist(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return false; + } + if (!wifi_playlist) + { + wifi_playlist = (WiFiPlaylist *)malloc(sizeof(WiFiPlaylist)); + if (!wifi_playlist) + { + FURI_LOG_E(TAG, "Failed to allocate playlist"); + return false; + } + wifi_playlist->count = 0; + } + // Load the playlist from storage + if (!load_playlist(wifi_playlist)) + { + FURI_LOG_E(TAG, "Failed to load playlist"); + + // playlist is empty? + submenu_reset(app->submenu_wifi); + submenu_set_header(app->submenu_wifi, "Saved APs"); + submenu_add_item(app->submenu_wifi, "[Add Network]", FlipWiFiSubmenuIndexWiFiSavedAddSSID, callback_submenu_choices, app); + } + else + { + // Update the submenu + flip_wifi_redraw_submenu_saved(app); + } + return true; +} +static void flip_wifi_free_playlist(void) +{ + if (wifi_playlist) + { + free(wifi_playlist); + wifi_playlist = NULL; } } -uint32_t callback_to_submenu_main(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; +static bool flip_wifi_alloc_views(void *context, uint32_t view) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return false; + } + switch (view) + { + case FlipWiFiViewWiFiScan: + if (!app->view_wifi) + { + if (!easy_flipper_set_view(&app->view_wifi, FlipWiFiViewGeneric, flip_wifi_view_draw_callback_scan, flip_wifi_view_input_callback_scan, callback_to_submenu_scan, &app->view_dispatcher, app)) + { + return false; + } + if (!app->view_wifi) + { + FURI_LOG_E(TAG, "Failed to allocate view for WiFi Scan"); + return false; + } + } + return true; + case FlipWiFiViewWiFiSaved: + if (!app->view_wifi) + { + if (!easy_flipper_set_view(&app->view_wifi, FlipWiFiViewGeneric, flip_wifi_view_draw_callback_saved, flip_wifi_view_input_callback_saved, callback_to_submenu_saved, &app->view_dispatcher, app)) + { + return false; + } + if (!app->view_wifi) + { + FURI_LOG_E(TAG, "Failed to allocate view for WiFi Scan"); + return false; + } + } + return true; + default: + return false; } - UNUSED(context); - ssid_index = 0; - return FlipWiFiViewSubmenuMain; } -uint32_t callback_to_submenu_scan(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; +static void flip_wifi_free_views(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return; + } + if (app->view_wifi) + { + free(app->view_wifi); + app->view_wifi = NULL; + view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewGeneric); } - UNUSED(context); - ssid_index = 0; - return FlipWiFiViewSubmenuScan; } -uint32_t callback_to_submenu_saved(void* context) { - if(!context) { - FURI_LOG_E(TAG, "Context is NULL"); - return VIEW_NONE; +static bool flip_wifi_alloc_widgets(void *context, uint32_t widget) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return false; } - UNUSED(context); - ssid_index = 0; - return FlipWiFiViewSubmenuSaved; + switch (widget) + { + case FlipWiFiViewAbout: + if (!app->widget_info) + { + if (!easy_flipper_set_widget(&app->widget_info, FlipWiFiViewAbout, "FlipWiFi v1.3.2\n-----\nFlipperHTTP companion app.\nScan and save WiFi networks.\n-----\nwww.github.com/jblanked", callback_to_submenu_main, &app->view_dispatcher)) + { + return false; + } + if (!app->widget_info) + { + FURI_LOG_E(TAG, "Failed to allocate widget for About"); + return false; + } + } + return true; + default: + return false; + } +} +static void flip_wifi_free_widgets(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return; + } + if (app->widget_info) + { + free(app->widget_info); + app->widget_info = NULL; + view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewAbout); + } +} +static bool flip_wifi_alloc_submenus(void *context, uint32_t view) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return false; + } + switch (view) + { + case FlipWiFiViewSubmenuScan: + if (!app->submenu_wifi) + { + if (!easy_flipper_set_submenu(&app->submenu_wifi, FlipWiFiViewSubmenu, "WiFi Nearby", callback_to_submenu_main, &app->view_dispatcher)) + { + return false; + } + if (!app->submenu_wifi) + { + FURI_LOG_E(TAG, "Failed to allocate submenu for WiFi Scan"); + return false; + } + } + return true; + case FlipWiFiViewSubmenuSaved: + if (!app->submenu_wifi) + { + if (!easy_flipper_set_submenu(&app->submenu_wifi, FlipWiFiViewSubmenu, "Saved APs", callback_to_submenu_main, &app->view_dispatcher)) + { + return false; + } + if (!app->submenu_wifi) + { + FURI_LOG_E(TAG, "Failed to allocate submenu for WiFi Saved"); + return false; + } + if (!flip_wifi_alloc_playlist(app)) + { + FURI_LOG_E(TAG, "Failed to allocate playlist"); + return false; + } + } + return true; + case FlipWiFiViewSubmenuCommands: + if (!app->submenu_wifi) + { + if (!easy_flipper_set_submenu(&app->submenu_wifi, FlipWiFiViewSubmenu, "Fast Commands", callback_to_submenu_main, &app->view_dispatcher)) + { + return false; + } + if (!app->submenu_wifi) + { + FURI_LOG_E(TAG, "Failed to allocate submenu for Commands"); + return false; + } + // PING, LIST, WIFI/LIST, IP/ADDRESS, and WIFI/IP. + submenu_add_item(app->submenu_wifi, "[CUSTOM]", FlipWiFiSubmenuIndexFastCommandStart + 0, callback_submenu_choices, app); + submenu_add_item(app->submenu_wifi, "PING", FlipWiFiSubmenuIndexFastCommandStart + 1, callback_submenu_choices, app); + submenu_add_item(app->submenu_wifi, "LIST", FlipWiFiSubmenuIndexFastCommandStart + 2, callback_submenu_choices, app); + submenu_add_item(app->submenu_wifi, "IP/ADDRESS", FlipWiFiSubmenuIndexFastCommandStart + 3, callback_submenu_choices, app); + submenu_add_item(app->submenu_wifi, "WIFI/IP", FlipWiFiSubmenuIndexFastCommandStart + 4, callback_submenu_choices, app); + } + return true; + } + return false; } -static void popup_callback_saved(void* context) { - FlipWiFiApp* app = (FlipWiFiApp*)context; - if(!app) { - FURI_LOG_E(TAG, "HelloWorldApp is NULL"); +static void flip_wifi_free_submenus(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); return; } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuSaved); + if (app->submenu_wifi) + { + free(app->submenu_wifi); + app->submenu_wifi = NULL; + view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewSubmenu); + } } -static void popup_callback_main(void* context) { - FlipWiFiApp* app = (FlipWiFiApp*)context; - if(!app) { - FURI_LOG_E(TAG, "HelloWorldApp is NULL"); + +static void flip_wifi_custom_command_updated(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); return; } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuMain); + if (!app->uart_text_input_temp_buffer) + { + FURI_LOG_E(TAG, "Text input buffer is NULL"); + return; + } + if (!app->uart_text_input_temp_buffer[0]) + { + FURI_LOG_E(TAG, "Text input buffer is empty"); + return; + } + // Send the custom command + flipper_http_send_data(app->uart_text_input_temp_buffer); + while (fhttp.last_response == NULL || strlen(fhttp.last_response) == 0) + { + furi_delay_ms(100); + } + // Switch to the view + char response[100]; + snprintf(response, sizeof(response), "%s", fhttp.last_response); + easy_flipper_dialog("", response); + flipper_http_deinit(); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu); +} + +static bool flip_wifi_alloc_text_inputs(void *context, uint32_t view) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return false; + } + app->uart_text_input_buffer_size = MAX_SSID_LENGTH; + if (!app->uart_text_input_buffer) + { + if (!easy_flipper_set_buffer(&app->uart_text_input_buffer, app->uart_text_input_buffer_size)) + { + FURI_LOG_E(TAG, "Failed to allocate text input buffer"); + return false; + } + if (!app->uart_text_input_buffer) + { + FURI_LOG_E(TAG, "Failed to allocate text input buffer"); + return false; + } + } + if (!app->uart_text_input_temp_buffer) + { + if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size)) + { + FURI_LOG_E(TAG, "Failed to allocate text input temp buffer"); + return false; + } + if (!app->uart_text_input_temp_buffer) + { + FURI_LOG_E(TAG, "Failed to allocate text input temp buffer"); + return false; + } + } + switch (view) + { + case FlipWiFiViewTextInputScan: + if (!app->uart_text_input) + { + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter WiFi Password", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, flip_wifi_text_updated_password_scan, callback_to_submenu_scan, &app->view_dispatcher, app)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Scan"); + return false; + } + if (!app->uart_text_input) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Scan"); + return false; + } + } + return true; + case FlipWiFiViewTextInputSaved: + if (!app->uart_text_input) + { + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter WiFi Password", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, flip_wifi_text_updated_password_saved, callback_to_submenu_saved, &app->view_dispatcher, app)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved"); + return false; + } + if (!app->uart_text_input) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved"); + return false; + } + } + return true; + case FlipWiFiViewTextInputSavedAddSSID: + if (!app->uart_text_input) + { + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter SSID", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, flip_wifi_text_updated_add_ssid, callback_to_submenu_saved, &app->view_dispatcher, app)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add SSID"); + return false; + } + if (!app->uart_text_input) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add SSID"); + return false; + } + } + return true; + case FlipWiFiViewTextInputSavedAddPassword: + if (!app->uart_text_input) + { + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter Password", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, flip_wifi_text_updated_add_password, callback_to_submenu_saved, &app->view_dispatcher, app)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password"); + return false; + } + if (!app->uart_text_input) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password"); + return false; + } + } + return true; + case FlipWiFiSubmenuIndexFastCommandStart: + if (!app->uart_text_input) + { + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter Command", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, flip_wifi_custom_command_updated, callback_to_submenu_saved, &app->view_dispatcher, app)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command"); + return false; + } + if (!app->uart_text_input) + { + FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command"); + return false; + } + } + return true; + } + return false; +} +static void flip_wifi_free_text_inputs(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return; + } + if (app->uart_text_input) + { + free(app->uart_text_input); + app->uart_text_input = NULL; + view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewTextInput); + } + if (app->uart_text_input_buffer) + { + free(app->uart_text_input_buffer); + app->uart_text_input_buffer = NULL; + } + if (app->uart_text_input_temp_buffer) + { + free(app->uart_text_input_temp_buffer); + app->uart_text_input_temp_buffer = NULL; + } +} + +void flip_wifi_free_all(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return; + } + flip_wifi_free_views(app); + flip_wifi_free_widgets(app); + flip_wifi_free_submenus(app); + flip_wifi_free_text_inputs(app); + flip_wifi_free_playlist(); +} + +static void flip_wifi_redraw_submenu_saved(void *context) +{ + // re draw the saved submenu + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); + return; + } + if (!app->submenu_wifi) + { + FURI_LOG_E(TAG, "Submenu is NULL"); + return; + } + if (!wifi_playlist) + { + FURI_LOG_E(TAG, "WiFi Playlist is NULL"); + return; + } + submenu_reset(app->submenu_wifi); + submenu_set_header(app->submenu_wifi, "Saved APs"); + submenu_add_item(app->submenu_wifi, "[Add Network]", FlipWiFiSubmenuIndexWiFiSavedAddSSID, callback_submenu_choices, app); + for (size_t i = 0; i < wifi_playlist->count; i++) + { + submenu_add_item(app->submenu_wifi, wifi_playlist->ssids[i], FlipWiFiSubmenuIndexWiFiSavedStart + i, callback_submenu_choices, app); + } +} + +static uint32_t callback_to_submenu_main(void *context) +{ + UNUSED(context); + ssid_index = 0; + return FlipWiFiViewSubmenuMain; +} +static uint32_t callback_to_submenu_scan(void *context) +{ + UNUSED(context); + ssid_index = 0; + return FlipWiFiViewSubmenu; +} +static uint32_t callback_to_submenu_saved(void *context) +{ + UNUSED(context); + ssid_index = 0; + return FlipWiFiViewSubmenu; +} +uint32_t callback_exit_app(void *context) +{ + UNUSED(context); + return VIEW_NONE; } // Callback for drawing the main screen -void flip_wifi_view_draw_callback_scan(Canvas* canvas, void* model) { +static void flip_wifi_view_draw_callback_scan(Canvas *canvas, void *model) +{ UNUSED(model); canvas_clear(canvas); canvas_set_font(canvas, FontPrimary); @@ -78,15 +497,15 @@ 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"); } -void flip_wifi_view_draw_callback_saved(Canvas* canvas, void* model) { +static void flip_wifi_view_draw_callback_saved(Canvas *canvas, void *model) +{ UNUSED(model); canvas_clear(canvas); canvas_set_font(canvas, FontPrimary); - canvas_draw_str(canvas, 0, 10, app_instance->wifi_playlist.ssids[ssid_index]); + canvas_draw_str(canvas, 0, 10, current_ssid); canvas_set_font(canvas, FontSecondary); - char password[64]; - snprintf( - password, sizeof(password), "Pass: %s", app_instance->wifi_playlist.passwords[ssid_index]); + char password[72]; + snprintf(password, sizeof(password), "Pass: %s", current_password); canvas_draw_str(canvas, 0, 20, password); canvas_draw_icon(canvas, 0, 54, &I_ButtonLeft_4x7); canvas_draw_str_aligned(canvas, 7, 54, AlignLeft, AlignTop, "Delete"); @@ -99,95 +518,106 @@ 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) { - FlipWiFiApp* app = (FlipWiFiApp*)context; - if(event->type == InputTypePress && event->key == InputKeyRight) { +static bool flip_wifi_view_input_callback_scan(InputEvent *event, void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (event->type == InputTypePress && event->key == InputKeyRight) + { // switch to text input to set password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInputScan); + flip_wifi_free_text_inputs(app); + if (!flip_wifi_alloc_text_inputs(app, FlipWiFiViewTextInputScan)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password"); + return false; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput); return true; } return false; } // Input callback for the view (async input handling) -bool flip_wifi_view_input_callback_saved(InputEvent* event, void* context) { - FlipWiFiApp* app = (FlipWiFiApp*)context; - if(!app) { +static bool flip_wifi_view_input_callback_saved(InputEvent *event, void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); return false; } - if(event->type == InputTypePress && event->key == InputKeyRight) { + if (event->type == InputTypePress && event->key == InputKeyRight) + { // set text input buffer as the selected password - strncpy( - app->uart_text_input_temp_buffer_password_saved, - app->wifi_playlist.passwords[ssid_index], - app->uart_text_input_buffer_size_password_saved); + strncpy(app->uart_text_input_temp_buffer, wifi_playlist->passwords[ssid_index], app->uart_text_input_buffer_size); // switch to text input to set password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInputSaved); + flip_wifi_free_text_inputs(app); + if (!flip_wifi_alloc_text_inputs(app, FlipWiFiViewTextInputSaved)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved"); + return false; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput); return true; - } else if(event->type == InputTypePress && event->key == InputKeyOk) { + } + else if (event->type == InputTypePress && event->key == InputKeyOk) + { // save the settings - if(app->wifi_playlist.ssids[ssid_index] == NULL || - app->wifi_playlist.passwords[ssid_index] == NULL) { + save_settings(wifi_playlist->ssids[ssid_index], wifi_playlist->passwords[ssid_index]); + + // initialize uart + if (!flipper_http_init(flipper_http_rx_callback, app)) + { + easy_flipper_dialog("[ERROR]", "Failed to initialize flipper http"); return false; } - if(!app->popup) { - if(!easy_flipper_set_popup( - &app->popup, - FlipWiFiViewPopup, - "[SUCCESS]", - 0, - 0, - "All FlipperHTTP apps will now\nuse the selected network.", - 0, - 40, - popup_callback_saved, - callback_to_submenu_saved, - &app->view_dispatcher, - app)) { - return false; - } - } - save_settings( - app->wifi_playlist.ssids[ssid_index], app->wifi_playlist.passwords[ssid_index]); - flipper_http_save_wifi( - app->wifi_playlist.ssids[ssid_index], app->wifi_playlist.passwords[ssid_index]); + // clear response + if (fhttp.last_response) + snprintf(fhttp.last_response, RX_BUF_SIZE, "%s", ""); + + if (!flipper_http_save_wifi(wifi_playlist->ssids[ssid_index], wifi_playlist->passwords[ssid_index])) + { + easy_flipper_dialog("[ERROR]", "Failed to save WiFi settings"); + return false; + } - flipper_http_connect_wifi(); + while (!fhttp.last_response || strlen(fhttp.last_response) == 0) + { + furi_delay_ms(100); + } - popup_set_header(app->popup, "[SUCCESS]", 0, 0, AlignLeft, AlignTop); - popup_set_text( - app->popup, - "All FlipperHTTP apps will now\nuse the selected network.", - 0, - 40, - AlignLeft, - AlignTop); - view_set_previous_callback(popup_get_view(app->popup), callback_to_submenu_saved); - popup_set_callback(app->popup, popup_callback_saved); + flipper_http_deinit(); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewPopup); + easy_flipper_dialog("[SUCCESS]", "All FlipperHTTP apps will now\nuse the selected network."); return true; - } else if(event->type == InputTypePress && event->key == InputKeyLeft) { - // delete the selected ssid and password - free(app->wifi_playlist.ssids[ssid_index]); - free(app->wifi_playlist.passwords[ssid_index]); - free(ssid_list[ssid_index]); + } + else if (event->type == InputTypePress && event->key == InputKeyLeft) + { // shift the remaining ssids and passwords - for(uint32_t i = ssid_index; i < app->wifi_playlist.count - 1; i++) { - app->wifi_playlist.ssids[i] = app->wifi_playlist.ssids[i + 1]; - app->wifi_playlist.passwords[i] = app->wifi_playlist.passwords[i + 1]; + for (uint32_t i = ssid_index; i < wifi_playlist->count - 1; i++) + { + // Use strncpy to prevent buffer overflows and ensure null termination + strncpy(wifi_playlist->ssids[i], wifi_playlist->ssids[i + 1], MAX_SSID_LENGTH - 1); + wifi_playlist->ssids[i][MAX_SSID_LENGTH - 1] = '\0'; // Ensure null-termination + + strncpy(wifi_playlist->passwords[i], wifi_playlist->passwords[i + 1], MAX_SSID_LENGTH - 1); + wifi_playlist->passwords[i][MAX_SSID_LENGTH - 1] = '\0'; // Ensure null-termination + + // Shift ssid_list ssid_list[i] = ssid_list[i + 1]; } - app->wifi_playlist.count--; + wifi_playlist->count--; + + // delete the last ssid and password + wifi_playlist->ssids[wifi_playlist->count][0] = '\0'; + wifi_playlist->passwords[wifi_playlist->count][0] = '\0'; // save the playlist to storage - save_playlist(&app->wifi_playlist); + save_playlist(wifi_playlist); // re draw the saved submenu flip_wifi_redraw_submenu_saved(app); // switch back to the saved view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuSaved); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu); return true; } return false; @@ -195,67 +625,70 @@ 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 -static 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)) { + while (*length > 0 && isspace((unsigned char)*start)) + { start++; (*length)--; } // Trim trailing whitespace - while(*length > 0 && isspace((unsigned char)start[*length - 1])) { + while (*length > 0 && isspace((unsigned char)start[*length - 1])) + { (*length)--; } return start; } -static bool flip_wifi_handle_scan() { - if(!app_instance) { +static bool flip_wifi_handle_scan(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { 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) { + 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); + easy_flipper_dialog("[ERROR]", "Failed to load data from /apps_data/flip_wifi/data/scan.txt"); return false; } uint32_t ssid_count = 0; - char* current_position = data_cstr; - char* next_comma = NULL; + char *current_position = (char *)furi_string_get_cstr(scan_data); + char *next_comma = NULL; // Manually split the string on commas - while((next_comma = strchr(current_position, ',')) != NULL) { + while ((next_comma = strchr(current_position, ',')) != NULL) + { // Calculate length of the SSID size_t ssid_length = next_comma - current_position; // Trim leading and trailing whitespace size_t trimmed_length = ssid_length; - char* trim_start = trim_whitespace(current_position, &trimmed_length); + char *trim_start = trim_whitespace(current_position, &trimmed_length); // Handle empty SSIDs resulting from consecutive commas - if(trimmed_length == 0) { + if (trimmed_length == 0) + { current_position = next_comma + 1; // Move past the comma continue; } // Allocate memory for the SSID and copy it ssid_list[ssid_count] = malloc(trimmed_length + 1); - if(ssid_list[ssid_count] == NULL) { + if (ssid_list[ssid_count] == NULL) + { FURI_LOG_E(TAG, "Memory allocation failed"); - free(data_cstr); + easy_flipper_dialog("[ERROR]", "Memory allocation failed"); furi_string_free(scan_data); return false; } @@ -263,7 +696,8 @@ static bool flip_wifi_handle_scan() { ssid_list[ssid_count][trimmed_length] = '\0'; // Null-terminate the string ssid_count++; - if(ssid_count >= MAX_WIFI_NETWORKS) { + if (ssid_count >= MAX_SCAN_NETWORKS) + { FURI_LOG_E(TAG, "Maximum SSID limit reached"); break; } @@ -272,18 +706,22 @@ static bool flip_wifi_handle_scan() { } // Handle the last SSID after the last comma (if any) - if(*current_position != '\0' && ssid_count < MAX_WIFI_NETWORKS) { + if (*current_position != '\0' && ssid_count < MAX_SCAN_NETWORKS) + { size_t ssid_length = strlen(current_position); // Trim leading and trailing whitespace size_t trimmed_length = ssid_length; - char* trim_start = trim_whitespace(current_position, &trimmed_length); + char *trim_start = trim_whitespace(current_position, &trimmed_length); // Handle empty SSIDs - if(trimmed_length > 0) { + if (trimmed_length > 0) + { ssid_list[ssid_count] = malloc(trimmed_length + 1); - if(ssid_list[ssid_count] == NULL) { + if (ssid_list[ssid_count] == NULL) + { FURI_LOG_E(TAG, "Memory allocation failed for the last SSID"); + easy_flipper_dialog("[ERROR]", "Memory allocation failed for the last SSID"); return false; } strncpy(ssid_list[ssid_count], trim_start, trimmed_length); @@ -293,214 +731,328 @@ static bool flip_wifi_handle_scan() { } // Add each SSID as a submenu item - 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) { + submenu_reset(app->submenu_wifi); + submenu_set_header(app->submenu_wifi, "WiFi Nearby"); + for (uint32_t i = 0; i < ssid_count; i++) + { + char *ssid_item = ssid_list[i]; + if (ssid_item == NULL) + { // skip any NULL entries continue; } char ssid[64]; snprintf(ssid, sizeof(ssid), "%s", ssid_item); - submenu_add_item( - app_instance->submenu_wifi_scan, - ssid, - FlipWiFiSubmenuIndexWiFiScanStart + i, - callback_submenu_choices, - app_instance); - } - free(data_cstr); + submenu_add_item(app->submenu_wifi, ssid, FlipWiFiSubmenuIndexWiFiScanStart + i, callback_submenu_choices, app); + } furi_string_free(scan_data); return true; } -void callback_submenu_choices(void* context, uint32_t index) { - FlipWiFiApp* app = (FlipWiFiApp*)context; - if(!app) { +void callback_submenu_choices(void *context, uint32_t index) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); return; } - switch(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; - } + flip_wifi_free_all(app); + if (!flip_wifi_alloc_submenus(app, FlipWiFiViewSubmenuScan)) + { + easy_flipper_dialog("[ERROR]", "Failed to allocate submenus for WiFi Scan"); + 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_text( - app->popup, - "WiFi Devboard Disconnected.\nPlease reconnect the board.", - 0, - 40, - AlignLeft, - AlignTop); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewPopup); + // initialize uart + if (!flipper_http_init(flipper_http_rx_callback, app)) + { + easy_flipper_dialog("[ERROR]", "Failed to initialize flipper http"); 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); - + bool _flip_wifi_handle_scan() + { + return flip_wifi_handle_scan(app); + } + Storage *storage = furi_record_open(RECORD_STORAGE); + // ensure flip_wifi directory is there + char directory_path[128]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi"); + storage_common_mkdir(storage, directory_path); + // ensure directory is there for saving data + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data"); + storage_common_mkdir(storage, directory_path); + furi_record_close(RECORD_STORAGE); // scan for wifi ad parse the results - flipper_http_loading_task( - flipper_http_scan_wifi, - flip_wifi_handle_scan, - FlipWiFiViewSubmenuScan, - FlipWiFiViewPopup, - &app->view_dispatcher); + flipper_http_loading_task(flipper_http_scan_wifi, _flip_wifi_handle_scan, FlipWiFiViewSubmenu, FlipWiFiViewSubmenuMain, &app->view_dispatcher); + flipper_http_deinit(); break; case FlipWiFiSubmenuIndexWiFiSaved: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuSaved); + flip_wifi_free_all(app); + if (!flip_wifi_alloc_submenus(app, FlipWiFiViewSubmenuSaved)) + { + FURI_LOG_E(TAG, "Failed to allocate submenus for WiFi Saved"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu); break; case FlipWiFiSubmenuIndexAbout: + flip_wifi_free_all(app); + if (!flip_wifi_alloc_widgets(app, FlipWiFiViewAbout)) + { + FURI_LOG_E(TAG, "Failed to allocate widget for About"); + return; + } view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewAbout); break; case FlipWiFiSubmenuIndexWiFiSavedAddSSID: - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInputSavedAddSSID); + flip_wifi_free_text_inputs(app); + if (!flip_wifi_alloc_text_inputs(app, FlipWiFiViewTextInputSavedAddSSID)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput); break; - case 100 ... 163: + case FlipWiFiSubmenuIndexCommands: + flip_wifi_free_all(app); + if (!flip_wifi_alloc_submenus(app, FlipWiFiViewSubmenuCommands)) + { + FURI_LOG_E(TAG, "Failed to allocate submenus for Commands"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu); + break; + case FlipWiFiSubmenuIndexFastCommandStart ... FlipWiFiSubmenuIndexFastCommandStart + 4: + // initialize uart + if (!flipper_http_init(flipper_http_rx_callback, app)) + { + easy_flipper_dialog("[ERROR]", "Failed to initialize flipper http"); + return; + } + // Handle fast commands + switch (index) + { + case FlipWiFiSubmenuIndexFastCommandStart + 0: + // CUSTOM - send to text input and return + flip_wifi_free_text_inputs(app); + if (!flip_wifi_alloc_text_inputs(app, FlipWiFiSubmenuIndexFastCommandStart)) + { + FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput); + return; + case FlipWiFiSubmenuIndexFastCommandStart + 1: + // PING + flipper_http_ping(); + break; + case FlipWiFiSubmenuIndexFastCommandStart + 2: + // LIST + flipper_http_list_commands(); + break; + case FlipWiFiSubmenuIndexFastCommandStart + 3: + // IP/ADDRESS + flipper_http_ip_address(); + break; + case FlipWiFiSubmenuIndexFastCommandStart + 4: + // WIFI/IP + flipper_http_ip_wifi(); + break; + default: + break; + } + while (fhttp.last_response == NULL || strlen(fhttp.last_response) == 0) + { + // Wait for the response + furi_delay_ms(100); + } + if (fhttp.last_response != NULL) + { + char response[100]; + snprintf(response, sizeof(response), "%s", fhttp.last_response); + easy_flipper_dialog("", response); + } + flipper_http_deinit(); + break; + case 100 ... 199: ssid_index = index - FlipWiFiSubmenuIndexWiFiScanStart; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewWiFiScan); + flip_wifi_free_views(app); + if (!flip_wifi_alloc_views(app, FlipWiFiViewWiFiScan)) + { + FURI_LOG_E(TAG, "Failed to allocate views for WiFi Scan"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewGeneric); break; - case 200 ... 263: + case 200 ... 299: ssid_index = index - FlipWiFiSubmenuIndexWiFiSavedStart; - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewWiFiSaved); + flip_wifi_free_views(app); + snprintf(current_ssid, sizeof(current_ssid), "%s", wifi_playlist->ssids[ssid_index]); + snprintf(current_password, sizeof(current_password), "%s", wifi_playlist->passwords[ssid_index]); + if (!flip_wifi_alloc_views(app, FlipWiFiViewWiFiSaved)) + { + FURI_LOG_E(TAG, "Failed to allocate views for WiFi Saved"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewGeneric); break; default: break; } } -void flip_wifi_text_updated_password_scan(void* context) { - FlipWiFiApp* app = (FlipWiFiApp*)context; - if(!app) { +void flip_wifi_text_updated_password_scan(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); return; } - // store the entered text - strncpy( - app->uart_text_input_buffer_password_scan, - app->uart_text_input_temp_buffer_password_scan, - app->uart_text_input_buffer_size_password_scan); - + // Store the entered text with buffer size limit + strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size - 1); // Ensure null-termination - app->uart_text_input_buffer_password_scan[app->uart_text_input_buffer_size_password_scan - 1] = - '\0'; + app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0'; - // add the SSID and password_scan to the playlist - app->wifi_playlist.ssids[app->wifi_playlist.count] = strdup(ssid_list[ssid_index]); - app->wifi_playlist.passwords[app->wifi_playlist.count] = - strdup(app->uart_text_input_buffer_password_scan); - app->wifi_playlist.count++; + if (!flip_wifi_alloc_playlist(app)) + { + FURI_LOG_E(TAG, "Failed to allocate playlist"); + return; + } - // save the playlist to storage - save_playlist(&app->wifi_playlist); + // Ensure ssid_index is valid + if (ssid_index >= MAX_SCAN_NETWORKS) + { + FURI_LOG_E(TAG, "Invalid ssid_index: %ld", ssid_index); + return; + } + + // Check if there's space in the playlist + if (wifi_playlist->count >= MAX_SAVED_NETWORKS) + { + FURI_LOG_E(TAG, "Playlist is full. Cannot add more entries."); + return; + } + FURI_LOG_I(TAG, "Adding SSID: %s", ssid_list[ssid_index]); + FURI_LOG_I(TAG, "Count: %d", wifi_playlist->count); + // Add the SSID and password to the playlist + snprintf(wifi_playlist->ssids[wifi_playlist->count], MAX_SSID_LENGTH, "%s", ssid_list[ssid_index]); + snprintf(wifi_playlist->passwords[wifi_playlist->count], MAX_SSID_LENGTH, "%s", app->uart_text_input_buffer); + wifi_playlist->count++; + // Save the updated playlist to storage + save_playlist(wifi_playlist); + + // Redraw the submenu to reflect changes flip_wifi_redraw_submenu_saved(app); - // switch to back to the scan view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuScan); + // Switch back to the scan view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu); } -void flip_wifi_text_updated_password_saved(void* context) { - FlipWiFiApp* app = (FlipWiFiApp*)context; - if(!app) { + +void flip_wifi_text_updated_password_saved(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); return; } // store the entered text - strncpy( - app->uart_text_input_buffer_password_saved, - app->uart_text_input_temp_buffer_password_saved, - app->uart_text_input_buffer_size_password_saved); + strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size); // Ensure null-termination - app->uart_text_input_buffer_password_saved[app->uart_text_input_buffer_size_password_saved - 1] = - '\0'; + app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0'; // update the password_saved in the playlist - app->wifi_playlist.passwords[ssid_index] = strdup(app->uart_text_input_buffer_password_saved); + snprintf(wifi_playlist->passwords[ssid_index], MAX_SSID_LENGTH, app->uart_text_input_buffer); // save the playlist to storage - save_playlist(&app->wifi_playlist); + save_playlist(wifi_playlist); // switch to back to the saved view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuSaved); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu); } -void flip_wifi_text_updated_add_ssid(void* context) { - FlipWiFiApp* app = (FlipWiFiApp*)context; - if(!app) { +void flip_wifi_text_updated_add_ssid(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); return; } + // check if empty + if (strlen(app->uart_text_input_temp_buffer) == 0) + { + easy_flipper_dialog("[ERROR]", "SSID cannot be empty"); + return; + } + // store the entered text - strncpy( - app->uart_text_input_buffer_add_ssid, - app->uart_text_input_temp_buffer_add_ssid, - app->uart_text_input_buffer_size_add_ssid); + strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size); // Ensure null-termination - app->uart_text_input_buffer_add_ssid[app->uart_text_input_buffer_size_add_ssid - 1] = '\0'; - - // do nothing for now, go to the next text input to set the password - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInputSavedAddPassword); + app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0'; + save_char("wifi-ssid", app->uart_text_input_buffer); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuMain); + text_input_reset(app->uart_text_input); + text_input_set_header_text(app->uart_text_input, "Enter Password"); + app->uart_text_input_buffer_size = MAX_SSID_LENGTH; + free(app->uart_text_input_buffer); + free(app->uart_text_input_temp_buffer); + easy_flipper_set_buffer(&app->uart_text_input_buffer, app->uart_text_input_buffer_size); + easy_flipper_set_buffer(&app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size); + text_input_set_result_callback(app->uart_text_input, flip_wifi_text_updated_add_password, app, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, false); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput); } -void flip_wifi_text_updated_add_password(void* context) { - FlipWiFiApp* app = (FlipWiFiApp*)context; - if(!app) { +void flip_wifi_text_updated_add_password(void *context) +{ + FlipWiFiApp *app = (FlipWiFiApp *)context; + if (!app) + { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); return; } - // store the entered text - strncpy( - app->uart_text_input_buffer_add_password, - app->uart_text_input_temp_buffer_add_password, - app->uart_text_input_buffer_size_add_password); + // check if empty + if (strlen(app->uart_text_input_temp_buffer) == 0) + { + easy_flipper_dialog("[ERROR]", "Password cannot be empty"); + return; + } + // store the entered text + strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size); // Ensure null-termination - app->uart_text_input_buffer_add_password[app->uart_text_input_buffer_size_add_password - 1] = - '\0'; + app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0'; + + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuMain); + + save_char("wifi-password", app->uart_text_input_buffer); + + char wifi_ssid[64]; + if (!load_char("wifi-ssid", wifi_ssid, sizeof(wifi_ssid))) + { + FURI_LOG_E(TAG, "Failed to load wifi ssid"); + return; + } // add the SSID and password_scan to the playlist - app->wifi_playlist.ssids[app->wifi_playlist.count] = - strdup(app->uart_text_input_buffer_add_ssid); - app->wifi_playlist.passwords[app->wifi_playlist.count] = - strdup(app->uart_text_input_buffer_add_password); - app->wifi_playlist.count++; + snprintf(wifi_playlist->ssids[wifi_playlist->count], MAX_SSID_LENGTH, wifi_ssid); + snprintf(wifi_playlist->passwords[wifi_playlist->count], MAX_SSID_LENGTH, app->uart_text_input_buffer); + wifi_playlist->count++; // save the playlist to storage - save_playlist(&app->wifi_playlist); + save_playlist(wifi_playlist); flip_wifi_redraw_submenu_saved(app); // switch to back to the saved view - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuSaved); -} + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu); +} \ No newline at end of file diff --git a/flip_wifi/callback/flip_wifi_callback.h b/flip_wifi/callback/flip_wifi_callback.h index 39c266223..2e14a2095 100644 --- a/flip_wifi/callback/flip_wifi_callback.h +++ b/flip_wifi/callback/flip_wifi_callback.h @@ -1,41 +1,8 @@ -#ifndef FLIP_WIFI_CALLBACK_H -#define FLIP_WIFI_CALLBACK_H - +#pragma once #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 +void flip_wifi_free_all(void *context); +uint32_t callback_exit_app(void *context); +void callback_submenu_choices(void *context, uint32_t index); \ No newline at end of file diff --git a/flip_wifi/easy_flipper/easy_flipper.c b/flip_wifi/easy_flipper/easy_flipper.c index 8b98e1a1b..c2ce949e4 100644 --- a/flip_wifi/easy_flipper/easy_flipper.c +++ b/flip_wifi/easy_flipper/easy_flipper.c @@ -1,13 +1,35 @@ #include +void easy_flipper_dialog( + char *header, + char *text) +{ + DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage *message = dialog_message_alloc(); + dialog_message_set_header( + message, header, 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + message, + text, + 0, + 63, + AlignLeft, + AlignBottom); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); +} + /** * @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) { +uint32_t easy_flipper_callback_exit_app(void *context) +{ // Exit the application - if(!context) { + if (!context) + { FURI_LOG_E(EASY_TAG, "Context is NULL"); return VIEW_NONE; } @@ -21,13 +43,16 @@ uint32_t easy_flipper_callback_exit_app(void* context) { * @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) { +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) { + *buffer = (char *)malloc(buffer_size); + if (!*buffer) + { FURI_LOG_E(EASY_TAG, "Failed to allocate buffer"); return false; } @@ -46,32 +71,39 @@ bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size) { * @return true if successful, false otherwise */ bool easy_flipper_set_view( - View** 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) { + 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) { + if (!*view) + { FURI_LOG_E(EASY_TAG, "Failed to allocate View"); return false; } - if(draw_callback) { + if (draw_callback) + { view_set_draw_callback(*view, draw_callback); } - if(input_callback) { + if (input_callback) + { view_set_input_callback(*view, input_callback); } - if(context) { + if (context) + { view_set_context(*view, context); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(*view, previous_callback); } view_dispatcher_add_view(*view_dispatcher, view_id, *view); @@ -85,18 +117,22 @@ bool easy_flipper_set_view( * @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) { +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) { + 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) { + if (context) + { view_dispatcher_set_event_callback_context(*view_dispatcher, context); } return true; @@ -113,24 +149,29 @@ bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui * @return true if successful, false otherwise */ bool easy_flipper_set_submenu( - Submenu** submenu, + Submenu **submenu, int32_t view_id, - char* title, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!submenu) { + 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) { + if (!*submenu) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Submenu"); return false; } - if(title) { + if (title) + { submenu_set_header(*submenu, title); } - if(previous_callback) { + 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)); @@ -147,20 +188,24 @@ bool easy_flipper_set_submenu( * @return true if successful, false otherwise */ bool easy_flipper_set_menu( - Menu** menu, + Menu **menu, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!menu) { + 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) { + if (!*menu) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Menu"); return false; } - if(previous_callback) { + 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)); @@ -177,24 +222,29 @@ bool easy_flipper_set_menu( * @return true if successful, false otherwise */ bool easy_flipper_set_widget( - Widget** widget, + Widget **widget, int32_t view_id, - char* text, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!widget) { + 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) { + if (!*widget) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Widget"); return false; } - if(text) { + if (text) + { widget_add_text_scroll_element(*widget, 0, 0, 128, 64, text); } - if(previous_callback) { + 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)); @@ -213,30 +263,33 @@ bool easy_flipper_set_widget( * @return true if successful, false otherwise */ bool easy_flipper_set_variable_item_list( - VariableItemList** 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) { + 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) { + if (!*variable_item_list) + { FURI_LOG_E(EASY_TAG, "Failed to allocate VariableItemList"); return false; } - if(enter_callback) { + 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); + 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)); + view_dispatcher_add_view(*view_dispatcher, view_id, variable_item_list_get_view(*variable_item_list)); return true; } @@ -249,38 +302,38 @@ bool easy_flipper_set_variable_item_list( * @return true if successful, false otherwise */ bool easy_flipper_set_text_input( - TextInput** text_input, + TextInput **text_input, int32_t view_id, - char* header_text, - char* text_input_temp_buffer, + 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) { + 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) { + if (!*text_input) + { FURI_LOG_E(EASY_TAG, "Failed to allocate TextInput"); return false; } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(text_input_get_view(*text_input), previous_callback); } - if(header_text) { + 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); + 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; @@ -295,38 +348,38 @@ bool easy_flipper_set_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_uart_text_input( - TextInput** uart_text_input, + TextInput **uart_text_input, int32_t view_id, - char* header_text, - char* uart_text_input_temp_buffer, + 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) { + 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) { + if (!*uart_text_input) + { FURI_LOG_E(EASY_TAG, "Failed to allocate UART_TextInput"); return false; } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(text_input_get_view(*uart_text_input), previous_callback); } - if(header_text) { + 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); + 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)); @@ -353,52 +406,63 @@ bool easy_flipper_set_uart_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_dialog_ex( - DialogEx** dialog_ex, + DialogEx **dialog_ex, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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) { + 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) { + if (!*dialog_ex) + { FURI_LOG_E(EASY_TAG, "Failed to allocate DialogEx"); return false; } - if(header) { + if (header) + { dialog_ex_set_header(*dialog_ex, header, header_x, header_y, AlignLeft, AlignTop); } - if(text) { + if (text) + { dialog_ex_set_text(*dialog_ex, text, text_x, text_y, AlignLeft, AlignTop); } - if(left_button_text) { + if (left_button_text) + { dialog_ex_set_left_button_text(*dialog_ex, left_button_text); } - if(right_button_text) { + if (right_button_text) + { dialog_ex_set_right_button_text(*dialog_ex, right_button_text); } - if(center_button_text) { + if (center_button_text) + { dialog_ex_set_center_button_text(*dialog_ex, center_button_text); } - if(result_callback) { + if (result_callback) + { dialog_ex_set_result_callback(*dialog_ex, result_callback); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(dialog_ex_get_view(*dialog_ex), previous_callback); } - if(context) { + if (context) + { dialog_ex_set_context(*dialog_ex, context); } view_dispatcher_add_view(*view_dispatcher, view_id, dialog_ex_get_view(*dialog_ex)); @@ -422,40 +486,48 @@ bool easy_flipper_set_dialog_ex( * @return true if successful, false otherwise */ bool easy_flipper_set_popup( - Popup** popup, + Popup **popup, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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) { + 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) { + if (!*popup) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Popup"); return false; } - if(header) { + if (header) + { popup_set_header(*popup, header, header_x, header_y, AlignLeft, AlignTop); } - if(text) { + if (text) + { popup_set_text(*popup, text, text_x, text_y, AlignLeft, AlignTop); } - if(result_callback) { + if (result_callback) + { popup_set_callback(*popup, result_callback); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(popup_get_view(*popup), previous_callback); } - if(context) { + if (context) + { popup_set_context(*popup, context); } view_dispatcher_add_view(*view_dispatcher, view_id, popup_get_view(*popup)); @@ -471,20 +543,24 @@ bool easy_flipper_set_popup( * @return true if successful, false otherwise */ bool easy_flipper_set_loading( - Loading** loading, + Loading **loading, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!loading) { + 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) { + if (!*loading) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Loading"); return false; } - if(previous_callback) { + 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)); @@ -497,16 +573,19 @@ bool easy_flipper_set_loading( * @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) { +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) { + if (!furi_string) + { FURI_LOG_E(EASY_TAG, "Failed to allocate FuriString"); return false; } furi_string_set_str(*furi_string, buffer); return true; -} +} \ No newline at end of file diff --git a/flip_wifi/easy_flipper/easy_flipper.h b/flip_wifi/easy_flipper/easy_flipper.h index 219e42c74..8c71106c7 100644 --- a/flip_wifi/easy_flipper/easy_flipper.h +++ b/flip_wifi/easy_flipper/easy_flipper.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -22,22 +23,27 @@ #include #include #include +#include #define EASY_TAG "EasyFlipper" +void easy_flipper_dialog( + char *header, + char *text); + /** * @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); +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); +bool easy_flipper_set_buffer(char **buffer, uint32_t buffer_size); /** * @brief Initialize a View object * @param view The View object to initialize @@ -49,13 +55,13 @@ bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size); * @return true if successful, false otherwise */ bool easy_flipper_set_view( - View** 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); + 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 @@ -64,7 +70,7 @@ bool easy_flipper_set_view( * @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); +bool easy_flipper_set_view_dispatcher(ViewDispatcher **view_dispatcher, Gui *gui, void *context); /** * @brief Initialize a Submenu object @@ -77,11 +83,11 @@ bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui * @return true if successful, false otherwise */ bool easy_flipper_set_submenu( - Submenu** submenu, + Submenu **submenu, int32_t view_id, - char* title, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + char *title, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a Menu object @@ -94,10 +100,10 @@ bool easy_flipper_set_submenu( * @return true if successful, false otherwise */ bool easy_flipper_set_menu( - Menu** menu, + Menu **menu, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a Widget object @@ -109,11 +115,11 @@ bool easy_flipper_set_menu( * @return true if successful, false otherwise */ bool easy_flipper_set_widget( - Widget** widget, + Widget **widget, int32_t view_id, - char* text, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + char *text, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a VariableItemList object @@ -127,12 +133,12 @@ bool easy_flipper_set_widget( * @return true if successful, false otherwise */ bool easy_flipper_set_variable_item_list( - VariableItemList** 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); + void (*enter_callback)(void *, uint32_t), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a TextInput object @@ -143,15 +149,15 @@ bool easy_flipper_set_variable_item_list( * @return true if successful, false otherwise */ bool easy_flipper_set_text_input( - TextInput** text_input, + TextInput **text_input, int32_t view_id, - char* header_text, - char* text_input_temp_buffer, + 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); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a TextInput object with extra symbols @@ -162,15 +168,15 @@ bool easy_flipper_set_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_uart_text_input( - TextInput** uart_text_input, + TextInput **uart_text_input, int32_t view_id, - char* header_text, - char* uart_text_input_temp_buffer, + 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); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a DialogEx object @@ -192,21 +198,21 @@ bool easy_flipper_set_uart_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_dialog_ex( - DialogEx** dialog_ex, + DialogEx **dialog_ex, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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); + 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 @@ -225,18 +231,18 @@ bool easy_flipper_set_dialog_ex( * @return true if successful, false otherwise */ bool easy_flipper_set_popup( - Popup** popup, + Popup **popup, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + char *text, uint16_t text_x, uint16_t text_y, - void (*result_callback)(void*), - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher, - void* context); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a Loading object @@ -247,10 +253,10 @@ bool easy_flipper_set_popup( * @return true if successful, false otherwise */ bool easy_flipper_set_loading( - Loading** loading, + Loading **loading, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Set a char butter to a FuriString @@ -258,6 +264,6 @@ bool easy_flipper_set_loading( * @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); +bool easy_flipper_set_char_to_furi_string(FuriString **furi_string, char *buffer); -#endif +#endif \ No newline at end of file diff --git a/flip_wifi/flip_storage/flip_wifi_storage.c b/flip_wifi/flip_storage/flip_wifi_storage.c index 0622b1210..f287bdc63 100644 --- a/flip_wifi/flip_storage/flip_wifi_storage.c +++ b/flip_wifi/flip_storage/flip_wifi_storage.c @@ -1,6 +1,6 @@ #include -char* app_ids[8] = { +static char *app_ids[8] = { "flip_wifi", "flip_store", "flip_social", @@ -8,216 +8,140 @@ char* app_ids[8] = { "flip_weather", "flip_library", "web_crawler", - "flip_rss"}; + "flip_world"}; // Function to save the playlist -void save_playlist(WiFiPlaylist* playlist) { - if(!playlist) { +void save_playlist(WiFiPlaylist *playlist) +{ + if (!playlist) + { FURI_LOG_E(TAG, "Playlist is NULL"); return; } // Create the directory for saving settings char directory_path[128]; - snprintf( - directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi"); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi"); // Open storage - Storage* storage = furi_record_open(RECORD_STORAGE); - if(!storage) { + Storage *storage = furi_record_open(RECORD_STORAGE); + if (!storage) + { FURI_LOG_E(TAG, "Failed to open storage record"); return; } // Create the directory storage_common_mkdir(storage, directory_path); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data"); + storage_common_mkdir(storage, directory_path); // Open the settings file - File* file = storage_file_alloc(storage); - if(!file) { + File *file = storage_file_alloc(storage); + if (!file) + { FURI_LOG_E(TAG, "Failed to allocate file handle"); furi_record_close(RECORD_STORAGE); return; } - if(!storage_file_open(file, WIFI_SSID_LIST_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + if (!storage_file_open(file, WIFI_SSID_LIST_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) + { FURI_LOG_E(TAG, "Failed to open settings file for writing: %s", WIFI_SSID_LIST_PATH); storage_file_free(file); furi_record_close(RECORD_STORAGE); return; } - for(size_t i = 0; i < playlist->count; i++) { - if(!playlist->ssids[i] || !playlist->passwords[i]) { - FURI_LOG_E(TAG, "Invalid SSID or password at index %zu", i); - continue; - } - size_t ssid_length = strlen(playlist->ssids[i]); - size_t password_length = strlen(playlist->passwords[i]); - if(storage_file_write(file, playlist->ssids[i], ssid_length) != ssid_length || - storage_file_write(file, ",", 1) != 1 || - storage_file_write(file, playlist->passwords[i], password_length) != password_length || - storage_file_write(file, "\n", 1) != 1) { - FURI_LOG_E(TAG, "Failed to write playlist"); + FuriString *json_result = furi_string_alloc(); + if (!json_result) + { + FURI_LOG_E(TAG, "Failed to allocate FuriString"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return; + } + furi_string_cat(json_result, "{\"ssids\":[\n"); + for (size_t i = 0; i < playlist->count; i++) + { + furi_string_cat_printf(json_result, "{\"ssid\":\"%s\",\"password\":\"%s\"}", playlist->ssids[i], playlist->passwords[i]); + if (i < playlist->count - 1) + { + furi_string_cat(json_result, ",\n"); } } - + furi_string_cat(json_result, "\n]}"); + size_t json_length = furi_string_size(json_result); + if (storage_file_write(file, furi_string_get_cstr(json_result), json_length) != json_length) + { + FURI_LOG_E(TAG, "Failed to write playlist to file"); + } + furi_string_free(json_result); storage_file_close(file); storage_file_free(file); furi_record_close(RECORD_STORAGE); } -// Function to load the playlist -bool load_playlist(WiFiPlaylist* playlist) { - if(!playlist) { +bool load_playlist(WiFiPlaylist *playlist) +{ + if (!playlist) + { FURI_LOG_E(TAG, "Playlist is NULL"); return false; } - // Initialize playlist count - playlist->count = 0; - - // Allocate memory for SSIDs and passwords if not already allocated - for(size_t i = 0; i < MAX_WIFI_NETWORKS; i++) { - if(!playlist->ssids[i]) { - playlist->ssids[i] = malloc(64); // Adjust size as needed - if(!playlist->ssids[i]) { - FURI_LOG_E(TAG, "Memory allocation failed for ssids[%zu]", i); - // Handle memory allocation failure (e.g., clean up and return) - return false; - } - } - - if(!playlist->passwords[i]) { - playlist->passwords[i] = malloc(64); // Adjust size as needed - if(!playlist->passwords[i]) { - FURI_LOG_E(TAG, "Memory allocation failed for passwords[%zu]", i); - // Handle memory allocation failure (e.g., clean up and return) - return false; - } - } - } - - // Open the settings file - Storage* storage = furi_record_open(RECORD_STORAGE); - if(!storage) { - FURI_LOG_E(TAG, "Failed to open storage record"); - return false; - } - - File* file = storage_file_alloc(storage); - if(!file) { - FURI_LOG_E(TAG, "Failed to allocate file handle"); - furi_record_close(RECORD_STORAGE); + FuriString *json_result = flipper_http_load_from_file(WIFI_SSID_LIST_PATH); + if (!json_result) + { + FURI_LOG_E(TAG, "Failed to load playlist from file"); return false; } - if(!storage_file_open(file, WIFI_SSID_LIST_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) { - FURI_LOG_E(TAG, "Failed to open settings file for reading: %s", WIFI_SSID_LIST_PATH); - storage_file_free(file); - furi_record_close(RECORD_STORAGE); - return false; // Return false if the file does not exist - } - - // Buffer to hold each line - char line_buffer[128]; - size_t line_pos = 0; - char ch; - - while(storage_file_read(file, &ch, 1) == 1) { - if(ch == '\n') { - // Null-terminate the line - line_buffer[line_pos] = '\0'; - - // Split the line into SSID and Password - char* comma_pos = strchr(line_buffer, ','); - if(comma_pos) { - *comma_pos = '\0'; // Replace comma with null character - - // Copy SSID - strncpy(playlist->ssids[playlist->count], line_buffer, 63); - playlist->ssids[playlist->count][63] = '\0'; // Ensure null-termination - - // Copy Password - strncpy(playlist->passwords[playlist->count], comma_pos + 1, 63); - playlist->passwords[playlist->count][63] = '\0'; // Ensure null-termination - - playlist->count++; - - if(playlist->count >= MAX_WIFI_NETWORKS) { - FURI_LOG_W( - TAG, "Reached maximum number of WiFi networks: %d", MAX_WIFI_NETWORKS); - break; - } - } else { - FURI_LOG_E(TAG, "Invalid line format (no comma found): %s", line_buffer); - } + // Initialize playlist count + playlist->count = 0; - // Reset line buffer position for the next line - line_pos = 0; - } else { - if(line_pos < sizeof(line_buffer) - 1) { - line_buffer[line_pos++] = ch; - } else { - FURI_LOG_E(TAG, "Line buffer overflow"); - // Optionally handle line overflow (e.g., skip the rest of the line) - line_pos = 0; - } + // Parse the JSON result + for (size_t i = 0; i < MAX_SAVED_NETWORKS; i++) + { + FuriString *json_data = get_json_array_value_furi("ssids", i, json_result); + if (!json_data) + { + break; } - } - - // Handle the last line if it does not end with a newline - if(line_pos > 0) { - line_buffer[line_pos] = '\0'; - char* comma_pos = strchr(line_buffer, ','); - if(comma_pos) { - *comma_pos = '\0'; // Replace comma with null character - - // Copy SSID - strncpy(playlist->ssids[playlist->count], line_buffer, 63); - playlist->ssids[playlist->count][63] = '\0'; // Ensure null-termination - - // Copy Password - strncpy(playlist->passwords[playlist->count], comma_pos + 1, 63); - playlist->passwords[playlist->count][63] = '\0'; // Ensure null-termination - - playlist->count++; - - if(playlist->count >= MAX_WIFI_NETWORKS) { - FURI_LOG_W(TAG, "Reached maximum number of WiFi networks: %d", MAX_WIFI_NETWORKS); - } - } else { - FURI_LOG_E(TAG, "Invalid line format (no comma found): %s", line_buffer); + FuriString *ssid = get_json_value_furi("ssid", json_data); + FuriString *password = get_json_value_furi("password", json_data); + if (!ssid || !password) + { + FURI_LOG_E(TAG, "Failed to get SSID or Password from JSON"); + furi_string_free(json_data); + break; } + snprintf(playlist->ssids[i], MAX_SSID_LENGTH, "%s", furi_string_get_cstr(ssid)); + snprintf(playlist->passwords[i], MAX_SSID_LENGTH, "%s", furi_string_get_cstr(password)); + playlist->count++; + furi_string_free(json_data); + furi_string_free(ssid); + furi_string_free(password); } - - // Close and free file resources - storage_file_close(file); - storage_file_free(file); - furi_record_close(RECORD_STORAGE); - + furi_string_free(json_result); return true; } -void save_settings(const char* ssid, const char* password) { +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 < 8; i++) { + for (size_t i = 0; i < 8; i++) + { // Construct the directory and file paths for the current app - snprintf( - edited_directory_path, - sizeof(edited_directory_path), - STORAGE_EXT_PATH_PREFIX "/apps_data/%s", - app_ids[i]); - snprintf( - edited_file_path, - sizeof(edited_file_path), - STORAGE_EXT_PATH_PREFIX "/apps_data/%s/settings.bin", - app_ids[i]); + snprintf(edited_directory_path, sizeof(edited_directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s", app_ids[i]); + snprintf(edited_file_path, sizeof(edited_file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s/settings.bin", app_ids[i]); // Open the storage record - Storage* storage = furi_record_open(RECORD_STORAGE); - if(!storage) { + Storage *storage = furi_record_open(RECORD_STORAGE); + if (!storage) + { FURI_LOG_E(TAG, "Failed to open storage record for app: %s", app_ids[i]); continue; // Skip to the next app } @@ -226,24 +150,26 @@ void save_settings(const char* ssid, const char* password) { storage_common_mkdir(storage, edited_directory_path); // Allocate a file handle - File* file = storage_file_alloc(storage); - if(!file) { + File *file = storage_file_alloc(storage); + if (!file) + { FURI_LOG_E(TAG, "Failed to allocate storage file for app: %s", app_ids[i]); furi_record_close(RECORD_STORAGE); continue; // Skip to the next app } // Open the file in read mode to read existing data - bool file_opened = - storage_file_open(file, edited_file_path, FSAM_READ, FSOM_OPEN_EXISTING); + bool file_opened = storage_file_open(file, edited_file_path, FSAM_READ, FSOM_OPEN_EXISTING); size_t file_size = 0; - uint8_t* buffer = NULL; + uint8_t *buffer = NULL; - if(file_opened) { + if (file_opened) + { // Get the file size file_size = storage_file_size(file); buffer = malloc(file_size); - if(!buffer) { + if (!buffer) + { FURI_LOG_E(TAG, "Failed to allocate buffer for app: %s", app_ids[i]); storage_file_close(file); storage_file_free(file); @@ -252,7 +178,8 @@ void save_settings(const char* ssid, const char* password) { } // Read the existing data - if(storage_file_read(file, buffer, file_size) != file_size) { + if (storage_file_read(file, buffer, file_size) != file_size) + { FURI_LOG_E(TAG, "Failed to read settings file for app: %s", app_ids[i]); free(buffer); storage_file_close(file); @@ -262,7 +189,9 @@ void save_settings(const char* ssid, const char* password) { } storage_file_close(file); - } else { + } + else + { // If the file doesn't exist, initialize an empty buffer file_size = 0; buffer = NULL; @@ -271,65 +200,70 @@ void save_settings(const char* ssid, const char* password) { storage_file_free(file); // Prepare new SSID and Password - size_t new_ssid_length = strlen(ssid) + 1; // Including null terminator + size_t new_ssid_length = strlen(ssid) + 1; // Including null terminator size_t new_password_length = strlen(password) + 1; // Including null terminator // Calculate the new file size - size_t new_file_size = - sizeof(size_t) + new_ssid_length + sizeof(size_t) + new_password_length; + size_t new_file_size = sizeof(size_t) + new_ssid_length + sizeof(size_t) + new_password_length; // If there is additional data beyond SSID and Password, preserve it size_t additional_data_size = 0; - uint8_t* additional_data = NULL; + uint8_t *additional_data = NULL; - if(buffer) { + if (buffer) + { // Parse existing SSID length - if(file_size >= sizeof(size_t)) { + if (file_size >= sizeof(size_t)) + { size_t existing_ssid_length; memcpy(&existing_ssid_length, buffer, sizeof(size_t)); // Parse existing Password length - if(file_size >= sizeof(size_t) + existing_ssid_length + sizeof(size_t)) { + if (file_size >= sizeof(size_t) + existing_ssid_length + sizeof(size_t)) + { size_t existing_password_length; - memcpy( - &existing_password_length, - buffer + sizeof(size_t) + existing_ssid_length, - sizeof(size_t)); + memcpy(&existing_password_length, buffer + sizeof(size_t) + existing_ssid_length, sizeof(size_t)); // Calculate the offset where additional data starts - size_t additional_offset = sizeof(size_t) + existing_ssid_length + - sizeof(size_t) + existing_password_length; - if(additional_offset < file_size) { + size_t additional_offset = sizeof(size_t) + existing_ssid_length + sizeof(size_t) + existing_password_length; + if (additional_offset < file_size) + { additional_data_size = file_size - additional_offset; additional_data = malloc(additional_data_size); - if(additional_data) { - memcpy( - additional_data, buffer + additional_offset, additional_data_size); - } else { - FURI_LOG_E( - TAG, - "Failed to allocate memory for additional data for app: %s", - app_ids[i]); + if (additional_data) + { + memcpy(additional_data, buffer + additional_offset, additional_data_size); + } + else + { + FURI_LOG_E(TAG, "Failed to allocate memory for additional data for app: %s", app_ids[i]); free(buffer); furi_record_close(RECORD_STORAGE); continue; } } - } else { + } + else + { FURI_LOG_E(TAG, "Settings file format invalid for app: %s", app_ids[i]); } - } else { + } + else + { FURI_LOG_E(TAG, "Settings file too small for app: %s", app_ids[i]); } } // Allocate a new buffer for updated data size_t total_new_size = new_file_size + additional_data_size; - uint8_t* new_buffer = malloc(total_new_size); - if(!new_buffer) { + uint8_t *new_buffer = malloc(total_new_size); + if (!new_buffer) + { FURI_LOG_E(TAG, "Failed to allocate new buffer for app: %s", app_ids[i]); - if(buffer) free(buffer); - if(additional_data) free(additional_data); + if (buffer) + free(buffer); + if (additional_data) + free(additional_data); furi_record_close(RECORD_STORAGE); continue; } @@ -349,25 +283,30 @@ void save_settings(const char* ssid, const char* password) { offset += new_password_length; // Append any additional data if present - if(additional_data_size > 0 && additional_data) { + if (additional_data_size > 0 && additional_data) + { memcpy(new_buffer + offset, additional_data, additional_data_size); offset += additional_data_size; } // Free temporary buffers - if(buffer) free(buffer); - if(additional_data) free(additional_data); + if (buffer) + free(buffer); + if (additional_data) + free(additional_data); // Open the file in write mode with FSOM_CREATE_ALWAYS to overwrite it file = storage_file_alloc(storage); - if(!file) { + if (!file) + { FURI_LOG_E(TAG, "Failed to allocate storage file for writing: %s", app_ids[i]); free(new_buffer); furi_record_close(RECORD_STORAGE); continue; } - if(!storage_file_open(file, edited_file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + if (!storage_file_open(file, edited_file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) + { FURI_LOG_E(TAG, "Failed to open settings file for writing: %s", edited_file_path); storage_file_free(file); free(new_buffer); @@ -376,7 +315,8 @@ void save_settings(const char* ssid, const char* password) { } // Write the updated buffer back to the file - if(storage_file_write(file, new_buffer, total_new_size) != total_new_size) { + if (storage_file_write(file, new_buffer, total_new_size) != total_new_size) + { FURI_LOG_E(TAG, "Failed to write updated settings for app: %s", app_ids[i]); } @@ -387,3 +327,100 @@ void save_settings(const char* ssid, const char* password) { furi_record_close(RECORD_STORAGE); } } +bool save_char( + const char *path_name, const char *value) +{ + if (!value) + { + return false; + } + // Create the directory for saving settings + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + if (!storage) + { + FURI_LOG_E(HTTP_TAG, "Failed to open storage record"); + return false; + } + storage_common_mkdir(storage, directory_path); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data"); + storage_common_mkdir(storage, directory_path); + + // Open the settings file + File *file = storage_file_alloc(storage); + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data/%s.txt", path_name); + + // 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; + } + + // Write the data to the file + size_t data_size = strlen(value) + 1; // Include null terminator + if (storage_file_write(file, value, 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; +} + +bool load_char( + const char *path_name, + char *value, + size_t value_size) +{ + if (!value) + { + return false; + } + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); + + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data/%s.txt", path_name); + + // 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 false; // Return false if the file does not exist + } + + // Read data into the buffer + size_t read_count = storage_file_read(file, value, value_size); + if (storage_file_get_error(file) != FSE_OK) + { + FURI_LOG_E(HTTP_TAG, "Error reading from file."); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + + // Ensure null-termination + value[read_count - 1] = '\0'; + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + return true; +} \ No newline at end of file diff --git a/flip_wifi/flip_storage/flip_wifi_storage.h b/flip_wifi/flip_storage/flip_wifi_storage.h index 1948e8608..cd59e975e 100644 --- a/flip_wifi/flip_storage/flip_wifi_storage.h +++ b/flip_wifi/flip_storage/flip_wifi_storage.h @@ -1,18 +1,22 @@ -#ifndef FLIP_WIFI_STORAGE_H -#define FLIP_WIFI_STORAGE_H +#pragma once #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]; +#define WIFI_SSID_LIST_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data/wifi_list.txt" // Function to save the playlist -void save_playlist(WiFiPlaylist* playlist); +void save_playlist(WiFiPlaylist *playlist); // Function to load the playlist -bool load_playlist(WiFiPlaylist* playlist); +bool load_playlist(WiFiPlaylist *playlist); + +void save_settings(const char *ssid, const char *password); + +bool save_char( + const char *path_name, const char *value); -void save_settings(const char* ssid, const char* password); -#endif +bool load_char( + const char *path_name, + char *value, + size_t value_size); diff --git a/flip_wifi/flip_wifi.c b/flip_wifi/flip_wifi.c index af54dcc69..52dccf196 100644 --- a/flip_wifi/flip_wifi.c +++ b/flip_wifi/flip_wifi.c @@ -1,83 +1,32 @@ #include "flip_wifi.h" - -FlipWiFiApp* app_instance = NULL; - +#include +WiFiPlaylist *wifi_playlist = NULL; // Function to free the resources used by FlipWiFiApp -void flip_wifi_app_free(FlipWiFiApp* app) { - if(!app) { +void flip_wifi_app_free(FlipWiFiApp *app) +{ + if (!app) + { FURI_LOG_E(TAG, "FlipWiFiApp is NULL"); return; } - // Free View(s) - if(app->view_wifi_scan) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewWiFiScan); - view_free(app->view_wifi_scan); - } - if(app->view_wifi_saved) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewWiFiSaved); - view_free(app->view_wifi_saved); - } - // Free Submenu(s) - if(app->submenu_main) { + if (app->submenu_main) + { view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewSubmenuMain); submenu_free(app->submenu_main); } - if(app->submenu_wifi_scan) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewSubmenuScan); - submenu_free(app->submenu_wifi_scan); - } - if(app->submenu_wifi_saved) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewSubmenuSaved); - submenu_free(app->submenu_wifi_saved); - } - - // Free Widget(s) - if(app->widget_info) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewAbout); - widget_free(app->widget_info); - } - - // Free Text Input(s) - if(app->uart_text_input_password_scan) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewTextInputScan); - text_input_free(app->uart_text_input_password_scan); - } - if(app->uart_text_input_password_saved) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewTextInputSaved); - text_input_free(app->uart_text_input_password_saved); - } - if(app->uart_text_input_add_ssid) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewTextInputSavedAddSSID); - text_input_free(app->uart_text_input_add_ssid); - } - if(app->uart_text_input_add_password) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewTextInputSavedAddPassword); - text_input_free(app->uart_text_input_add_password); - } - - // free playlist - for(size_t i = 0; i < app->wifi_playlist.count; i++) { - if(app->wifi_playlist.ssids[i]) free(app->wifi_playlist.ssids[i]); - if(app->wifi_playlist.passwords[i]) free(app->wifi_playlist.passwords[i]); - } - - // free popup - if(app->popup) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewPopup); - popup_free(app->popup); - } - // deinitalize flipper http - flipper_http_deinit(); + flip_wifi_free_all(app); // 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); // free the app - if(app) free(app); + if (app) + free(app); } diff --git a/flip_wifi/flip_wifi.h b/flip_wifi/flip_wifi.h index 93b87256e..bf943bb5a 100644 --- a/flip_wifi/flip_wifi.h +++ b/flip_wifi/flip_wifi.h @@ -5,86 +5,75 @@ #include #include -#define TAG "FlipWiFi" -#define MAX_WIFI_NETWORKS 25 +#define TAG "FlipWiFi" +#define MAX_SCAN_NETWORKS 100 +#define MAX_SAVED_NETWORKS 25 +#define MAX_SSID_LENGTH 64 // Define the submenu items for our FlipWiFi application -typedef enum { +typedef enum +{ FlipWiFiSubmenuIndexAbout, // FlipWiFiSubmenuIndexWiFiScan, FlipWiFiSubmenuIndexWiFiSaved, + FlipWiFiSubmenuIndexCommands, // FlipWiFiSubmenuIndexWiFiSavedAddSSID, // + FlipWiFiSubmenuIndexFastCommandStart = 50, FlipWiFiSubmenuIndexWiFiScanStart = 100, FlipWiFiSubmenuIndexWiFiSavedStart = 200, } FlipWiFiSubmenuIndex; // Define a single view for our FlipWiFi application -typedef enum { - FlipWiFiViewWiFiScan, // The view for the wifi scan screen +typedef enum +{ + FlipWiFiViewWiFiScan, // The view for the wifi scan screen FlipWiFiViewWiFiSaved, // The view for the wifi scan screen - FlipWiFiViewSubmenuMain, // The submenu for the main screen - FlipWiFiViewSubmenuScan, // The submenu for the wifi scan screen - FlipWiFiViewSubmenuSaved, // The submenu for the wifi scan screen - FlipWiFiViewAbout, // The about screen - FlipWiFiViewTextInputScan, // The text input screen for the wifi scan screen - FlipWiFiViewTextInputSaved, // The text input screen for the wifi saved screen // - FlipWiFiViewTextInputSavedAddSSID, // The text input screen for the wifi saved screen + FlipWiFiViewSubmenuMain, // The submenu for the main screen + FlipWiFiViewSubmenuScan, // The submenu for the wifi scan screen + FlipWiFiViewSubmenuSaved, // The submenu for the wifi saved screen + FlipWiFiViewSubmenuCommands, // The submenu for the fast commands screen + FlipWiFiViewAbout, // The about screen + FlipWiFiViewTextInputScan, // The text input screen for the wifi scan screen + FlipWiFiViewTextInputSaved, // The text input screen for the wifi saved screen + // + FlipWiFiViewTextInputSavedAddSSID, // The text input screen for the wifi saved screen FlipWiFiViewTextInputSavedAddPassword, // The text input screen for the wifi saved screen // - FlipWiFiViewPopup, // The popup screen + FlipWiFiViewGeneric, // generic view + FlipWiFiViewSubmenu, // generic submenu + FlipWiFiViewTextInput, // generic text input } FlipWiFiView; // Define the WiFiPlaylist structure -typedef struct { - char* ssids[MAX_WIFI_NETWORKS]; - char* passwords[MAX_WIFI_NETWORKS]; +typedef struct +{ + char ssids[MAX_SAVED_NETWORKS][MAX_SSID_LENGTH]; + char passwords[MAX_SAVED_NETWORKS][MAX_SSID_LENGTH]; size_t count; } WiFiPlaylist; // Each screen will have its own view -typedef struct { - ViewDispatcher* view_dispatcher; // Switches between our views - Popup* popup; // The popup for the app - View* view_wifi_scan; // The view for the wifi scan screen - View* view_wifi_saved; // The view for the wifi saved screen - Submenu* submenu_main; // The submenu for the main screen - Submenu* submenu_wifi_scan; // The submenu for the wifi scan screen - Submenu* submenu_wifi_saved; // The submenu for the saved wifi screen - Widget* widget_info; // The widget - VariableItemList* variable_item_list_wifi; // The variable item list (settngs) - VariableItem* variable_item_ssid; // The variable item - TextInput* uart_text_input_password_scan; // The text input for the wifi scan screen - TextInput* uart_text_input_password_saved; // The text input for the wifi saved screen - // - TextInput* uart_text_input_add_ssid; // The text input for the wifi saved screen - TextInput* uart_text_input_add_password; // The text input for the wifi saved screen - - char* uart_text_input_buffer_password_scan; // Buffer for the text input - char* uart_text_input_temp_buffer_password_scan; // Temporary buffer for the text input - uint32_t uart_text_input_buffer_size_password_scan; // Size of the text input buffer - - char* uart_text_input_buffer_password_saved; // Buffer for the text input - char* uart_text_input_temp_buffer_password_saved; // Temporary buffer for the text input - uint32_t uart_text_input_buffer_size_password_saved; // Size of the text input buffer - - char* uart_text_input_buffer_add_ssid; // Buffer for the text input - char* uart_text_input_temp_buffer_add_ssid; // Temporary buffer for the text input - uint32_t uart_text_input_buffer_size_add_ssid; // Size of the text input buffer - - char* uart_text_input_buffer_add_password; // Buffer for the text input - char* uart_text_input_temp_buffer_add_password; // Temporary buffer for the text input - uint32_t uart_text_input_buffer_size_add_password; // Size of the text input buffer - - WiFiPlaylist wifi_playlist; // The playlist of wifi networks +typedef struct +{ + ViewDispatcher *view_dispatcher; // Switches between our views + Widget *widget_info; // The widget for the about screen + View *view_wifi; // generic view for the wifi scan and saved screens + Submenu *submenu_main; // The submenu for the main screen + Submenu *submenu_wifi; // generic submenu for the wifi scan and saved screens + VariableItemList *variable_item_list_wifi; // The variable item list (settngs) + VariableItem *variable_item_ssid; // The variable item + TextInput *uart_text_input; // The text input screen + char *uart_text_input_buffer; // Buffer for the text input + char *uart_text_input_temp_buffer; // Temporary buffer for the text input + uint32_t uart_text_input_buffer_size; // Size of the text input buffer } FlipWiFiApp; -extern FlipWiFiApp* app_instance; - // Function to free the resources used by FlipWiFiApp -void flip_wifi_app_free(FlipWiFiApp* app); +void flip_wifi_app_free(FlipWiFiApp *app); +extern WiFiPlaylist *wifi_playlist; // The playlist of wifi networks -#endif // FLIP_WIFI_E_H +#endif // FLIP_WIFI_E_H \ No newline at end of file diff --git a/flip_wifi/flipper_http/flipper_http.c b/flip_wifi/flipper_http/flipper_http.c index 060d7f58c..50206bb86 100644 --- a/flip_wifi/flipper_http/flipper_http.c +++ b/flip_wifi/flipper_http/flipper_http.c @@ -6,17 +6,21 @@ 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, + 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); + char *file_path) +{ + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); - if(start_new_file) { + 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)) { + 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); @@ -24,15 +28,19 @@ bool flipper_http_append_to_file( } } // Open the file in write mode - if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + 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 { + } + else + { // Open the file in append mode - if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_OPEN_APPEND)) { + 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); @@ -41,7 +49,8 @@ bool flipper_http_append_to_file( } // Write the data to the file - if(storage_file_write(file, data, data_size) != data_size) { + 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); @@ -55,32 +64,37 @@ bool flipper_http_append_to_file( return true; } -FuriString* flipper_http_load_from_file(char* file_path) { +FuriString *flipper_http_load_from_file(char *file_path) +{ // Open the storage record - Storage* storage = furi_record_open(RECORD_STORAGE); - if(!storage) { + 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) { + 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)) { + 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) { + 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); @@ -92,8 +106,9 @@ FuriString* flipper_http_load_from_file(char* file_path) { furi_string_reset(str_result); // Define a buffer to hold the read data - uint8_t* buffer = (uint8_t*)malloc(MAX_FILE_SHOW); - if(!buffer) { + 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); @@ -104,7 +119,8 @@ FuriString* flipper_http_load_from_file(char* file_path) { // 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) { + 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); @@ -114,7 +130,8 @@ FuriString* flipper_http_load_from_file(char* file_path) { } // Append each byte to the FuriString - for(size_t i = 0; i < read_count; i++) { + for (size_t i = 0; i < read_count; i++) + { furi_string_push_back(str_result, buffer[i]); } @@ -138,39 +155,48 @@ FuriString* flipper_http_load_from_file(char* file_path) { * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -int32_t flipper_http_worker(void* context) { +int32_t flipper_http_worker(void *context) +{ UNUSED(context); size_t rx_line_pos = 0; - while(1) { + while (1) + { uint32_t events = furi_thread_flags_wait( WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever); - if(events & WorkerEvtStop) { + if (events & WorkerEvtStop) + { break; } - if(events & WorkerEvtRxDone) { + if (events & WorkerEvtRxDone) + { // Continuously read from the stream buffer until it's empty - while(!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) { + 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) { + if (received == 0) + { // No more data to read break; } // Append the received byte to the file if saving is enabled - if(fhttp.save_bytes) { + 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)) { + 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; @@ -179,9 +205,11 @@ int32_t flipper_http_worker(void* context) { } // Handle line buffering only if callback is set (text data) - if(fhttp.handle_rx_line_cb) { + if (fhttp.handle_rx_line_cb) + { // Handle line buffering - if(c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1) { + 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 @@ -189,7 +217,9 @@ int32_t flipper_http_worker(void* context) { // Reset the line buffer position rx_line_pos = 0; - } else { + } + else + { rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer } } @@ -206,7 +236,8 @@ int32_t flipper_http_worker(void* context) { * @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) { +void get_timeout_timer_callback(void *context) +{ UNUSED(context); FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving the end."); @@ -230,11 +261,13 @@ void get_timeout_timer_callback(void* context) { * @note This function will handle received data asynchronously via the callback. */ void _flipper_http_rx_callback( - FuriHalSerialHandle* handle, + FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, - void* context) { + void *context) +{ UNUSED(context); - if(event == FuriHalSerialRxEventData) { + 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); @@ -249,23 +282,28 @@ void _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) { +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) { + 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) { + 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) { + if (!fhttp.rx_thread) + { FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread."); furi_stream_buffer_free(fhttp.flipper_http_stream); return false; @@ -283,13 +321,15 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { 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)) { + 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) { + 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); @@ -312,11 +352,12 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { // 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 + FuriTimerTypeOnce, // One-shot timer + &fhttp // Context passed to callback ); - if(!fhttp.get_timeout_timer) { + 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); @@ -333,8 +374,9 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { // 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) { + 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; } @@ -349,8 +391,10 @@ bool flipper_http_init(FlipperHTTP_Callback callback, void* context) { * @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) { +void flipper_http_deinit() +{ + if (fhttp.serial_handle == NULL) + { FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?"); return; } @@ -373,13 +417,15 @@ void flipper_http_deinit() { furi_stream_buffer_free(fhttp.flipper_http_stream); // Free the timer - if(fhttp.get_timeout_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) { + if (fhttp.last_response) + { free(fhttp.last_response); fhttp.last_response = NULL; } @@ -394,34 +440,38 @@ void flipper_http_deinit() { * @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) { +bool flipper_http_send_data(const char *data) +{ size_t data_length = strlen(data); - if(data_length == 0) { + 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 + 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] = '\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))) { + 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); + 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); @@ -437,9 +487,11 @@ bool flipper_http_send_data(const char* data) { * @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)) { +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; } @@ -455,9 +507,11 @@ bool flipper_http_ping() { * @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)) { +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; } @@ -472,9 +526,11 @@ bool flipper_http_list_commands() { * @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)) { +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; } @@ -489,9 +545,11 @@ bool flipper_http_led_on() { * @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)) { +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; } @@ -508,8 +566,10 @@ bool flipper_http_led_off() { * @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) { +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; } @@ -517,12 +577,14 @@ bool flipper_http_parse_json(const char* key, const char* json_data) { char buffer[256]; int ret = snprintf(buffer, sizeof(buffer), "[PARSE]{\"key\":\"%s\",\"json\":%s}", key, json_data); - if(ret < 0 || ret >= (int)sizeof(buffer)) { + 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)) { + if (!flipper_http_send_data(buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse command."); return false; } @@ -540,8 +602,10 @@ bool flipper_http_parse_json(const char* key, const char* json_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) { +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; } @@ -554,12 +618,14 @@ bool flipper_http_parse_json_array(const char* key, int index, const char* json_ key, index, json_data); - if(ret < 0 || ret >= (int)sizeof(buffer)) { + 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)) { + if (!flipper_http_send_data(buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse array command."); return false; } @@ -574,21 +640,30 @@ bool flipper_http_parse_json_array(const char* key, int index, const char* json_ * @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; - } +bool flipper_http_scan_wifi() +{ // custom for FlipWiFi app fhttp.just_started_get = true; + // ensure the data folder exists + snprintf( fhttp.file_path, sizeof(fhttp.file_path), - STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/scan.txt"); + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data/scan.txt"); + + // ensure the file is empty + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_simply_remove_recursive(storage, fhttp.file_path); + furi_record_close(RECORD_STORAGE); fhttp.save_received_data = true; + if (!flipper_http_send_data("[WIFI/SCAN]")) + { + FURI_LOG_E("FlipperHTTP", "Failed to send WiFi scan command."); + return false; + } // The response will be handled asynchronously via the callback return true; } @@ -599,24 +674,34 @@ bool flipper_http_scan_wifi() { * @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) { +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; } + + // custom for FlipWiFi app + fhttp.just_started_get = true; + char buffer[256]; int ret = snprintf( buffer, sizeof(buffer), "[WIFI/SAVE]{\"ssid\":\"%s\",\"password\":\"%s\"}", ssid, password); - if(ret < 0 || ret >= (int)sizeof(buffer)) { + 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)) { + if (!flipper_http_send_data(buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command."); return false; } + fhttp.state = RECEIVING; + // The response will be handled asynchronously via the callback return true; } @@ -627,8 +712,10 @@ bool flipper_http_save_wifi(const char* ssid, const char* password) { * @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]")) { +bool flipper_http_ip_address() +{ + if (!flipper_http_send_data("[IP/ADDRESS]")) + { FURI_LOG_E("FlipperHTTP", "Failed to send IP address command."); return false; } @@ -643,9 +730,11 @@ bool flipper_http_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_wifi() { - const char* command = "[WIFI/IP]"; - if(!flipper_http_send_data(command)) { +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; } @@ -660,8 +749,10 @@ bool flipper_http_ip_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]")) { +bool flipper_http_disconnect_wifi() +{ + if (!flipper_http_send_data("[WIFI/DISCONNECT]")) + { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command."); return false; } @@ -676,8 +767,10 @@ bool flipper_http_disconnect_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]")) { +bool flipper_http_connect_wifi() +{ + if (!flipper_http_send_data("[WIFI/CONNECT]")) + { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command."); return false; } @@ -693,8 +786,10 @@ bool flipper_http_connect_wifi() { * @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) { +bool flipper_http_get_request(const char *url) +{ + if (!url) + { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request."); return false; } @@ -702,13 +797,15 @@ bool flipper_http_get_request(const char* url) { // Prepare GET request command char command[256]; int ret = snprintf(command, sizeof(command), "[GET]%s", url); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command."); return false; } @@ -724,8 +821,10 @@ bool flipper_http_get_request(const char* url) { * @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) { +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; @@ -735,13 +834,15 @@ bool flipper_http_get_request_with_headers(const char* url, const char* 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)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); return false; } @@ -757,8 +858,10 @@ bool flipper_http_get_request_with_headers(const char* url, const char* headers) * @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) { +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; } @@ -767,13 +870,15 @@ bool flipper_http_get_request_bytes(const char* url, const char* 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)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); return false; } @@ -791,10 +896,12 @@ bool flipper_http_get_request_bytes(const char* url, const char* headers) { * @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) { + 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."); @@ -810,13 +917,15 @@ bool flipper_http_post_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); return false; } @@ -833,8 +942,10 @@ bool flipper_http_post_request_with_headers( * @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) { +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; @@ -849,13 +960,15 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); return false; } @@ -873,10 +986,12 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const * @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) { + 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; @@ -891,13 +1006,15 @@ bool flipper_http_put_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data."); return false; } @@ -915,10 +1032,12 @@ bool flipper_http_put_request_with_headers( * @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) { + 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."); @@ -934,14 +1053,16 @@ bool flipper_http_delete_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data."); return false; } @@ -957,26 +1078,31 @@ bool flipper_http_delete_request_with_headers( * @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) { +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') { + 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) { + 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) { + if (fhttp.state != INACTIVE && fhttp.state != ISSUE) + { fhttp.state = RECEIVING; } @@ -984,24 +1110,28 @@ void flipper_http_rx_callback(const char* line, void* context) { // 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)) { + 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) { + 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) { + 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) { + 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); @@ -1011,14 +1141,17 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.save_bytes = false; fhttp.save_received_data = false; - if(fhttp.is_bytes_request) { + 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++) { + 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) { + 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); @@ -1028,9 +1161,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1042,9 +1176,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1052,18 +1187,21 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_get) { + 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) { + 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) { + 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); @@ -1073,14 +1211,17 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.save_bytes = false; fhttp.save_received_data = false; - if(fhttp.is_bytes_request) { + 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++) { + 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) { + 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); @@ -1090,9 +1231,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1104,9 +1246,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1114,18 +1257,21 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_post) { + 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) { + 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) { + 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); @@ -1139,9 +1285,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1149,18 +1296,21 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_put) { + 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) { + 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) { + 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); @@ -1174,9 +1324,10 @@ void flipper_http_rx_callback(const char* line, void* context) { } // 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)) { + 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; @@ -1184,22 +1335,29 @@ void flipper_http_rx_callback(const char* line, void* context) { return; } - if(!fhttp.just_started_delete) { + 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) { + if (strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL) + { FURI_LOG_I(HTTP_TAG, "Operation succeeded."); - } else if(strstr(line, "[INFO]") != NULL) { + } + 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) { + if (fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL) + { fhttp.state = IDLE; } - } else if(strstr(line, "[GET/SUCCESS]") != NULL) { + } + 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); @@ -1209,7 +1367,9 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.just_started_bytes = true; file_buffer_len = 0; return; - } else if(strstr(line, "[POST/SUCCESS]") != NULL) { + } + 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); @@ -1219,67 +1379,86 @@ void flipper_http_rx_callback(const char* line, void* context) { fhttp.just_started_bytes = true; file_buffer_len = 0; return; - } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { + } + 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) { + } + 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) { + } + else if (strstr(line, "[DISCONNECTED]") != NULL) + { FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully."); - } else if(strstr(line, "[ERROR]") != NULL) { + } + 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) { + } + 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) { + if (fhttp.state == INACTIVE) + { fhttp.state = IDLE; return; } } - if(fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) { + if (fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL) + { fhttp.state = IDLE; - } else if(fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) { + } + else if (fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL) + { fhttp.state = INACTIVE; - } else { + } + 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; +char *trim(const char *str) +{ + const char *end; + char *trimmed_str; size_t len; // Trim leading space - while(isspace((unsigned char)*str)) + while (isspace((unsigned char)*str)) str++; // All spaces? - if(*str == 0) return strdup(""); // Return an empty string if 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)) + 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) { + trimmed_str = (char *)malloc(len + 1); + if (trimmed_str == NULL) + { return NULL; // Handle memory allocation failure } @@ -1296,21 +1475,25 @@ char* trim(const char* str) { * @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 +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 { + } + 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) { + 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) + if (!parse_json()) // parse the JSON before switching to the view (synchonous) { FURI_LOG_E(HTTP_TAG, "Failed to parse the JSON..."); return false; @@ -1327,17 +1510,18 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars * @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; +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) { + if (!loading) + { FURI_LOG_E(HTTP_TAG, "Failed to allocate loading"); view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); @@ -1350,7 +1534,8 @@ void flipper_http_loading_task( view_dispatcher_switch_to_view(*view_dispatcher, loading_view_id); // Make the request - if(!flipper_http_process_response_async(http_request, parse_response)) { + 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); diff --git a/flip_wifi/flipper_http/flipper_http.h b/flip_wifi/flipper_http/flipper_http.h index c072967e0..4458594ff 100644 --- a/flip_wifi/flipper_http/flipper_http.h +++ b/flip_wifi/flipper_http/flipper_http.h @@ -15,69 +15,72 @@ // 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 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 +#define BAUDRATE (115200) // UART baudrate +#define RX_BUF_SIZE 2048 // 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); +typedef void (*FlipperHTTP_Callback)(const char *line, void *context); // State variable to track the UART state -typedef enum { - INACTIVE, // Inactive state - IDLE, // Default state +typedef enum +{ + INACTIVE, // Inactive state + IDLE, // Default state RECEIVING, // Receiving data - SENDING, // Sending data - ISSUE, // Issue with connection + SENDING, // Sending data + ISSUE, // Issue with connection } SerialState; // Event Flags for UART Worker Thread -typedef enum { +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 +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 + 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 *last_response; char file_path[256]; // Path to save the received data // Timer-related members - FuriTimer* get_timeout_timer; // Timer for HTTP request timeout + 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 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 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 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 + 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; + 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 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; @@ -93,12 +96,12 @@ extern size_t file_buffer_len; // 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, + const void *data, size_t data_size, bool start_new_file, - char* file_path); + char *file_path); -FuriString* flipper_http_load_from_file(char* file_path); +FuriString *flipper_http_load_from_file(char *file_path); // UART worker thread /** @@ -108,7 +111,7 @@ FuriString* flipper_http_load_from_file(char* file_path); * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -int32_t flipper_http_worker(void* context); +int32_t flipper_http_worker(void *context); // Timer callback function /** @@ -117,7 +120,7 @@ int32_t flipper_http_worker(void* context); * @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); +void get_timeout_timer_callback(void *context); // UART RX Handler Callback (Interrupt Context) /** @@ -129,9 +132,9 @@ void get_timeout_timer_callback(void* context); * @note This function will handle received data asynchronously via the callback. */ void _flipper_http_rx_callback( - FuriHalSerialHandle* handle, + FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, - void* context); + void *context); // UART initialization function /** @@ -141,7 +144,7 @@ void _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); +bool flipper_http_init(FlipperHTTP_Callback callback, void *context); // Deinitialize UART /** @@ -158,7 +161,7 @@ void flipper_http_deinit(); * @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); +bool flipper_http_send_data(const char *data); // Function to send a PING request /** @@ -202,7 +205,7 @@ bool flipper_http_led_off(); * @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); +bool flipper_http_parse_json(const char *key, const char *json_data); // Function to parse JSON array data /** @@ -213,7 +216,7 @@ bool flipper_http_parse_json(const char* key, const char* json_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); +bool flipper_http_parse_json_array(const char *key, int index, const char *json_data); // Function to scan for WiFi networks /** @@ -229,7 +232,7 @@ bool flipper_http_scan_wifi(); * @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); +bool flipper_http_save_wifi(const char *ssid, const char *password); // Function to get IP address of WiFi Devboard /** @@ -270,7 +273,7 @@ bool flipper_http_connect_wifi(); * @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); +bool flipper_http_get_request(const char *url); // Function to send a GET request with headers /** @@ -280,7 +283,7 @@ bool flipper_http_get_request(const char* url); * @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); +bool flipper_http_get_request_with_headers(const char *url, const char *headers); // Function to send a GET request with headers and return bytes /** @@ -290,7 +293,7 @@ bool flipper_http_get_request_with_headers(const char* url, const char* headers) * @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); +bool flipper_http_get_request_bytes(const char *url, const char *headers); // Function to send a POST request with headers /** @@ -302,9 +305,9 @@ bool flipper_http_get_request_bytes(const char* url, const char* headers); * @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); + const char *url, + const char *headers, + const char *payload); // Function to send a POST request with headers and return bytes /** @@ -315,7 +318,7 @@ bool flipper_http_post_request_with_headers( * @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); +bool flipper_http_post_request_bytes(const char *url, const char *headers, const char *payload); // Function to send a PUT request with headers /** @@ -327,9 +330,9 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const * @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); + const char *url, + const char *headers, + const char *payload); // Function to send a DELETE request with headers /** @@ -341,9 +344,9 @@ bool flipper_http_put_request_with_headers( * @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); + const char *url, + const char *headers, + const char *payload); // Function to handle received data asynchronously /** @@ -353,10 +356,10 @@ bool flipper_http_delete_request_with_headers( * @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); +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); +char *trim(const char *str); /** * @brief Process requests and parse JSON data asynchronously * @param http_request The function to send the request @@ -374,11 +377,10 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars * @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); +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/jsmn.c b/flip_wifi/jsmn/jsmn.c index eb33b3cc7..d101530c6 100644 --- a/flip_wifi/jsmn/jsmn.c +++ b/flip_wifi/jsmn/jsmn.c @@ -7,17 +7,17 @@ */ #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; +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) { + if (parser->toknext >= num_tokens) + { return NULL; } tok = &tokens[parser->toknext++]; @@ -32,8 +32,9 @@ static jsmntok_t* /** * Fills token type and boundaries. */ -static void - jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { +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; @@ -43,19 +44,19 @@ static void /** * 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; +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]) { + 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 ':': @@ -72,7 +73,8 @@ static int jsmn_parse_primitive( /* to quiet a warning from gcc*/ break; } - if(js[parser->pos] < 32 || js[parser->pos] >= 127) { + if (js[parser->pos] < 32 || js[parser->pos] >= 127) + { parser->pos = start; return JSMN_ERROR_INVAL; } @@ -84,12 +86,14 @@ static int jsmn_parse_primitive( #endif found: - if(tokens == NULL) { + if (tokens == NULL) + { parser->pos--; return 0; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { parser->pos = start; return JSMN_ERROR_NOMEM; } @@ -104,29 +108,31 @@ static int jsmn_parse_primitive( /** * 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; +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++) { + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { char c = js[parser->pos]; /* Quote: end of string */ - if(c == '\"') { - if(tokens == NULL) { + if (c == '\"') + { + if (tokens == NULL) + { return 0; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { parser->pos = start; return JSMN_ERROR_NOMEM; } @@ -138,10 +144,12 @@ static int jsmn_parse_string( } /* Backslash: Quoted symbol expected */ - if(c == '\\' && parser->pos + 1 < len) { + if (c == '\\' && parser->pos + 1 < len) + { int i; parser->pos++; - switch(js[parser->pos]) { + switch (js[parser->pos]) + { /* Allowed escaped symbols */ case '\"': case '/': @@ -155,11 +163,13 @@ static int jsmn_parse_string( /* Allows escaped symbol \uXXXX */ case 'u': parser->pos++; - for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + 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 */ + 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; } @@ -181,7 +191,8 @@ static int jsmn_parse_string( /** * Create JSON parser over an array of tokens */ -void jsmn_init(jsmn_parser* parser) { +void jsmn_init(jsmn_parser *parser) +{ parser->pos = 0; parser->toknext = 0; parser->toksuper = -1; @@ -190,38 +201,41 @@ void jsmn_init(jsmn_parser* parser) { /** * 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 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; + jsmntok_t *token; int count = parser->toknext; - for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { char c; jsmntype_t type; c = js[parser->pos]; - switch(c) { + switch (c) + { case '{': case '[': count++; - if(tokens == NULL) { + if (tokens == NULL) + { break; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { return JSMN_ERROR_NOMEM; } - if(parser->toksuper != -1) { - jsmntok_t* t = &tokens[parser->toksuper]; + 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) { + if (t->type == JSMN_OBJECT) + { return JSMN_ERROR_INVAL; } #endif @@ -236,26 +250,33 @@ int jsmn_parse( break; case '}': case ']': - if(tokens == NULL) { + if (tokens == NULL) + { break; } type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); #ifdef JSMN_PARENT_LINKS - if(parser->toknext < 1) { + 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) { + 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) { + if (token->parent == -1) + { + if (token->type != type || parser->toksuper == -1) + { return JSMN_ERROR_INVAL; } break; @@ -263,10 +284,13 @@ int jsmn_parse( token = &tokens[token->parent]; } #else - for(i = parser->toknext - 1; i >= 0; i--) { + for (i = parser->toknext - 1; i >= 0; i--) + { token = &tokens[i]; - if(token->start != -1 && token->end == -1) { - if(token->type != type) { + if (token->start != -1 && token->end == -1) + { + if (token->type != type) + { return JSMN_ERROR_INVAL; } parser->toksuper = -1; @@ -275,12 +299,15 @@ int jsmn_parse( } } /* Error if unmatched closing bracket */ - if(i == -1) { + if (i == -1) + { return JSMN_ERROR_INVAL; } - for(; i >= 0; i--) { + for (; i >= 0; i--) + { token = &tokens[i]; - if(token->start != -1 && token->end == -1) { + if (token->start != -1 && token->end == -1) + { parser->toksuper = i; break; } @@ -289,11 +316,13 @@ int jsmn_parse( break; case '\"': r = jsmn_parse_string(parser, js, len, tokens, num_tokens); - if(r < 0) { + if (r < 0) + { return r; } count++; - if(parser->toksuper != -1 && tokens != NULL) { + if (parser->toksuper != -1 && tokens != NULL) + { tokens[parser->toksuper].size++; } break; @@ -306,15 +335,19 @@ int jsmn_parse( 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) { + 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) { + 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; } @@ -340,9 +373,12 @@ int jsmn_parse( 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)) { + 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; } } @@ -351,11 +387,13 @@ int jsmn_parse( default: #endif r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); - if(r < 0) { + if (r < 0) + { return r; } count++; - if(parser->toksuper != -1 && tokens != NULL) { + if (parser->toksuper != -1 && tokens != NULL) + { tokens[parser->toksuper].size++; } break; @@ -368,10 +406,13 @@ int jsmn_parse( } } - if(tokens != NULL) { - for(i = parser->toknext - 1; i >= 0; i--) { + 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) { + if (tokens[i].start != -1 && tokens[i].end == -1) + { return JSMN_ERROR_PART; } } @@ -381,10 +422,12 @@ int jsmn_parse( } // 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) { +char *get_json(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); @@ -392,30 +435,41 @@ 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) { - if(tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && - strncmp(json + tok->start, s, tok->end - tok->start) == 0) { +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) { +char *get_json_value(char *key, const char *json_data) +{ // Parse the JSON feed - if(json_data != NULL) { + if (json_data != NULL) + { jsmn_parser parser; jsmn_init(&parser); - + uint32_t max_tokens = json_token_count(json_data); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + return NULL; + } // Allocate tokens array on the heap - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); - if(tokens == NULL) { + 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) { + if (ret < 0) + { // Handle parsing errors FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); free(tokens); @@ -423,19 +477,23 @@ char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { } // Ensure that the root element is an object - if(ret < 1 || tokens[0].type != JSMN_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) { + 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) { + char *value = malloc(length + 1); + if (value == NULL) + { FURI_LOG_E("JSMM.H", "Failed to allocate memory for value."); free(tokens); return NULL; @@ -450,102 +508,146 @@ char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { // Free the token array if key was not found free(tokens); - } else { + } + else + { FURI_LOG_E("JSMM.H", "JSON data is NULL"); } - FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON."); + char warning[128]; + snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key); + FURI_LOG_E("JSMM.H", warning); 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) { +// Helper function to skip a token and all its descendants. +// Returns the index of the next token after skipping this one. +// On error or out of bounds, returns -1. +static int skip_token(const jsmntok_t *tokens, int start, int total) +{ + if (start < 0 || start >= total) + return -1; + + int i = start; + if (tokens[i].type == JSMN_OBJECT) + { + // For an object: size is number of key-value pairs + int pairs = tokens[i].size; + i++; // move to first key-value pair + for (int p = 0; p < pairs; p++) + { + // skip key (primitive/string) + i++; + if (i >= total) + return -1; + // skip value (which could be object/array and must be skipped recursively) + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; // i is now just past the object + } + else if (tokens[i].type == JSMN_ARRAY) + { + // For an array: size is number of elements + int elems = tokens[i].size; + i++; // move to first element + for (int e = 0; e < elems; e++) + { + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; // i is now just past the array + } + else + { + // Primitive or string token, just skip it + return i + 1; + } +} + +// Revised get_json_array_value +char *get_json_array_value(char *key, uint32_t index, const char *json_data) +{ + // Always extract the full array each time from the original json_data + char *array_str = get_json_value(key, json_data); + if (array_str == NULL) + { FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); return NULL; } + uint32_t max_tokens = json_token_count(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + free(array_str); + 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) { + 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) { + 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) { + 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); + if (index >= (uint32_t)tokens[0].size) + { + // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", 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."); + // Find the index-th element: start from token[1], which is the first element + int elem_token = 1; + for (uint32_t i = 0; i < index; i++) + { + elem_token = skip_token(tokens, elem_token, ret); + if (elem_token == -1 || elem_token >= ret) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i); free(tokens); free(array_str); return NULL; } } - // Extract the array element - jsmntok_t element = tokens[current_token]; + // Now elem_token should point to the token of the requested element + jsmntok_t element = tokens[elem_token]; int length = element.end - element.start; - char* value = malloc(length + 1); - if(value == NULL) { + char *value = malloc(length + 1); + if (!value) + { 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 + value[length] = '\0'; - // Clean up free(tokens); free(array_str); @@ -553,21 +655,30 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // 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) { +char **get_json_array_values(char *key, char *json_data, 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) { + char *array_str = get_json_value(key, json_data); + if (array_str == NULL) + { FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); return NULL; } - + uint32_t max_tokens = json_token_count(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + free(array_str); + 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) { + 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; @@ -575,7 +686,8 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // Parse the JSON array int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); - if(ret < 0) { + if (ret < 0) + { FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); free(tokens); free(array_str); @@ -583,7 +695,8 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in } // Ensure the root element is an array - if(tokens[0].type != JSMN_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); @@ -592,8 +705,9 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // 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) { + 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); @@ -604,15 +718,18 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // 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) { + 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) { + 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; @@ -622,10 +739,12 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in 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) { + 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++) { + for (int j = 0; j < actual_num_values; j++) + { free(values[j]); } free(values); @@ -647,14 +766,17 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in *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) { + 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++) { + for (int i = actual_num_values; i < array_size; i++) + { free(values[i]); } } @@ -664,3 +786,18 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in free(array_str); return values; } + +int json_token_count(const char *json) +{ + if (json == NULL) + { + return JSMN_ERROR_INVAL; + } + + jsmn_parser parser; + jsmn_init(&parser); + + // Pass NULL for tokens and 0 for num_tokens to get the token count only + int ret = jsmn_parse(&parser, json, strlen(json), NULL, 0); + return ret; // If ret >= 0, it represents the number of tokens needed. +} diff --git a/flip_wifi/jsmn/jsmn.h b/flip_wifi/jsmn/jsmn.h index cd95a0e58..5f96e5596 100644 --- a/flip_wifi/jsmn/jsmn.h +++ b/flip_wifi/jsmn/jsmn.h @@ -17,9 +17,11 @@ #define JSMN_H #include +#include #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif #ifdef JSMN_STATIC @@ -27,72 +29,17 @@ extern "C" { #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); + 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); + 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 */ @@ -109,23 +56,19 @@ JSMN_API int jsmn_parse( #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); +char *get_json(const char *key, const char *value); // Helper function to compare JSON keys -int jsoneq(const char* json, jsmntok_t* tok, const char* s); +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); +char *get_json_value(char *key, const char *json_data); // Revised get_json_array_value function -char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t max_tokens); +char *get_json_array_value(char *key, uint32_t index, const char *json_data); // 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); +char **get_json_array_values(char *key, char *json_data, int *num_values); + +int json_token_count(const char *json); #endif /* JB_JSMN_EDIT */ diff --git a/flip_wifi/jsmn/jsmn_furi.c b/flip_wifi/jsmn/jsmn_furi.c new file mode 100644 index 000000000..d3bde9365 --- /dev/null +++ b/flip_wifi/jsmn/jsmn_furi.c @@ -0,0 +1,736 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * [License text continues...] + */ + +#include + +// Forward declarations of helper functions +static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s); +static int skip_token(const jsmntok_t *tokens, int start, int total); + +/** + * 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) +{ + if (parser->toknext >= num_tokens) + { + return NULL; + } + jsmntok_t *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. + * Now uses FuriString to access characters. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const size_t num_tokens) +{ + size_t len = furi_string_size(js); + int start = parser->pos; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + switch (c) + { +#ifndef JSMN_STRICT + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + break; + } + if (c < 32 || c >= 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; + } + jsmntok_t *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. + * Now uses FuriString to access characters. + */ +static int jsmn_parse_string(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const size_t num_tokens) +{ + size_t len = furi_string_size(js); + int start = parser->pos; + parser->pos++; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + if (c == '\"') + { + if (tokens == NULL) + { + return 0; + } + jsmntok_t *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; + } + + if (c == '\\' && (parser->pos + 1) < len) + { + parser->pos++; + char esc = furi_string_get_char(js, parser->pos); + switch (esc) + { + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + case 'u': + { + parser->pos++; + for (int i = 0; i < 4 && parser->pos < len; i++) + { + char hex = furi_string_get_char(js, parser->pos); + if (!((hex >= '0' && hex <= '9') || + (hex >= 'A' && hex <= 'F') || + (hex >= 'a' && hex <= 'f'))) + { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + } + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Create JSON parser + */ +void jsmn_init_furi(jsmn_parser *parser) +{ + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +/** + * Parse JSON string and fill tokens. + * Now uses FuriString for the input JSON. + */ +int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const unsigned int num_tokens) +{ + size_t len = furi_string_size(js); + int r; + int i; + int count = parser->toknext; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + jsmntype_t type; + + switch (c) + { + case '{': + case '[': + { + count++; + if (tokens == NULL) + { + break; + } + jsmntok_t *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 + 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; + } + { + jsmntok_t *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 + { + jsmntok_t *token; + 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; + } + } + 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, 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 ' ': + // Whitespace - ignore + 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 + 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': + 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 + default: +#endif + r = jsmn_parse_primitive(parser, js, tokens, num_tokens); + if (r < 0) + return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + { + tokens[parser->toksuper].size++; + } + break; +#ifdef JSMN_STRICT + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) + { + for (i = parser->toknext - 1; i >= 0; i--) + { + if (tokens[i].start != -1 && tokens[i].end == -1) + { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +// Helper function to create a JSON object: {"key":"value"} +FuriString *get_json_furi(const FuriString *key, const FuriString *value) +{ + FuriString *result = furi_string_alloc(); + furi_string_printf(result, "{\"%s\":\"%s\"}", + furi_string_get_cstr(key), + furi_string_get_cstr(value)); + return result; // Caller responsible for furi_string_free +} + +// Helper function to compare JSON keys +static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s) +{ + size_t s_len = furi_string_size(s); + size_t tok_len = tok->end - tok->start; + + if (tok->type != JSMN_STRING) + return -1; + if (s_len != tok_len) + return -1; + + FuriString *sub = furi_string_alloc_set(json); + furi_string_mid(sub, tok->start, tok_len); + + int res = furi_string_cmp(sub, s); + furi_string_free(sub); + + return (res == 0) ? 0 : -1; +} + +// Skip a token and its descendants +static int skip_token(const jsmntok_t *tokens, int start, int total) +{ + if (start < 0 || start >= total) + return -1; + + int i = start; + if (tokens[i].type == JSMN_OBJECT) + { + int pairs = tokens[i].size; + i++; + for (int p = 0; p < pairs; p++) + { + i++; // skip key + if (i >= total) + return -1; + i = skip_token(tokens, i, total); // skip value + if (i == -1) + return -1; + } + return i; + } + else if (tokens[i].type == JSMN_ARRAY) + { + int elems = tokens[i].size; + i++; + for (int e = 0; e < elems; e++) + { + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; + } + else + { + return i + 1; + } +} + +/** + * Parse JSON and return the value associated with a given char* key. + */ +FuriString *get_json_value_furi(const char *key, const FuriString *json_data) +{ + if (json_data == NULL) + { + FURI_LOG_E("JSMM.H", "JSON data is NULL"); + return NULL; + } + uint32_t max_tokens = json_token_count_furi(json_data); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + return NULL; + } + // Create a temporary FuriString from key + FuriString *key_str = furi_string_alloc(); + furi_string_cat_str(key_str, key); + + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(key_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, json_data, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); + free(tokens); + furi_string_free(key_str); + return NULL; + } + + if (ret < 1 || tokens[0].type != JSMN_OBJECT) + { + FURI_LOG_E("JSMM.H", "Root element is not an object."); + free(tokens); + furi_string_free(key_str); + return NULL; + } + + for (int i = 1; i < ret; i++) + { + if (jsoneq_furi(json_data, &tokens[i], key_str) == 0) + { + int length = tokens[i + 1].end - tokens[i + 1].start; + FuriString *value = furi_string_alloc_set(json_data); + furi_string_mid(value, tokens[i + 1].start, length); + free(tokens); + furi_string_free(key_str); + return value; + } + } + + free(tokens); + furi_string_free(key_str); + char warning[128]; + snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key); + FURI_LOG_E("JSMM.H", warning); + return NULL; +} + +/** + * Return the value at a given index in a JSON array for a given char* key. + */ +FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data) +{ + FuriString *array_str = get_json_value_furi(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key"); + return NULL; + } + uint32_t max_tokens = json_token_count_furi(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (ret < 1 || tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key is not an array."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (index >= (uint32_t)tokens[0].size) + { + // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int elem_token = 1; + for (uint32_t i = 0; i < index; i++) + { + elem_token = skip_token(tokens, elem_token, ret); + if (elem_token == -1 || elem_token >= ret) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i); + free(tokens); + furi_string_free(array_str); + return NULL; + } + } + + jsmntok_t element = tokens[elem_token]; + int length = element.end - element.start; + + FuriString *value = furi_string_alloc_set(array_str); + furi_string_mid(value, element.start, length); + + free(tokens); + furi_string_free(array_str); + + return value; +} + +/** + * Extract all object values from a JSON array associated with a given char* key. + */ +FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values) +{ + *num_values = 0; + // Convert key to FuriString and call get_json_value_furi + FuriString *array_str = get_json_value_furi(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key"); + return NULL; + } + + uint32_t max_tokens = json_token_count_furi(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key is not an array."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int array_size = tokens[0].size; + FuriString **values = (FuriString **)malloc(array_size * sizeof(FuriString *)); + if (values == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int actual_num_values = 0; + int current_token = 1; + 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]; + + int length = element.end - element.start; + FuriString *value = furi_string_alloc_set(array_str); + furi_string_mid(value, element.start, length); + + values[actual_num_values] = value; + actual_num_values++; + + // Skip this element and its descendants + current_token = skip_token(tokens, current_token, ret); + if (current_token == -1) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens after element %d.", i); + break; + } + } + + *num_values = actual_num_values; + if (actual_num_values < array_size) + { + FuriString **reduced_values = (FuriString **)realloc(values, actual_num_values * sizeof(FuriString *)); + if (reduced_values != NULL) + { + values = reduced_values; + } + } + + free(tokens); + furi_string_free(array_str); + return values; +} + +uint32_t json_token_count_furi(const FuriString *json) +{ + if (json == NULL) + { + return JSMN_ERROR_INVAL; + } + + jsmn_parser parser; + jsmn_init_furi(&parser); + + // Pass NULL for tokens and 0 for num_tokens to get the token count only + int ret = jsmn_parse_furi(&parser, json, NULL, 0); + return ret; // If ret >= 0, it represents the number of tokens needed. +} diff --git a/flip_wifi/jsmn/jsmn_furi.h b/flip_wifi/jsmn/jsmn_furi.h new file mode 100644 index 000000000..cb01f38ca --- /dev/null +++ b/flip_wifi/jsmn/jsmn_furi.h @@ -0,0 +1,74 @@ +/* + * 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_FURI_H +#define JSMN_FURI_H + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + + JSMN_API void jsmn_init_furi(jsmn_parser *parser); + JSMN_API int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/* Implementation in jsmn_furi.c */ +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_FURI_H */ + +#ifndef JB_JSMN_FURI_EDIT +#define JB_JSMN_FURI_EDIT + +// Helper function to create a JSON object +FuriString *get_json_furi(const FuriString *key, const FuriString *value); + +// Updated signatures to accept const char* key +FuriString *get_json_value_furi(const char *key, const FuriString *json_data); +FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data); +FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values); + +uint32_t json_token_count_furi(const FuriString *json); +/* Example usage: +char *json = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; +FuriString *json_data = char_to_furi_string(json); +if (!json_data) +{ + FURI_LOG_E(TAG, "Failed to allocate FuriString"); + return -1; +} +FuriString *value = get_json_value_furi("key1", json_data, json_token_count_furi(json_data)); +if (value) +{ + FURI_LOG_I(TAG, "Value: %s", furi_string_get_cstr(value)); + furi_string_free(value); +} +furi_string_free(json_data); +*/ +#endif /* JB_JSMN_EDIT */ diff --git a/flip_wifi/jsmn/jsmn_h.c b/flip_wifi/jsmn/jsmn_h.c new file mode 100644 index 000000000..59480e6e6 --- /dev/null +++ b/flip_wifi/jsmn/jsmn_h.c @@ -0,0 +1,15 @@ +#include +FuriString *char_to_furi_string(const char *str) +{ + FuriString *furi_str = furi_string_alloc(); + if (!furi_str) + { + return NULL; + } + for (size_t i = 0; i < strlen(str); i++) + { + furi_string_push_back(furi_str, str[i]); + } + return furi_str; +} +bool jsmn_memory_check(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); } \ No newline at end of file diff --git a/flip_wifi/jsmn/jsmn_h.h b/flip_wifi/jsmn/jsmn_h.h new file mode 100644 index 000000000..97d53e7ff --- /dev/null +++ b/flip_wifi/jsmn/jsmn_h.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include +#include +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 +{ + JSMN_ERROR_NOMEM = -1, + JSMN_ERROR_INVAL = -2, + JSMN_ERROR_PART = -3 +}; + +typedef struct +{ + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +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; + +typedef struct +{ + char *key; + char *value; +} JSON; + +typedef struct +{ + FuriString *key; + FuriString *value; +} FuriJSON; + +FuriString *char_to_furi_string(const char *str); + +// check memory +bool jsmn_memory_check(size_t heap_size); diff --git a/flip_world/.gitattributes b/flip_world/.gitattributes new file mode 100644 index 000000000..dfe077042 --- /dev/null +++ b/flip_world/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/flip_world/.gitsubtree b/flip_world/.gitsubtree new file mode 100644 index 000000000..dc338fa12 --- /dev/null +++ b/flip_world/.gitsubtree @@ -0,0 +1 @@ +https://github.com/jblanked/FlipWorld main / diff --git a/flip_world/LICENSE b/flip_world/LICENSE new file mode 100644 index 000000000..c5e43e291 --- /dev/null +++ b/flip_world/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 jblanked + +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. diff --git a/flip_world/README.md b/flip_world/README.md new file mode 100644 index 000000000..3af806a3e --- /dev/null +++ b/flip_world/README.md @@ -0,0 +1,84 @@ +# FlipWorld + +The first open-world multiplayer game for the Flipper Zero, best played with the VGM. Here's a video tutorial: https://www.youtube.com/watch?v=Qp7qmYMfdUA + +## Requirements + +- WiFi Developer Board, Raspberry Pi, or ESP32 device with the FlipperHTTP flash: [FlipperHTTP GitHub](https://github.com/jblanked/FlipperHTTP) +- 2.4 GHz WiFi access point + +## How It Works + +FlipWorld and FlipSocial are connected. Your login information is the same in both apps; if you register an account in either app, you can log in to both using that information. This also means that your friends, messages, and achievements are synced between apps. You only need a username and password to start, which are set in the User Settings. Keep in mind your username will be displayed to others, so choose wisely. + +**Settings** + +- **WiFi**: Enter your SSID and password to connect to your 2.4 GHz network. +- **User**: Add or update your username and password (this is the same login information as your FlipSocial account). +- **Game**: Install the Official World Pack, set your FPS (30, 60, 120, or 240), and select whether you want the screen backlight to always be on, the sound to be on, and the vibration to be on. + +**Controls** + +- **Press/Hold LEFT**: Turn left if not already facing left, then walk left if the button is still pressed. +- **Press/Hold RIGHT**: Turn right if not already facing right, then walk right if the button is still pressed. +- **Press/Hold UP**: Walk up. +- **Press/Hold DOWN**: Walk down. +- **Press/Hold OK**: Attack/Teleport (set to attack until all enemies are defeated). + +**Player Attributes** + +- **Health**: The amount of life points the player has. +- **XP**: The amount of experience points the player has. +- **Level**: The rank/level of the player. +- **Strength**: The attack power of the player's attacks. +- **Health Regeneration**: The amount of health a player gains per second. +- **Attack Timer**: The duration the player must wait between attacks. + +As a new player, you have 100 health, 0 XP, 10 strength, 1 health regeneration, an attack timer of 1, and are level 1. Each level, the player gains an extra 1 strength and 10 health. Additionally, the amount of XP needed to level up increases exponentially by 1.5. For example, to reach level 2, you need 100 XP; for level 3, 150 XP; for level 4, 225 XP; and so on. + +**Enemies** + +Enemies have similar attributes to players but do not have XP or health regeneration. For example, level 1 enemies have 100 health and 10 strength, just like a level 1 player. + +**Attacks** + +If an enemy attacks you, your health decreases by the enemy's strength (attack power). Additionally, if an enemy defeats you, your XP decreases by the enemy's strength. Conversely, when you successfully attack an enemy, you gain health equal to 10% of the enemy's strength and increase your XP by the enemy's full strength. + +An enemy attack registers if the enemy is facing you and collides with you. However, to attack an enemy successfully, the enemy must be facing away from you, and you must collide with them while pressing `OK`. + + +## Short Tutorial + +1. Ensure your WiFi Developer Board and Video Game Module are flashed with FlipperHTTP. +2. Install the app. +3. Restart your Flipper Zero, then open FlipWorld. +4. Click `Settings -> WiFi`, then input your WiFi SSID and password. +5. Hit the `BACK` button, click `User`. If your username is not present, click `Username` and add one. Do the same for the password field. +6. Go back to the main menu and hit `Play`. It will register an account if necessary and fetch data from our API that's used to render our graphics. + +## Roadmap + +**v0.2** +- Game Mechanics +- Video Game Module support + +**v0.3** +- Stability patch + +**v0.4** +- New game features + +**v0.5** +- ??? + +**v0.6** +- ??? + +**v0.7** +- ??? + +**v0.8** +- Multiplayer support + +**v1.0** +- Official release diff --git a/flip_world/alloc/alloc.c b/flip_world/alloc/alloc.c new file mode 100644 index 000000000..d034ac17a --- /dev/null +++ b/flip_world/alloc/alloc.c @@ -0,0 +1,96 @@ +#include +#include + +/** + * @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) +{ + UNUSED(context); + return VIEW_NONE; // Return VIEW_NONE to exit the app +} + +// Function to allocate resources for the FlipWorldApp +FlipWorldApp *flip_world_app_alloc() +{ + FlipWorldApp *app = (FlipWorldApp *)malloc(sizeof(FlipWorldApp)); + + Gui *gui = furi_record_open(RECORD_GUI); + + // 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_world_custom_event_callback); + // Main view + if (!easy_flipper_set_view(&app->view_loader, FlipWorldViewLoader, flip_world_loader_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app)) + { + return NULL; + } + flip_world_loader_init(app->view_loader); + if (!easy_flipper_set_widget(&app->widget_result, FlipWorldViewWidgetResult, "", callback_to_submenu, &app->view_dispatcher)) + { + return NULL; + } + + // Submenu + if (!easy_flipper_set_submenu(&app->submenu, FlipWorldViewSubmenu, VERSION_TAG, callback_exit_app, &app->view_dispatcher)) + { + return NULL; + } + submenu_add_item(app->submenu, "Play", FlipWorldSubmenuIndexRun, callback_submenu_choices, app); + submenu_add_item(app->submenu, "About", FlipWorldSubmenuIndexAbout, callback_submenu_choices, app); + submenu_add_item(app->submenu, "Settings", FlipWorldSubmenuIndexSettings, callback_submenu_choices, app); + // + + // Switch to the main view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); + + return app; +} + +// Function to free the resources used by FlipWorldApp +void flip_world_app_free(FlipWorldApp *app) +{ + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + + // Free Submenu(s) + if (app->submenu) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewSubmenu); + submenu_free(app->submenu); + } + // Free Widget(s) + if (app->widget_result) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewWidgetResult); + widget_free(app->widget_result); + } + + // Free View(s) + if (app->view_loader) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewLoader); + flip_world_loader_free_model(app->view_loader); + view_free(app->view_loader); + } + + free_all_views(app, true, true); + + // free the view dispatcher + view_dispatcher_free(app->view_dispatcher); + + // close the gui + furi_record_close(RECORD_GUI); + + // free the app + if (app) + free(app); +} diff --git a/flip_world/alloc/alloc.h b/flip_world/alloc/alloc.h new file mode 100644 index 000000000..6cd5b87f2 --- /dev/null +++ b/flip_world/alloc/alloc.h @@ -0,0 +1,5 @@ +#pragma once +#include + +FlipWorldApp *flip_world_app_alloc(); +void flip_world_app_free(FlipWorldApp *app); \ No newline at end of file diff --git a/flip_world/app.c b/flip_world/app.c new file mode 100644 index 000000000..96db706db --- /dev/null +++ b/flip_world/app.c @@ -0,0 +1,64 @@ +#include +#include + +// Entry point for the FlipWorld application +int32_t flip_world_main(void *p) +{ + // Suppress unused parameter warning + UNUSED(p); + + // Initialize the FlipWorld application + FlipWorldApp *app = flip_world_app_alloc(); + if (!app) + { + FURI_LOG_E(TAG, "Failed to allocate FlipWorldApp"); + return -1; + } + + // initialize the VGM + furi_hal_gpio_init_simple(&gpio_ext_pc1, GpioModeOutputPushPull); + furi_hal_gpio_write(&gpio_ext_pc1, false); // pull pin 15 low + + // check if board is connected (Derek Jamison) + FlipperHTTP *fhttp = flipper_http_alloc(); + if (!fhttp) + { + easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero."); + return -1; + } + + if (!flipper_http_ping(fhttp)) + { + FURI_LOG_E(TAG, "Failed to ping the device"); + flipper_http_free(fhttp); + return -1; + } + + // Try to wait for pong response. + uint32_t counter = 10; + while (fhttp->state == INACTIVE && --counter > 0) + { + FURI_LOG_D(TAG, "Waiting for PONG"); + furi_delay_ms(100); // this causes a BusFault + } + + flipper_http_free(fhttp); + if (counter == 0) + { + easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed."); + } + + // save app version + char app_version[16]; + snprintf(app_version, sizeof(app_version), "%f", (double)VERSION); + save_char("app_version", app_version); + + // Run the view dispatcher + view_dispatcher_run(app->view_dispatcher); + + // Free the resources used by the FlipWorld application + flip_world_app_free(app); + + // Return 0 to indicate success + return 0; +} diff --git a/flip_world/app.png b/flip_world/app.png new file mode 100644 index 000000000..1b7196829 Binary files /dev/null and b/flip_world/app.png differ diff --git a/flip_world/application.fam b/flip_world/application.fam new file mode 100644 index 000000000..920b2f883 --- /dev/null +++ b/flip_world/application.fam @@ -0,0 +1,21 @@ +App( + appid="flip_world", + name="FlipWorld", + apptype=FlipperAppType.EXTERNAL, + entry_point="flip_world_main", + stack_size=4 * 1024, + fap_icon="app.png", + fap_category="GPIO/FlipperHTTP", + fap_description="The first open-world multiplayer game, best played with the VGM.", + fap_icon_assets="assets", + fap_file_assets="file_assets", + fap_extbuild=( + ExtFile( + path="${FAP_SRC_DIR}/file_assets", + command="${PYTHON3} ${FAP_SRC_DIR}/engine/scripts/sprite_builder.py ${FAP_SRC_DIR.abspath}/sprites ${TARGET.abspath}/sprites", + ), + ), + fap_author="JBlanked", + fap_weburl="https://github.com/jblanked/FlipWorld", + fap_version="0.3", +) diff --git a/flip_world/assets/01-home.png b/flip_world/assets/01-home.png new file mode 100644 index 000000000..a13d71ecd Binary files /dev/null and b/flip_world/assets/01-home.png differ diff --git a/flip_world/assets/02-town.png b/flip_world/assets/02-town.png new file mode 100644 index 000000000..3a50d107b Binary files /dev/null and b/flip_world/assets/02-town.png differ diff --git a/flip_world/assets/03-town.png b/flip_world/assets/03-town.png new file mode 100644 index 000000000..3a2f37ae4 Binary files /dev/null and b/flip_world/assets/03-town.png differ diff --git a/flip_world/assets/04-town.png b/flip_world/assets/04-town.png new file mode 100644 index 000000000..4df399d03 Binary files /dev/null and b/flip_world/assets/04-town.png differ diff --git a/flip_world/assets/05-tree.png b/flip_world/assets/05-tree.png new file mode 100644 index 000000000..47fca819a Binary files /dev/null and b/flip_world/assets/05-tree.png differ diff --git a/flip_world/assets/06-tree.png b/flip_world/assets/06-tree.png new file mode 100644 index 000000000..34a92f52c Binary files /dev/null and b/flip_world/assets/06-tree.png differ diff --git a/flip_world/assets/CHANGELOG.md b/flip_world/assets/CHANGELOG.md new file mode 100644 index 000000000..527fca847 --- /dev/null +++ b/flip_world/assets/CHANGELOG.md @@ -0,0 +1,20 @@ +**0.3 (2025-01-14)** +- Added new worlds. +- Improved memory allocation. +- Updated API integration to load and save player attributes. +- Upgraded FlipperHTTP to the latest version. + +**0.2 (2025-01-02)** +- Added support for the Video Game Module (requires a FlipperHTTP flash). +- Introduced various enemy types to enhance gameplay. +- Added features for player health, XP, level, health regeneration, attack, and strength. +- Implemented vibration, sound, and LED notifications when a player is attacking or being attacked. +- Displayed the player's username above their character and showed the player's health, XP, and level in the bottom left corner of the screen at all times. +- Updated all game icons for improved visual appeal. +- Upgraded to the latest version of the FlipperHTTP library. +- Revised toggles in the Game Settings to ensure they work as intended. +- Improved collision mechanics for more accurate interactions. +- Updated the default icon representing the player's character. + +**0.1 (2024-12-21)** +- Initial release. \ No newline at end of file diff --git a/flip_world/assets/README.md b/flip_world/assets/README.md new file mode 100644 index 000000000..074b8228a --- /dev/null +++ b/flip_world/assets/README.md @@ -0,0 +1,81 @@ +The first open-world multiplayer game for the Flipper Zero, best played with the VGM. + +## Requirements + +- WiFi Developer Board, Raspberry Pi, or ESP32 device with the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP +- 2.4 GHz WiFi access point + +## How It Works + +FlipWorld and FlipSocial are connected. Your login information is the same in both apps; if you register an account in either app, you can log in to both using that information. This also means that your friends, messages, and achievements are synced between apps. You only need a username and password to start, which are set in the User Settings. Keep in mind your username will be displayed to others, so choose wisely. + +**Settings** + +- **WiFi**: Enter your SSID and password to connect to your 2.4 GHz network. +- **User**: Add or update your username and password (this is the same login information as your FlipSocial account). +- **Game**: Install the Official World Pack, set your FPS (30, 60, 120, or 240), and select whether you want the screen backlight to always be on, the sound to be on, and the vibration to be on. + +**Controls** + +- **Press/Hold LEFT**: Turn left if not already facing left, then walk left if the button is still pressed. +- **Press/Hold RIGHT**: Turn right if not already facing right, then walk right if the button is still pressed. +- **Press/Hold UP**: Walk up. +- **Press/Hold DOWN**: Walk down. +- **Press/Hold OK**: Attack/Teleport (set to attack until all enemies are defeated). + +**Player Attributes** + +- **Health**: The amount of life points the player has. +- **XP**: The amount of experience points the player has. +- **Level**: The rank/level of the player. +- **Strength**: The attack power of the player's attacks. +- **Health Regeneration**: The amount of health a player gains per second. +- **Attack Timer**: The duration the player must wait between attacks. + +As a new player, you have 100 health, 0 XP, 10 strength, 1 health regeneration, an attack timer of 1, and are level 1. Each level, the player gains an extra 1 strength and 10 health. Additionally, the amount of XP needed to level up increases exponentially by 1.5. For example, to reach level 2, you need 100 XP; for level 3, 150 XP; for level 4, 225 XP; and so on. + +**Enemies** + +Enemies have similar attributes to players but do not have XP or health regeneration. For example, level 1 enemies have 100 health and 10 strength, just like a level 1 player. + +**Attacks** + +If an enemy attacks you, your health decreases by the enemy's strength (attack power). Additionally, if an enemy defeats you, your XP decreases by the enemy's strength. Conversely, when you successfully attack an enemy, you gain health equal to 10% of the enemy's strength and increase your XP by the enemy's full strength. + +An enemy attack registers if the enemy is facing you and collides with you. However, to attack an enemy successfully, the enemy must be facing away from you, and you must collide with them while pressing `OK`. + +## Short Tutorial + +1. Ensure your WiFi Developer Board and Video Game Module are flashed with FlipperHTTP. +2. Install the app. +3. Restart your Flipper Zero, then open FlipWorld. +4. Click "Settings -> WiFi", then input your WiFi SSID and password. +5. Hit the "BACK" button, click "User". If your username is not present, click "Username" and add one. Do the same for the password field. +6. Go back to the main menu and hit "Play". It will register an account if necessary and fetch data from our API that's used to render our graphics. + +## Roadmap + +**v0.2** +- Game Mechanics +- Video Game Module support + +**v0.3** +- Stability patch + +**v0.4** +- New game features + +**v0.5** +- ??? + +**v0.6** +- ??? + +**v0.7** +- ??? + +**v0.8** +- Multiplayer support + +**v1.0** +- Official release diff --git a/flip_world/assets/WarningDolphin_45x42.png b/flip_world/assets/WarningDolphin_45x42.png new file mode 100644 index 000000000..d766ffbb4 Binary files /dev/null and b/flip_world/assets/WarningDolphin_45x42.png differ diff --git a/flip_world/assets/icon_alien_gun_10x10px.png b/flip_world/assets/icon_alien_gun_10x10px.png new file mode 100644 index 000000000..676a6f4aa Binary files /dev/null and b/flip_world/assets/icon_alien_gun_10x10px.png differ diff --git a/flip_world/assets/icon_axe_10x10px.png b/flip_world/assets/icon_axe_10x10px.png new file mode 100644 index 000000000..338e9d8d7 Binary files /dev/null and b/flip_world/assets/icon_axe_10x10px.png differ diff --git a/flip_world/assets/icon_axe_16x16px.png b/flip_world/assets/icon_axe_16x16px.png new file mode 100644 index 000000000..ebd807616 Binary files /dev/null and b/flip_world/assets/icon_axe_16x16px.png differ diff --git a/flip_world/assets/icon_axe_alt_10x10px.png b/flip_world/assets/icon_axe_alt_10x10px.png new file mode 100644 index 000000000..c7702e6e2 Binary files /dev/null and b/flip_world/assets/icon_axe_alt_10x10px.png differ diff --git a/flip_world/assets/icon_axe_alt_16x16px.png b/flip_world/assets/icon_axe_alt_16x16px.png new file mode 100644 index 000000000..7e2d29fa8 Binary files /dev/null and b/flip_world/assets/icon_axe_alt_16x16px.png differ diff --git a/flip_world/assets/icon_bow_10x10px.png b/flip_world/assets/icon_bow_10x10px.png new file mode 100644 index 000000000..c7889df88 Binary files /dev/null and b/flip_world/assets/icon_bow_10x10px.png differ diff --git a/flip_world/assets/icon_chest_closed_16x13px.png b/flip_world/assets/icon_chest_closed_16x13px.png new file mode 100644 index 000000000..6f04fb41b Binary files /dev/null and b/flip_world/assets/icon_chest_closed_16x13px.png differ diff --git a/flip_world/assets/icon_chest_open_16x16px.png b/flip_world/assets/icon_chest_open_16x16px.png new file mode 100644 index 000000000..2eb27e27e Binary files /dev/null and b/flip_world/assets/icon_chest_open_16x16px.png differ diff --git a/flip_world/assets/icon_earth_15x16.png b/flip_world/assets/icon_earth_15x16.png new file mode 100644 index 000000000..a586c215c Binary files /dev/null and b/flip_world/assets/icon_earth_15x16.png differ diff --git a/flip_world/assets/icon_fence_16x8px.png b/flip_world/assets/icon_fence_16x8px.png new file mode 100644 index 000000000..b730291db Binary files /dev/null and b/flip_world/assets/icon_fence_16x8px.png differ diff --git a/flip_world/assets/icon_fence_end_16x8px.png b/flip_world/assets/icon_fence_end_16x8px.png new file mode 100644 index 000000000..4dc7945d5 Binary files /dev/null and b/flip_world/assets/icon_fence_end_16x8px.png differ diff --git a/flip_world/assets/icon_fence_vertical_end_6x8px.png b/flip_world/assets/icon_fence_vertical_end_6x8px.png new file mode 100644 index 000000000..4b27759e7 Binary files /dev/null and b/flip_world/assets/icon_fence_vertical_end_6x8px.png differ diff --git a/flip_world/assets/icon_fence_vertical_start_6x15px.png b/flip_world/assets/icon_fence_vertical_start_6x15px.png new file mode 100644 index 000000000..0fa1d2036 Binary files /dev/null and b/flip_world/assets/icon_fence_vertical_start_6x15px.png differ diff --git a/flip_world/assets/icon_flower_16x16.png b/flip_world/assets/icon_flower_16x16.png new file mode 100644 index 000000000..d369a9cbf Binary files /dev/null and b/flip_world/assets/icon_flower_16x16.png differ diff --git a/flip_world/assets/icon_home_15x16.png b/flip_world/assets/icon_home_15x16.png new file mode 100644 index 000000000..2f485731b Binary files /dev/null and b/flip_world/assets/icon_home_15x16.png differ diff --git a/flip_world/assets/icon_house_3d_34x45px.png b/flip_world/assets/icon_house_3d_34x45px.png new file mode 100644 index 000000000..bb81e9f03 Binary files /dev/null and b/flip_world/assets/icon_house_3d_34x45px.png differ diff --git a/flip_world/assets/icon_house_48x32px.png b/flip_world/assets/icon_house_48x32px.png new file mode 100644 index 000000000..858648337 Binary files /dev/null and b/flip_world/assets/icon_house_48x32px.png differ diff --git a/flip_world/assets/icon_info_15x16.png b/flip_world/assets/icon_info_15x16.png new file mode 100644 index 000000000..176683969 Binary files /dev/null and b/flip_world/assets/icon_info_15x16.png differ diff --git a/flip_world/assets/icon_lake_bottom_31x12px.png b/flip_world/assets/icon_lake_bottom_31x12px.png new file mode 100644 index 000000000..b30dbd1c5 Binary files /dev/null and b/flip_world/assets/icon_lake_bottom_31x12px.png differ diff --git a/flip_world/assets/icon_lake_bottom_left_24x22px.png b/flip_world/assets/icon_lake_bottom_left_24x22px.png new file mode 100644 index 000000000..455ef89c1 Binary files /dev/null and b/flip_world/assets/icon_lake_bottom_left_24x22px.png differ diff --git a/flip_world/assets/icon_lake_bottom_right_24x22px.png b/flip_world/assets/icon_lake_bottom_right_24x22px.png new file mode 100644 index 000000000..f34acb8b9 Binary files /dev/null and b/flip_world/assets/icon_lake_bottom_right_24x22px.png differ diff --git a/flip_world/assets/icon_lake_left_11x31px.png b/flip_world/assets/icon_lake_left_11x31px.png new file mode 100644 index 000000000..89a789f65 Binary files /dev/null and b/flip_world/assets/icon_lake_left_11x31px.png differ diff --git a/flip_world/assets/icon_lake_right_11x31.png b/flip_world/assets/icon_lake_right_11x31.png new file mode 100644 index 000000000..138916409 Binary files /dev/null and b/flip_world/assets/icon_lake_right_11x31.png differ diff --git a/flip_world/assets/icon_lake_top_31x12px.png b/flip_world/assets/icon_lake_top_31x12px.png new file mode 100644 index 000000000..ae717e1f4 Binary files /dev/null and b/flip_world/assets/icon_lake_top_31x12px.png differ diff --git a/flip_world/assets/icon_lake_top_left_24x22px.png b/flip_world/assets/icon_lake_top_left_24x22px.png new file mode 100644 index 000000000..f5d1eee2d Binary files /dev/null and b/flip_world/assets/icon_lake_top_left_24x22px.png differ diff --git a/flip_world/assets/icon_lake_top_right_24x22px.png b/flip_world/assets/icon_lake_top_right_24x22px.png new file mode 100644 index 000000000..2a5472156 Binary files /dev/null and b/flip_world/assets/icon_lake_top_right_24x22px.png differ diff --git a/flip_world/assets/icon_man_7x16.png b/flip_world/assets/icon_man_7x16.png new file mode 100644 index 000000000..d475bbe5a Binary files /dev/null and b/flip_world/assets/icon_man_7x16.png differ diff --git a/flip_world/assets/icon_menu_128x64px.png b/flip_world/assets/icon_menu_128x64px.png new file mode 100644 index 000000000..8482581a1 Binary files /dev/null and b/flip_world/assets/icon_menu_128x64px.png differ diff --git a/flip_world/assets/icon_plant_16x16.png b/flip_world/assets/icon_plant_16x16.png new file mode 100644 index 000000000..af9c4aba9 Binary files /dev/null and b/flip_world/assets/icon_plant_16x16.png differ diff --git a/flip_world/assets/icon_plant_fern_18x16px.png b/flip_world/assets/icon_plant_fern_18x16px.png new file mode 100644 index 000000000..e7558649a Binary files /dev/null and b/flip_world/assets/icon_plant_fern_18x16px.png differ diff --git a/flip_world/assets/icon_plant_pointy_13x16px.png b/flip_world/assets/icon_plant_pointy_13x16px.png new file mode 100644 index 000000000..289401915 Binary files /dev/null and b/flip_world/assets/icon_plant_pointy_13x16px.png differ diff --git a/flip_world/assets/icon_rock_large_18x19px.png b/flip_world/assets/icon_rock_large_18x19px.png new file mode 100644 index 000000000..89a381c18 Binary files /dev/null and b/flip_world/assets/icon_rock_large_18x19px.png differ diff --git a/flip_world/assets/icon_rock_medium_16x14px.png b/flip_world/assets/icon_rock_medium_16x14px.png new file mode 100644 index 000000000..fe19d5400 Binary files /dev/null and b/flip_world/assets/icon_rock_medium_16x14px.png differ diff --git a/flip_world/assets/icon_rock_small_10x8px.png b/flip_world/assets/icon_rock_small_10x8px.png new file mode 100644 index 000000000..2d03db8a2 Binary files /dev/null and b/flip_world/assets/icon_rock_small_10x8px.png differ diff --git a/flip_world/assets/icon_sword_10x10px.png b/flip_world/assets/icon_sword_10x10px.png new file mode 100644 index 000000000..d52d7a0ab Binary files /dev/null and b/flip_world/assets/icon_sword_10x10px.png differ diff --git a/flip_world/assets/icon_tree_16x16.png b/flip_world/assets/icon_tree_16x16.png new file mode 100644 index 000000000..0829ddfdc Binary files /dev/null and b/flip_world/assets/icon_tree_16x16.png differ diff --git a/flip_world/assets/icon_tree_29x30px.png b/flip_world/assets/icon_tree_29x30px.png new file mode 100644 index 000000000..c15cd61a7 Binary files /dev/null and b/flip_world/assets/icon_tree_29x30px.png differ diff --git a/flip_world/assets/icon_tree_48x48px.png b/flip_world/assets/icon_tree_48x48px.png new file mode 100644 index 000000000..0d3ef1036 Binary files /dev/null and b/flip_world/assets/icon_tree_48x48px.png differ diff --git a/flip_world/assets/icon_woman_9x16.png b/flip_world/assets/icon_woman_9x16.png new file mode 100644 index 000000000..f0ddab645 Binary files /dev/null and b/flip_world/assets/icon_woman_9x16.png differ diff --git a/flip_world/assets/icon_world_change_128x64px.png b/flip_world/assets/icon_world_change_128x64px.png new file mode 100644 index 000000000..3c65fc8ce Binary files /dev/null and b/flip_world/assets/icon_world_change_128x64px.png differ diff --git a/flip_world/callback/callback.c b/flip_world/callback/callback.c new file mode 100644 index 000000000..a440558f0 --- /dev/null +++ b/flip_world/callback/callback.c @@ -0,0 +1,1977 @@ +#include +#include "engine/engine.h" +#include "engine/game_engine.h" +#include "engine/game_manager_i.h" +#include "engine/level_i.h" +#include "engine/entity_i.h" +#include "game/storage.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 + +static void frame_cb(GameEngine *engine, Canvas *canvas, InputState input, void *context) +{ + UNUSED(engine); + GameManager *game_manager = context; + game_manager_input_set(game_manager, input); + game_manager_update(game_manager); + game_manager_render(game_manager, canvas); +} + +static int32_t game_app(void *p) +{ + UNUSED(p); + GameManager *game_manager = game_manager_alloc(); + if (!game_manager) + { + FURI_LOG_E("Game", "Failed to allocate game manager"); + return -1; + } + + // Setup game engine settings... + GameEngineSettings settings = game_engine_settings_init(); + settings.target_fps = game_fps_choices_2[game_fps_index]; + settings.show_fps = game.show_fps; + settings.always_backlight = strstr(yes_or_no_choices[game_screen_always_on_index], "Yes") != NULL; + settings.frame_callback = frame_cb; + settings.context = game_manager; + GameEngine *engine = game_engine_alloc(settings); + if (!engine) + { + FURI_LOG_E("Game", "Failed to allocate game engine"); + game_manager_free(game_manager); + return -1; + } + game_manager_engine_set(game_manager, engine); + + // Allocate custom game context if needed + void *game_context = NULL; + if (game.context_size > 0) + { + game_context = malloc(game.context_size); + game_manager_game_context_set(game_manager, game_context); + } + + // Start the game + game.start(game_manager, game_context); + + // 1) Run the engine + game_engine_run(engine); + + // 2) Stop the game FIRST, so it can do any internal cleanup + game.stop(game_context); + + // 3) Now free the engine + game_engine_free(engine); + + // 4) Now free the manager + game_manager_free(game_manager); + + // 5) Finally, free your custom context if it was allocated + if (game_context) + { + free(game_context); + } + + // 6) Check for leftover entities + int32_t entities = entities_get_count(); + if (entities != 0) + { + FURI_LOG_E("Game", "Memory leak detected: %ld entities still allocated", entities); + return -1; + } + + return 0; +} + +static void flip_world_request_error_draw(Canvas *canvas, DataLoaderModel *model) +{ + if (canvas == NULL) + { + FURI_LOG_E(TAG, "flip_world_request_error_draw - canvas is NULL"); + DEV_CRASH(); + return; + } + if (model->fhttp->last_response != NULL) + { + if (strstr(model->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(model->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(model->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(model->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", model->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 bool alloc_about_view(void *context); +static bool alloc_text_input_view(void *context, char *title); +static bool alloc_variable_item_list(void *context, uint32_t view_id); +// +static void wifi_settings_item_selected(void *context, uint32_t index); +static void text_updated_wifi_ssid(void *context); +static void text_updated_wifi_pass(void *context); +static void text_updated_username(void *context); +static void text_updated_password(void *context); +// +static void flip_world_game_fps_change(VariableItem *item); +static void game_settings_item_selected(void *context, uint32_t index); +static void user_settings_item_selected(void *context, uint32_t index); +static void flip_world_game_screen_always_on_change(VariableItem *item); +static void flip_world_game_sound_on_change(VariableItem *item); +static void flip_world_game_vibration_on_change(VariableItem *item); + +uint32_t callback_to_submenu(void *context) +{ + UNUSED(context); + return FlipWorldViewSubmenu; +} +static uint32_t callback_to_wifi_settings(void *context) +{ + UNUSED(context); + return FlipWorldViewVariableItemList; +} +static uint32_t callback_to_settings(void *context) +{ + UNUSED(context); + return FlipWorldViewSettings; +} + +static void flip_world_view_about_draw_callback(Canvas *canvas, void *model) +{ + UNUSED(model); + canvas_clear(canvas); + // canvas_set_font_custom(canvas, FONT_SIZE_XLARGE); + canvas_draw_str(canvas, 0, 10, VERSION_TAG); + // canvas_set_font_custom(canvas, FONT_SIZE_MEDIUM); + canvas_set_font_custom(canvas, FONT_SIZE_SMALL); + canvas_draw_str(canvas, 0, 20, "Dev: JBlanked, codeallnight"); + canvas_draw_str(canvas, 0, 30, "GFX: the1anonlypr3"); + canvas_draw_str(canvas, 0, 40, "github.com/jblanked/FlipWorld"); + + canvas_draw_str_multi(canvas, 0, 55, "The first open world multiplayer\ngame on the Flipper Zero."); +} + +// alloc +static bool alloc_about_view(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return false; + } + if (!app->view_about) + { + if (!easy_flipper_set_view(&app->view_about, FlipWorldViewAbout, flip_world_view_about_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app)) + { + return false; + } + if (!app->view_about) + { + return false; + } + } + return true; +} + +static bool alloc_text_input_view(void *context, char *title) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return false; + } + if (!title) + { + FURI_LOG_E(TAG, "Title is NULL"); + return false; + } + app->text_input_buffer_size = 64; + if (!app->text_input_buffer) + { + if (!easy_flipper_set_buffer(&app->text_input_buffer, app->text_input_buffer_size)) + { + return false; + } + } + if (!app->text_input_temp_buffer) + { + if (!easy_flipper_set_buffer(&app->text_input_temp_buffer, app->text_input_buffer_size)) + { + return false; + } + } + if (!app->text_input) + { + if (!easy_flipper_set_uart_text_input( + &app->text_input, + FlipWorldViewTextInput, + title, + app->text_input_temp_buffer, + app->text_input_buffer_size, + strcmp(title, "SSID") == 0 ? text_updated_wifi_ssid : strcmp(title, "Password") == 0 ? text_updated_wifi_pass + : strcmp(title, "Username-Login") == 0 ? text_updated_username + : text_updated_password, + callback_to_wifi_settings, + &app->view_dispatcher, + app)) + { + return false; + } + if (!app->text_input) + { + return false; + } + char ssid[64]; + char pass[64]; + char username[64]; + char password[64]; + if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass), username, sizeof(username), password, sizeof(password))) + { + if (strcmp(title, "SSID") == 0) + { + strncpy(app->text_input_temp_buffer, ssid, app->text_input_buffer_size); + } + else if (strcmp(title, "Password") == 0) + { + strncpy(app->text_input_temp_buffer, pass, app->text_input_buffer_size); + } + else if (strcmp(title, "Username-Login") == 0) + { + strncpy(app->text_input_temp_buffer, username, app->text_input_buffer_size); + } + else if (strcmp(title, "Password-Login") == 0) + { + strncpy(app->text_input_temp_buffer, password, app->text_input_buffer_size); + } + } + } + return true; +} +static bool alloc_variable_item_list(void *context, uint32_t view_id) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return false; + } + char ssid[64]; + char pass[64]; + char username[64]; + char password[64]; + if (!app->variable_item_list) + { + switch (view_id) + { + case FlipWorldSubmenuIndexWiFiSettings: + if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, wifi_settings_item_selected, callback_to_settings, &app->view_dispatcher, app)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return false; + } + + if (!app->variable_item_list) + { + FURI_LOG_E(TAG, "Variable item list is NULL"); + return false; + } + + if (!app->variable_item_wifi_ssid) + { + app->variable_item_wifi_ssid = variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL); + variable_item_set_current_value_text(app->variable_item_wifi_ssid, ""); + } + if (!app->variable_item_wifi_pass) + { + app->variable_item_wifi_pass = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL); + variable_item_set_current_value_text(app->variable_item_wifi_pass, ""); + } + if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass), username, sizeof(username), password, sizeof(password))) + { + variable_item_set_current_value_text(app->variable_item_wifi_ssid, ssid); + // variable_item_set_current_value_text(app->variable_item_wifi_pass, pass); + save_char("WiFi-SSID", ssid); + save_char("WiFi-Password", pass); + save_char("Flip-Social-Username", username); + save_char("Flip-Social-Password", password); + } + break; + case FlipWorldSubmenuIndexGameSettings: + if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, game_settings_item_selected, callback_to_settings, &app->view_dispatcher, app)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return false; + } + + if (!app->variable_item_list) + { + FURI_LOG_E(TAG, "Variable item list is NULL"); + return false; + } + + if (!app->variable_item_game_download_world) + { + app->variable_item_game_download_world = variable_item_list_add(app->variable_item_list, "Install Official World Pack", 0, NULL, NULL); + variable_item_set_current_value_text(app->variable_item_game_download_world, ""); + } + if (!app->variable_item_game_fps) + { + app->variable_item_game_fps = variable_item_list_add(app->variable_item_list, "FPS", 4, flip_world_game_fps_change, NULL); + variable_item_set_current_value_index(app->variable_item_game_fps, 0); + variable_item_set_current_value_text(app->variable_item_game_fps, game_fps_choices[0]); + } + if (!app->variable_item_game_screen_always_on) + { + app->variable_item_game_screen_always_on = variable_item_list_add(app->variable_item_list, "Keep Screen On?", 2, flip_world_game_screen_always_on_change, NULL); + variable_item_set_current_value_index(app->variable_item_game_screen_always_on, 1); + variable_item_set_current_value_text(app->variable_item_game_screen_always_on, yes_or_no_choices[1]); + } + if (!app->variable_item_game_sound_on) + { + app->variable_item_game_sound_on = variable_item_list_add(app->variable_item_list, "Sound On?", 2, flip_world_game_sound_on_change, NULL); + variable_item_set_current_value_index(app->variable_item_game_sound_on, 0); + variable_item_set_current_value_text(app->variable_item_game_sound_on, yes_or_no_choices[0]); + } + if (!app->variable_item_game_vibration_on) + { + app->variable_item_game_vibration_on = variable_item_list_add(app->variable_item_list, "Vibration On?", 2, flip_world_game_vibration_on_change, NULL); + variable_item_set_current_value_index(app->variable_item_game_vibration_on, 0); + variable_item_set_current_value_text(app->variable_item_game_vibration_on, yes_or_no_choices[0]); + } + char _game_fps[8]; + if (load_char("Game-FPS", _game_fps, sizeof(_game_fps))) + { + int index = strcmp(_game_fps, "30") == 0 ? 0 : strcmp(_game_fps, "60") == 0 ? 1 + : strcmp(_game_fps, "120") == 0 ? 2 + : strcmp(_game_fps, "240") == 0 ? 3 + : 0; + variable_item_set_current_value_text(app->variable_item_game_fps, game_fps_choices[index]); + variable_item_set_current_value_index(app->variable_item_game_fps, index); + } + char _game_screen_always_on[8]; + if (load_char("Game-Screen-Always-On", _game_screen_always_on, sizeof(_game_screen_always_on))) + { + int index = strcmp(_game_screen_always_on, "No") == 0 ? 0 : strcmp(_game_screen_always_on, "Yes") == 0 ? 1 + : 0; + variable_item_set_current_value_text(app->variable_item_game_screen_always_on, yes_or_no_choices[index]); + variable_item_set_current_value_index(app->variable_item_game_screen_always_on, index); + } + char _game_sound_on[8]; + if (load_char("Game-Sound-On", _game_sound_on, sizeof(_game_sound_on))) + { + int index = strcmp(_game_sound_on, "No") == 0 ? 0 : strcmp(_game_sound_on, "Yes") == 0 ? 1 + : 0; + variable_item_set_current_value_text(app->variable_item_game_sound_on, yes_or_no_choices[index]); + variable_item_set_current_value_index(app->variable_item_game_sound_on, index); + } + char _game_vibration_on[8]; + if (load_char("Game-Vibration-On", _game_vibration_on, sizeof(_game_vibration_on))) + { + int index = strcmp(_game_vibration_on, "No") == 0 ? 0 : strcmp(_game_vibration_on, "Yes") == 0 ? 1 + : 0; + variable_item_set_current_value_text(app->variable_item_game_vibration_on, yes_or_no_choices[index]); + variable_item_set_current_value_index(app->variable_item_game_vibration_on, index); + } + break; + case FlipWorldSubmenuIndexUserSettings: + if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, user_settings_item_selected, callback_to_settings, &app->view_dispatcher, app)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return false; + } + + if (!app->variable_item_list) + { + FURI_LOG_E(TAG, "Variable item list is NULL"); + return false; + } + + // if logged in, show profile info, otherwise show login/register + if (is_logged_in() || is_logged_in_to_flip_social()) + { + if (!app->variable_item_user_username) + { + app->variable_item_user_username = variable_item_list_add(app->variable_item_list, "Username", 0, NULL, NULL); + variable_item_set_current_value_text(app->variable_item_user_username, ""); + } + if (!app->variable_item_user_password) + { + app->variable_item_user_password = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL); + variable_item_set_current_value_text(app->variable_item_user_password, ""); + } + if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass), username, sizeof(username), password, sizeof(password))) + { + variable_item_set_current_value_text(app->variable_item_user_username, username); + variable_item_set_current_value_text(app->variable_item_user_password, "*****"); + } + } + else + { + if (!app->variable_item_user_username) + { + app->variable_item_user_username = variable_item_list_add(app->variable_item_list, "Username", 0, NULL, NULL); + variable_item_set_current_value_text(app->variable_item_user_username, ""); + } + if (!app->variable_item_user_password) + { + app->variable_item_user_password = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL); + variable_item_set_current_value_text(app->variable_item_user_password, ""); + } + } + break; + } + } + return true; +} +static bool alloc_submenu_settings(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return false; + } + if (!app->submenu_settings) + { + if (!easy_flipper_set_submenu(&app->submenu_settings, FlipWorldViewSettings, "Settings", callback_to_submenu, &app->view_dispatcher)) + { + return NULL; + } + if (!app->submenu_settings) + { + return false; + } + submenu_add_item(app->submenu_settings, "WiFi", FlipWorldSubmenuIndexWiFiSettings, callback_submenu_choices, app); + submenu_add_item(app->submenu_settings, "Game", FlipWorldSubmenuIndexGameSettings, callback_submenu_choices, app); + submenu_add_item(app->submenu_settings, "User", FlipWorldSubmenuIndexUserSettings, callback_submenu_choices, app); + } + return true; +} +// free +static void free_about_view(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + if (app->view_about) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewAbout); + view_free(app->view_about); + app->view_about = NULL; + } +} + +static void free_text_input_view(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + if (app->text_input) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewTextInput); + text_input_free(app->text_input); + app->text_input = NULL; + } + if (app->text_input_buffer) + { + free(app->text_input_buffer); + app->text_input_buffer = NULL; + } + if (app->text_input_temp_buffer) + { + free(app->text_input_temp_buffer); + app->text_input_temp_buffer = NULL; + } +} +static void free_variable_item_list(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + if (app->variable_item_list) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewVariableItemList); + variable_item_list_free(app->variable_item_list); + app->variable_item_list = NULL; + } + if (app->variable_item_wifi_ssid) + { + free(app->variable_item_wifi_ssid); + app->variable_item_wifi_ssid = NULL; + } + if (app->variable_item_wifi_pass) + { + free(app->variable_item_wifi_pass); + app->variable_item_wifi_pass = NULL; + } + if (app->variable_item_game_fps) + { + free(app->variable_item_game_fps); + app->variable_item_game_fps = NULL; + } + if (app->variable_item_game_screen_always_on) + { + free(app->variable_item_game_screen_always_on); + app->variable_item_game_screen_always_on = NULL; + } + if (app->variable_item_game_download_world) + { + free(app->variable_item_game_download_world); + app->variable_item_game_download_world = NULL; + } + if (app->variable_item_game_sound_on) + { + free(app->variable_item_game_sound_on); + app->variable_item_game_sound_on = NULL; + } + if (app->variable_item_game_vibration_on) + { + free(app->variable_item_game_vibration_on); + app->variable_item_game_vibration_on = NULL; + } + if (app->variable_item_user_username) + { + free(app->variable_item_user_username); + app->variable_item_user_username = NULL; + } + if (app->variable_item_user_password) + { + free(app->variable_item_user_password); + app->variable_item_user_password = NULL; + } +} +static void free_submenu_settings(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + if (app->submenu_settings) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewSettings); + submenu_free(app->submenu_settings); + app->submenu_settings = NULL; + } +} +static FuriThreadId thread_id; +static bool game_thread_running = false; +void free_all_views(void *context, bool should_free_variable_item_list, bool should_free_submenu_settings) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + if (should_free_variable_item_list) + { + free_variable_item_list(app); + } + free_about_view(app); + free_text_input_view(app); + + // free game thread + if (game_thread_running) + { + game_thread_running = false; + furi_thread_flags_set(thread_id, WorkerEvtStop); + furi_thread_free(thread_id); + } + + if (should_free_submenu_settings) + free_submenu_settings(app); +} +static bool fetch_world_list(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(TAG, "fhttp is NULL"); + easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return."); + return false; + } + // Create the directory for saving worlds + char directory_path[128]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + + // free storage + furi_record_close(RECORD_STORAGE); + + snprintf( + fhttp->file_path, + sizeof(fhttp->file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json"); + + fhttp->save_received_data = true; + return flipper_http_get_request_with_headers(fhttp, "https://www.flipsocial.net/api/world/v3/list/10/", "{\"Content-Type\":\"application/json\"}"); +} +// we will load the palyer stats from the API and save them +// in player_spawn game method, it will load the player stats that we saved +static bool fetch_player_stats(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(TAG, "fhttp is NULL"); + easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return."); + return false; + } + char username[64]; + if (!load_char("Flip-Social-Username", username, sizeof(username))) + { + FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); + easy_flipper_dialog("Error", "Failed to load saved username. Go to settings to update."); + return false; + } + char url[128]; + snprintf(url, sizeof(url), "https://www.flipsocial.net/api/user/game-stats/%s/", username); + snprintf( + fhttp->file_path, + sizeof(fhttp->file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player/player_stats.json"); + + fhttp->save_received_data = true; + return flipper_http_get_request_with_headers(fhttp, url, "{\"Content-Type\":\"application/json\"}"); +} + +static bool start_game_thread(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "app is NULL"); + easy_flipper_dialog("Error", "app is NULL. Press BACK to return."); + return false; + } + // free game thread + if (game_thread_running) + { + game_thread_running = false; + furi_thread_flags_set(thread_id, WorkerEvtStop); + furi_thread_free(thread_id); + } + // start game thread + FuriThread *thread = furi_thread_alloc_ex("game", 2048, game_app, app); + if (!thread) + { + FURI_LOG_E(TAG, "Failed to allocate game thread"); + easy_flipper_dialog("Error", "Failed to allocate game thread. Restart your Flipper."); + return false; + } + furi_thread_start(thread); + thread_id = furi_thread_get_id(thread); + game_thread_running = true; + return true; +} +// combine register, login, and world list fetch into one function to switch to the loader view +static bool flip_world_fetch_game(DataLoaderModel *model) +{ + FlipWorldApp *app = (FlipWorldApp *)model->parser_context; + if (!app) + { + FURI_LOG_E(TAG, "app is NULL"); + easy_flipper_dialog("Error", "app is NULL. Press BACK to return."); + return false; + } + if (model->request_index == 0) + { + // login + char username[64]; + char password[64]; + if (!load_char("Flip-Social-Username", username, sizeof(username))) + { + FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + easy_flipper_dialog("Error", "Failed to load saved username\nGo to user settings to update."); + return false; + } + if (!load_char("Flip-Social-Password", password, sizeof(password))) + { + FURI_LOG_E(TAG, "Failed to load Flip-Social-Password"); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + easy_flipper_dialog("Error", "Failed to load saved password\nGo to settings to update."); + return false; + } + char payload[256]; + snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"password\":\"%s\"}", username, password); + return flipper_http_post_request_with_headers(model->fhttp, "https://www.flipsocial.net/api/user/login/", "{\"Content-Type\":\"application/json\"}", payload); + } + else if (model->request_index == 1) + { + // check if login was successful + char is_logged_in[8]; + if (!load_char("is_logged_in", is_logged_in, sizeof(is_logged_in))) + { + FURI_LOG_E(TAG, "Failed to load is_logged_in"); + easy_flipper_dialog("Error", "Failed to load is_logged_in\nGo to user settings to update."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + return false; + } + if (strcmp(is_logged_in, "false") == 0 && strcmp(model->title, "Registering...") == 0) + { + // register + char username[64]; + char password[64]; + if (!load_char("Flip-Social-Username", username, sizeof(username))) + { + FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); + easy_flipper_dialog("Error", "Failed to load saved username. Go to settings to update."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + return false; + } + if (!load_char("Flip-Social-Password", password, sizeof(password))) + { + FURI_LOG_E(TAG, "Failed to load Flip-Social-Password"); + easy_flipper_dialog("Error", "Failed to load saved password. Go to settings to update."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + return false; + } + char payload[172]; + snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"password\":\"%s\"}", username, password); + model->title = "Registering..."; + return flipper_http_post_request_with_headers(model->fhttp, "https://www.flipsocial.net/api/user/register/", "{\"Content-Type\":\"application/json\"}", payload); + } + else + { + model->title = "Fetching World List.."; + return fetch_world_list(model->fhttp); + } + } + else if (model->request_index == 2) + { + model->title = "Fetching World List.."; + return fetch_world_list(model->fhttp); + } + else if (model->request_index == 3) + { + snprintf( + model->fhttp->file_path, + sizeof(model->fhttp->file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json"); + + FuriString *world_list = flipper_http_load_from_file(model->fhttp->file_path); + if (!world_list) + { + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + FURI_LOG_E(TAG, "Failed to load world list"); + easy_flipper_dialog("Error", "Failed to load world list. Go to game settings to download packs."); + return false; + } + FuriString *first_world = get_json_array_value_furi("worlds", 0, world_list); + if (!first_world) + { + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + FURI_LOG_E(TAG, "Failed to get first world"); + easy_flipper_dialog("Error", "Failed to get first world. Go to game settings to download packs."); + furi_string_free(world_list); + return false; + } + if (world_exists(furi_string_get_cstr(first_world))) + { + furi_string_free(world_list); + furi_string_free(first_world); + + if (!start_game_thread(app)) + { + FURI_LOG_E(TAG, "Failed to start game thread"); + easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + return "Failed to start game thread"; + } + return true; + } + snprintf( + model->fhttp->file_path, + sizeof(model->fhttp->file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", furi_string_get_cstr(first_world)); + + model->fhttp->save_received_data = true; + char url[128]; + snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v3/get/world/%s/", furi_string_get_cstr(first_world)); + furi_string_free(world_list); + furi_string_free(first_world); + return flipper_http_get_request_with_headers(model->fhttp, url, "{\"Content-Type\":\"application/json\"}"); + } + FURI_LOG_E(TAG, "Unknown request index"); + return false; +} +static char *flip_world_parse_game(DataLoaderModel *model) +{ + FlipWorldApp *app = (FlipWorldApp *)model->parser_context; + + if (model->request_index == 0) + { + if (!model->fhttp->last_response) + { + save_char("is_logged_in", "false"); + // Go back to the main menu + easy_flipper_dialog("Error", "Response is empty. Press BACK to return."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); + return "Response is empty..."; + } + + // Check for successful conditions + if (strstr(model->fhttp->last_response, "[SUCCESS]") != NULL || strstr(model->fhttp->last_response, "User found") != NULL) + { + save_char("is_logged_in", "true"); + model->title = "Login successful!"; + model->title = "Fetching World List.."; + return "Login successful!"; + } + + // Check if user not found + if (strstr(model->fhttp->last_response, "User not found") != NULL) + { + save_char("is_logged_in", "false"); + model->title = "Registering..."; + return "Account not found...\nRegistering now.."; // if they see this an issue happened switching to register + } + + // If not success, not found, check length conditions + size_t resp_len = strlen(model->fhttp->last_response); + if (resp_len == 0 || resp_len > 127) + { + // Empty or too long means failed login + save_char("is_logged_in", "false"); + // Go back to the main menu + easy_flipper_dialog("Error", "Failed to login. Press BACK to return."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); + return "Failed to login..."; + } + + // Handle any other unknown response as a failure + save_char("is_logged_in", "false"); + // Go back to the main menu + easy_flipper_dialog("Error", "Failed to login. Press BACK to return."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); + return "Failed to login..."; + } + else if (model->request_index == 1) + { + if (strcmp(model->title, "Registering...") == 0) + { + // check registration response + if (model->fhttp->last_response != NULL && (strstr(model->fhttp->last_response, "[SUCCESS]") != NULL || strstr(model->fhttp->last_response, "User created") != NULL)) + { + save_char("is_logged_in", "true"); + char username[64]; + char password[64]; + // load the username and password, then save them + if (!load_char("Flip-Social-Username", username, sizeof(username))) + { + FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); + easy_flipper_dialog("Error", "Failed to load Flip-Social-Username"); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); + return "Failed to load Flip-Social-Username"; + } + if (!load_char("Flip-Social-Password", password, sizeof(password))) + { + FURI_LOG_E(TAG, "Failed to load Flip-Social-Password"); + easy_flipper_dialog("Error", "Failed to load Flip-Social-Password"); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); + return "Failed to load Flip-Social-Password"; + } + // load wifi ssid,pass then save + char ssid[64]; + char pass[64]; + if (!load_char("WiFi-SSID", ssid, sizeof(ssid))) + { + FURI_LOG_E(TAG, "Failed to load WiFi-SSID"); + easy_flipper_dialog("Error", "Failed to load WiFi-SSID"); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); + return "Failed to load WiFi-SSID"; + } + if (!load_char("WiFi-Password", pass, sizeof(pass))) + { + FURI_LOG_E(TAG, "Failed to load WiFi-Password"); + easy_flipper_dialog("Error", "Failed to load WiFi-Password"); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); + return "Failed to load WiFi-Password"; + } + save_settings(ssid, pass, username, password); + model->title = "Fetching World List.."; + return "Account created!"; + } + else if (strstr(model->fhttp->last_response, "Username or password not provided") != NULL) + { + easy_flipper_dialog("Error", "Please enter your credentials.\nPress BACK to return."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + return "Please enter your credentials."; + } + else if (strstr(model->fhttp->last_response, "User already exists") != NULL || strstr(model->fhttp->last_response, "Multiple users found") != NULL) + { + easy_flipper_dialog("Error", "Registration failed...\nUsername already exists.\nPress BACK to return."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + return "Username already exists."; + } + else + { + easy_flipper_dialog("Error", "Registration failed...\nUpdate your credentials.\nPress BACK to return."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + return "Registration failed..."; + } + } + else + { + if (!start_game_thread(app)) + { + FURI_LOG_E(TAG, "Failed to start game thread"); + easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + return "Failed to start game thread"; + } + return "Thanks for playing FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close."; + } + } + else if (model->request_index == 2) + { + return "Welcome to FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close."; + } + else if (model->request_index == 3) + { + if (!start_game_thread(app)) + { + FURI_LOG_E(TAG, "Failed to start game thread"); + easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + return "Failed to start game thread"; + } + return "Thanks for playing FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close."; + } + easy_flipper_dialog("Error", "Unknown error. Press BACK to return."); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now + return "Unknown error"; +} +void flip_world_switch_to_view_get_game(FlipWorldApp *app) +{ + flip_world_generic_switch_to_view(app, "Starting Game..", flip_world_fetch_game, flip_world_parse_game, 5, callback_to_submenu, FlipWorldViewLoader); +} + +void callback_submenu_choices(void *context, uint32_t index) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + switch (index) + { + case FlipWorldSubmenuIndexRun: + free_all_views(app, true, true); + if (!is_enough_heap(45000)) // lowered from 60k to 45k since we saved 15k bytes + { + easy_flipper_dialog("Error", "Not enough heap memory.\nPlease restart your Flipper."); + return; + } + // check if logged in + if (is_logged_in() || is_logged_in_to_flip_social()) + { + FlipperHTTP *fhttp = flipper_http_alloc(); + if (!fhttp) + { + FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP"); + easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return."); + return; + } + bool fetch_world_list_i() + { + return fetch_world_list(fhttp); + } + bool parse_world_list_i() + { + return fhttp->state != ISSUE; + } + + bool fetch_player_stats_i() + { + return fetch_player_stats(fhttp); + } + + 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(app->view_dispatcher, FlipWorldViewSubmenu); + flipper_http_free(fhttp); + return; + } + + view_dispatcher_add_view(app->view_dispatcher, loading_view_id, loading_get_view(loading)); + + // Switch to the loading view + view_dispatcher_switch_to_view(app->view_dispatcher, loading_view_id); + + // Make the request + if (!flipper_http_process_response_async(fhttp, fetch_world_list_i, parse_world_list_i) || + !flipper_http_process_response_async(fhttp, fetch_player_stats_i, set_player_context)) + { + FURI_LOG_E(HTTP_TAG, "Failed to make request"); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); + view_dispatcher_remove_view(app->view_dispatcher, loading_view_id); + loading_free(loading); + flipper_http_free(fhttp); + } + else + { + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); + view_dispatcher_remove_view(app->view_dispatcher, loading_view_id); + loading_free(loading); + flipper_http_free(fhttp); + } + if (!start_game_thread(app)) + { + FURI_LOG_E(TAG, "Failed to start game thread"); + easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return."); + return; + } + + easy_flipper_dialog("Starting Game", "Please wait..."); + } + else + { + flip_world_switch_to_view_get_game(app); + } + break; + case FlipWorldSubmenuIndexAbout: + free_all_views(app, true, true); + if (!alloc_about_view(app)) + { + FURI_LOG_E(TAG, "Failed to allocate about view"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewAbout); + break; + case FlipWorldSubmenuIndexSettings: + free_all_views(app, true, true); + if (!alloc_submenu_settings(app)) + { + FURI_LOG_E(TAG, "Failed to allocate settings view"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSettings); + break; + case FlipWorldSubmenuIndexWiFiSettings: + free_all_views(app, true, false); + if (!alloc_variable_item_list(app, FlipWorldSubmenuIndexWiFiSettings)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); + break; + case FlipWorldSubmenuIndexGameSettings: + free_all_views(app, true, false); + if (!alloc_variable_item_list(app, FlipWorldSubmenuIndexGameSettings)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); + break; + case FlipWorldSubmenuIndexUserSettings: + free_all_views(app, true, false); + if (!alloc_variable_item_list(app, FlipWorldSubmenuIndexUserSettings)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); + break; + default: + break; + } +} + +static void text_updated_wifi_ssid(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + + // store the entered text + strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size); + + // Ensure null-termination + app->text_input_buffer[app->text_input_buffer_size - 1] = '\0'; + + // save the setting + save_char("WiFi-SSID", app->text_input_buffer); + + // update the variable item text + if (app->variable_item_wifi_ssid) + { + variable_item_set_current_value_text(app->variable_item_wifi_ssid, app->text_input_buffer); + + // get value of password + char pass[64]; + char username[64]; + char password[64]; + if (load_char("WiFi-Password", pass, sizeof(pass))) + { + if (strlen(pass) > 0 && strlen(app->text_input_buffer) > 0) + { + // save the settings + load_char("Flip-Social-Username", username, sizeof(username)); + load_char("Flip-Social-Password", password, sizeof(password)); + save_settings(app->text_input_buffer, pass, username, password); + + // initialize the http + FlipperHTTP *fhttp = flipper_http_alloc(); + if (fhttp) + { + // save the wifi if the device is connected + if (!flipper_http_save_wifi(fhttp, app->text_input_buffer, pass)) + { + easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed."); + } + + // free the resources + flipper_http_free(fhttp); + } + else + { + easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero."); + } + } + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); +} +static void text_updated_wifi_pass(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + + // store the entered text + strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size); + + // Ensure null-termination + app->text_input_buffer[app->text_input_buffer_size - 1] = '\0'; + + // save the setting + save_char("WiFi-Password", app->text_input_buffer); + + // update the variable item text + if (app->variable_item_wifi_pass) + { + // variable_item_set_current_value_text(app->variable_item_wifi_pass, app->text_input_buffer); + } + + // get value of ssid + char ssid[64]; + char username[64]; + char password[64]; + if (load_char("WiFi-SSID", ssid, sizeof(ssid))) + { + if (strlen(ssid) > 0 && strlen(app->text_input_buffer) > 0) + { + // save the settings + load_char("Flip-Social-Username", username, sizeof(username)); + load_char("Flip-Social-Password", password, sizeof(password)); + save_settings(ssid, app->text_input_buffer, username, password); + + // initialize the http + FlipperHTTP *fhttp = flipper_http_alloc(); + if (fhttp) + { + // save the wifi if the device is connected + if (!flipper_http_save_wifi(fhttp, ssid, app->text_input_buffer)) + { + easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed."); + } + + // free the resources + flipper_http_free(fhttp); + } + else + { + easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero."); + } + } + } + + // switch to the settings view + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); +} +static void text_updated_username(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + + // store the entered text + strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size); + + // Ensure null-termination + app->text_input_buffer[app->text_input_buffer_size - 1] = '\0'; + + // save the setting + save_char("Flip-Social-Username", app->text_input_buffer); + + // update the variable item text + if (app->variable_item_user_username) + { + variable_item_set_current_value_text(app->variable_item_user_username, app->text_input_buffer); + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); // back to user settings +} +static void text_updated_password(void *context) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + + // store the entered text + strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size); + + // Ensure null-termination + app->text_input_buffer[app->text_input_buffer_size - 1] = '\0'; + + // save the setting + save_char("Flip-Social-Password", app->text_input_buffer); + + // update the variable item text + if (app->variable_item_user_password) + { + variable_item_set_current_value_text(app->variable_item_user_password, app->text_input_buffer); + } + + // get value of username + char username[64]; + char ssid[64]; + char pass[64]; + if (load_char("Flip-Social-Username", username, sizeof(username))) + { + if (strlen(username) > 0 && strlen(app->text_input_buffer) > 0) + { + // save the settings + load_char("WiFi-SSID", ssid, sizeof(ssid)); + load_char("WiFi-Password", pass, sizeof(pass)); + save_settings(ssid, pass, username, app->text_input_buffer); + } + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); // back to user settings +} + +static void wifi_settings_item_selected(void *context, uint32_t index) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + char ssid[64]; + char pass[64]; + char username[64]; + char password[64]; + switch (index) + { + case 0: // Input SSID + free_all_views(app, false, false); + if (!alloc_text_input_view(app, "SSID")) + { + FURI_LOG_E(TAG, "Failed to allocate text input view"); + return; + } + // load SSID + if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass), username, sizeof(username), password, sizeof(password))) + { + strncpy(app->text_input_temp_buffer, ssid, app->text_input_buffer_size - 1); + app->text_input_temp_buffer[app->text_input_buffer_size - 1] = '\0'; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewTextInput); + break; + case 1: // Input Password + free_all_views(app, false, false); + if (!alloc_text_input_view(app, "Password")) + { + FURI_LOG_E(TAG, "Failed to allocate text input view"); + return; + } + // load password + if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass), username, sizeof(username), password, sizeof(password))) + { + strncpy(app->text_input_temp_buffer, pass, app->text_input_buffer_size - 1); + app->text_input_temp_buffer[app->text_input_buffer_size - 1] = '\0'; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewTextInput); + break; + default: + FURI_LOG_E(TAG, "Unknown configuration item index"); + break; + } +} +static void flip_world_game_fps_change(VariableItem *item) +{ + uint8_t index = variable_item_get_current_value_index(item); + game_fps_index = index; + variable_item_set_current_value_text(item, game_fps_choices[index]); + variable_item_set_current_value_index(item, index); + + // save the fps + save_char("Game-FPS", game_fps_choices[index]); +} +static void flip_world_game_screen_always_on_change(VariableItem *item) +{ + uint8_t index = variable_item_get_current_value_index(item); + game_screen_always_on_index = index; + variable_item_set_current_value_text(item, yes_or_no_choices[index]); + variable_item_set_current_value_index(item, index); + + // save the screen always on + save_char("Game-Screen-Always-On", yes_or_no_choices[index]); +} +static void flip_world_game_sound_on_change(VariableItem *item) +{ + uint8_t index = variable_item_get_current_value_index(item); + game_sound_on_index = index; + variable_item_set_current_value_text(item, yes_or_no_choices[index]); + variable_item_set_current_value_index(item, index); + + // save the screen always on + save_char("Game-Sound-On", yes_or_no_choices[index]); +} +static void flip_world_game_vibration_on_change(VariableItem *item) +{ + uint8_t index = variable_item_get_current_value_index(item); + game_vibration_on_index = index; + variable_item_set_current_value_text(item, yes_or_no_choices[index]); + variable_item_set_current_value_index(item, index); + + // save the screen always on + save_char("Game-Vibration-On", yes_or_no_choices[index]); +} + +static bool flip_world_fetch_worlds(DataLoaderModel *model) +{ + if (!model || !model->fhttp) + { + FURI_LOG_E(TAG, "model or fhttp is NULL"); + return false; + } + // Create the directory for saving settings + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + furi_record_close(RECORD_STORAGE); + snprintf( + model->fhttp->file_path, + sizeof(model->fhttp->file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list_full.json"); + model->fhttp->save_received_data = true; + return flipper_http_get_request_with_headers(model->fhttp, "https://www.flipsocial.net/api/world/v3/get/10/", "{\"Content-Type\":\"application/json\"}"); +} +static char *flip_world_parse_worlds(DataLoaderModel *model) +{ + UNUSED(model); + return "World Pack Installed"; +} +static void flip_world_switch_to_view_get_worlds(FlipWorldApp *app) +{ + flip_world_generic_switch_to_view(app, "Fetching World Pack..", flip_world_fetch_worlds, flip_world_parse_worlds, 1, callback_to_submenu, FlipWorldViewLoader); +} +static void game_settings_item_selected(void *context, uint32_t index) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + switch (index) + { + case 0: // Download all world data s one huge json + flip_world_switch_to_view_get_worlds(app); + case 1: // Change FPS + break; + case 2: // Screen Always On + break; + } +} +static void user_settings_item_selected(void *context, uint32_t index) +{ + FlipWorldApp *app = (FlipWorldApp *)context; + if (!app) + { + FURI_LOG_E(TAG, "FlipWorldApp is NULL"); + return; + } + switch (index) + { + case 0: // Username + free_all_views(app, false, false); + if (!alloc_text_input_view(app, "Username-Login")) + { + FURI_LOG_E(TAG, "Failed to allocate text input view"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewTextInput); + break; + case 1: // Password + free_all_views(app, false, false); + if (!alloc_text_input_view(app, "Password-Login")) + { + FURI_LOG_E(TAG, "Failed to allocate text input view"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewTextInput); + break; + } +} + +static void flip_world_widget_set_text(char *message, Widget **widget) +{ + if (widget == NULL) + { + FURI_LOG_E(TAG, "flip_world_set_widget_text - widget is NULL"); + DEV_CRASH(); + return; + } + if (message == NULL) + { + FURI_LOG_E(TAG, "flip_world_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_world_loader_draw_callback(Canvas *canvas, void *model) +{ + if (!canvas || !model) + { + FURI_LOG_E(TAG, "flip_world_loader_draw_callback - canvas or model is NULL"); + return; + } + + DataLoaderModel *data_loader_model = (DataLoaderModel *)model; + SerialState http_state = data_loader_model->fhttp->state; + 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_world_request_error_draw(canvas, data_loader_model); + 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, "Fetching..."); + 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_world_loader_process_callback(void *context) +{ + if (context == NULL) + { + FURI_LOG_E(TAG, "flip_world_loader_process_callback - context is NULL"); + DEV_CRASH(); + return; + } + + FlipWorldApp *app = (FlipWorldApp *)context; + View *view = app->view_loader; + + DataState current_data_state; + DataLoaderModel *loader_model = NULL; + with_view_model(view, DataLoaderModel * model, { current_data_state = model->data_state; loader_model = model; }, false); + if (!loader_model || !loader_model->fhttp) + { + FURI_LOG_E(TAG, "Model or fhttp is NULL"); + DEV_CRASH(); + return; + } + + 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(model->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 (loader_model->fhttp->state == IDLE && loader_model->fhttp->last_response != NULL) + { + if (strstr(loader_model->fhttp->last_response, "[PONG]") != NULL) + { + FURI_LOG_DEV(TAG, "PONG received."); + } + else if (strncmp(loader_model->fhttp->last_response, "[SUCCESS]", 9) == 0) + { + FURI_LOG_DEV(TAG, "SUCCESS received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL"); + } + else if (strncmp(loader_model->fhttp->last_response, "[ERROR]", 9) == 0) + { + FURI_LOG_DEV(TAG, "ERROR received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL"); + } + else if (strlen(loader_model->fhttp->last_response) == 0) + { + // Still waiting on response + } + else + { + with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateReceived; }, true); + } + } + else if (loader_model->fhttp->state == SENDING || loader_model->fhttp->state == RECEIVING) + { + // continue waiting + } + else if (loader_model->fhttp->state == INACTIVE) + { + // inactive. try again + } + else if (loader_model->fhttp->state == ISSUE) + { + with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateError; }, true); + } + else + { + FURI_LOG_DEV(TAG, "Unexpected state: %d lastresp: %s", loader_model->fhttp->state, loader_model->fhttp->last_response ? loader_model->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", model->fhttp->last_response ? model->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_world_widget_set_text(model->data_text != NULL ? model->data_text : "", &app->widget_result); + if (model->data_text != NULL) + { + free(model->data_text); + model->data_text = NULL; + } + view_set_previous_callback(widget_get_view(app->widget_result), model->back_callback); + view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewWidgetResult); + } + }, + true); + } +} + +static void flip_world_loader_timer_callback(void *context) +{ + if (context == NULL) + { + FURI_LOG_E(TAG, "flip_world_loader_timer_callback - context is NULL"); + DEV_CRASH(); + return; + } + FlipWorldApp *app = (FlipWorldApp *)context; + view_dispatcher_send_custom_event(app->view_dispatcher, FlipWorldCustomEventProcess); +} + +static void flip_world_loader_on_enter(void *context) +{ + if (context == NULL) + { + FURI_LOG_E(TAG, "flip_world_loader_on_enter - context is NULL"); + DEV_CRASH(); + return; + } + FlipWorldApp *app = (FlipWorldApp *)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_world_loader_timer_callback, FuriTimerTypePeriodic, app); + } + furi_timer_start(model->timer, 250); + }, + true); +} + +static void flip_world_loader_on_exit(void *context) +{ + if (context == NULL) + { + FURI_LOG_E(TAG, "flip_world_loader_on_exit - context is NULL"); + DEV_CRASH(); + return; + } + FlipWorldApp *app = (FlipWorldApp *)context; + View *view = app->view_loader; + with_view_model( + view, + DataLoaderModel * model, + { + if (model->timer) + { + furi_timer_stop(model->timer); + } + }, + false); +} + +void flip_world_loader_init(View *view) +{ + if (view == NULL) + { + FURI_LOG_E(TAG, "flip_world_loader_init - view is NULL"); + DEV_CRASH(); + return; + } + view_allocate_model(view, ViewModelTypeLocking, sizeof(DataLoaderModel)); + view_set_enter_callback(view, flip_world_loader_on_enter); + view_set_exit_callback(view, flip_world_loader_on_exit); +} + +void flip_world_loader_free_model(View *view) +{ + if (view == NULL) + { + FURI_LOG_E(TAG, "flip_world_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) + { + // do not free the context here, it is the app context + // free(model->parser_context); + // model->parser_context = NULL; + } + if (model->fhttp) + { + flipper_http_free(model->fhttp); + model->fhttp = NULL; + } + }, + false); +} + +bool flip_world_custom_event_callback(void *context, uint32_t index) +{ + if (context == NULL) + { + FURI_LOG_E(TAG, "flip_world_custom_event_callback - context is NULL"); + DEV_CRASH(); + return false; + } + + switch (index) + { + case FlipWorldCustomEventProcess: + flip_world_loader_process_callback(context); + return true; + default: + FURI_LOG_DEV(TAG, "flip_world_custom_event_callback. Unknown index: %ld", index); + return false; + } +} + +void flip_world_generic_switch_to_view(FlipWorldApp *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_world_generic_switch_to_view - app is NULL"); + DEV_CRASH(); + return; + } + + View *view = app->view_loader; + if (view == NULL) + { + FURI_LOG_E(TAG, "flip_world_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; + // + model->parser_context = app; + if (!model->fhttp) + { + model->fhttp = flipper_http_alloc(); + } + }, + true); + + view_dispatcher_switch_to_view(app->view_dispatcher, view_id); +} diff --git a/flip_world/callback/callback.h b/flip_world/callback/callback.h new file mode 100644 index 000000000..f2d3f854a --- /dev/null +++ b/flip_world/callback/callback.h @@ -0,0 +1,45 @@ +#pragma once +#include +#include + +void free_all_views(void *context, bool should_free_variable_item_list, bool should_free_submenu_settings); +void callback_submenu_choices(void *context, uint32_t index); +uint32_t callback_to_submenu(void *context); + +// Add edits by Derek Jamison +typedef enum DataState DataState; +enum DataState +{ + DataStateInitial, + DataStateRequested, + DataStateReceived, + DataStateParsed, + DataStateParseError, + DataStateError, +}; + +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; + FlipperHTTP *fhttp; +}; +void flip_world_generic_switch_to_view(FlipWorldApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id); + +void flip_world_loader_draw_callback(Canvas *canvas, void *model); + +void flip_world_loader_init(View *view); + +void flip_world_loader_free_model(View *view); +bool flip_world_custom_event_callback(void *context, uint32_t index); \ No newline at end of file diff --git a/flip_world/easy_flipper/easy_flipper.c b/flip_world/easy_flipper/easy_flipper.c new file mode 100644 index 000000000..fe23e9717 --- /dev/null +++ b/flip_world/easy_flipper/easy_flipper.c @@ -0,0 +1,627 @@ +#include + +void easy_flipper_dialog( + char *header, + char *text) +{ + DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage *message = dialog_message_alloc(); + dialog_message_set_header( + message, header, 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + message, + text, + 0, + 63, + AlignLeft, + AlignBottom); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); +} + +/** + * @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; +} + +bool easy_flipper_set_text_box( + TextBox **text_box, + int32_t view_id, + char *text, + bool start_at_end, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher) +{ + if (!text_box) + { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_text_box"); + return false; + } + *text_box = text_box_alloc(); + if (!*text_box) + { + FURI_LOG_E(EASY_TAG, "Failed to allocate TextBox"); + return false; + } + if (text) + { + text_box_set_text(*text_box, text); + } + if (previous_callback) + { + view_set_previous_callback(text_box_get_view(*text_box), previous_callback); + } + text_box_set_font(*text_box, TextBoxFontText); + if (start_at_end) + { + text_box_set_focus(*text_box, TextBoxFocusEnd); + } + view_dispatcher_add_view(*view_dispatcher, view_id, text_box_get_view(*text_box)); + return true; +} \ No newline at end of file diff --git a/flip_world/easy_flipper/easy_flipper.h b/flip_world/easy_flipper/easy_flipper.h new file mode 100644 index 000000000..1da7471de --- /dev/null +++ b/flip_world/easy_flipper/easy_flipper.h @@ -0,0 +1,287 @@ +#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 +#include +#include + +#define EASY_TAG "EasyFlipper" + +void easy_flipper_dialog( + char *header, + char *text); + +/** + * @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); + +/** + * @brief Initialize a TextBox object + * @param text_box The TextBox object to initialize + * @param view_id The ID/Index of the view + * @param text The text to display in the text box + * @param start_at_end Start the text box at the end + * @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_box( + TextBox **text_box, + int32_t view_id, + char *text, + bool start_at_end, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); + +#endif \ No newline at end of file diff --git a/flip_world/engine/LICENSE b/flip_world/engine/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/flip_world/engine/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/flip_world/engine/canvas.c b/flip_world/engine/canvas.c new file mode 100644 index 000000000..29faa0fa6 --- /dev/null +++ b/flip_world/engine/canvas.c @@ -0,0 +1,28 @@ +#include +#include "canvas.h" + +void canvas_printf(Canvas* canvas, uint8_t x, uint8_t y, const char* format, ...) { + FuriString* string = furi_string_alloc(); + va_list args; + va_start(args, format); + furi_string_vprintf(string, format, args); + va_end(args); + + canvas_draw_str(canvas, x, y, furi_string_get_cstr(string)); + + furi_string_free(string); +} + +size_t canvas_printf_width(Canvas* canvas, const char* format, ...) { + FuriString* string = furi_string_alloc(); + va_list args; + va_start(args, format); + furi_string_vprintf(string, format, args); + va_end(args); + + size_t size = canvas_string_width(canvas, furi_string_get_cstr(string)); + + furi_string_free(string); + + return size; +} \ No newline at end of file diff --git a/flip_world/engine/canvas.h b/flip_world/engine/canvas.h new file mode 100644 index 000000000..0c4bd0cd7 --- /dev/null +++ b/flip_world/engine/canvas.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Print formatted string to canvas + * + * @param canvas canvas instance + * @param x x position + * @param y y position + * @param format format string + * @param ... arguments + */ +void canvas_printf(Canvas* canvas, uint8_t x, uint8_t y, const char* format, ...); + +/** + * @brief Get width of formatted string + * + * @param canvas canvas instance + * @param format format string + * @param ... arguments + * @return size_t width of formatted string + */ +size_t canvas_printf_width(Canvas* canvas, const char* format, ...); + +#ifdef __cplusplus +} +#endif diff --git a/flip_world/engine/clock_timer.c b/flip_world/engine/clock_timer.c new file mode 100644 index 000000000..3f98e6440 --- /dev/null +++ b/flip_world/engine/clock_timer.c @@ -0,0 +1,53 @@ +#include "clock_timer.h" +#include + +#include +#include +#include + +#define FURI_HAL_CLOCK_TIMER TIM2 +#define FURI_HAL_CLOCK_TIMER_BUS FuriHalBusTIM2 +#define FURI_HAL_CLOCK_TIMER_IRQ FuriHalInterruptIdTIM2 + +typedef struct { + ClockTimerCallback callback; + void* context; +} ClockTimer; + +static ClockTimer clock_timer = { + .callback = NULL, + .context = NULL, +}; + +static void clock_timer_isr(void* context) { + if(clock_timer.callback) { + clock_timer.callback(context); + } + + LL_TIM_ClearFlag_UPDATE(FURI_HAL_CLOCK_TIMER); +} + +void clock_timer_start(ClockTimerCallback callback, void* context, float period) { + clock_timer.callback = callback; + clock_timer.context = context; + + furi_hal_bus_enable(FURI_HAL_CLOCK_TIMER_BUS); + + // init timer to produce interrupts + LL_TIM_InitTypeDef TIM_InitStruct = {0}; + TIM_InitStruct.Autoreload = (SystemCoreClock / period) - 1; + LL_TIM_Init(FURI_HAL_CLOCK_TIMER, &TIM_InitStruct); + + furi_hal_interrupt_set_isr(FURI_HAL_CLOCK_TIMER_IRQ, clock_timer_isr, clock_timer.context); + + LL_TIM_EnableIT_UPDATE(FURI_HAL_CLOCK_TIMER); + LL_TIM_EnableCounter(FURI_HAL_CLOCK_TIMER); +} + +void clock_timer_stop(void) { + LL_TIM_DisableIT_UPDATE(FURI_HAL_CLOCK_TIMER); + LL_TIM_DisableCounter(FURI_HAL_CLOCK_TIMER); + + furi_hal_bus_disable(FURI_HAL_CLOCK_TIMER_BUS); + furi_hal_interrupt_set_isr(FURI_HAL_CLOCK_TIMER_IRQ, NULL, NULL); +} \ No newline at end of file diff --git a/flip_world/engine/clock_timer.h b/flip_world/engine/clock_timer.h new file mode 100644 index 000000000..f8b12e22d --- /dev/null +++ b/flip_world/engine/clock_timer.h @@ -0,0 +1,15 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*ClockTimerCallback)(void* context); + +void clock_timer_start(ClockTimerCallback callback, void* context, float period); + +void clock_timer_stop(void); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/engine.h b/flip_world/engine/engine.h new file mode 100644 index 000000000..fb04513b1 --- /dev/null +++ b/flip_world/engine/engine.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include "game_engine.h" +#include "level.h" +#include "entity.h" +#include "game_manager.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef struct + { + float target_fps; + bool show_fps; + bool always_backlight; + void (*start)(GameManager *game_manager, void *context); + void (*stop)(void *context); + size_t context_size; + } Game; + + extern const Game game; + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/entity.c b/flip_world/engine/entity.c new file mode 100644 index 000000000..043a1ece6 --- /dev/null +++ b/flip_world/engine/entity.c @@ -0,0 +1,217 @@ +#include "entity.h" +#include "entity_i.h" +#include +#include + +#ifdef ENTITY_DEBUG +#define ENTITY_D(...) FURI_LOG_D("Entity", __VA_ARGS__) +#else +#define ENTITY_D(...) +#endif + +static int32_t entities_count = 0; + +int32_t entities_get_count(void) { + return entities_count; +} + +Entity* entity_alloc(const EntityDescription* description) { + entities_count++; + Entity* entity = malloc(sizeof(Entity)); + entity->position = VECTOR_ZERO; + entity->description = description; + entity->context = NULL; + if(description && description->context_size > 0) { + entity->context = malloc(description->context_size); + } + entity->collider = NULL; + entity->collider_offset = VECTOR_ZERO; + ENTITY_D("Allocated at %p", entity); + entity->collider_dirty = false; + return entity; +} + +void entity_collider_add_circle(Entity* entity, float radius) { + furi_check(entity->collider == NULL, "Collider already added"); + entity->collider = malloc(sizeof(Collider)); + entity->collider->type = ColliderTypeCircle; + entity->collider->circle.radius = radius; + entity->collider_dirty = true; +} + +void entity_collider_add_rect(Entity* entity, float width, float height) { + furi_check(entity->collider == NULL, "Collider already added"); + entity->collider = malloc(sizeof(Collider)); + entity->collider->type = ColliderTypeRect; + entity->collider->rect.half_width = width / 2; + entity->collider->rect.half_height = height / 2; + entity->collider_dirty = true; +} + +void entity_collider_remove(Entity* entity) { + furi_check(entity->collider != NULL, "Collider not added"); + free(entity->collider); + entity->collider = NULL; + entity->collider_dirty = false; +} + +void entity_collider_offset_set(Entity* entity, Vector offset) { + entity->collider_offset = offset; + entity->collider_dirty = true; +} + +Vector entity_collider_offset_get(Entity* entity) { + return entity->collider_offset; +} + +static Vector entity_collider_position_get(Entity* entity) { + return (Vector){ + .x = entity->position.x + entity->collider_offset.x, + .y = entity->position.y + entity->collider_offset.y, + }; +} + +void entity_free(Entity* entity) { + entities_count--; + ENTITY_D("Freeing at %p", entity); + if(entity->context) { + free(entity->context); + } + if(entity->collider) { + free(entity->collider); + } + free(entity); +} + +const EntityDescription* entity_description_get(Entity* entity) { + return entity->description; +} + +Vector entity_pos_get(Entity* entity) { + return entity->position; +} + +void entity_pos_set(Entity* entity, Vector position) { + entity->position = position; + entity->collider_dirty = true; +} + +void* entity_context_get(Entity* entity) { + return entity->context; +} + +void entity_call_start(Entity* entity, GameManager* manager) { + if(entity->description && entity->description->start) { + entity->description->start(entity, manager, entity->context); + } +} + +void entity_call_stop(Entity* entity, GameManager* manager) { + if(entity->description && entity->description->stop) { + entity->description->stop(entity, manager, entity->context); + } +} + +void entity_call_update(Entity* entity, GameManager* manager) { + if(entity->description && entity->description->update) { + entity->description->update(entity, manager, entity->context); + } +} + +void entity_call_render(Entity* entity, GameManager* manager, Canvas* canvas) { + if(entity->description && entity->description->render) { + entity->description->render(entity, manager, canvas, entity->context); + } +} + +void entity_call_collision(Entity* entity, Entity* other, GameManager* manager) { + if(entity->description && entity->description->collision) { + entity->description->collision(entity, other, manager, entity->context); + } +} + +bool entity_collider_circle_circle(Entity* entity, Entity* other) { + Vector pos1 = entity_collider_position_get(entity); + Vector pos2 = entity_collider_position_get(other); + + float dx = pos1.x - pos2.x; + float dy = pos1.y - pos2.y; + float distance = sqrtf(dx * dx + dy * dy); + return distance < entity->collider->circle.radius + other->collider->circle.radius; +} + +bool entity_collider_rect_rect(Entity* entity, Entity* other) { + Vector pos1 = entity_collider_position_get(entity); + Vector pos2 = entity_collider_position_get(other); + + float left1 = pos1.x - entity->collider->rect.half_width; + float right1 = pos1.x + entity->collider->rect.half_width; + float top1 = pos1.y - entity->collider->rect.half_height; + float bottom1 = pos1.y + entity->collider->rect.half_height; + + float left2 = pos2.x - other->collider->rect.half_width; + float right2 = pos2.x + other->collider->rect.half_width; + float top2 = pos2.y - other->collider->rect.half_height; + float bottom2 = pos2.y + other->collider->rect.half_height; + + return left1 < right2 && right1 > left2 && top1 < bottom2 && bottom1 > top2; +} + +bool entity_collider_circle_rect(Entity* circle, Entity* rect) { + Vector pos1 = entity_collider_position_get(circle); + Vector pos2 = entity_collider_position_get(rect); + + float left = pos2.x - rect->collider->rect.half_width; + float right = pos2.x + rect->collider->rect.half_width; + float top = pos2.y - rect->collider->rect.half_height; + float bottom = pos2.y + rect->collider->rect.half_height; + + float closestX = fmaxf(left, fminf(pos1.x, right)); + float closestY = fmaxf(top, fminf(pos1.y, bottom)); + + float dx = pos1.x - closestX; + float dy = pos1.y - closestY; + float distance = sqrtf(dx * dx + dy * dy); + return distance < circle->collider->circle.radius; +} + +bool entity_collider_check_collision(Entity* entity, Entity* other) { + furi_check(entity->collider); + furi_check(other->collider); + + if(entity->collider->type == ColliderTypeCircle) { + Entity* circle = entity; + if(other->collider->type == ColliderTypeCircle) { + return entity_collider_circle_circle(circle, other); + } else { + return entity_collider_circle_rect(circle, other); + } + } else { + Entity* rect = entity; + if(other->collider->type == ColliderTypeCircle) { + return entity_collider_circle_rect(other, rect); + } else { + return entity_collider_rect_rect(rect, other); + } + } +} + +bool entity_collider_exists(Entity* entity) { + return entity->collider != NULL; +} + +void entity_send_event( + Entity* sender, + Entity* receiver, + GameManager* manager, + uint32_t type, + EntityEventValue value) { + if(receiver->description && receiver->description->event) { + EntityEvent event = { + .type = type, + .sender = sender, + .value = value, + }; + receiver->description->event(receiver, manager, event, receiver->context); + } +} \ No newline at end of file diff --git a/flip_world/engine/entity.h b/flip_world/engine/entity.h new file mode 100644 index 000000000..2bd95a2a1 --- /dev/null +++ b/flip_world/engine/entity.h @@ -0,0 +1,65 @@ +#pragma once +#include "vector.h" +#include "game_engine.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct Entity Entity; + +typedef struct Level Level; + +typedef union { + uint32_t value; + void* pointer; +} EntityEventValue; + +typedef struct { + uint32_t type; + Entity* sender; + EntityEventValue value; +} EntityEvent; + +typedef struct Level Level; +typedef struct GameManager GameManager; + +typedef struct { + void (*start)(Entity* self, GameManager* manager, void* context); + void (*stop)(Entity* self, GameManager* manager, void* context); + void (*update)(Entity* self, GameManager* manager, void* context); + void (*render)(Entity* self, GameManager* manager, Canvas* canvas, void* context); + void (*collision)(Entity* self, Entity* other, GameManager* manager, void* context); + void (*event)(Entity* self, GameManager* manager, EntityEvent event, void* context); + size_t context_size; +} EntityDescription; + +const EntityDescription* entity_description_get(Entity* entity); + +Vector entity_pos_get(Entity* entity); + +void entity_pos_set(Entity* entity, Vector position); + +void* entity_context_get(Entity* entity); + +void entity_collider_add_circle(Entity* entity, float radius); + +void entity_collider_add_rect(Entity* entity, float width, float height); + +void entity_collider_remove(Entity* entity); + +void entity_collider_offset_set(Entity* entity, Vector offset); + +Vector entity_collider_offset_get(Entity* entity); + +void entity_send_event( + Entity* sender, + Entity* receiver, + GameManager* manager, + uint32_t type, + EntityEventValue value); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/entity_i.h b/flip_world/engine/entity_i.h new file mode 100644 index 000000000..1f60d944f --- /dev/null +++ b/flip_world/engine/entity_i.h @@ -0,0 +1,58 @@ +#pragma once +#include "entity.h" +#include "game_manager.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + ColliderTypeCircle, + ColliderTypeRect, +} ColliderType; + +typedef struct { + ColliderType type; + union { + struct { + float radius; + } circle; + struct { + float half_width; + float half_height; + } rect; + }; +} Collider; + +struct Entity { + Vector position; + const EntityDescription* description; + void* context; + Collider* collider; + Vector collider_offset; + bool collider_dirty; +}; + +Entity* entity_alloc(const EntityDescription* behaviour); + +void entity_free(Entity* entity); + +void entity_call_start(Entity* entity, GameManager* manager); + +void entity_call_stop(Entity* entity, GameManager* manager); + +void entity_call_update(Entity* entity, GameManager* manager); + +void entity_call_render(Entity* entity, GameManager* manager, Canvas* canvas); + +void entity_call_collision(Entity* entity, Entity* other, GameManager* manager); + +bool entity_collider_check_collision(Entity* entity, Entity* other); + +bool entity_collider_exists(Entity* entity); + +int32_t entities_get_count(void); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/game_engine.c b/flip_world/engine/game_engine.c new file mode 100644 index 000000000..1a50d06cb --- /dev/null +++ b/flip_world/engine/game_engine.c @@ -0,0 +1,199 @@ +#include "game_engine.h" +#include +#include +#include +#include +#include "clock_timer.h" + +typedef _Atomic uint32_t AtomicUint32; + +GameEngineSettings game_engine_settings_init() { + GameEngineSettings settings; + settings.target_fps = 30.0f; + settings.show_fps = false; + settings.always_backlight = true; + settings.start_callback = NULL; + settings.frame_callback = NULL; + settings.stop_callback = NULL; + settings.context = NULL; + return settings; +} + +struct GameEngine { + Gui* gui; + NotificationApp* notifications; + FuriPubSub* input_pubsub; + FuriThreadId thread_id; + GameEngineSettings settings; + float fps; +}; + +typedef enum { + GameThreadFlagUpdate = 1 << 0, + GameThreadFlagStop = 1 << 1, +} GameThreadFlag; + +#define GameThreadFlagMask (GameThreadFlagUpdate | GameThreadFlagStop) + +GameEngine* game_engine_alloc(GameEngineSettings settings) { + furi_check(settings.frame_callback != NULL); + + GameEngine* engine = malloc(sizeof(GameEngine)); + engine->gui = furi_record_open(RECORD_GUI); + engine->notifications = furi_record_open(RECORD_NOTIFICATION); + engine->input_pubsub = furi_record_open(RECORD_INPUT_EVENTS); + engine->thread_id = furi_thread_get_current_id(); + engine->settings = settings; + engine->fps = 1.0f; + + return engine; +} + +void game_engine_free(GameEngine* engine) { + furi_record_close(RECORD_GUI); + furi_record_close(RECORD_NOTIFICATION); + furi_record_close(RECORD_INPUT_EVENTS); + free(engine); +} + +static void clock_timer_callback(void* context) { + GameEngine* engine = context; + furi_thread_flags_set(engine->thread_id, GameThreadFlagUpdate); +} + +static const GameKey keys[] = { + [InputKeyUp] = GameKeyUp, + [InputKeyDown] = GameKeyDown, + [InputKeyRight] = GameKeyRight, + [InputKeyLeft] = GameKeyLeft, + [InputKeyOk] = GameKeyOk, + [InputKeyBack] = GameKeyBack, +}; + +static const size_t keys_count = sizeof(keys) / sizeof(keys[0]); + +static void input_events_callback(const void* value, void* context) { + AtomicUint32* input_state = context; + const InputEvent* event = value; + + if(event->key < keys_count) { + switch(event->type) { + case InputTypePress: + *input_state |= (keys[event->key]); + break; + case InputTypeRelease: + *input_state &= ~(keys[event->key]); + break; + default: + break; + } + } +} + +void game_engine_run(GameEngine* engine) { + // input state + AtomicUint32 input_state = 0; + uint32_t input_prev_state = 0; + + // set backlight if needed + if(engine->settings.always_backlight) { + notification_message(engine->notifications, &sequence_display_backlight_enforce_on); + } + + // acquire gui canvas + Canvas* canvas = gui_direct_draw_acquire(engine->gui); + + // subscribe to input events + FuriPubSubSubscription* input_subscription = + furi_pubsub_subscribe(engine->input_pubsub, input_events_callback, &input_state); + + // call start callback, if any + if(engine->settings.start_callback) { + engine->settings.start_callback(engine, engine->settings.context); + } + + // start "game update" timer + clock_timer_start(clock_timer_callback, engine, engine->settings.target_fps); + + // init fps counter + uint32_t time_start = DWT->CYCCNT; + + while(true) { + uint32_t flags = + furi_thread_flags_wait(GameThreadFlagMask, FuriFlagWaitAny, FuriWaitForever); + furi_check((flags & FuriFlagError) == 0); + + if(flags & GameThreadFlagUpdate) { + // update fps counter + uint32_t time_end = DWT->CYCCNT; + uint32_t time_delta = time_end - time_start; + time_start = time_end; + + // update input state + uint32_t input_current_state = input_state; + InputState input = { + .held = input_current_state, + .pressed = input_current_state & ~input_prev_state, + .released = ~input_current_state & input_prev_state, + }; + input_prev_state = input_current_state; + + // clear screen + canvas_reset(canvas); + + // calculate actual fps + engine->fps = (float)SystemCoreClock / time_delta; + + // do the work + engine->settings.frame_callback(engine, canvas, input, engine->settings.context); + + // show fps if needed + if(engine->settings.show_fps) { + canvas_set_color(canvas, ColorXOR); + canvas_printf(canvas, 0, 7, "%u", (uint32_t)roundf(engine->fps)); + } + + // and output screen buffer + canvas_commit(canvas); + + // throttle a bit + furi_delay_tick(2); + } + + if(flags & GameThreadFlagStop) { + break; + } + } + + // stop timer + clock_timer_stop(); + + // call stop callback, if any + if(engine->settings.stop_callback) { + engine->settings.stop_callback(engine, engine->settings.context); + } + + // release gui canvas and unsubscribe from input events + gui_direct_draw_release(engine->gui); + furi_pubsub_unsubscribe(engine->input_pubsub, input_subscription); + + if(engine->settings.always_backlight) { + notification_message(engine->notifications, &sequence_display_backlight_enforce_auto); + } +} + +void game_engine_stop(GameEngine* engine) { + furi_thread_flags_set(engine->thread_id, GameThreadFlagStop); +} + +float game_engine_get_delta_time(GameEngine* engine) { + return 1.0f / engine->fps; +} + +float game_engine_get_delta_frames(GameEngine* engine) { + return engine->fps / engine->settings.target_fps; +} + +void game_engine_show_fps_set(GameEngine* engine, bool show_fps) { + engine->settings.show_fps = show_fps; +} \ No newline at end of file diff --git a/flip_world/engine/game_engine.h b/flip_world/engine/game_engine.h new file mode 100644 index 000000000..4bfb71d13 --- /dev/null +++ b/flip_world/engine/game_engine.h @@ -0,0 +1,88 @@ +#pragma once +#include +#include "canvas.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + GameKeyUp = 1 << 0, + GameKeyDown = 1 << 1, + GameKeyRight = 1 << 2, + GameKeyLeft = 1 << 3, + GameKeyOk = 1 << 4, + GameKeyBack = 1 << 5, +} GameKey; + +typedef struct { + uint32_t held; // mask of GameKey held in current frame + uint32_t pressed; // mask of GameKey pressed in current frame + uint32_t released; // mask of GameKey released in current frame +} InputState; + +typedef struct GameEngine GameEngine; + +typedef void (*GameEngineStartCallback)(GameEngine* engine, void* context); + +typedef void (*GameEngineStopCallback)(GameEngine* engine, void* context); + +typedef void ( + *GameEngineFrameCallback)(GameEngine* engine, Canvas* canvas, InputState input, void* context); + +typedef struct { + float target_fps; // target fps + bool show_fps; // show fps counter + bool always_backlight; // keep backlight on + GameEngineStartCallback start_callback; // called when engine starts + GameEngineFrameCallback frame_callback; // frame callback, called at target fps + GameEngineStopCallback stop_callback; // called when engine stops + void* context; // user context passed to callback +} GameEngineSettings; + +/** Default settings initializer */ +GameEngineSettings game_engine_settings_init(void); + +/** Game Engine allocator + * @param settings engine settings + * @return GameEngine* GameEngine instance + */ +GameEngine* game_engine_alloc(GameEngineSettings settings); + +/** Run the Game Engine. Blocks until game_engine_stop() is called. + * @param engine GameEngine instance + */ +void game_engine_run(GameEngine* engine); + +/** Free the Game Engine + * @param engine GameEngine instance + */ +void game_engine_free(GameEngine* engine); + +/** Stop the Game Engine, will not block execution + * @param engine GameEngine instance + */ +void game_engine_stop(GameEngine* engine); + +/** Get delta time between current and previous frame + * @param engine GameEngine instance + * @return float delta time in seconds + */ +float game_engine_get_delta_time(GameEngine* engine); + +/** Get delta frames between current and previous frame + * @param engine GameEngine instance + * @return float delta frames + */ +float game_engine_get_delta_frames(GameEngine* engine); + +/** Enable/disable show fps counter + * @param engine GameEngine instance + * @param show_fps show fps counter + */ +void game_engine_show_fps_set(GameEngine* engine, bool show_fps); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/game_manager.c b/flip_world/engine/game_manager.c new file mode 100644 index 000000000..81fd2a303 --- /dev/null +++ b/flip_world/engine/game_manager.c @@ -0,0 +1,162 @@ +#include "game_manager.h" +#include "level_i.h" +#include +#include +#include + +typedef struct { + Sprite* sprite; + FuriString* path; +} SpriteCache; + +LIST_DEF(LevelList, Level*, M_POD_OPLIST); +LIST_DEF(SpriteCacheList, SpriteCache, M_POD_OPLIST); + +struct GameManager { + LevelList_t levels; + Level* current_level; + Level* next_level; + + GameEngine* engine; + InputState input; + void* game_context; + + SpriteCacheList_t sprites; +}; + +GameManager* game_manager_alloc() { + GameManager* manager = malloc(sizeof(GameManager)); + LevelList_init(manager->levels); + manager->current_level = NULL; + manager->next_level = NULL; + manager->engine = NULL; + manager->game_context = NULL; + memset(&manager->input, 0, sizeof(InputState)); + SpriteCacheList_init(manager->sprites); + return manager; +} + +void game_manager_free(GameManager* manager) { + level_call_stop(manager->current_level); + + // Free all levels + { + LevelList_it_t it; + LevelList_it(it, manager->levels); + while(!LevelList_end_p(it)) { + level_call_free(*LevelList_cref(it)); + level_free(*LevelList_cref(it)); + LevelList_next(it); + } + } + + LevelList_clear(manager->levels); + + // Free all sprites + { + SpriteCacheList_it_t it; + SpriteCacheList_it(it, manager->sprites); + while(!SpriteCacheList_end_p(it)) { + furi_string_free(SpriteCacheList_cref(it)->path); + sprite_free(SpriteCacheList_cref(it)->sprite); + SpriteCacheList_next(it); + } + } + SpriteCacheList_clear(manager->sprites); + free(manager); +} + +Level* game_manager_add_level(GameManager* manager, const LevelBehaviour* behaviour) { + UNUSED(manager); + Level* level = level_alloc(behaviour, manager); + LevelList_push_back(manager->levels, level); + level_call_alloc(level); + if(!manager->current_level) { + manager->current_level = level; + level_call_start(level); + } + return level; +} + +void game_manager_next_level_set(GameManager* manager, Level* next_level) { + manager->next_level = next_level; +} + +void game_manager_game_stop(GameManager* manager) { + GameEngine* engine = game_manager_engine_get(manager); + game_engine_stop(engine); +} + +Level* game_manager_current_level_get(GameManager* manager) { + return manager->current_level; +} + +void game_manager_update(GameManager* manager) { + if(manager->next_level) { + level_call_stop(manager->current_level); + manager->current_level = manager->next_level; + level_call_start(manager->current_level); + manager->next_level = NULL; + } + + level_update(manager->current_level, manager); +} + +void game_manager_render(GameManager* manager, Canvas* canvas) { + level_render(manager->current_level, manager, canvas); +} + +void game_manager_engine_set(GameManager* manager, GameEngine* engine) { + manager->engine = engine; +} + +void game_manager_input_set(GameManager* manager, InputState input) { + manager->input = input; +} + +void game_manager_game_context_set(GameManager* manager, void* context) { + manager->game_context = context; +} + +GameEngine* game_manager_engine_get(GameManager* manager) { + return manager->engine; +} + +InputState game_manager_input_get(GameManager* manager) { + return manager->input; +} + +void* game_manager_game_context_get(GameManager* manager) { + return manager->game_context; +} + +void game_manager_show_fps_set(GameManager* manager, bool show_fps) { + GameEngine* engine = game_manager_engine_get(manager); + game_engine_show_fps_set(engine, show_fps); +} + +Sprite* game_manager_sprite_load(GameManager* manager, const char* path) { + SpriteCacheList_it_t it; + SpriteCacheList_it(it, manager->sprites); + while(!SpriteCacheList_end_p(it)) { + if(furi_string_cmp(SpriteCacheList_cref(it)->path, path) == 0) { + return SpriteCacheList_cref(it)->sprite; + } + SpriteCacheList_next(it); + } + + FuriString* path_full = furi_string_alloc_set(APP_ASSETS_PATH("sprites/")); + furi_string_cat(path_full, path); + + Sprite* sprite = sprite_alloc(furi_string_get_cstr(path_full)); + if(sprite) { + SpriteCache cache = { + .sprite = sprite, + .path = furi_string_alloc_set(path), + }; + SpriteCacheList_push_back(manager->sprites, cache); + } + furi_string_free(path_full); + + return sprite; +} \ No newline at end of file diff --git a/flip_world/engine/game_manager.h b/flip_world/engine/game_manager.h new file mode 100644 index 000000000..42113531d --- /dev/null +++ b/flip_world/engine/game_manager.h @@ -0,0 +1,40 @@ +#pragma once +#include "level.h" +#include "game_engine.h" +#include "sprite.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct GameManager GameManager; + +Level* game_manager_add_level(GameManager* manager, const LevelBehaviour* behaviour); + +void game_manager_next_level_set(GameManager* manager, Level* level); + +Level* game_manager_current_level_get(GameManager* manager); + +GameEngine* game_manager_engine_get(GameManager* manager); + +InputState game_manager_input_get(GameManager* manager); + +void* game_manager_game_context_get(GameManager* manager); + +void game_manager_game_stop(GameManager* manager); + +void game_manager_show_fps_set(GameManager* manager, bool show_fps); + +/** + * @brief Load a sprite from a file + * Sprite will be cached and reused if the same file is loaded again + * + * @param manager game manager instance + * @param path sprite file path, relative to the game's assets folder + * @return Sprite* or NULL if the sprite could not be loaded + */ +Sprite* game_manager_sprite_load(GameManager* manager, const char* path); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/game_manager_i.h b/flip_world/engine/game_manager_i.h new file mode 100644 index 000000000..a80f047ed --- /dev/null +++ b/flip_world/engine/game_manager_i.h @@ -0,0 +1,24 @@ +#pragma once +#include "game_manager.h" + +#ifdef __cplusplus +extern "C" { +#endif + +GameManager* game_manager_alloc(void); + +void game_manager_free(GameManager* manager); + +void game_manager_update(GameManager* manager); + +void game_manager_render(GameManager* manager, Canvas* canvas); + +void game_manager_engine_set(GameManager* manager, GameEngine* engine); + +void game_manager_input_set(GameManager* manager, InputState input); + +void game_manager_game_context_set(GameManager* manager, void* context); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/level.c b/flip_world/engine/level.c new file mode 100644 index 000000000..8ac9786d7 --- /dev/null +++ b/flip_world/engine/level.c @@ -0,0 +1,214 @@ +#include "level.h" +#include "level_i.h" +#include "entity_i.h" +#include +#include + +LIST_DEF(EntityList, Entity*, M_POD_OPLIST); +#define M_OPL_EntityList_t() LIST_OPLIST(EntityList) +#define FOREACH(name, list) for \ + M_EACH(name, list, EntityList_t) + +#define LEVEL_DEBUG(...) FURI_LOG_D("Level", __VA_ARGS__) +#define LEVEL_ERROR(...) FURI_LOG_E("Level", __VA_ARGS__) + +struct Level { + EntityList_t entities; + EntityList_t to_add; + EntityList_t to_remove; + const LevelBehaviour* behaviour; + void* context; + GameManager* manager; +}; + +Level* level_alloc(const LevelBehaviour* behaviour, GameManager* manager) { + Level* level = malloc(sizeof(Level)); + level->manager = manager; + EntityList_init(level->entities); + EntityList_init(level->to_add); + EntityList_init(level->to_remove); + level->behaviour = behaviour; + if(behaviour->context_size > 0) { + level->context = malloc(behaviour->context_size); + } else { + level->context = NULL; + } + LEVEL_DEBUG("Allocated level at %p", level); + return level; +} + +static void level_process_add(Level* level) { + // move entities from to_add to entities + FOREACH(item, level->to_add) { + EntityList_push_back(level->entities, *item); + } + EntityList_clear(level->to_add); +} + +static void level_process_remove(Level* level) { + // remove entities in to_remove from entities and free them + FOREACH(item, level->to_remove) { + entity_free(*item); + EntityList_it_t it; + + // find and remove the entity from the entities list + for(EntityList_it(it, level->entities); !EntityList_end_p(it); EntityList_next(it)) { + if(*EntityList_ref(it) == *item) { + EntityList_remove(level->entities, it); + break; + } + } + } + EntityList_clear(level->to_remove); +} + +static bool level_entity_in_list_p(EntityList_t list, Entity* entity) { + FOREACH(item, list) { + if(*item == entity) { + return true; + } + } + return false; +} + +void level_free(Level* level) { + level_clear(level); + + EntityList_clear(level->entities); + EntityList_clear(level->to_add); + EntityList_clear(level->to_remove); + + if(level->behaviour->context_size > 0) { + free(level->context); + } + + LEVEL_DEBUG("Freeing level at %p", level); + free(level); +} + +void level_clear(Level* level) { + size_t iterations = 0; + + do { + // process to_add and to_remove + level_process_add(level); + level_process_remove(level); + + // remove entities from entities list + FOREACH(item, level->entities) { + if(!level_entity_in_list_p(level->to_remove, *item)) { + level_remove_entity(level, *item); + } + } + + // check if we are looping too many times + iterations++; + if(iterations >= 100) { + LEVEL_ERROR("Level free looped too many times"); + } + + // entity_call_stop can call level_remove_entity or level_add_entity + // so we need to process to_add and to_remove again + } while(!EntityList_empty_p(level->to_add) || !EntityList_empty_p(level->to_remove)); +} + +Entity* level_add_entity(Level* level, const EntityDescription* description) { + Entity* entity = entity_alloc(description); + EntityList_push_back(level->to_add, entity); + entity_call_start(entity, level->manager); + return entity; +} + +void level_remove_entity(Level* level, Entity* entity) { + EntityList_push_back(level->to_remove, entity); + entity_call_stop(entity, level->manager); +} + +void level_send_event( + Level* level, + Entity* sender, + const EntityDescription* receiver_desc, + uint32_t type, + EntityEventValue value) { + FOREACH(item, level->entities) { + if(receiver_desc == entity_description_get(*item) || receiver_desc == NULL) { + entity_send_event(sender, *item, level->manager, type, value); + } + } +} + +static void level_process_update(Level* level, GameManager* manager) { + FOREACH(item, level->entities) { + entity_call_update(*item, manager); + } +} + +static void level_process_collision(Level* level, GameManager* manager) { + EntityList_it_t it_first; + EntityList_it_t it_second; + + EntityList_it(it_first, level->entities); + while(!EntityList_end_p(it_first)) { + Entity* first = *EntityList_ref(it_first); + if(entity_collider_exists(first)) { + // start second iterator at the next entity, + // so we don't check the same pair twice + EntityList_it_set(it_second, it_first); + EntityList_next(it_second); + while(!EntityList_end_p(it_second)) { + Entity* second = *EntityList_ref(it_second); + if(first->collider_dirty || second->collider_dirty) { + if(entity_collider_exists(second)) { + if(entity_collider_check_collision(first, second)) { + entity_call_collision(first, second, manager); + entity_call_collision(second, first, manager); + } + } + } + EntityList_next(it_second); + } + } + EntityList_next(it_first); + } + + FOREACH(item, level->entities) { + (*item)->collider_dirty = false; + } +} + +void level_update(Level* level, GameManager* manager) { + level_process_add(level); + level_process_remove(level); + level_process_update(level, manager); + level_process_collision(level, manager); +} + +void level_render(Level* level, GameManager* manager, Canvas* canvas) { + FOREACH(item, level->entities) { + entity_call_render(*item, manager, canvas); + } +} + +void level_call_start(Level* level) { + if(level->behaviour->start) { + level->behaviour->start(level, level->manager, level->context); + } +} + +void level_call_stop(Level* level) { + if(level->behaviour->stop) { + level->behaviour->stop(level, level->manager, level->context); + } +} + +void level_call_alloc(Level* level) { + if(level->behaviour->alloc) { + level->behaviour->alloc(level, level->manager, level->context); + } +} + +void level_call_free(Level* level) { + if(level->behaviour->free) { + level->behaviour->free(level, level->manager, level->context); + } +} \ No newline at end of file diff --git a/flip_world/engine/level.h b/flip_world/engine/level.h new file mode 100644 index 000000000..0b745e4b0 --- /dev/null +++ b/flip_world/engine/level.h @@ -0,0 +1,63 @@ +#pragma once +#include +#include "entity.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef struct GameManager GameManager; + + typedef struct + { + void (*alloc)(Level *level, GameManager *manager, void *context); + void (*free)(Level *level, GameManager *manager, void *context); + void (*start)(Level *level, GameManager *manager, void *context); + void (*stop)(Level *level, GameManager *manager, void *context); + size_t context_size; + } LevelBehaviour; + + /** + * @brief Remove all entities from the level + * + * @param level level instance + */ + void level_clear(Level *level); + + /** + * @brief Add an entity to the level + * + * @param level level instance + * @param behaviour entity behaviour + * @return Entity* + */ + Entity *level_add_entity(Level *level, const EntityDescription *behaviour); + + /** + * @brief Remove an entity from the level + * + * @param level level instance + * @param entity entity to remove + */ + void level_remove_entity(Level *level, Entity *entity); + + /** + * @brief Send an event to all entities of a certain type in the level + * + * @param level level instance + * @param sender entity that sends the event + * @param receiver_desc entity description that will receive the event, NULL for all entities + * @param type event type + * @param value event value + */ + void level_send_event( + Level *level, + Entity *sender, + const EntityDescription *receiver_desc, + uint32_t type, + EntityEventValue value); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/level_i.h b/flip_world/engine/level_i.h new file mode 100644 index 000000000..2bdd83592 --- /dev/null +++ b/flip_world/engine/level_i.h @@ -0,0 +1,26 @@ +#pragma once +#include "level.h" + +#ifdef __cplusplus +extern "C" { +#endif + +Level* level_alloc(const LevelBehaviour* behaviour, GameManager* manager); + +void level_free(Level* level); + +void level_update(Level* level, GameManager* manager); + +void level_render(Level* level, GameManager* manager, Canvas* canvas); + +void level_call_alloc(Level* level); + +void level_call_free(Level* level); + +void level_call_start(Level* level); + +void level_call_stop(Level* level); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/main.c b/flip_world/engine/main.c new file mode 100644 index 000000000..b423e812c --- /dev/null +++ b/flip_world/engine/main.c @@ -0,0 +1,59 @@ +#include +#include "engine.h" +#include "game_engine.h" +#include "game_manager_i.h" +#include "level_i.h" +#include "entity_i.h" + +// static void frame_cb(GameEngine *engine, Canvas *canvas, InputState input, void *context) +// { +// UNUSED(engine); +// GameManager *game_manager = context; +// game_manager_input_set(game_manager, input); +// game_manager_update(game_manager); +// game_manager_render(game_manager, canvas); +// } + +// int32_t game_app(void *p) +// { +// UNUSED(p); +// GameManager *game_manager = game_manager_alloc(); + +// GameEngineSettings settings = game_engine_settings_init(); +// settings.target_fps = game.target_fps; +// settings.show_fps = game.show_fps; +// settings.always_backlight = game.always_backlight; +// settings.frame_callback = frame_cb; +// settings.context = game_manager; + +// GameEngine *engine = game_engine_alloc(settings); +// game_manager_engine_set(game_manager, engine); + +// void *game_context = NULL; +// if (game.context_size > 0) +// { +// game_context = malloc(game.context_size); +// game_manager_game_context_set(game_manager, game_context); +// } +// game.start(game_manager, game_context); + +// game_engine_run(engine); +// game_engine_free(engine); + +// game_manager_free(game_manager); + +// game.stop(game_context); +// if (game_context) +// { +// free(game_context); +// } + +// int32_t entities = entities_get_count(); +// if (entities != 0) +// { +// FURI_LOG_E("Game", "Memory leak detected: %ld entities still allocated", entities); +// return -1; +// } + +// return 0; +// } \ No newline at end of file diff --git a/flip_world/engine/scripts/sprite_builder.py b/flip_world/engine/scripts/sprite_builder.py new file mode 100644 index 000000000..10a0d8632 --- /dev/null +++ b/flip_world/engine/scripts/sprite_builder.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +import argparse +import io +import logging +import os +import struct + +from PIL import Image, ImageOps + +# XBM flipper sprite (.fxbm) is 1-bit depth, width-padded to 8 bits +# file format: +# uint32 size of the rest of the file in bytes +# uint32 width in px +# uint32 height in px +# uint8[] pixel data, every row is padded to 8 bits (like XBM) + +def image2xbm(input_file_path): + with Image.open(input_file_path) as im: + with io.BytesIO() as output: + bw = im.convert("1") + bw = ImageOps.invert(bw) + bw.save(output, format="XBM") + return output.getvalue() + +def xbm2fxbm(data): + # hell as it is, but it works + f = io.StringIO(data.decode().strip()) + width = int(f.readline().strip().split(" ")[2]) + height = int(f.readline().strip().split(" ")[2]) + data = f.read().strip().replace("\n", "").replace(" ", "").split("=")[1][:-1] + data_str = data[1:-1].replace(",", " ").replace("0x", "") + image_bin = bytearray.fromhex(data_str) + + output = struct.pack("accel_scale = accel_fs_modes[full_scale].value; + uint8_t reg_value = accel_fs_modes[full_scale].reg_mask | rate; + return icm42688p_write_reg(icm42688p->spi_bus, ICM42688_ACCEL_CONFIG0, reg_value); +} + +float icm42688p_accel_get_full_scale(ICM42688P* icm42688p) { + return icm42688p->accel_scale; +} + +bool icm42688p_gyro_config( + ICM42688P* icm42688p, + ICM42688PGyroFullScale full_scale, + ICM42688PDataRate rate) { + icm42688p->gyro_scale = gyro_fs_modes[full_scale].value; + uint8_t reg_value = gyro_fs_modes[full_scale].reg_mask | rate; + return icm42688p_write_reg(icm42688p->spi_bus, ICM42688_GYRO_CONFIG0, reg_value); +} + +float icm42688p_gyro_get_full_scale(ICM42688P* icm42688p) { + return icm42688p->gyro_scale; +} + +bool icm42688p_read_accel_raw(ICM42688P* icm42688p, ICM42688PRawData* data) { + bool ret = icm42688p_read_mem( + icm42688p->spi_bus, ICM42688_ACCEL_DATA_X1, (uint8_t*)data, sizeof(ICM42688PRawData)); + return ret; +} + +bool icm42688p_read_gyro_raw(ICM42688P* icm42688p, ICM42688PRawData* data) { + bool ret = icm42688p_read_mem( + icm42688p->spi_bus, ICM42688_GYRO_DATA_X1, (uint8_t*)data, sizeof(ICM42688PRawData)); + return ret; +} + +bool icm42688p_write_gyro_offset(ICM42688P* icm42688p, ICM42688PScaledData* scaled_data) { + if((fabsf(scaled_data->x) > 64.f) || (fabsf(scaled_data->y) > 64.f) || + (fabsf(scaled_data->z) > 64.f)) { + return false; + } + + uint16_t offset_x = (uint16_t)(-(int16_t)(scaled_data->x * 32.f) * 16) >> 4; + uint16_t offset_y = (uint16_t)(-(int16_t)(scaled_data->y * 32.f) * 16) >> 4; + uint16_t offset_z = (uint16_t)(-(int16_t)(scaled_data->z * 32.f) * 16) >> 4; + + uint8_t offset_regs[9]; + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 4); + icm42688p_read_mem(icm42688p->spi_bus, ICM42688_OFFSET_USER0, offset_regs, 5); + + offset_regs[0] = offset_x & 0xFF; + offset_regs[1] = (offset_x & 0xF00) >> 8; + offset_regs[1] |= (offset_y & 0xF00) >> 4; + offset_regs[2] = offset_y & 0xFF; + offset_regs[3] = offset_z & 0xFF; + offset_regs[4] &= 0xF0; + offset_regs[4] |= (offset_z & 0x0F00) >> 8; + + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_OFFSET_USER0, offset_regs[0]); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_OFFSET_USER1, offset_regs[1]); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_OFFSET_USER2, offset_regs[2]); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_OFFSET_USER3, offset_regs[3]); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_OFFSET_USER4, offset_regs[4]); + + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 0); + return true; +} + +void icm42688p_apply_scale(ICM42688PRawData* raw_data, float full_scale, ICM42688PScaledData* data) { + data->x = ((float)(raw_data->x)) / 32768.f * full_scale; + data->y = ((float)(raw_data->y)) / 32768.f * full_scale; + data->z = ((float)(raw_data->z)) / 32768.f * full_scale; +} + +void icm42688p_apply_scale_fifo( + ICM42688P* icm42688p, + ICM42688PFifoPacket* fifo_data, + ICM42688PScaledData* accel_data, + ICM42688PScaledData* gyro_data) { + float full_scale = icm42688p->accel_scale; + accel_data->x = ((float)(fifo_data->a_x)) / 32768.f * full_scale; + accel_data->y = ((float)(fifo_data->a_y)) / 32768.f * full_scale; + accel_data->z = ((float)(fifo_data->a_z)) / 32768.f * full_scale; + + full_scale = icm42688p->gyro_scale; + gyro_data->x = ((float)(fifo_data->g_x)) / 32768.f * full_scale; + gyro_data->y = ((float)(fifo_data->g_y)) / 32768.f * full_scale; + gyro_data->z = ((float)(fifo_data->g_z)) / 32768.f * full_scale; +} + +float icm42688p_read_temp(ICM42688P* icm42688p) { + uint8_t reg_val[2]; + + icm42688p_read_mem(icm42688p->spi_bus, ICM42688_TEMP_DATA1, reg_val, 2); + int16_t temp_int = (reg_val[0] << 8) | reg_val[1]; + return ((float)temp_int / 132.48f) + 25.f; +} + +void icm42688_fifo_enable( + ICM42688P* icm42688p, + ICM42688PIrqCallback irq_callback, + void* irq_context) { + // FIFO mode: stream + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_FIFO_CONFIG, (1 << 6)); + // Little-endian data, FIFO count in records + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INTF_CONFIG0, (1 << 7) | (1 << 6)); + // FIFO partial read, FIFO packet: gyro + accel TODO: 20bit + icm42688p_write_reg( + icm42688p->spi_bus, ICM42688_FIFO_CONFIG1, (1 << 6) | (1 << 5) | (1 << 1) | (1 << 0)); + // FIFO irq watermark + uint16_t fifo_watermark = 1; + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_FIFO_CONFIG2, fifo_watermark & 0xFF); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_FIFO_CONFIG3, fifo_watermark >> 8); + + // IRQ1: push-pull, active high + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_CONFIG, (1 << 1) | (1 << 0)); + // Clear IRQ on status read + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_CONFIG0, 0); + // IRQ pulse duration + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_CONFIG1, (1 << 6) | (1 << 5)); + + uint8_t reg_data = 0; + icm42688p_read_reg(icm42688p->spi_bus, ICM42688_INT_STATUS, ®_data); + + furi_hal_gpio_init(icm42688p->irq_pin, GpioModeInterruptRise, GpioPullDown, GpioSpeedVeryHigh); + furi_hal_gpio_remove_int_callback(icm42688p->irq_pin); + furi_hal_gpio_add_int_callback(icm42688p->irq_pin, irq_callback, irq_context); + + // IRQ1 source: FIFO threshold + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE0, (1 << 2)); +} + +void icm42688_fifo_disable(ICM42688P* icm42688p) { + furi_hal_gpio_remove_int_callback(icm42688p->irq_pin); + furi_hal_gpio_init(icm42688p->irq_pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow); + + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE0, 0); + + // FIFO mode: bypass + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_FIFO_CONFIG, 0); +} + +uint16_t icm42688_fifo_get_count(ICM42688P* icm42688p) { + uint16_t reg_val = 0; + icm42688p_read_mem(icm42688p->spi_bus, ICM42688_FIFO_COUNTH, (uint8_t*)®_val, 2); + return reg_val; +} + +bool icm42688_fifo_read(ICM42688P* icm42688p, ICM42688PFifoPacket* data) { + icm42688p_read_mem( + icm42688p->spi_bus, ICM42688_FIFO_DATA, (uint8_t*)data, sizeof(ICM42688PFifoPacket)); + return (data->header) & (1 << 7); +} + +ICM42688P* icm42688p_alloc(FuriHalSpiBusHandle* spi_bus, const GpioPin* irq_pin) { + ICM42688P* icm42688p = malloc(sizeof(ICM42688P)); + icm42688p->spi_bus = spi_bus; + icm42688p->irq_pin = irq_pin; + return icm42688p; +} + +void icm42688p_free(ICM42688P* icm42688p) { + free(icm42688p); +} + +bool icm42688p_init(ICM42688P* icm42688p) { + furi_hal_spi_bus_handle_init(icm42688p->spi_bus); + + // Software reset + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 0); // Set reg bank to 0 + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_DEVICE_CONFIG, 0x01); // SPI Mode 0, SW reset + furi_delay_ms(1); + + uint8_t reg_value = 0; + bool read_ok = icm42688p_read_reg(icm42688p->spi_bus, ICM42688_WHO_AM_I, ®_value); + if(!read_ok) { + FURI_LOG_E(TAG, "Chip ID read failed"); + return false; + } else if(reg_value != ICM42688_WHOAMI) { + FURI_LOG_E( + TAG, "Sensor returned wrong ID 0x%02X, expected 0x%02X", reg_value, ICM42688_WHOAMI); + return false; + } + + // Disable all interrupts + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE0, 0); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE1, 0); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE3, 0); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE4, 0); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 4); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE6, 0); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE7, 0); + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 0); + + // Data format: little endian + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INTF_CONFIG0, 0); + + // Enable all sensors + icm42688p_write_reg( + icm42688p->spi_bus, + ICM42688_PWR_MGMT0, + ICM42688_PWR_TEMP_ON | ICM42688_PWR_GYRO_MODE_LN | ICM42688_PWR_ACCEL_MODE_LN); + furi_delay_ms(45); + + icm42688p_accel_config(icm42688p, AccelFullScale16G, DataRate1kHz); + icm42688p_gyro_config(icm42688p, GyroFullScale2000DPS, DataRate1kHz); + + return true; +} + +bool icm42688p_deinit(ICM42688P* icm42688p) { + // Software reset + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 0); // Set reg bank to 0 + icm42688p_write_reg(icm42688p->spi_bus, ICM42688_DEVICE_CONFIG, 0x01); // SPI Mode 0, SW reset + + furi_hal_spi_bus_handle_deinit(icm42688p->spi_bus); + return true; +} diff --git a/flip_world/engine/sensors/ICM42688P/ICM42688P.h b/flip_world/engine/sensors/ICM42688P/ICM42688P.h new file mode 100644 index 000000000..b04fb9809 --- /dev/null +++ b/flip_world/engine/sensors/ICM42688P/ICM42688P.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + DataRate32kHz = 0x01, + DataRate16kHz = 0x02, + DataRate8kHz = 0x03, + DataRate4kHz = 0x04, + DataRate2kHz = 0x05, + DataRate1kHz = 0x06, + DataRate200Hz = 0x07, + DataRate100Hz = 0x08, + DataRate50Hz = 0x09, + DataRate25Hz = 0x0A, + DataRate12_5Hz = 0x0B, + DataRate6_25Hz = 0x0C, // Accelerometer only + DataRate3_125Hz = 0x0D, // Accelerometer only + DataRate1_5625Hz = 0x0E, // Accelerometer only + DataRate500Hz = 0x0F, +} ICM42688PDataRate; + +typedef enum { + AccelFullScale16G = 0, + AccelFullScale8G, + AccelFullScale4G, + AccelFullScale2G, + AccelFullScaleTotal, +} ICM42688PAccelFullScale; + +typedef enum { + GyroFullScale2000DPS = 0, + GyroFullScale1000DPS, + GyroFullScale500DPS, + GyroFullScale250DPS, + GyroFullScale125DPS, + GyroFullScale62_5DPS, + GyroFullScale31_25DPS, + GyroFullScale15_625DPS, + GyroFullScaleTotal, +} ICM42688PGyroFullScale; + +typedef struct { + int16_t x; + int16_t y; + int16_t z; +} __attribute__((packed)) ICM42688PRawData; + +typedef struct { + uint8_t header; + int16_t a_x; + int16_t a_y; + int16_t a_z; + int16_t g_x; + int16_t g_y; + int16_t g_z; + uint8_t temp; + uint16_t ts; +} __attribute__((packed)) ICM42688PFifoPacket; + +typedef struct { + float x; + float y; + float z; +} ICM42688PScaledData; + +typedef struct ICM42688P ICM42688P; + +typedef void (*ICM42688PIrqCallback)(void* ctx); + +ICM42688P* icm42688p_alloc(FuriHalSpiBusHandle* spi_bus, const GpioPin* irq_pin); + +bool icm42688p_init(ICM42688P* icm42688p); + +bool icm42688p_deinit(ICM42688P* icm42688p); + +void icm42688p_free(ICM42688P* icm42688p); + +bool icm42688p_accel_config( + ICM42688P* icm42688p, + ICM42688PAccelFullScale full_scale, + ICM42688PDataRate rate); + +float icm42688p_accel_get_full_scale(ICM42688P* icm42688p); + +bool icm42688p_gyro_config( + ICM42688P* icm42688p, + ICM42688PGyroFullScale full_scale, + ICM42688PDataRate rate); + +float icm42688p_gyro_get_full_scale(ICM42688P* icm42688p); + +bool icm42688p_read_accel_raw(ICM42688P* icm42688p, ICM42688PRawData* data); + +bool icm42688p_read_gyro_raw(ICM42688P* icm42688p, ICM42688PRawData* data); + +bool icm42688p_write_gyro_offset(ICM42688P* icm42688p, ICM42688PScaledData* scaled_data); + +void icm42688p_apply_scale(ICM42688PRawData* raw_data, float full_scale, ICM42688PScaledData* data); + +void icm42688p_apply_scale_fifo( + ICM42688P* icm42688p, + ICM42688PFifoPacket* fifo_data, + ICM42688PScaledData* accel_data, + ICM42688PScaledData* gyro_data); + +float icm42688p_read_temp(ICM42688P* icm42688p); + +void icm42688_fifo_enable( + ICM42688P* icm42688p, + ICM42688PIrqCallback irq_callback, + void* irq_context); + +void icm42688_fifo_disable(ICM42688P* icm42688p); + +uint16_t icm42688_fifo_get_count(ICM42688P* icm42688p); + +bool icm42688_fifo_read(ICM42688P* icm42688p, ICM42688PFifoPacket* data); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/sensors/ICM42688P/ICM42688P_regs.h b/flip_world/engine/sensors/ICM42688P/ICM42688P_regs.h new file mode 100644 index 000000000..1967534df --- /dev/null +++ b/flip_world/engine/sensors/ICM42688P/ICM42688P_regs.h @@ -0,0 +1,176 @@ +#pragma once + +#define ICM42688_WHOAMI 0x47 + +// Bank 0 +#define ICM42688_DEVICE_CONFIG 0x11 +#define ICM42688_DRIVE_CONFIG 0x13 +#define ICM42688_INT_CONFIG 0x14 +#define ICM42688_FIFO_CONFIG 0x16 +#define ICM42688_TEMP_DATA1 0x1D +#define ICM42688_TEMP_DATA0 0x1E +#define ICM42688_ACCEL_DATA_X1 0x1F +#define ICM42688_ACCEL_DATA_X0 0x20 +#define ICM42688_ACCEL_DATA_Y1 0x21 +#define ICM42688_ACCEL_DATA_Y0 0x22 +#define ICM42688_ACCEL_DATA_Z1 0x23 +#define ICM42688_ACCEL_DATA_Z0 0x24 +#define ICM42688_GYRO_DATA_X1 0x25 +#define ICM42688_GYRO_DATA_X0 0x26 +#define ICM42688_GYRO_DATA_Y1 0x27 +#define ICM42688_GYRO_DATA_Y0 0x28 +#define ICM42688_GYRO_DATA_Z1 0x29 +#define ICM42688_GYRO_DATA_Z0 0x2A +#define ICM42688_TMST_FSYNCH 0x2B +#define ICM42688_TMST_FSYNCL 0x2C +#define ICM42688_INT_STATUS 0x2D +#define ICM42688_FIFO_COUNTH 0x2E +#define ICM42688_FIFO_COUNTL 0x2F +#define ICM42688_FIFO_DATA 0x30 +#define ICM42688_APEX_DATA0 0x31 +#define ICM42688_APEX_DATA1 0x32 +#define ICM42688_APEX_DATA2 0x33 +#define ICM42688_APEX_DATA3 0x34 +#define ICM42688_APEX_DATA4 0x35 +#define ICM42688_APEX_DATA5 0x36 +#define ICM42688_INT_STATUS2 0x37 +#define ICM42688_INT_STATUS3 0x38 +#define ICM42688_SIGNAL_PATH_RESET 0x4B +#define ICM42688_INTF_CONFIG0 0x4C +#define ICM42688_INTF_CONFIG1 0x4D +#define ICM42688_PWR_MGMT0 0x4E +#define ICM42688_GYRO_CONFIG0 0x4F +#define ICM42688_ACCEL_CONFIG0 0x50 +#define ICM42688_GYRO_CONFIG1 0x51 +#define ICM42688_GYRO_ACCEL_CONFIG0 0x52 +#define ICM42688_ACCEL_CONFIG1 0x53 +#define ICM42688_TMST_CONFIG 0x54 +#define ICM42688_APEX_CONFIG0 0x56 +#define ICM42688_SMD_CONFIG 0x57 +#define ICM42688_FIFO_CONFIG1 0x5F +#define ICM42688_FIFO_CONFIG2 0x60 +#define ICM42688_FIFO_CONFIG3 0x61 +#define ICM42688_FSYNC_CONFIG 0x62 +#define ICM42688_INT_CONFIG0 0x63 +#define ICM42688_INT_CONFIG1 0x64 +#define ICM42688_INT_SOURCE0 0x65 +#define ICM42688_INT_SOURCE1 0x66 +#define ICM42688_INT_SOURCE3 0x68 +#define ICM42688_INT_SOURCE4 0x69 +#define ICM42688_FIFO_LOST_PKT0 0x6C +#define ICM42688_FIFO_LOST_PKT1 0x6D +#define ICM42688_SELF_TEST_CONFIG 0x70 +#define ICM42688_WHO_AM_I 0x75 +#define ICM42688_REG_BANK_SEL 0x76 + +// Bank 1 +#define ICM42688_SENSOR_CONFIG0 0x03 +#define ICM42688_GYRO_CONFIG_STATIC2 0x0B +#define ICM42688_GYRO_CONFIG_STATIC3 0x0C +#define ICM42688_GYRO_CONFIG_STATIC4 0x0D +#define ICM42688_GYRO_CONFIG_STATIC5 0x0E +#define ICM42688_GYRO_CONFIG_STATIC6 0x0F +#define ICM42688_GYRO_CONFIG_STATIC7 0x10 +#define ICM42688_GYRO_CONFIG_STATIC8 0x11 +#define ICM42688_GYRO_CONFIG_STATIC9 0x12 +#define ICM42688_GYRO_CONFIG_STATIC10 0x13 +#define ICM42688_XG_ST_DATA 0x5F +#define ICM42688_YG_ST_DATA 0x60 +#define ICM42688_ZG_ST_DATA 0x61 +#define ICM42688_TMSTVAL0 0x62 +#define ICM42688_TMSTVAL1 0x63 +#define ICM42688_TMSTVAL2 0x64 +#define ICM42688_INTF_CONFIG4 0x7A +#define ICM42688_INTF_CONFIG5 0x7B +#define ICM42688_INTF_CONFIG6 0x7C + +// Bank 2 +#define ICM42688_ACCEL_CONFIG_STATIC2 0x03 +#define ICM42688_ACCEL_CONFIG_STATIC3 0x04 +#define ICM42688_ACCEL_CONFIG_STATIC4 0x05 +#define ICM42688_XA_ST_DATA 0x3B +#define ICM42688_YA_ST_DATA 0x3C +#define ICM42688_ZA_ST_DATA 0x3D + +// Bank 4 +#define ICM42688_APEX_CONFIG1 0x40 +#define ICM42688_APEX_CONFIG2 0x41 +#define ICM42688_APEX_CONFIG3 0x42 +#define ICM42688_APEX_CONFIG4 0x43 +#define ICM42688_APEX_CONFIG5 0x44 +#define ICM42688_APEX_CONFIG6 0x45 +#define ICM42688_APEX_CONFIG7 0x46 +#define ICM42688_APEX_CONFIG8 0x47 +#define ICM42688_APEX_CONFIG9 0x48 +#define ICM42688_ACCEL_WOM_X_THR 0x4A +#define ICM42688_ACCEL_WOM_Y_THR 0x4B +#define ICM42688_ACCEL_WOM_Z_THR 0x4C +#define ICM42688_INT_SOURCE6 0x4D +#define ICM42688_INT_SOURCE7 0x4E +#define ICM42688_INT_SOURCE8 0x4F +#define ICM42688_INT_SOURCE9 0x50 +#define ICM42688_INT_SOURCE10 0x51 +#define ICM42688_OFFSET_USER0 0x77 +#define ICM42688_OFFSET_USER1 0x78 +#define ICM42688_OFFSET_USER2 0x79 +#define ICM42688_OFFSET_USER3 0x7A +#define ICM42688_OFFSET_USER4 0x7B +#define ICM42688_OFFSET_USER5 0x7C +#define ICM42688_OFFSET_USER6 0x7D +#define ICM42688_OFFSET_USER7 0x7E +#define ICM42688_OFFSET_USER8 0x7F + +// PWR_MGMT0 +#define ICM42688_PWR_TEMP_ON (0 << 5) +#define ICM42688_PWR_TEMP_OFF (1 << 5) +#define ICM42688_PWR_IDLE (1 << 4) +#define ICM42688_PWR_GYRO_MODE_OFF (0 << 2) +#define ICM42688_PWR_GYRO_MODE_LN (3 << 2) +#define ICM42688_PWR_ACCEL_MODE_OFF (0 << 0) +#define ICM42688_PWR_ACCEL_MODE_LP (2 << 0) +#define ICM42688_PWR_ACCEL_MODE_LN (3 << 0) + +// GYRO_CONFIG0 +#define ICM42688_GFS_2000DPS (0x00 << 5) +#define ICM42688_GFS_1000DPS (0x01 << 5) +#define ICM42688_GFS_500DPS (0x02 << 5) +#define ICM42688_GFS_250DPS (0x03 << 5) +#define ICM42688_GFS_125DPS (0x04 << 5) +#define ICM42688_GFS_62_5DPS (0x05 << 5) +#define ICM42688_GFS_31_25DPS (0x06 << 5) +#define ICM42688_GFS_15_625DPS (0x07 << 5) + +#define ICM42688_GODR_32kHz 0x01 +#define ICM42688_GODR_16kHz 0x02 +#define ICM42688_GODR_8kHz 0x03 +#define ICM42688_GODR_4kHz 0x04 +#define ICM42688_GODR_2kHz 0x05 +#define ICM42688_GODR_1kHz 0x06 +#define ICM42688_GODR_200Hz 0x07 +#define ICM42688_GODR_100Hz 0x08 +#define ICM42688_GODR_50Hz 0x09 +#define ICM42688_GODR_25Hz 0x0A +#define ICM42688_GODR_12_5Hz 0x0B +#define ICM42688_GODR_500Hz 0x0F + +// ACCEL_CONFIG0 +#define ICM42688_AFS_16G (0x00 << 5) +#define ICM42688_AFS_8G (0x01 << 5) +#define ICM42688_AFS_4G (0x02 << 5) +#define ICM42688_AFS_2G (0x03 << 5) + +#define ICM42688_AODR_32kHz 0x01 +#define ICM42688_AODR_16kHz 0x02 +#define ICM42688_AODR_8kHz 0x03 +#define ICM42688_AODR_4kHz 0x04 +#define ICM42688_AODR_2kHz 0x05 +#define ICM42688_AODR_1kHz 0x06 +#define ICM42688_AODR_200Hz 0x07 +#define ICM42688_AODR_100Hz 0x08 +#define ICM42688_AODR_50Hz 0x09 +#define ICM42688_AODR_25Hz 0x0A +#define ICM42688_AODR_12_5Hz 0x0B +#define ICM42688_AODR_6_25Hz 0x0C +#define ICM42688_AODR_3_125Hz 0x0D +#define ICM42688_AODR_1_5625Hz 0x0E +#define ICM42688_AODR_500Hz 0x0F diff --git a/flip_world/engine/sensors/imu.c b/flip_world/engine/sensors/imu.c new file mode 100644 index 000000000..cae742d1c --- /dev/null +++ b/flip_world/engine/sensors/imu.c @@ -0,0 +1,326 @@ +#include +#include "imu.h" +#include "ICM42688P/ICM42688P.h" + +#define TAG "IMU" + +#define ACCEL_GYRO_RATE DataRate100Hz + +#define FILTER_SAMPLE_FREQ 100.f +#define FILTER_BETA 0.08f + +#define SAMPLE_RATE_DIV 5 + +#define SENSITIVITY_K 30.f +#define EXP_RATE 1.1f + +#define IMU_CALI_AVG 64 + +typedef enum { + ImuStop = (1 << 0), + ImuNewData = (1 << 1), +} ImuThreadFlags; + +#define FLAGS_ALL (ImuStop | ImuNewData) + +typedef struct { + float q0; + float q1; + float q2; + float q3; + float roll; + float pitch; + float yaw; +} ImuProcessedData; + +typedef struct { + FuriThread* thread; + ICM42688P* icm42688p; + ImuProcessedData processed_data; +} ImuThread; + +static void imu_madgwick_filter( + ImuProcessedData* out, + ICM42688PScaledData* accel, + ICM42688PScaledData* gyro); + +static void imu_irq_callback(void* context) { + furi_assert(context); + ImuThread* imu = context; + furi_thread_flags_set(furi_thread_get_id(imu->thread), ImuNewData); +} + +static void imu_process_data(ImuThread* imu, ICM42688PFifoPacket* in_data) { + ICM42688PScaledData accel_data; + ICM42688PScaledData gyro_data; + + // Get accel and gyro data in g and degrees/s + icm42688p_apply_scale_fifo(imu->icm42688p, in_data, &accel_data, &gyro_data); + + // Gyro: degrees/s to rads/s + gyro_data.x = gyro_data.x / 180.f * M_PI; + gyro_data.y = gyro_data.y / 180.f * M_PI; + gyro_data.z = gyro_data.z / 180.f * M_PI; + + // Sensor Fusion algorithm + ImuProcessedData* out = &imu->processed_data; + imu_madgwick_filter(out, &accel_data, &gyro_data); + + // Quaternion to euler angles + float roll = atan2f( + out->q0 * out->q1 + out->q2 * out->q3, 0.5f - out->q1 * out->q1 - out->q2 * out->q2); + float pitch = asinf(-2.0f * (out->q1 * out->q3 - out->q0 * out->q2)); + float yaw = atan2f( + out->q1 * out->q2 + out->q0 * out->q3, 0.5f - out->q2 * out->q2 - out->q3 * out->q3); + + // Euler angles: rads to degrees + out->roll = roll / M_PI * 180.f; + out->pitch = pitch / M_PI * 180.f; + out->yaw = yaw / M_PI * 180.f; +} + +static void calibrate_gyro(ImuThread* imu) { + ICM42688PRawData data; + ICM42688PScaledData offset_scaled = {.x = 0.f, .y = 0.f, .z = 0.f}; + + icm42688p_write_gyro_offset(imu->icm42688p, &offset_scaled); + furi_delay_ms(10); + + int32_t avg_x = 0; + int32_t avg_y = 0; + int32_t avg_z = 0; + + for(uint8_t i = 0; i < IMU_CALI_AVG; i++) { + icm42688p_read_gyro_raw(imu->icm42688p, &data); + avg_x += data.x; + avg_y += data.y; + avg_z += data.z; + furi_delay_ms(2); + } + + data.x = avg_x / IMU_CALI_AVG; + data.y = avg_y / IMU_CALI_AVG; + data.z = avg_z / IMU_CALI_AVG; + + icm42688p_apply_scale(&data, icm42688p_gyro_get_full_scale(imu->icm42688p), &offset_scaled); + FURI_LOG_I( + TAG, + "Offsets: x %f, y %f, z %f", + (double)offset_scaled.x, + (double)offset_scaled.y, + (double)offset_scaled.z); + icm42688p_write_gyro_offset(imu->icm42688p, &offset_scaled); +} + +// static float imu_angle_diff(float a, float b) { +// float diff = a - b; +// if(diff > 180.f) +// diff -= 360.f; +// else if(diff < -180.f) +// diff += 360.f; + +// return diff; +// } + +static int32_t imu_thread(void* context) { + furi_assert(context); + ImuThread* imu = context; + + // float yaw_last = 0.f; + // float pitch_last = 0.f; + // float diff_x = 0.f; + // float diff_y = 0.f; + + calibrate_gyro(imu); + + icm42688p_accel_config(imu->icm42688p, AccelFullScale16G, ACCEL_GYRO_RATE); + icm42688p_gyro_config(imu->icm42688p, GyroFullScale2000DPS, ACCEL_GYRO_RATE); + + imu->processed_data.q0 = 1.f; + imu->processed_data.q1 = 0.f; + imu->processed_data.q2 = 0.f; + imu->processed_data.q3 = 0.f; + icm42688_fifo_enable(imu->icm42688p, imu_irq_callback, imu); + + while(1) { + uint32_t events = furi_thread_flags_wait(FLAGS_ALL, FuriFlagWaitAny, FuriWaitForever); + + if(events & ImuStop) { + break; + } + + if(events & ImuNewData) { + uint16_t data_pending = icm42688_fifo_get_count(imu->icm42688p); + ICM42688PFifoPacket data; + while(data_pending--) { + icm42688_fifo_read(imu->icm42688p, &data); + imu_process_data(imu, &data); + } + } + } + + icm42688_fifo_disable(imu->icm42688p); + + return 0; +} + +ImuThread* imu_start(ICM42688P* icm42688p) { + ImuThread* imu = malloc(sizeof(ImuThread)); + imu->icm42688p = icm42688p; + imu->thread = furi_thread_alloc_ex("ImuThread", 4096, imu_thread, imu); + + furi_thread_start(imu->thread); + + return imu; +} + +void imu_stop(ImuThread* imu) { + furi_assert(imu); + + furi_thread_flags_set(furi_thread_get_id(imu->thread), ImuStop); + + furi_thread_join(imu->thread); + furi_thread_free(imu->thread); + + free(imu); +} + +static float imu_inv_sqrt(float number) { + union { + float f; + uint32_t i; + } conv = {.f = number}; + conv.i = 0x5F3759Df - (conv.i >> 1); + conv.f *= 1.5f - (number * 0.5f * conv.f * conv.f); + return conv.f; +} + +/* Simple madgwik filter, based on: https://github.com/arduino-libraries/MadgwickAHRS/ */ + +static void imu_madgwick_filter( + ImuProcessedData* out, + ICM42688PScaledData* accel, + ICM42688PScaledData* gyro) { + float recipNorm; + float s0, s1, s2, s3; + float qDot1, qDot2, qDot3, qDot4; + float _2q0, _2q1, _2q2, _2q3, _4q0, _4q1, _4q2, _8q1, _8q2, q0q0, q1q1, q2q2, q3q3; + + // Rate of change of quaternion from gyroscope + qDot1 = 0.5f * (-out->q1 * gyro->x - out->q2 * gyro->y - out->q3 * gyro->z); + qDot2 = 0.5f * (out->q0 * gyro->x + out->q2 * gyro->z - out->q3 * gyro->y); + qDot3 = 0.5f * (out->q0 * gyro->y - out->q1 * gyro->z + out->q3 * gyro->x); + qDot4 = 0.5f * (out->q0 * gyro->z + out->q1 * gyro->y - out->q2 * gyro->x); + + // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation) + if(!((accel->x == 0.0f) && (accel->y == 0.0f) && (accel->z == 0.0f))) { + // Normalise accelerometer measurement + recipNorm = imu_inv_sqrt(accel->x * accel->x + accel->y * accel->y + accel->z * accel->z); + accel->x *= recipNorm; + accel->y *= recipNorm; + accel->z *= recipNorm; + + // Auxiliary variables to avoid repeated arithmetic + _2q0 = 2.0f * out->q0; + _2q1 = 2.0f * out->q1; + _2q2 = 2.0f * out->q2; + _2q3 = 2.0f * out->q3; + _4q0 = 4.0f * out->q0; + _4q1 = 4.0f * out->q1; + _4q2 = 4.0f * out->q2; + _8q1 = 8.0f * out->q1; + _8q2 = 8.0f * out->q2; + q0q0 = out->q0 * out->q0; + q1q1 = out->q1 * out->q1; + q2q2 = out->q2 * out->q2; + q3q3 = out->q3 * out->q3; + + // Gradient decent algorithm corrective step + s0 = _4q0 * q2q2 + _2q2 * accel->x + _4q0 * q1q1 - _2q1 * accel->y; + s1 = _4q1 * q3q3 - _2q3 * accel->x + 4.0f * q0q0 * out->q1 - _2q0 * accel->y - _4q1 + + _8q1 * q1q1 + _8q1 * q2q2 + _4q1 * accel->z; + s2 = 4.0f * q0q0 * out->q2 + _2q0 * accel->x + _4q2 * q3q3 - _2q3 * accel->y - _4q2 + + _8q2 * q1q1 + _8q2 * q2q2 + _4q2 * accel->z; + s3 = 4.0f * q1q1 * out->q3 - _2q1 * accel->x + 4.0f * q2q2 * out->q3 - _2q2 * accel->y; + recipNorm = + imu_inv_sqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude + s0 *= recipNorm; + s1 *= recipNorm; + s2 *= recipNorm; + s3 *= recipNorm; + + // Apply feedback step + qDot1 -= FILTER_BETA * s0; + qDot2 -= FILTER_BETA * s1; + qDot3 -= FILTER_BETA * s2; + qDot4 -= FILTER_BETA * s3; + } + + // Integrate rate of change of quaternion to yield quaternion + out->q0 += qDot1 * (1.0f / FILTER_SAMPLE_FREQ); + out->q1 += qDot2 * (1.0f / FILTER_SAMPLE_FREQ); + out->q2 += qDot3 * (1.0f / FILTER_SAMPLE_FREQ); + out->q3 += qDot4 * (1.0f / FILTER_SAMPLE_FREQ); + + // Normalise quaternion + recipNorm = imu_inv_sqrt( + out->q0 * out->q0 + out->q1 * out->q1 + out->q2 * out->q2 + out->q3 * out->q3); + out->q0 *= recipNorm; + out->q1 *= recipNorm; + out->q2 *= recipNorm; + out->q3 *= recipNorm; +} + +/* IMU API */ + +struct Imu { + FuriHalSpiBusHandle* icm42688p_device; + ICM42688P* icm42688p; + ImuThread* thread; + bool present; +}; + +Imu* imu_alloc(void) { + Imu* imu = malloc(sizeof(Imu)); + imu->icm42688p_device = malloc(sizeof(FuriHalSpiBusHandle)); + memcpy(imu->icm42688p_device, &furi_hal_spi_bus_handle_external, sizeof(FuriHalSpiBusHandle)); + imu->icm42688p_device->cs = &gpio_ext_pc3; + + imu->icm42688p = icm42688p_alloc(imu->icm42688p_device, &gpio_ext_pb2); + imu->present = icm42688p_init(imu->icm42688p); + + if(imu->present) { + imu->thread = imu_start(imu->icm42688p); + } + + return imu; +} + +void imu_free(Imu* imu) { + if(imu->present) { + imu_stop(imu->thread); + } + icm42688p_deinit(imu->icm42688p); + icm42688p_free(imu->icm42688p); + free(imu->icm42688p_device); + free(imu); +} + +bool imu_present(Imu* imu) { + return imu->present; +} + +float imu_pitch_get(Imu* imu) { + // we pretend that reading a float is an atomic operation + return imu->thread->processed_data.pitch; +} + +float imu_roll_get(Imu* imu) { + // we pretend that reading a float is an atomic operation + return imu->thread->processed_data.roll; +} + +float imu_yaw_get(Imu* imu) { + // we pretend that reading a float is an atomic operation + return imu->thread->processed_data.yaw; +} \ No newline at end of file diff --git a/flip_world/engine/sensors/imu.h b/flip_world/engine/sensors/imu.h new file mode 100644 index 000000000..42a08fe0c --- /dev/null +++ b/flip_world/engine/sensors/imu.h @@ -0,0 +1,15 @@ +#pragma once + +typedef struct Imu Imu; + +Imu* imu_alloc(void); + +void imu_free(Imu* imu); + +bool imu_present(Imu* imu); + +float imu_pitch_get(Imu* imu); + +float imu_roll_get(Imu* imu); + +float imu_yaw_get(Imu* imu); \ No newline at end of file diff --git a/flip_world/engine/sprite.c b/flip_world/engine/sprite.c new file mode 100644 index 000000000..395f8a31b --- /dev/null +++ b/flip_world/engine/sprite.c @@ -0,0 +1,69 @@ +#include "sprite.h" +#include + +#ifdef SPRITE_DEBUG +#define SPRITE_D(...) FURI_LOG_D("Sprite", __VA_ARGS__) +#define SPRITE_E(...) FURI_LOG_E("Sprite", __VA_ARGS__) +#else +#define SPRITE_D(...) +#define SPRITE_E(...) +#endif + +struct Sprite { + uint32_t width; + uint32_t height; + uint8_t data[]; +}; + +Sprite* sprite_alloc(const char* path) { + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + Sprite* sprite = NULL; + + do { + if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { + SPRITE_E("Failed to open file: %s", path); + break; + } + + uint32_t size = 0; + if(storage_file_read(file, &size, sizeof(size)) != sizeof(size)) { + SPRITE_E("Failed to read file size: %s", path); + break; + } + + sprite = malloc(size); + if(storage_file_read(file, sprite, size) != size) { + SPRITE_E("Failed to read file: %s", path); + free(sprite); + sprite = NULL; + break; + } + + SPRITE_D( + "Loaded sprite: %s, width: %lu, height: %lu", path, sprite->width, sprite->height); + } while(false); + + storage_file_free(file); + + return sprite; +} + +void sprite_free(Sprite* sprite) { + free(sprite); +} + +size_t sprite_get_width(Sprite* sprite) { + return sprite->width; +} + +size_t sprite_get_height(Sprite* sprite) { + return sprite->height; +} + +void canvas_draw_sprite(Canvas* canvas, Sprite* sprite, int32_t x, int32_t y) { + furi_check(sprite->width); + furi_check(sprite->height); + furi_check(sprite->data); + canvas_draw_xbm(canvas, x, y, sprite->width, sprite->height, sprite->data); +} \ No newline at end of file diff --git a/flip_world/engine/sprite.h b/flip_world/engine/sprite.h new file mode 100644 index 000000000..992f96e0a --- /dev/null +++ b/flip_world/engine/sprite.h @@ -0,0 +1,44 @@ +#pragma once +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct Sprite Sprite; + +/** Sprite allocator + * @return Sprite* Sprite instance or NULL, if failed + */ +Sprite* sprite_alloc(const char* path); + +/** Sprite deallocator + * @param sprite Sprite instance + */ +void sprite_free(Sprite* sprite); + +/** Get sprite width + * @param sprite Sprite instance + * @return size_t sprite width + */ +size_t sprite_get_width(Sprite* sprite); + +/** Get sprite height + * @param sprite Sprite instance + * @return size_t sprite height + */ +size_t sprite_get_height(Sprite* sprite); + +/** Draw sprite on canvas + * @param canvas Canvas instance + * @param sprite Sprite instance + * @param x x coordinate + * @param y y coordinate + */ +void canvas_draw_sprite(Canvas* canvas, Sprite* sprite, int32_t x, int32_t y); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/engine/vector.c b/flip_world/engine/vector.c new file mode 100644 index 000000000..64224988d --- /dev/null +++ b/flip_world/engine/vector.c @@ -0,0 +1,33 @@ +#include "vector.h" + +Vector vector_add(Vector a, Vector b) { + return (Vector){.x = a.x + b.x, .y = a.y + b.y}; +} + +Vector vector_sub(Vector a, Vector b) { + return (Vector){.x = a.x - b.x, .y = a.y - b.y}; +} + +Vector vector_mul(Vector a, Vector b) { + return (Vector){.x = a.x * b.x, .y = a.y * b.y}; +} + +Vector vector_div(Vector a, Vector b) { + return (Vector){.x = a.x / b.x, .y = a.y / b.y}; +} + +Vector vector_addf(Vector a, float b) { + return (Vector){.x = a.x + b, .y = a.y + b}; +} + +Vector vector_subf(Vector a, float b) { + return (Vector){.x = a.x - b, .y = a.y - b}; +} + +Vector vector_mulf(Vector a, float b) { + return (Vector){.x = a.x * b, .y = a.y * b}; +} + +Vector vector_divf(Vector a, float b) { + return (Vector){.x = a.x / b, .y = a.y / b}; +} \ No newline at end of file diff --git a/flip_world/engine/vector.h b/flip_world/engine/vector.h new file mode 100644 index 000000000..6a842f8d5 --- /dev/null +++ b/flip_world/engine/vector.h @@ -0,0 +1,32 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + float x; + float y; +} Vector; + +#define VECTOR_ZERO ((Vector){0, 0}) + +Vector vector_add(Vector a, Vector b); + +Vector vector_sub(Vector a, Vector b); + +Vector vector_mul(Vector a, Vector b); + +Vector vector_div(Vector a, Vector b); + +Vector vector_addf(Vector a, float b); + +Vector vector_subf(Vector a, float b); + +Vector vector_mulf(Vector a, float b); + +Vector vector_divf(Vector a, float b); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/flip_world/file_assets/sprites/enemy_left_cyclops_10x11px.fxbm b/flip_world/file_assets/sprites/enemy_left_cyclops_10x11px.fxbm new file mode 100644 index 000000000..5a37f7007 Binary files /dev/null and b/flip_world/file_assets/sprites/enemy_left_cyclops_10x11px.fxbm differ diff --git a/flip_world/file_assets/sprites/enemy_left_ghost_15x15px.fxbm b/flip_world/file_assets/sprites/enemy_left_ghost_15x15px.fxbm new file mode 100644 index 000000000..6605fad93 Binary files /dev/null and b/flip_world/file_assets/sprites/enemy_left_ghost_15x15px.fxbm differ diff --git a/flip_world/file_assets/sprites/enemy_left_ogre_10x13px.fxbm b/flip_world/file_assets/sprites/enemy_left_ogre_10x13px.fxbm new file mode 100644 index 000000000..b47eb80b3 Binary files /dev/null and b/flip_world/file_assets/sprites/enemy_left_ogre_10x13px.fxbm differ diff --git a/flip_world/file_assets/sprites/enemy_right_cyclops_10x11px.fxbm b/flip_world/file_assets/sprites/enemy_right_cyclops_10x11px.fxbm new file mode 100644 index 000000000..a26712b7c Binary files /dev/null and b/flip_world/file_assets/sprites/enemy_right_cyclops_10x11px.fxbm differ diff --git a/flip_world/file_assets/sprites/enemy_right_ghost_15x15px.fxbm b/flip_world/file_assets/sprites/enemy_right_ghost_15x15px.fxbm new file mode 100644 index 000000000..6256440a9 Binary files /dev/null and b/flip_world/file_assets/sprites/enemy_right_ghost_15x15px.fxbm differ diff --git a/flip_world/file_assets/sprites/enemy_right_ogre_10x13px.fxbm b/flip_world/file_assets/sprites/enemy_right_ogre_10x13px.fxbm new file mode 100644 index 000000000..886bdded8 Binary files /dev/null and b/flip_world/file_assets/sprites/enemy_right_ogre_10x13px.fxbm differ diff --git a/flip_world/file_assets/sprites/player_left_axe_15x11px.fxbm b/flip_world/file_assets/sprites/player_left_axe_15x11px.fxbm new file mode 100644 index 000000000..270014143 Binary files /dev/null and b/flip_world/file_assets/sprites/player_left_axe_15x11px.fxbm differ diff --git a/flip_world/file_assets/sprites/player_left_bow_13x11px.fxbm b/flip_world/file_assets/sprites/player_left_bow_13x11px.fxbm new file mode 100644 index 000000000..41d8e186c Binary files /dev/null and b/flip_world/file_assets/sprites/player_left_bow_13x11px.fxbm differ diff --git a/flip_world/file_assets/sprites/player_left_naked_10x10px.fxbm b/flip_world/file_assets/sprites/player_left_naked_10x10px.fxbm new file mode 100644 index 000000000..4204c43c5 Binary files /dev/null and b/flip_world/file_assets/sprites/player_left_naked_10x10px.fxbm differ diff --git a/flip_world/file_assets/sprites/player_left_sword_15x11px.fxbm b/flip_world/file_assets/sprites/player_left_sword_15x11px.fxbm new file mode 100644 index 000000000..869ec9fb2 Binary files /dev/null and b/flip_world/file_assets/sprites/player_left_sword_15x11px.fxbm differ diff --git a/flip_world/file_assets/sprites/player_right_axe_15x11px.fxbm b/flip_world/file_assets/sprites/player_right_axe_15x11px.fxbm new file mode 100644 index 000000000..ce5c2645b Binary files /dev/null and b/flip_world/file_assets/sprites/player_right_axe_15x11px.fxbm differ diff --git a/flip_world/file_assets/sprites/player_right_bow_13x11.fxbm b/flip_world/file_assets/sprites/player_right_bow_13x11.fxbm new file mode 100644 index 000000000..3f6f82055 Binary files /dev/null and b/flip_world/file_assets/sprites/player_right_bow_13x11.fxbm differ diff --git a/flip_world/file_assets/sprites/player_right_naked_10x10px.fxbm b/flip_world/file_assets/sprites/player_right_naked_10x10px.fxbm new file mode 100644 index 000000000..73d6e277a Binary files /dev/null and b/flip_world/file_assets/sprites/player_right_naked_10x10px.fxbm differ diff --git a/flip_world/file_assets/sprites/player_right_sword_15x11px.fxbm b/flip_world/file_assets/sprites/player_right_sword_15x11px.fxbm new file mode 100644 index 000000000..7a488e395 Binary files /dev/null and b/flip_world/file_assets/sprites/player_right_sword_15x11px.fxbm differ diff --git a/flip_world/flip_storage/storage.c b/flip_world/flip_storage/storage.c new file mode 100644 index 000000000..f10a88bef --- /dev/null +++ b/flip_world/flip_storage/storage.c @@ -0,0 +1,664 @@ + +#include "flip_storage/storage.h" + +// Forward declaration for use in other functions +static bool load_flip_social_settings( + char *ssid, + size_t ssid_size, + char *password, + size_t password_size, + char *login_username_logged_out, + size_t username_out_size, + char *login_username_logged_in, + size_t username_in_size, + char *login_password_logged_out, + size_t password_out_size, + char *change_password_logged_in, + size_t change_password_size, + char *change_bio_logged_in, + size_t change_bio_size, + char *is_logged_in, + size_t is_logged_in_size); + +void save_settings( + const char *wifi_ssid, + const char *wifi_password, + const char *username, + const char *password) +{ + // Create the directory for saving settings + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + + // Open the settings file + File *file = storage_file_alloc(storage); + if (!storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) + { + FURI_LOG_E(TAG, "Failed to open settings file for writing: %s", SETTINGS_PATH); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return; + } + + // Save the wifi_ssid length and data + size_t wifi_ssid_length = strlen(wifi_ssid) + 1; // Include null terminator + if (storage_file_write(file, &wifi_ssid_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, wifi_ssid, wifi_ssid_length) != wifi_ssid_length) + { + FURI_LOG_E(TAG, "Failed to write wifi_SSID"); + } + + // Save the wifi_password length and data + size_t wifi_password_length = strlen(wifi_password) + 1; // Include null terminator + if (storage_file_write(file, &wifi_password_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, wifi_password, wifi_password_length) != wifi_password_length) + { + FURI_LOG_E(TAG, "Failed to write wifi_password"); + } + + // Save the username length and data + size_t username_length = strlen(username) + 1; // Include null terminator + if (storage_file_write(file, &username_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, username, username_length) != username_length) + { + FURI_LOG_E(TAG, "Failed to write username"); + } + + // Save the password length and data + size_t password_length = strlen(password) + 1; // Include null terminator + if (storage_file_write(file, &password_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, password, password_length) != password_length) + { + FURI_LOG_E(TAG, "Failed to write password"); + } + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); +} + +bool load_settings( + char *wifi_ssid, + size_t wifi_ssid_size, + char *wifi_password, + size_t wifi_password_size, + char *username, + size_t username_size, + char *password, + size_t password_size) +{ + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); + + if (!storage_file_open(file, SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) + { + FURI_LOG_E(TAG, "Failed to open settings file for reading: %s", SETTINGS_PATH); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; // Return false if the file does not exist + } + + // Load the wifi_ssid + size_t wifi_ssid_length; + if (storage_file_read(file, &wifi_ssid_length, sizeof(size_t)) != sizeof(size_t) || wifi_ssid_length > wifi_ssid_size || + storage_file_read(file, wifi_ssid, wifi_ssid_length) != wifi_ssid_length) + { + FURI_LOG_E(TAG, "Failed to read wifi_SSID"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + wifi_ssid[wifi_ssid_length - 1] = '\0'; // Ensure null-termination + + // Load the wifi_password + size_t wifi_password_length; + if (storage_file_read(file, &wifi_password_length, sizeof(size_t)) != sizeof(size_t) || wifi_password_length > wifi_password_size || + storage_file_read(file, wifi_password, wifi_password_length) != wifi_password_length) + { + FURI_LOG_E(TAG, "Failed to read wifi_password"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + wifi_password[wifi_password_length - 1] = '\0'; // Ensure null-termination + + // Load the username + size_t username_length; + if (storage_file_read(file, &username_length, sizeof(size_t)) != sizeof(size_t) || username_length > username_size || + storage_file_read(file, username, username_length) != username_length) + { + FURI_LOG_E(TAG, "Failed to read username"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + username[username_length - 1] = '\0'; // Ensure null-termination + + // Load the password + size_t password_length; + if (storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || password_length > password_size || + storage_file_read(file, password, password_length) != password_length) + { + FURI_LOG_E(TAG, "Failed to read password"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + password[password_length - 1] = '\0'; // Ensure null-termination + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + return true; +} + +bool save_char( + const char *path_name, const char *value) +{ + if (!value) + { + return false; + } + // Create the directory for saving settings + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data"); + storage_common_mkdir(storage, directory_path); + + // Open the settings file + File *file = storage_file_alloc(storage); + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/%s.txt", path_name); + + // 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; + } + + // Write the data to the file + size_t data_size = strlen(value) + 1; // Include null terminator + if (storage_file_write(file, value, 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; +} + +bool load_char( + const char *path_name, + char *value, + size_t value_size) +{ + if (!value) + { + return false; + } + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); + + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/%s.txt", path_name); + + // 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 + } + + // Read data into the buffer + size_t read_count = storage_file_read(file, value, value_size); + if (storage_file_get_error(file) != FSE_OK) + { + FURI_LOG_E(HTTP_TAG, "Error reading from file."); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + + // Ensure null-termination + value[read_count - 1] = '\0'; + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + return strlen(value) > 0; +} + +FuriString *load_furi_world( + const char *name) +{ + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); + char file_path[128]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name); + // 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; +} + +bool world_exists(const char *name) +{ + if (!name) + { + FURI_LOG_E(TAG, "Invalid name"); + return false; + } + Storage *storage = furi_record_open(RECORD_STORAGE); + if (!storage) + { + FURI_LOG_E(TAG, "Failed to open storage"); + return false; + } + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name); + bool does_exist = storage_file_exists(storage, file_path); + + // Clean up + furi_record_close(RECORD_STORAGE); + return does_exist; +} + +bool save_world_names(const FuriString *json) +{ + // Create the directory for saving settings + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + + // Open the settings file + File *file = storage_file_alloc(storage); + char file_path[128]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json"); + + // 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; + } + + // Write the data to the file + size_t data_size = furi_string_size(json) + 1; // Include null terminator + if (storage_file_write(file, furi_string_get_cstr(json), 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; +} + +static FuriString *flip_social_info(char *key) +{ + char ssid[64]; + char password[64]; + char login_username_logged_out[64]; + char login_username_logged_in[64]; + char login_password_logged_out[64]; + char change_password_logged_in[64]; + char change_bio_logged_in[64]; + char is_logged_in[64]; + FuriString *result = furi_string_alloc(); + if (!result) + { + FURI_LOG_E(TAG, "Failed to allocate FuriString"); + return NULL; + } + if (!load_flip_social_settings(ssid, sizeof(ssid), password, sizeof(password), login_username_logged_out, sizeof(login_username_logged_out), login_username_logged_in, sizeof(login_username_logged_in), login_password_logged_out, sizeof(login_password_logged_out), change_password_logged_in, sizeof(change_password_logged_in), change_bio_logged_in, sizeof(change_bio_logged_in), is_logged_in, sizeof(is_logged_in))) + { + FURI_LOG_E(TAG, "Failed to load flip social settings"); + return NULL; + } + if (strcmp(key, "ssid") == 0) + { + furi_string_set_str(result, ssid); + } + else if (strcmp(key, "password") == 0) + { + furi_string_set_str(result, password); + } + else if (strcmp(key, "login_username_logged_out") == 0) + { + furi_string_set_str(result, login_username_logged_out); + } + else if (strcmp(key, "login_username_logged_in") == 0) + { + furi_string_set_str(result, login_username_logged_in); + } + else if (strcmp(key, "login_password_logged_out") == 0) + { + furi_string_set_str(result, login_password_logged_out); + } + else if (strcmp(key, "change_password_logged_in") == 0) + { + furi_string_set_str(result, change_password_logged_in); + } + else if (strcmp(key, "change_bio_logged_in") == 0) + { + furi_string_set_str(result, change_bio_logged_in); + } + else if (strcmp(key, "is_logged_in") == 0) + { + furi_string_set_str(result, is_logged_in); + } + else + { + FURI_LOG_E(TAG, "Invalid key"); + furi_string_free(result); + return NULL; + } + return result; +} + +bool is_logged_in_to_flip_social() +{ + // load flip social settings and check if logged in + FuriString *is_logged_in = flip_social_info("is_logged_in"); + if (!is_logged_in) + { + FURI_LOG_E(TAG, "Failed to load is_logged_in"); + return false; + } + if (furi_string_cmp(is_logged_in, "true") == 0) + { + // copy the logged_in FlipSocaial settings to FlipWorld + FuriString *username = flip_social_info("login_username_logged_in"); + FuriString *password = flip_social_info("change_password_logged_in"); + FuriString *wifi_password = flip_social_info("password"); + FuriString *wifi_ssid = flip_social_info("ssid"); + if (!username || !password || !wifi_password || !wifi_ssid) + { + furi_string_free(username); + furi_string_free(password); + furi_string_free(wifi_password); + furi_string_free(wifi_ssid); + return false; + } + save_settings(furi_string_get_cstr(wifi_ssid), furi_string_get_cstr(wifi_password), furi_string_get_cstr(username), furi_string_get_cstr(password)); + furi_string_free(username); + furi_string_free(password); + furi_string_free(wifi_password); + furi_string_free(wifi_ssid); + furi_string_free(is_logged_in); + return true; + } + furi_string_free(is_logged_in); + return false; +} + +bool is_logged_in() +{ + char is_logged_in[64]; + if (load_char("is_logged_in", is_logged_in, sizeof(is_logged_in))) + { + return strcmp(is_logged_in, "true") == 0; + } + return false; +} + +static bool load_flip_social_settings( + char *ssid, + size_t ssid_size, + char *password, + size_t password_size, + char *login_username_logged_out, + size_t username_out_size, + char *login_username_logged_in, + size_t username_in_size, + char *login_password_logged_out, + size_t password_out_size, + char *change_password_logged_in, + size_t change_password_size, + char *change_bio_logged_in, + size_t change_bio_size, + char *is_logged_in, + size_t is_logged_in_size) +{ + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); + + // file path from flipsocial + char file_path[128]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/settings.bin"); + if (!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) + { + FURI_LOG_E(TAG, "Failed to open settings file for reading: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; // Return false if the file does not exist + } + + // Load the ssid + size_t ssid_length; + if (storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || ssid_length > ssid_size || + storage_file_read(file, ssid, ssid_length) != ssid_length) + { + FURI_LOG_E(TAG, "Failed to read SSID"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + else + { + ssid[ssid_length - 1] = '\0'; // Ensure null-termination + } + + // Load the password + size_t password_length; + if (storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || password_length > password_size || + storage_file_read(file, password, password_length) != password_length) + { + FURI_LOG_E(TAG, "Failed to read password"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + else + { + password[password_length - 1] = '\0'; // Ensure null-termination + } + + // Load the login_username_logged_out + size_t username_out_length; + if (storage_file_read(file, &username_out_length, sizeof(size_t)) != sizeof(size_t) || username_out_length > username_out_size || + storage_file_read(file, login_username_logged_out, username_out_length) != username_out_length) + { + FURI_LOG_E(TAG, "Failed to read login_username_logged_out"); + // storage_file_close(file); + // storage_file_free(file); + // furi_record_close(RECORD_STORAGE); + // return false; + } + else + { + login_username_logged_out[username_out_length - 1] = '\0'; // Ensure null-termination + } + + // Load the login_username_logged_in + size_t username_in_length; + if (storage_file_read(file, &username_in_length, sizeof(size_t)) != sizeof(size_t) || username_in_length > username_in_size || + storage_file_read(file, login_username_logged_in, username_in_length) != username_in_length) + { + FURI_LOG_E(TAG, "Failed to read login_username_logged_in"); + // storage_file_close(file); + // storage_file_free(file); + // furi_record_close(RECORD_STORAGE); + // return false; + } + else + { + login_username_logged_in[username_in_length - 1] = '\0'; // Ensure null-termination + } + + // Load the login_password_logged_out + size_t password_out_length; + if (storage_file_read(file, &password_out_length, sizeof(size_t)) != sizeof(size_t) || password_out_length > password_out_size || + storage_file_read(file, login_password_logged_out, password_out_length) != password_out_length) + { + FURI_LOG_E(TAG, "Failed to read login_password_logged_out"); + // storage_file_close(file); + // storage_file_free(file); + // furi_record_close(RECORD_STORAGE); + // return false; + } + else + { + login_password_logged_out[password_out_length - 1] = '\0'; // Ensure null-termination + } + + // Load the change_password_logged_in + size_t change_password_length; + if (storage_file_read(file, &change_password_length, sizeof(size_t)) != sizeof(size_t) || change_password_length > change_password_size || + storage_file_read(file, change_password_logged_in, change_password_length) != change_password_length) + { + FURI_LOG_E(TAG, "Failed to read change_password_logged_in"); + // storage_file_close(file); + // storage_file_free(file); + // furi_record_close(RECORD_STORAGE); + // return false; + } + else + { + change_password_logged_in[change_password_length - 1] = '\0'; // Ensure null-termination + } + + // Load the is_logged_in + size_t is_logged_in_length; + if (storage_file_read(file, &is_logged_in_length, sizeof(size_t)) != sizeof(size_t) || is_logged_in_length > is_logged_in_size || + storage_file_read(file, is_logged_in, is_logged_in_length) != is_logged_in_length) + { + FURI_LOG_E(TAG, "Failed to read is_logged_in"); + // storage_file_close(file); + // storage_file_free(file); + // furi_record_close(RECORD_STORAGE); + // return false; + } + else + { + is_logged_in[is_logged_in_length - 1] = '\0'; // Ensure null-termination + } + + // Load the change_bio_logged_in + size_t change_bio_length; + if (storage_file_read(file, &change_bio_length, sizeof(size_t)) != sizeof(size_t) || change_bio_length > change_bio_size || + storage_file_read(file, change_bio_logged_in, change_bio_length) != change_bio_length) + { + FURI_LOG_E(TAG, "Failed to read change_bio_logged_in"); + // storage_file_close(file); + // storage_file_free(file); + // furi_record_close(RECORD_STORAGE); + // return false; + } + else + { + change_bio_logged_in[change_bio_length - 1] = '\0'; // Ensure null-termination + } + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + return true; +} diff --git a/flip_world/flip_storage/storage.h b/flip_world/flip_storage/storage.h new file mode 100644 index 000000000..8d13a84c7 --- /dev/null +++ b/flip_world/flip_storage/storage.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/settings.bin" + +void save_settings( + const char *wifi_ssid, + const char *wifi_password, + const char *username, + const char *password); + +bool load_settings( + char *wifi_ssid, + size_t wifi_ssid_size, + char *wifi_password, + size_t wifi_password_size, + char *username, + size_t username_size, + char *password, + size_t password_size); + +bool save_char( + const char *path_name, const char *value); + +bool load_char( + const char *path_name, + char *value, + size_t value_size); + +FuriString *load_furi_world( + const char *name); + +bool world_exists( + const char *name); + +bool save_world_names(const FuriString *json); +bool is_logged_in_to_flip_social(); +bool is_logged_in(); \ No newline at end of file diff --git a/flip_world/flip_world.c b/flip_world/flip_world.c new file mode 100644 index 000000000..d30b779b5 --- /dev/null +++ b/flip_world/flip_world.c @@ -0,0 +1,9 @@ +#include +char *game_fps_choices[] = {"30", "60", "120", "240"}; +const float game_fps_choices_2[] = {30.0, 60.0, 120.0, 240.0}; +int game_fps_index = 0; +char *yes_or_no_choices[] = {"No", "Yes"}; +int game_screen_always_on_index = 1; +int game_sound_on_index = 0; +int game_vibration_on_index = 0; +bool is_enough_heap(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); } // 1KB buffer \ No newline at end of file diff --git a/flip_world/flip_world.h b/flip_world/flip_world.h new file mode 100644 index 000000000..c7954ebff --- /dev/null +++ b/flip_world/flip_world.h @@ -0,0 +1,82 @@ +#pragma once +#include +#include +#include + +// added by Derek Jamison to lower memory usage +#undef FURI_LOG_E +#define FURI_LOG_E(tag, msg, ...) +// + +#define TAG "FlipWorld" +#define VERSION 0.3 +#define VERSION_TAG "FlipWorld v0.3" + +// Define the submenu items for our FlipWorld application +typedef enum +{ + FlipWorldSubmenuIndexRun, // Click to run the FlipWorld application + FlipWorldSubmenuIndexAbout, + FlipWorldSubmenuIndexSettings, + FlipWorldSubmenuIndexWiFiSettings, + FlipWorldSubmenuIndexGameSettings, + FlipWorldSubmenuIndexUserSettings, +} FlipWorldSubmenuIndex; + +// Define a single view for our FlipWorld application +typedef enum +{ + FlipWorldViewSubmenu, // The submenu + FlipWorldViewAbout, // The about screen + FlipWorldViewSettings, // The settings screen + FlipWorldViewVariableItemList, // The variable item list screen + FlipWorldViewTextInput, // The text input screen + // + FlipWorldViewWidgetResult, // The text box that displays the random fact + FlipWorldViewLoader, // The loader screen retrieves data from the internet +} FlipWorldView; + +// Define a custom event for our FlipWorld application +typedef enum +{ + FlipWorldCustomEventProcess, + FlipWorldCustomEventPlay, // Play the game +} FlipWorldCustomEvent; + +// Each screen will have its own view +typedef struct +{ + View *view_loader; + Widget *widget_result; + // + ViewDispatcher *view_dispatcher; // Switches between our views + View *view_about; // The about screen + Submenu *submenu; // The submenu + Submenu *submenu_settings; // The settings submenu + VariableItemList *variable_item_list; // The variable item list (settngs) + VariableItem *variable_item_wifi_ssid; // The variable item for WiFi SSID + VariableItem *variable_item_wifi_pass; // The variable item for WiFi password + // + VariableItem *variable_item_game_fps; // The variable item for Game FPS + VariableItem *variable_item_game_screen_always_on; // The variable item for Screen always on + VariableItem *variable_item_game_download_world; // The variable item for Download world + VariableItem *variable_item_game_sound_on; // The variable item for Sound on + VariableItem *variable_item_game_vibration_on; // The variable item for Vibration on + // + VariableItem *variable_item_user_username; // The variable item for the User username + VariableItem *variable_item_user_password; // The variable item for the User password + + TextInput *text_input; // The text input + char *text_input_buffer; // Buffer for the text input + char *text_input_temp_buffer; // Temporary buffer for the text input + uint32_t text_input_buffer_size; // Size of the text input buffer +} FlipWorldApp; + +extern char *game_fps_choices[]; +extern const float game_fps_choices_2[]; +extern int game_fps_index; +extern char *yes_or_no_choices[]; +extern int game_screen_always_on_index; +extern int game_sound_on_index; +extern int game_vibration_on_index; +bool is_enough_heap(size_t heap_size); diff --git a/flip_world/flipper_http/flipper_http.c b/flip_world/flipper_http/flipper_http.c new file mode 100644 index 000000000..ee94507a6 --- /dev/null +++ b/flip_world/flipper_http/flipper_http.c @@ -0,0 +1,1816 @@ +// Description: Flipper HTTP API (For use with Flipper Zero and the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP) +// License: MIT +// Author: JBlanked +// File: flipper_http.c +#include // change this to where flipper_http.h is located + +// 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); + FURI_LOG_E(HTTP_TAG, "Failed to open file for reading: %s", file_path); + return NULL; + } + + // 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]); + } + + // Clean up + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + free(buffer); + return str_result; +} + +FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit) +{ + // 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); + FURI_LOG_E(HTTP_TAG, "Failed to open file for reading: %s", file_path); + return NULL; + } + + if (memmgr_get_free_heap() < limit) + { + FURI_LOG_E(HTTP_TAG, "Not enough heap to read file."); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Allocate a buffer to hold the read data + uint8_t *buffer = (uint8_t *)malloc(limit); + if (!buffer) + { + FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Allocate a FuriString with preallocated capacity + FuriString *str_result = furi_string_alloc(); + if (!str_result) + { + FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString"); + free(buffer); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + furi_string_reserve(str_result, limit); + + // Read data into the buffer + size_t read_count = storage_file_read(file, buffer, limit); + if (storage_file_get_error(file) != FSE_OK) + { + FURI_LOG_E(HTTP_TAG, "Error reading from file."); + furi_string_free(str_result); + free(buffer); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + if (read_count == 0) + { + FURI_LOG_E(HTTP_TAG, "No data read from file."); + furi_string_free(str_result); + free(buffer); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Append the entire buffer to FuriString in one operation + furi_string_cat_str(str_result, (char *)buffer); + + // 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 FlipperHTTP context. + * @note This function will handle received data asynchronously via the callback. + */ +// UART worker thread +int32_t flipper_http_worker(void *context) +{ + if (!context) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return -1; + } + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return -1; + } + 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 + fhttp->file_buffer[fhttp->file_buffer_len++] = c; + // Write to file if buffer is full + if (fhttp->file_buffer_len >= FILE_BUFFER_SIZE) + { + if (!flipper_http_append_to_file( + fhttp->file_buffer, + fhttp->file_buffer_len, + fhttp->just_started_bytes, + fhttp->file_path)) + { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); + } + fhttp->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) + { + fhttp->rx_line_buffer[rx_line_pos] = '\0'; // Null-terminate the line + + // Invoke the callback with the complete line + fhttp->handle_rx_line_cb(fhttp->rx_line_buffer, fhttp->callback_context); + + // Reset the line buffer position + rx_line_pos = 0; + } + else + { + fhttp->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 FlipperHTTP context. + * @note This function will be called when the GET request times out. + */ +void get_timeout_timer_callback(void *context) +{ + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + FURI_LOG_E(HTTP_TAG, "Timeout reached 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 FlipperHTTP context. + * @note This function will handle received data asynchronously via the callback. + */ +void _flipper_http_rx_callback( + FuriHalSerialHandle *handle, + FuriHalSerialRxEvent event, + void *context) +{ + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + 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 FlipperHTTP context if the UART was initialized successfully, NULL otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +FlipperHTTP *flipper_http_alloc() +{ + FlipperHTTP *fhttp = (FlipperHTTP *)malloc(sizeof(FlipperHTTP)); + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to allocate FlipperHTTP."); + return NULL; + } + memset(fhttp, 0, sizeof(FlipperHTTP)); // Initialize allocated memory to zero + + 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."); + free(fhttp); + return NULL; + } + + 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); + free(fhttp); + return NULL; + } + + 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); // Corrected context + furi_thread_set_callback(fhttp->rx_thread, flipper_http_worker); + + fhttp->handle_rx_line_cb = flipper_http_rx_callback; + fhttp->callback_context = fhttp; + + 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."); + // Cleanup resources + 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); + free(fhttp); + return NULL; + } + + 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_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); + free(fhttp); + return NULL; + } + + // 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 corrected callback and context + furi_hal_serial_async_rx_start(fhttp->serial_handle, _flipper_http_rx_callback, fhttp, false); // Corrected context + + // 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 // Corrected context + ); + + 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); + free(fhttp); + return NULL; + } + + // 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."); + // Cleanup resources + furi_timer_free(fhttp->get_timeout_timer); + 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); + free(fhttp); + return NULL; + } + memset(fhttp->last_response, 0, RX_BUF_SIZE); // Initialize last_response + + fhttp->state = IDLE; + + // FURI_LOG_I(HTTP_TAG, "UART initialized successfully."); + return fhttp; +} + +// Deinitialize UART +/** + * @brief Deinitialize UART. + * @return void + * @param fhttp The FlipperHTTP context + * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. + */ +void flipper_http_free(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + 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; + } + + // Free the FlipperHTTP context + free(fhttp); + fhttp = 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, const char *data) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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. + * @param fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[PING]"; + if (!flipper_http_send_data(fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_list_commands(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[LIST]"; + if (!flipper_http_send_data(fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_on(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[LED/ON]"; + if (!flipper_http_send_data(fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_off(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[LED/OFF]"; + if (!flipper_http_send_data(fhttp, 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, const char *key, const char *json_data) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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(fhttp, 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, const char *key, int index, const char *json_data) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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(fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_scan_wifi(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[WIFI/SCAN]"; + if (!flipper_http_send_data(fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_save_wifi(FlipperHTTP *fhttp, const char *ssid, const char *password) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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(fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_address(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[IP/ADDRESS]"; + if (!flipper_http_send_data(fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_wifi(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[WIFI/IP]"; + if (!flipper_http_send_data(fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_disconnect_wifi(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[WIFI/DISCONNECT]"; + if (!flipper_http_send_data(fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_connect_wifi(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[WIFI/CONNECT]"; + if (!flipper_http_send_data(fhttp, 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, const char *url) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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(fhttp, 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, const char *url, const char *headers) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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(fhttp, 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, const char *url, const char *headers) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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(fhttp, 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 fhttp The FlipperHTTP context + * @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( + FlipperHTTP *fhttp, + const char *url, + const char *headers, + const char *payload) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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[512]; + 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(fhttp, 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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(fhttp, 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 fhttp The FlipperHTTP context + * @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( + FlipperHTTP *fhttp, + const char *url, + const char *headers, + const char *payload) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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(fhttp, 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 fhttp The FlipperHTTP context + * @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( + FlipperHTTP *fhttp, + const char *url, + const char *headers, + const char *payload) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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(fhttp, 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 trim leading and trailing spaces and newlines from a constant string +static 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; +} + +// Function to handle received data asynchronously +/** + * @brief Callback function to handle received data asynchronously. + * @return void + * @param line The received line. + * @param context The FlipperHTTP context. + * @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) +{ + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + if (!line) + { + 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 <= fhttp->file_buffer_len - marker_len; i++) + { + // Check if the marker is found + if (memcmp(&fhttp->file_buffer[i], marker, marker_len) == 0) + { + // Remove the marker by shifting the remaining data left + size_t remaining_len = fhttp->file_buffer_len - (i + marker_len); + memmove(&fhttp->file_buffer[i], &fhttp->file_buffer[i + marker_len], remaining_len); + fhttp->file_buffer_len -= marker_len; + break; + } + } + + // If there is data left in the buffer, append it to the file + if (fhttp->file_buffer_len > 0) + { + if (!flipper_http_append_to_file(fhttp->file_buffer, fhttp->file_buffer_len, false, fhttp->file_path)) + { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + } + fhttp->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 <= fhttp->file_buffer_len - marker_len; i++) + { + // Check if the marker is found + if (memcmp(&fhttp->file_buffer[i], marker, marker_len) == 0) + { + // Remove the marker by shifting the remaining data left + size_t remaining_len = fhttp->file_buffer_len - (i + marker_len); + memmove(&fhttp->file_buffer[i], &fhttp->file_buffer[i + marker_len], remaining_len); + fhttp->file_buffer_len -= marker_len; + break; + } + } + + // If there is data left in the buffer, append it to the file + if (fhttp->file_buffer_len > 0) + { + if (!flipper_http_append_to_file(fhttp->file_buffer, fhttp->file_buffer_len, false, fhttp->file_path)) + { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); + } + fhttp->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; + fhttp->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; + fhttp->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; + } +} + +/** + * @brief Process requests and parse JSON data asynchronously + * @param fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, bool (*http_request)(void), bool (*parse_json)(void)) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, + bool (*http_request)(void), + bool (*parse_response)(void), + uint32_t success_view_id, + uint32_t failure_view_id, + ViewDispatcher **view_dispatcher) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + if (fhttp->state == INACTIVE) + { + view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); + return; + } + 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(fhttp, 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); // comment this out if you experience a freeze +} \ No newline at end of file diff --git a/flip_world/flipper_http/flipper_http.h b/flip_world/flipper_http/flipper_http.h new file mode 100644 index 000000000..7bd6b67ca --- /dev/null +++ b/flip_world/flipper_http/flipper_http.h @@ -0,0 +1,408 @@ +// Description: Flipper HTTP API (For use with Flipper Zero and the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP) +// License: MIT +// Author: JBlanked +// File: flipper_http.h +#pragma once + +#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 "FlipWorld" // change this to your app name +#define http_tag "flip_world" // 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 3000 // UART RX line buffer size (increase for large responses) +#define MAX_FILE_SHOW 3000 // 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 + + char rx_line_buffer[RX_LINE_BUFFER_SIZE]; + uint8_t file_buffer[FILE_BUFFER_SIZE]; + size_t file_buffer_len; +} FlipperHTTP; + +// 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); +FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit); + +// 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 FlipperHTTP context if the UART was initialized successfully, NULL otherwise. + * @note The received data will be handled asynchronously via the callback. + */ +FlipperHTTP *flipper_http_alloc(); + +// Deinitialize UART +/** + * @brief Deinitialize UART. + * @return void + * @param fhttp The FlipperHTTP context + * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. + */ +void flipper_http_free(FlipperHTTP *fhttp); + +// 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, 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. + * @param fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp); + +// Function to list available commands +/** + * @brief Send a command to list available commands. + * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_list_commands(FlipperHTTP *fhttp); + +// Function to turn on the LED +/** + * @brief Allow the LED to display while processing. + * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_on(FlipperHTTP *fhttp); + +// Function to turn off the LED +/** + * @brief Disable the LED from displaying while processing. + * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_led_off(FlipperHTTP *fhttp); + +// Function to parse JSON data +/** + * @brief Parse JSON data. + * @return true if the JSON data was parsed successfully, false otherwise. + * @param fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_scan_wifi(FlipperHTTP *fhttp); + +// 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_save_wifi(FlipperHTTP *fhttp, 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_address(FlipperHTTP *fhttp); + +// 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_ip_wifi(FlipperHTTP *fhttp); + +// 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_disconnect_wifi(FlipperHTTP *fhttp); + +// 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. + * @param fhttp The FlipperHTTP context + * @note The received data will be handled asynchronously via the callback. + */ +bool flipper_http_connect_wifi(FlipperHTTP *fhttp); + +// 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context + * @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( + FlipperHTTP *fhttp, + 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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context + * @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( + FlipperHTTP *fhttp, + 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 fhttp The FlipperHTTP context + * @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( + FlipperHTTP *fhttp, + 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 FlipperHTTP context. + * @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); + +/** + * @brief Process requests and parse JSON data asynchronously + * @param fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, bool (*http_request)(void), bool (*parse_json)(void)); + +/** + * @brief Perform a task while displaying a loading screen + * @param fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, + bool (*http_request)(void), + bool (*parse_response)(void), + uint32_t success_view_id, + uint32_t failure_view_id, + ViewDispatcher **view_dispatcher); diff --git a/flip_world/font/font.c b/flip_world/font/font.c new file mode 100644 index 000000000..85e2d2a61 --- /dev/null +++ b/flip_world/font/font.c @@ -0,0 +1,83 @@ +#include + +static const uint8_t u8g2_font_4x6_tf[]; + +bool canvas_set_font_custom(Canvas *canvas, FontSize font_size) +{ + if (!canvas) + { + return false; + } + switch (font_size) + { + case FONT_SIZE_SMALL: + canvas_set_custom_u8g2_font(canvas, u8g2_font_4x6_tf); + break; + default: + return false; + } + return true; +} + +void canvas_draw_str_multi(Canvas *canvas, uint8_t x, uint8_t y, const char *str) +{ + if (!canvas || !str) + { + return; + } + elements_multiline_text(canvas, x, y, str); +} + +/* + Fontname: -Misc-Fixed-Medium-R-Normal--6-60-75-75-C-40-ISO10646-1 + Copyright: Public domain font. Share and enjoy. + Glyphs: 191/919 + BBX Build Mode: 0 +*/ +static const uint8_t u8g2_font_4x6_tf[] = + "\277\0\2\2\3\3\2\4\4\4\6\0\377\5\377\5\377\0\351\1\323\5\216 \5\200\315\0!\6\351\310" + "\254\0\42\6\223\313$\25#\12\254\310\244\64T\32*\1$\11\263\307\245\241GJ\0%\10\253\310d" + "\324F\1&\11\254\310\305\24\253\230\2'\5\321\313\10(\7\362\307\251f\0)\10\262\307\304T)\0" + "*\7\253\310\244j\65+\10\253\310\305\264b\2,\6\222\307)\0-\5\213\312\14.\5\311\310\4/" + "\7\253\310Ve\4\60\10\253\310UCU\0\61\7\253\310%Y\15\62\7\253\310\65S\32\63\10\253\310" + "\314\224\27\0\64\10\253\310$\65b\1\65\10\253\310\214\250\27\0\66\7\253\310M\325\2\67\10\253\310\314" + "TF\0\70\7\253\310\255\326\2\71\7\253\310\265\344\2:\6\341\310\304\0;\7\252\307e\250\0<\7" + "\253\310\246\272\0=\6\233\311\354\1>\7\253\310\344\252\4\77\10\253\310\350\224a\2@\6\253\310-[" + "A\10\253\310UC\251\0B\10\253\310\250\264\322\2C\10\253\310U\62U\0D\10\253\310\250d-\0" + "E\10\253\310\214\250\342\0F\10\253\310\214\250b\4G\10\253\310\315\244\222\0H\10\253\310$\65\224\12" + "I\7\253\310\254X\15J\7\253\310\226\252\2K\10\253\310$\265\222\12L\7\253\310\304\346\0M\10\253" + "\310\244\61\224\12N\10\253\310\252\241$\0O\7\253\310UV\5P\10\253\310\250\264b\4Q\7\263\307" + "UV\35R\10\253\310\250\264\222\12S\7\253\310\355\274\0T\7\253\310\254\330\2U\7\253\310$\327\10" + "V\10\253\310$k\244\4W\10\253\310$\65\206\12X\10\253\310$\325R\1Y\10\253\310$UV\0" + "Z\7\253\310\314T\16[\6\352\310\254J\134\7\253\310\304\134\6]\6\252\310\250j^\5\223\313\65_" + "\5\213\307\14`\6\322\313\304\0a\7\243\310-\225\4b\10\253\310D\225\324\2c\6\243\310\315,d" + "\10\253\310\246\245\222\0e\6\243\310USf\10\253\310\246\264b\2g\10\253\307\255$\27\0h\10\253" + "\310D\225\254\0i\10\253\310e$\323\0j\10\263\307fX.\0k\10\253\310\304\264\222\12l\7\253" + "\310\310\326\0m\10\243\310\244\241T\0n\7\243\310\250d\5o\7\243\310U\252\2p\10\253\307\250\264" + "b\4q\10\253\307-\225d\0r\10\243\310\244\25#\0s\7\243\310\215\274\0t\10\253\310\245\25s" + "\0u\7\243\310$+\11v\7\243\310$\253\2w\10\243\310$\65T\0x\7\243\310\244\62\25y\10" + "\253\307$\225\344\2z\7\243\310\314\224\6{\10\263\307\246$\353\0|\6\351\310\14\1}\11\263\307\344" + "\250b\212\0~\7\224\313%\225\0\240\5\200\315\0\241\6\351\310\244\1\242\10\253\310\245\21W\2\243\7" + "\253\310\246\250\32\244\10\244\310\304$U\14\245\10\253\310\244j\305\4\246\6\351\310(\1\247\10\263\307\215" + "T\311\5\250\6\213\314\244\0\251\11\264\307\251\270\226L\12\252\7\253\310\255\244\7\253\10\234\311%\25S" + "\0\254\6\223\311\314\0\255\5\213\312\14\256\10\244\311\251\261\222\2\257\5\213\314\14\260\6\233\312u\1\261" + "\10\253\310\245\225\321\0\262\6\242\311(\3\263\7\252\310(\251\0\264\6\322\313)\0\265\10\253\307$k" + "E\0\266\7\254\310\15\265z\267\5\311\312\4\270\6\322\310)\0\271\6\242\311\255\2\272\7\253\310u\243" + "\1\273\10\234\311\244\230T\2\274\10\264\307\344\32U;\275\10\264\307\344J\307,\276\11\264\307\350\230Q" + "\262\3\277\10\253\310e\230\262\0\300\10\253\310\344\224\206\12\301\10\253\310\246j\250\0\302\10\253\310\310\224" + "\206\12\303\10\253\310\215\224\206\12\304\10\253\310\244\326P\1\305\10\253\310\305\224\206\12\306\11\254\310\215\224" + "\206\252\4\307\10\263\307U\62\65\1\310\10\253\310\304\241\342\0\311\7\253\310\216\25\7\312\10\253\310\215\221" + "\342\0\313\10\253\310\244\261\342\0\314\10\253\310\304\25\323\0\315\10\253\310\216\24\323\0\316\10\253\310\245\25" + "\323\0\317\7\253\310\244\262\32\320\11\254\310\314\264\252\221\0\321\10\254\310%\225\256\12\322\10\253\310\344\224" + "T\5\323\10\253\310\246JU\0\324\10\253\310\305\224T\5\325\10\254\310\215\325\31\1\326\10\253\310\244\226" + "\252\0\327\6\233\311\244\16\330\7\253\310\35j\1\331\10\253\310\344\224\324\10\332\10\253\310\246J\215\0\333" + "\10\253\310e\224\324\10\334\10\253\310\244\234\324\10\335\10\253\310\346T&\0\336\10\253\310D\225V\4\337" + "\10\263\307U+\15\11\340\10\253\310\344\270\222\0\341\10\253\310\246\270\222\0\342\10\253\310i\264\222\0\343" + "\11\254\310%\25U\251\0\344\10\253\310\244\214V\22\345\10\253\310e\270\222\0\346\10\244\310\215\264\342\0" + "\347\10\253\307UZ%\0\350\10\253\310\344\224\246\0\351\7\253\310\246j\12\352\10\253\310\310\224\246\0\353" + "\7\253\310\244\326\24\354\7\253\310\344X\15\355\6\253\310\316j\356\7\253\310u\246\1\357\10\253\310\244," + "\323\0\360\10\253\310\244rU\0\361\11\254\310%\225dj\1\362\10\253\310\344\230Z\0\363\10\253\310\246" + "\230Z\0\364\10\253\310e\230Z\0\365\7\253\310l\324\5\366\10\253\310\244\214\272\0\367\10\253\310e\264" + "Q\2\370\7\243\310-\265\0\371\10\253\310\344\224T\22\372\10\253\310\246J%\1\373\10\253\310e\224T" + "\22\374\10\253\310\244\234T\22\375\10\263\307\246j\304\5\376\11\263\307\304\250\322\212\0\377\11\263\307\244\234" + "F\134\0\0\0\0\4\377\377\0"; \ No newline at end of file diff --git a/flip_world/font/font.h b/flip_world/font/font.h new file mode 100644 index 000000000..d29a6ca74 --- /dev/null +++ b/flip_world/font/font.h @@ -0,0 +1,10 @@ +#pragma once +#include +#include +#include +typedef enum +{ + FONT_SIZE_SMALL = 1, +} FontSize; +extern bool canvas_set_font_custom(Canvas *canvas, FontSize font_size); +extern void canvas_draw_str_multi(Canvas *canvas, uint8_t x, uint8_t y, const char *str); \ No newline at end of file diff --git a/flip_world/game/draw.c b/flip_world/game/draw.c new file mode 100644 index 000000000..2f460aee7 --- /dev/null +++ b/flip_world/game/draw.c @@ -0,0 +1,124 @@ +#include + +// Global variables to store camera position +int camera_x = 0; +int camera_y = 0; + +// Background rendering function (no collision detection) +void draw_background(Canvas *canvas, Vector pos) +{ + // Clear the canvas + canvas_clear(canvas); + + // Calculate camera offset to center the player + camera_x = pos.x - (SCREEN_WIDTH / 2); + camera_y = pos.y - (SCREEN_HEIGHT / 2); + + // Clamp camera position to prevent showing areas outside the world + camera_x = CLAMP(camera_x, WORLD_WIDTH - SCREEN_WIDTH, 0); + camera_y = CLAMP(camera_y, WORLD_HEIGHT - SCREEN_HEIGHT, 0); + + // Draw the outer bounds adjusted by camera offset + draw_bounds(canvas); +} + +// Draw the user stats (health, xp, and level) +void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager) +{ + GameContext *game_context = game_manager_game_context_get(manager); + PlayerContext *player = game_context->player_context; + + // first draw a black rectangle to make the text more readable + canvas_invert_color(canvas); + canvas_draw_box(canvas, pos.x - 1, pos.y - 7, 34, 21); + canvas_invert_color(canvas); + + char health[32]; + char xp[32]; + char level[32]; + + snprintf(health, sizeof(health), "HP : %ld", player->health); + snprintf(xp, sizeof(xp), "XP : %ld", player->xp); + snprintf(level, sizeof(level), "LVL: %ld", player->level); + + canvas_set_font_custom(canvas, FONT_SIZE_SMALL); + canvas_draw_str(canvas, pos.x, pos.y, health); + canvas_draw_str(canvas, pos.x, pos.y + 7, xp); + canvas_draw_str(canvas, pos.x, pos.y + 14, level); +} + +void draw_username(Canvas *canvas, Vector pos, char *username) +{ + // first draw a black rectangle to make the text more readable + // draw box around the username + canvas_invert_color(canvas); + canvas_draw_box(canvas, pos.x - camera_x - (strlen(username) * 2) - 1, pos.y - camera_y - 14, strlen(username) * 4 + 1, 8); + canvas_invert_color(canvas); + + // draw username over player's head + canvas_set_font_custom(canvas, FONT_SIZE_SMALL); + canvas_draw_str(canvas, pos.x - camera_x - (strlen(username) * 2), pos.y - camera_y - 7, username); +} + +// Draw a line of icons (16 width) +void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon) +{ + for (int i = 0; i < amount; i++) + { + if (horizontal) + { + // check if element is outside the world + if (pos.x + (i * 17) > WORLD_WIDTH) + { + break; + } + + canvas_draw_icon(canvas, pos.x + (i * 17) - camera_x, pos.y - camera_y, icon); + } + else + { + // check if element is outside the world + if (pos.y + (i * 17) > WORLD_HEIGHT) + { + break; + } + + canvas_draw_icon(canvas, pos.x - camera_x, pos.y + (i * 17) - camera_y, icon); + } + } +} +char g_temp_spawn_name[32]; +// Draw an icon at a specific position (with collision detection) +void spawn_icon(Level *level, const char *icon_id, float x, float y) +{ + snprintf(g_temp_spawn_name, sizeof(g_temp_spawn_name), "%s", icon_id); + Entity *e = level_add_entity(level, &icon_desc); + entity_pos_set(e, (Vector){x, y}); +} +// Draw a line of icons at a specific position (with collision detection) +void spawn_icon_line(Level *level, const char *icon_id, float x, float y, uint8_t amount, bool horizontal) +{ + for (int i = 0; i < amount; i++) + { + if (horizontal) + { + // check if element is outside the world + if (x + (i * 17) > WORLD_WIDTH) + { + break; + } + + spawn_icon(level, icon_id, x + (i * 17), y); + } + else + { + // check if element is outside the world + if (y + (i * 17) > WORLD_HEIGHT) + { + break; + } + + spawn_icon(level, icon_id, x, y + (i * 17)); + } + } +} \ No newline at end of file diff --git a/flip_world/game/draw.h b/flip_world/game/draw.h new file mode 100644 index 000000000..474d4561f --- /dev/null +++ b/flip_world/game/draw.h @@ -0,0 +1,15 @@ +#pragma once +#include "game/icon.h" +#include + +// Global variables to store camera position +extern int camera_x; +extern int camera_y; +void draw_background(Canvas *canvas, Vector pos); +void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager); +void draw_username(Canvas *canvas, Vector pos, char *username); +void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon); +void spawn_icon(Level *level, const char *icon_id, float x, float y); +void spawn_icon_line(Level *level, const char *icon_id, float x, float y, uint8_t amount, bool horizontal); +extern char g_temp_spawn_name[32]; +// create custom icons at https://lopaka.app/sandbox \ No newline at end of file diff --git a/flip_world/game/enemy.c b/flip_world/game/enemy.c new file mode 100644 index 000000000..3568073b4 --- /dev/null +++ b/flip_world/game/enemy.c @@ -0,0 +1,699 @@ +// enemy.c +#include +#include + +static EnemyContext *enemy_context_generic; + +// Allocation function +static EnemyContext *enemy_generic_alloc( + const char *id, + int index, + Vector size, + Vector start_position, + Vector end_position, + float move_timer, // Wait duration before moving again + float speed, + float attack_timer, + float strength, + float health) +{ + if (!enemy_context_generic) + { + enemy_context_generic = malloc(sizeof(EnemyContext)); + } + if (!enemy_context_generic) + { + FURI_LOG_E("Game", "Failed to allocate EnemyContext"); + return NULL; + } + snprintf(enemy_context_generic->id, sizeof(enemy_context_generic->id), "%s", id); + enemy_context_generic->index = index; + enemy_context_generic->size = size; + enemy_context_generic->start_position = start_position; + enemy_context_generic->end_position = end_position; + enemy_context_generic->move_timer = move_timer; // Set wait duration + enemy_context_generic->elapsed_move_timer = 0.0f; // Initialize elapsed timer + enemy_context_generic->speed = speed; + enemy_context_generic->attack_timer = attack_timer; + enemy_context_generic->strength = strength; + enemy_context_generic->health = health; + // Initialize other fields as needed + enemy_context_generic->sprite_right = NULL; // Assign appropriate sprite + enemy_context_generic->sprite_left = NULL; // Assign appropriate sprite + enemy_context_generic->direction = ENEMY_RIGHT; // Default direction + enemy_context_generic->state = ENEMY_MOVING_TO_END; // Start in IDLE state + // Set radius based on size, for example, average of size.x and size.y divided by 2 + enemy_context_generic->radius = (size.x + size.y) / 4.0f; + return enemy_context_generic; +} + +// Free function +static void enemy_generic_free(void *context) +{ + if (!context) + { + FURI_LOG_E("Game", "Enemy generic free: Invalid context"); + return; + } + free(context); + context = NULL; + if (enemy_context_generic) + { + free(enemy_context_generic); + enemy_context_generic = NULL; + } +} + +// Enemy start function +static void enemy_start(Entity *self, GameManager *manager, void *context) +{ + UNUSED(manager); + if (!self || !context) + { + FURI_LOG_E("Game", "Enemy start: Invalid parameters"); + return; + } + if (!enemy_context_generic) + { + FURI_LOG_E("Game", "Enemy start: Enemy context not set"); + return; + } + + EnemyContext *enemy_context = (EnemyContext *)context; + // Copy fields from generic context + snprintf(enemy_context->id, sizeof(enemy_context->id), "%s", enemy_context_generic->id); + enemy_context->index = enemy_context_generic->index; + enemy_context->size = enemy_context_generic->size; + enemy_context->start_position = enemy_context_generic->start_position; + enemy_context->end_position = enemy_context_generic->end_position; + enemy_context->move_timer = enemy_context_generic->move_timer; + enemy_context->elapsed_move_timer = enemy_context_generic->elapsed_move_timer; + enemy_context->speed = enemy_context_generic->speed; + enemy_context->attack_timer = enemy_context_generic->attack_timer; + enemy_context->strength = enemy_context_generic->strength; + enemy_context->health = enemy_context_generic->health; + enemy_context->sprite_right = enemy_context_generic->sprite_right; + enemy_context->sprite_left = enemy_context_generic->sprite_left; + enemy_context->direction = enemy_context_generic->direction; + enemy_context->state = enemy_context_generic->state; + enemy_context->radius = enemy_context_generic->radius; + + // Set enemy's initial position based on start_position + entity_pos_set(self, enemy_context->start_position); + + // Add collision circle based on the enemy's radius + entity_collider_add_circle(self, enemy_context->radius); +} + +// Enemy render function +static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, void *context) +{ + if (!self || !context || !canvas || !manager) + return; + + EnemyContext *enemy_context = (EnemyContext *)context; + GameContext *game_context = game_manager_game_context_get(manager); + + // Get the position of the enemy + Vector pos = entity_pos_get(self); + + // Choose sprite based on direction + Sprite *current_sprite = NULL; + if (enemy_context->direction == ENEMY_LEFT) + { + current_sprite = enemy_context->sprite_left; + } + else + { + current_sprite = enemy_context->sprite_right; + } + + // Draw enemy sprite relative to camera, centered on the enemy's position + canvas_draw_sprite( + canvas, + current_sprite, + pos.x - camera_x - (enemy_context->size.x / 2), + pos.y - camera_y - (enemy_context->size.y / 2)); + + // instead of username, draw health + char health_str[32]; + snprintf(health_str, sizeof(health_str), "%.0f", (double)enemy_context->health); + draw_username(canvas, pos, health_str); + + // Draw user stats (this has to be done for all enemies) + draw_user_stats(canvas, (Vector){0, 50}, manager); + + // draw player username from GameContext + Vector posi = entity_pos_get(game_context->player); + draw_username(canvas, posi, game_context->player_context->username); +} + +static void send_attack_notification(GameContext *game_context, EnemyContext *enemy_context, bool player_attacked) +{ + if (!game_context || !enemy_context) + { + FURI_LOG_E("Game", "Send attack notification: Invalid parameters"); + return; + } + + NotificationApp *notifications = furi_record_open(RECORD_NOTIFICATION); + + const bool vibration_allowed = strstr(yes_or_no_choices[game_vibration_on_index], "Yes") != NULL; + const bool sound_allowed = strstr(yes_or_no_choices[game_sound_on_index], "Yes") != NULL; + + if (player_attacked) + { + if (vibration_allowed && sound_allowed) + { + notification_message(notifications, &sequence_success); + } + else if (vibration_allowed && !sound_allowed) + { + notification_message(notifications, &sequence_single_vibro); + } + else if (!vibration_allowed && sound_allowed) + { + // change this to sound later + notification_message(notifications, &sequence_blink_blue_100); + } + else + { + notification_message(notifications, &sequence_blink_blue_100); + } + FURI_LOG_I("Game", "Player attacked enemy '%s'!", enemy_context->id); + } + else + { + if (vibration_allowed && sound_allowed) + { + notification_message(notifications, &sequence_error); + } + else if (vibration_allowed && !sound_allowed) + { + notification_message(notifications, &sequence_single_vibro); + } + else if (!vibration_allowed && sound_allowed) + { + // change this to sound later + notification_message(notifications, &sequence_blink_red_100); + } + else + { + notification_message(notifications, &sequence_blink_red_100); + } + + FURI_LOG_I("Game", "Enemy '%s' attacked the player!", enemy_context->id); + } + + // close the notifications + furi_record_close(RECORD_NOTIFICATION); +} + +// Enemy collision function +static void enemy_collision(Entity *self, Entity *other, GameManager *manager, void *context) +{ + if (!self || !other || !context || !manager) + { + FURI_LOG_E("Game", "Enemy collision: Invalid parameters"); + return; + } + + // Check if the enemy collided with the player + if (entity_description_get(other) == &player_desc) + { + // Retrieve enemy context + EnemyContext *enemy_context = (EnemyContext *)context; + GameContext *game_context = game_manager_game_context_get(manager); + if (!enemy_context) + { + FURI_LOG_E("Game", "Enemy collision: EnemyContext is NULL"); + return; + } + if (!game_context) + { + FURI_LOG_E("Game", "Enemy collision: GameContext is NULL"); + return; + } + + // Get positions of the enemy and the player + Vector enemy_pos = entity_pos_get(self); + Vector player_pos = entity_pos_get(other); + + // Determine if the enemy is facing the player or player is facing the enemy + bool enemy_is_facing_player = false; + bool player_is_facing_enemy = false; + + // Determine if the enemy is facing the player + if ((enemy_context->direction == ENEMY_LEFT && player_pos.x < enemy_pos.x) || + (enemy_context->direction == ENEMY_RIGHT && player_pos.x > enemy_pos.x) || + (enemy_context->direction == ENEMY_UP && player_pos.y < enemy_pos.y) || + (enemy_context->direction == ENEMY_DOWN && player_pos.y > enemy_pos.y)) + { + enemy_is_facing_player = true; + } + + // Determine if the player is facing the enemy + if ((game_context->player_context->direction == PLAYER_LEFT && enemy_pos.x < player_pos.x) || + (game_context->player_context->direction == PLAYER_RIGHT && enemy_pos.x > player_pos.x) || + (game_context->player_context->direction == PLAYER_UP && enemy_pos.y < player_pos.y) || + (game_context->player_context->direction == PLAYER_DOWN && enemy_pos.y > player_pos.y)) + { + player_is_facing_enemy = true; + } + + // Handle Player Attacking Enemy (Press OK, facing enemy, and enemy not facing player) + if (player_is_facing_enemy && game_context->user_input == GameKeyOk && !enemy_is_facing_player) + { + if (game_context->player_context->elapsed_attack_timer >= game_context->player_context->attack_timer) + { + send_attack_notification(game_context, enemy_context, true); + + // Reset player's elapsed attack timer + game_context->player_context->elapsed_attack_timer = 0.0f; + enemy_context->elapsed_attack_timer = 0.0f; // Reset enemy's attack timer to block enemy attack + + // Increase XP by the enemy's strength + game_context->player_context->xp += enemy_context->strength; + + // Increase healthy by 10% of the enemy's strength + game_context->player_context->health += enemy_context->strength * 0.1f; + if (game_context->player_context->health > game_context->player_context->max_health) + { + game_context->player_context->health = game_context->player_context->max_health; + } + + // Decrease enemy health by player strength + enemy_context->health -= game_context->player_context->strength; + + if (enemy_context->health <= 0) + { + FURI_LOG_I("Game", "Enemy '%s' is dead.. resetting enemy position and health", enemy_context->id); + enemy_context->state = ENEMY_DEAD; + + // Reset enemy position and health + enemy_context->health = 100; // this needs to be set to the enemy's max health + + // remove from game context and set in safe zone + game_context->enemies[enemy_context->index] = NULL; + game_context->enemy_count--; + entity_collider_remove(self); + entity_pos_set(self, (Vector){-100, -100}); + return; + } + else + { + FURI_LOG_I("Game", "Enemy '%s' took %f damage from player", enemy_context->id, (double)game_context->player_context->strength); + enemy_context->state = ENEMY_ATTACKED; + + // Bounce the enemy back by X units opposite their last movement direction + enemy_pos.x -= game_context->player_context->dx * enemy_context->radius; + enemy_pos.y -= game_context->player_context->dy * enemy_context->radius; + entity_pos_set(self, enemy_pos); + + // Reset enemy's movement direction to prevent immediate re-collision + game_context->player_context->dx = 0; + game_context->player_context->dy = 0; + } + } + else + { + FURI_LOG_I("Game", "Player attack on enemy '%s' is on cooldown: %f seconds remaining", enemy_context->id, (double)(game_context->player_context->attack_timer - game_context->player_context->elapsed_attack_timer)); + } + } + // Handle Enemy Attacking Player (enemy facing player) + else if (enemy_is_facing_player) + { + if (enemy_context->elapsed_attack_timer >= enemy_context->attack_timer) + { + send_attack_notification(game_context, enemy_context, false); + + // Reset enemy's elapsed attack timer + enemy_context->elapsed_attack_timer = 0.0f; + + // Decrease player health by enemy strength + game_context->player_context->health -= enemy_context->strength; + + if (game_context->player_context->health <= 0) + { + FURI_LOG_I("Game", "Player is dead.. resetting player position and health"); + game_context->player_context->state = PLAYER_DEAD; + + // Reset player position and health + entity_pos_set(other, game_context->player_context->start_position); + game_context->player_context->health = game_context->player_context->max_health; + + // subtract player's XP by the enemy's strength + game_context->player_context->xp -= enemy_context->strength; + if ((int)game_context->player_context->xp < 0) + { + game_context->player_context->xp = 0; + } + } + else + { + FURI_LOG_I("Game", "Player took %f damage from enemy '%s'", (double)enemy_context->strength, enemy_context->id); + game_context->player_context->state = PLAYER_ATTACKED; + + // Bounce the player back by X units opposite their last movement direction + player_pos.x -= game_context->player_context->dx * enemy_context->radius; + player_pos.y -= game_context->player_context->dy * enemy_context->radius; + entity_pos_set(other, player_pos); + + // Reset player's movement direction to prevent immediate re-collision + game_context->player_context->dx = 0; + game_context->player_context->dy = 0; + } + } + else + { + FURI_LOG_I("Game", "Enemy '%s' attack on player is on cooldown: %f seconds remaining", enemy_context->id, (double)(enemy_context->attack_timer - enemy_context->elapsed_attack_timer)); + } + } + else // handle other collisions + { + // bounce player and enemy away from each other + Vector player_pos = entity_pos_get(other); + Vector enemy_pos = entity_pos_get(self); + + // Calculate the direction vector from player to enemy + Vector direction_vector = { + enemy_pos.x - player_pos.x, + enemy_pos.y - player_pos.y}; + + // Normalize the direction vector + float length = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y); + if (length != 0) + { + direction_vector.x /= length; + direction_vector.y /= length; + } + + // Move the player and enemy away from each other + player_pos.y -= direction_vector.y * 3; + entity_pos_set(other, player_pos); + + enemy_pos.x += direction_vector.x * 3; + entity_pos_set(self, enemy_pos); + + // Reset player's movement direction to prevent immediate re-collision + game_context->player_context->dx = 0; + game_context->player_context->dy = 0; + } + + // Reset enemy's state + enemy_context->state = ENEMY_IDLE; + enemy_context->elapsed_move_timer = 0.0f; + + if (game_context->player_context->state == PLAYER_DEAD) + { + // Reset player's position and health + entity_pos_set(other, game_context->player_context->start_position); + game_context->player_context->health = 100; + } + } +} + +// Enemy update function +static void enemy_update(Entity *self, GameManager *manager, void *context) +{ + if (!self || !context || !manager) + return; + + EnemyContext *enemy_context = (EnemyContext *)context; + if (!enemy_context || enemy_context->state == ENEMY_DEAD) + { + return; + } + + GameContext *game_context = game_manager_game_context_get(manager); + if (!game_context) + { + FURI_LOG_E("Game", "Enemy update: Failed to get GameContext"); + return; + } + + float delta_time = 1.0f / game_context->fps; + + // Increment the elapsed_attack_timer for the enemy + enemy_context->elapsed_attack_timer += delta_time; + + switch (enemy_context->state) + { + case ENEMY_IDLE: + // Increment the elapsed_move_timer + enemy_context->elapsed_move_timer += delta_time; + + // Check if it's time to move again + if (enemy_context->elapsed_move_timer >= enemy_context->move_timer) + { + // Determine the next state based on the current position + Vector current_pos = entity_pos_get(self); + if (fabs(current_pos.x - enemy_context->start_position.x) < (double)1.0 && + fabs(current_pos.y - enemy_context->start_position.y) < (double)1.0) + { + enemy_context->state = ENEMY_MOVING_TO_END; + } + else + { + enemy_context->state = ENEMY_MOVING_TO_START; + } + enemy_context->elapsed_move_timer = 0.0f; + } + break; + + case ENEMY_MOVING_TO_END: + case ENEMY_MOVING_TO_START: + { + // Determine the target position based on the current state + Vector target_position = (enemy_context->state == ENEMY_MOVING_TO_END) ? enemy_context->end_position : enemy_context->start_position; + + // Get current position + Vector current_pos = entity_pos_get(self); + Vector direction_vector = {0, 0}; + + // Calculate direction towards the target + if (current_pos.x < target_position.x) + { + direction_vector.x = 1.0f; + enemy_context->direction = ENEMY_RIGHT; + } + else if (current_pos.x > target_position.x) + { + direction_vector.x = -1.0f; + enemy_context->direction = ENEMY_LEFT; + } + + if (current_pos.y < target_position.y) + { + direction_vector.y = 1.0f; + enemy_context->direction = ENEMY_DOWN; + } + else if (current_pos.y > target_position.y) + { + direction_vector.y = -1.0f; + enemy_context->direction = ENEMY_UP; + } + + // Normalize direction vector + float length = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y); + if (length != 0) + { + direction_vector.x /= length; + direction_vector.y /= length; + } + + // Update position based on direction and speed + Vector new_pos = current_pos; + new_pos.x += direction_vector.x * enemy_context->speed * delta_time; + new_pos.y += direction_vector.y * enemy_context->speed * delta_time; + + // Clamp the position to the target to prevent overshooting + if ((direction_vector.x > 0.0f && new_pos.x > target_position.x) || + (direction_vector.x < 0.0f && new_pos.x < target_position.x)) + { + new_pos.x = target_position.x; + } + + if ((direction_vector.y > 0.0f && new_pos.y > target_position.y) || + (direction_vector.y < 0.0f && new_pos.y < target_position.y)) + { + new_pos.y = target_position.y; + } + + entity_pos_set(self, new_pos); + + // Check if the enemy has reached or surpassed the target_position + bool reached_x = fabs(new_pos.x - target_position.x) < (double)1.0; + bool reached_y = fabs(new_pos.y - target_position.y) < (double)1.0; + + // If reached the target position on both axes, transition to IDLE + if (reached_x && reached_y) + { + enemy_context->state = ENEMY_IDLE; + enemy_context->elapsed_move_timer = 0.0f; + } + } + break; + + default: + break; + } +} + +// Free function for the entity +static void enemy_free(Entity *self, GameManager *manager, void *context) +{ + UNUSED(self); + UNUSED(manager); + if (context) + enemy_generic_free(context); +} + +// Enemy behavior structure +static const EntityDescription _generic_enemy = { + .start = enemy_start, + .stop = enemy_free, + .update = enemy_update, + .render = enemy_render, + .collision = enemy_collision, + .event = NULL, + .context_size = sizeof(EnemyContext), +}; + +// Enemy function to return the entity description +const EntityDescription *enemy( + GameManager *manager, + const char *id, + int index, + Vector start_position, + Vector end_position, + float move_timer, // Wait duration before moving again + float speed, + float attack_timer, + float strength, + float health) +{ + SpriteContext *sprite_context = get_sprite_context(id); + if (!sprite_context) + { + FURI_LOG_E("Game", "Failed to get SpriteContext"); + return NULL; + } + + // Allocate a new EnemyContext with provided parameters + enemy_context_generic = enemy_generic_alloc( + id, + index, + (Vector){sprite_context->width, sprite_context->height}, + start_position, + end_position, + move_timer, // Set wait duration + speed, + attack_timer, + strength, + health); + if (!enemy_context_generic) + { + FURI_LOG_E("Game", "Failed to allocate EnemyContext"); + return NULL; + } + + enemy_context_generic->sprite_right = game_manager_sprite_load(manager, sprite_context->right_file_name); + enemy_context_generic->sprite_left = game_manager_sprite_load(manager, sprite_context->left_file_name); + + // Set initial direction based on start and end positions + if (start_position.x < end_position.x) + { + enemy_context_generic->direction = ENEMY_RIGHT; + } + else + { + enemy_context_generic->direction = ENEMY_LEFT; + } + + // Set initial state based on movement + if (start_position.x != end_position.x || start_position.y != end_position.y) + { + enemy_context_generic->state = ENEMY_MOVING_TO_END; + } + else + { + enemy_context_generic->state = ENEMY_IDLE; + } + + return &_generic_enemy; +} + +void spawn_enemy_json_furi(Level *level, GameManager *manager, FuriString *json) +{ + if (!level) + { + FURI_LOG_E("Game", "Level is NULL"); + return; + } + if (!json) + { + FURI_LOG_E("Game", "JSON is NULL"); + return; + } + if (!manager) + { + FURI_LOG_E("Game", "GameManager is NULL"); + return; + } + // parameters: id, index, size.x, size.y, start_position.x, start_position.y, end_position.x, end_position.y, move_timer, speed, attack_timer, strength, health + FuriString *id = get_json_value_furi("id", json); + FuriString *_index = get_json_value_furi("index", json); + // + FuriString *start_position = get_json_value_furi("start_position", json); + FuriString *start_position_x = get_json_value_furi("x", start_position); + FuriString *start_position_y = get_json_value_furi("y", start_position); + // + FuriString *end_position = get_json_value_furi("end_position", json); + FuriString *end_position_x = get_json_value_furi("x", end_position); + FuriString *end_position_y = get_json_value_furi("y", end_position); + // + FuriString *move_timer = get_json_value_furi("move_timer", json); + FuriString *speed = get_json_value_furi("speed", json); + FuriString *attack_timer = get_json_value_furi("attack_timer", json); + FuriString *strength = get_json_value_furi("strength", json); + FuriString *health = get_json_value_furi("health", json); + // + + if (!id || !_index || !start_position || !start_position_x || !start_position_y || !end_position || !end_position_x || !end_position_y || !move_timer || !speed || !attack_timer || !strength || !health) + { + FURI_LOG_E("Game", "Failed to parse JSON values"); + return; + } + + GameContext *game_context = game_manager_game_context_get(manager); + if (game_context && game_context->enemy_count < MAX_ENEMIES && !game_context->enemies[game_context->enemy_count]) + { + game_context->enemies[game_context->enemy_count] = level_add_entity(level, enemy( + manager, + furi_string_get_cstr(id), + atoi(furi_string_get_cstr(_index)), + (Vector){strtod(furi_string_get_cstr(start_position_x), NULL), strtod(furi_string_get_cstr(start_position_y), NULL)}, + (Vector){strtod(furi_string_get_cstr(end_position_x), NULL), strtod(furi_string_get_cstr(end_position_y), NULL)}, + strtod(furi_string_get_cstr(move_timer), NULL), + strtod(furi_string_get_cstr(speed), NULL), + strtod(furi_string_get_cstr(attack_timer), NULL), + strtod(furi_string_get_cstr(strength), NULL), + strtod(furi_string_get_cstr(health), NULL))); + game_context->enemy_count++; + } + + furi_string_free(id); + furi_string_free(_index); + furi_string_free(start_position); + furi_string_free(start_position_x); + furi_string_free(start_position_y); + furi_string_free(end_position); + furi_string_free(end_position_x); + furi_string_free(end_position_y); + furi_string_free(move_timer); + furi_string_free(speed); + furi_string_free(attack_timer); + furi_string_free(strength); + furi_string_free(health); +} \ No newline at end of file diff --git a/flip_world/game/enemy.h b/flip_world/game/enemy.h new file mode 100644 index 000000000..bea4fd987 --- /dev/null +++ b/flip_world/game/enemy.h @@ -0,0 +1,58 @@ +#pragma once +#include +#include "flip_world.h" + +typedef enum +{ + ENEMY_IDLE, + ENEMY_MOVING, + ENEMY_MOVING_TO_START, + ENEMY_MOVING_TO_END, + ENEMY_ATTACKING, + ENEMY_ATTACKED, + ENEMY_DEAD +} EnemyState; + +typedef enum +{ + ENEMY_UP, + ENEMY_DOWN, + ENEMY_LEFT, + ENEMY_RIGHT +} EnemyDirection; + +// EnemyContext definition +typedef struct +{ + char id[64]; // Unique ID for the enemy type + int index; // Index for the specific enemy instance + Vector size; // Size of the enemy + Sprite *sprite_right; // Enemy sprite when looking right + Sprite *sprite_left; // Enemy sprite when looking left + EnemyDirection direction; // Direction the enemy is facing + EnemyState state; // Current state of the enemy + Vector start_position; // Start position of the enemy + Vector end_position; // End position of the enemy + float move_timer; // Timer for the enemy movement + float elapsed_move_timer; // Elapsed time for the enemy movement + float radius; // Collision radius for the enemy + float speed; // Speed of the enemy + float attack_timer; // Cooldown duration between attacks + float elapsed_attack_timer; // Time elapsed since the last attack + float strength; // Damage the enemy deals + float health; // Health of the enemy +} EnemyContext; + +const EntityDescription *enemy( + GameManager *manager, + const char *id, + int index, + Vector start_position, + Vector end_position, + float move_timer, // Wait duration before moving again + float speed, + float attack_timer, + float strength, + float health); + +void spawn_enemy_json_furi(Level *level, GameManager *manager, FuriString *json); \ No newline at end of file diff --git a/flip_world/game/game.c b/flip_world/game/game.c new file mode 100644 index 000000000..c95dfa4b6 --- /dev/null +++ b/flip_world/game/game.c @@ -0,0 +1,101 @@ +#include +#include + +/****** Game ******/ +/* + Write here the start code for your game, for example: creating a level and so on. + Game context is allocated (game.context_size) and passed to this function, you can use it to store your game data. +*/ +static void game_start(GameManager *game_manager, void *ctx) +{ + // Do some initialization here, for example you can load score from storage. + // For simplicity, we will just set it to 0. + GameContext *game_context = ctx; + game_context->fps = game_fps_choices_2[game_fps_index]; + game_context->player_context = NULL; + game_context->current_level = 0; + game_context->ended_early = false; + game_context->level_count = 0; + + // set all levels to NULL + for (int i = 0; i < MAX_LEVELS; i++) + { + game_context->levels[i] = NULL; + } + + // attempt to allocate all levels + for (int i = 0; i < MAX_LEVELS; i++) + { + if (!allocate_level(game_manager, i)) + { + if (i == 0) + { + FURI_LOG_E("Game", "Failed to allocate level %d, loading default level", i); + game_context->levels[0] = game_manager_add_level(game_manager, generic_level("town_world_v2", 0)); + game_context->level_count = 1; + break; + } + FURI_LOG_E("Game", "No more levels to load"); + break; + } + game_context->level_count++; + } + + // imu + game_context->imu = imu_alloc(); + game_context->imu_present = imu_present(game_context->imu); +} + +/* + Write here the stop code for your game, for example, freeing memory, if it was allocated. + You don't need to free level, sprites or entities, it will be done automatically. + Also, you don't need to free game_context, it will be done automatically, after this function. +*/ +static void game_stop(void *ctx) +{ + if (!ctx) + { + FURI_LOG_E("Game", "Invalid game context"); + return; + } + + GameContext *game_context = ctx; + if (!game_context) + { + FURI_LOG_E("Game", "Game context is NULL"); + return; + } + + imu_free(game_context->imu); + game_context->imu = NULL; + + if (game_context->player_context) + { + FURI_LOG_I("Game", "Game ending"); + if (!game_context->ended_early) + { + easy_flipper_dialog("Game Over", "Thanks for playing Flip World!\nHit BACK then wait for\nthe game to save."); + } + else + { + easy_flipper_dialog("Game Over", "Ran out of memory so the\ngame ended early.\nHit BACK to exit."); + } + FURI_LOG_I("Game", "Saving player context"); + save_player_context_api(game_context->player_context); + FURI_LOG_I("Game", "Player context saved"); + easy_flipper_dialog("Game Saved", "Hit BACK to exit."); + } +} + +/* + Your game configuration, do not rename this variable, but you can change its content here. +*/ + +const Game game = { + .target_fps = 0, // set to 0 because we set this in game_app (callback.c line 22) + .show_fps = false, // show fps counter on the screen + .always_backlight = true, // keep display backlight always on + .start = game_start, // will be called once, when game starts + .stop = game_stop, // will be called once, when game stops + .context_size = sizeof(GameContext), // size of game context +}; diff --git a/flip_world/game/game.h b/flip_world/game/game.h new file mode 100644 index 000000000..4b9b9a51a --- /dev/null +++ b/flip_world/game/game.h @@ -0,0 +1,8 @@ +#pragma once +#include "engine/engine.h" +#include +#include +#include +#include +#include "flip_world.h" +#include diff --git a/flip_world/game/icon.c b/flip_world/game/icon.c new file mode 100644 index 000000000..fc095aec6 --- /dev/null +++ b/flip_world/game/icon.c @@ -0,0 +1,395 @@ +#include "game/icon.h" + +static void icon_collision(Entity *self, Entity *other, GameManager *manager, void *context) +{ + UNUSED(manager); + UNUSED(self); + IconContext *icon_ctx = (IconContext *)context; + if (!icon_ctx) + { + FURI_LOG_E("Game", "Icon context is NULL"); + return; + } + + if (entity_description_get(other) == &player_desc) + { + PlayerContext *player = (PlayerContext *)entity_context_get(other); + if (player) + { + Vector pos = entity_pos_get(other); + // Bounce back by 2 + pos.x -= player->dx * 2; + pos.y -= player->dy * 2; + entity_pos_set(other, pos); + + // Reset movement to prevent re-collision + player->dx = 0; + player->dy = 0; + } + } +} + +static void icon_render(Entity *self, GameManager *manager, Canvas *canvas, void *context) +{ + UNUSED(manager); + IconContext *icon_ctx = (IconContext *)context; + if (!icon_ctx) + { + FURI_LOG_E("Game", "Icon context is NULL"); + return; + } + Vector pos = entity_pos_get(self); + + // Draw the icon, centered + canvas_draw_icon( + canvas, + pos.x - camera_x - icon_ctx->width / 2, + pos.y - camera_y - icon_ctx->height / 2, + icon_ctx->icon); +} + +static void icon_start(Entity *self, GameManager *manager, void *context) +{ + UNUSED(manager); + + IconContext *icon_ctx_self = (IconContext *)context; + if (!icon_ctx_self) + { + FURI_LOG_E("Game", "Icon context self is NULL"); + return; + } + IconContext *icon_ctx = entity_context_get(self); + if (!icon_ctx) + { + FURI_LOG_E("Game", "Icon context is NULL"); + return; + } + + IconContext *loaded_data = get_icon_context(g_temp_spawn_name); + if (!loaded_data) + { + FURI_LOG_E("Game", "Failed to find icon data for %s", g_temp_spawn_name); + return; + } + + icon_ctx_self->icon = loaded_data->icon; + icon_ctx_self->width = loaded_data->width; + icon_ctx_self->height = loaded_data->height; + icon_ctx->icon = loaded_data->icon; + icon_ctx->width = loaded_data->width; + icon_ctx->height = loaded_data->height; + + Vector pos = entity_pos_get(self); + pos.x += icon_ctx_self->width / 2; + pos.y += icon_ctx_self->height / 2; + entity_pos_set(self, pos); + + entity_collider_add_circle( + self, + (icon_ctx_self->width + icon_ctx_self->height) / 4); + + free(loaded_data); +} + +// -------------- Stop callback -------------- +static void icon_free(Entity *self, GameManager *manager, void *context) +{ + UNUSED(self); + UNUSED(manager); + UNUSED(context); + if (context) + { + free(context); + } +} + +// -------------- Entity description -------------- +const EntityDescription icon_desc = { + .start = icon_start, + .stop = icon_free, + .update = NULL, + .render = icon_render, + .collision = icon_collision, + .event = NULL, + .context_size = sizeof(IconContext), +}; + +static IconContext *icon_generic_alloc(const char *id, const Icon *icon, uint8_t width, uint8_t height) +{ + IconContext *ctx = malloc(sizeof(IconContext)); + if (!ctx) + { + FURI_LOG_E("Game", "Failed to allocate IconContext"); + return NULL; + } + snprintf(ctx->id, sizeof(ctx->id), "%s", id); + ctx->icon = icon; + ctx->width = width; + ctx->height = height; + return ctx; +} + +IconContext *get_icon_context(const char *name) +{ + // if (strcmp(name, "earth") == 0) + // { + // return icon_generic_alloc("earth", &I_icon_earth_15x16, 15, 16); + // } + // else if (strcmp(name, "home") == 0) + // { + // return icon_generic_alloc("home", &I_icon_home_15x16, 15, 16); + // } + if (strcmp(name, "house") == 0) + { + return icon_generic_alloc("house", &I_icon_house_48x32px, 48, 32); + } + // else if (strcmp(name, "house_3d") == 0) + // { + // return icon_generic_alloc("house_3d", &I_icon_house_3d_34x45px, 34, 45); + // } + // else if (strcmp(name, "info") == 0) + // { + // return icon_generic_alloc("info", &I_icon_info_15x16, 15, 16); + // } + else if (strcmp(name, "man") == 0) + { + return icon_generic_alloc("man", &I_icon_man_7x16, 7, 16); + } + else if (strcmp(name, "plant") == 0) + { + return icon_generic_alloc("plant", &I_icon_plant_16x16, 16, 16); + } + // else if (strcmp(name, "plant_fern") == 0) + // { + // return icon_generic_alloc("plant_fern", &I_icon_plant_fern_18x16px, 18, 16); + // } + // else if (strcmp(name, "plant_pointy") == 0) + // { + // return icon_generic_alloc("plant_pointy", &I_icon_plant_pointy_13x16px, 13, 16); + // } + else if (strcmp(name, "tree") == 0) + { + return icon_generic_alloc("tree", &I_icon_tree_16x16, 16, 16); + } + // else if (strcmp(name, "tree_29x30") == 0) + // { + // return icon_generic_alloc("tree_29x30", &I_icon_tree_29x30px, 29, 30); + // } + // else if (strcmp(name, "tree_48x48") == 0) + // { + // return icon_generic_alloc("tree_48x48", &I_icon_tree_48x48px, 48, 48); + // } + else if (strcmp(name, "woman") == 0) + { + return icon_generic_alloc("woman", &I_icon_woman_9x16, 9, 16); + } + // else if (strcmp(name, "chest_closed") == 0) + // { + // return icon_generic_alloc("chest_closed", &I_icon_chest_closed_16x13px, 16, 13); + // } + // else if (strcmp(name, "chest_open") == 0) + // { + // return icon_generic_alloc("chest_open", &I_icon_chest_open_16x16px, 16, 16); + // } + else if (strcmp(name, "fence") == 0) + { + return icon_generic_alloc("fence", &I_icon_fence_16x8px, 16, 8); + } + else if (strcmp(name, "fence_end") == 0) + { + return icon_generic_alloc("fence_end", &I_icon_fence_end_16x8px, 16, 8); + } + // else if (strcmp(name, "fence_vertical_end") == 0) + // { + // return icon_generic_alloc("fence_vertical_end", &I_icon_fence_vertical_end_6x8px, 6, 8); + // } + // else if (strcmp(name, "fence_vertical_start") == 0) + // { + // return icon_generic_alloc("fence_vertical_start", &I_icon_fence_vertical_start_6x15px, 6, 15); + // } + else if (strcmp(name, "flower") == 0) + { + return icon_generic_alloc("flower", &I_icon_flower_16x16, 16, 16); + } + else if (strcmp(name, "lake_bottom") == 0) + { + return icon_generic_alloc("lake_bottom", &I_icon_lake_bottom_31x12px, 31, 12); + } + else if (strcmp(name, "lake_bottom_left") == 0) + { + return icon_generic_alloc("lake_bottom_left", &I_icon_lake_bottom_left_24x22px, 24, 22); + } + else if (strcmp(name, "lake_bottom_right") == 0) + { + return icon_generic_alloc("lake_bottom_right", &I_icon_lake_bottom_right_24x22px, 24, 22); + } + else if (strcmp(name, "lake_left") == 0) + { + return icon_generic_alloc("lake_left", &I_icon_lake_left_11x31px, 11, 31); + } + else if (strcmp(name, "lake_right") == 0) + { + // Assuming 11x31 + return icon_generic_alloc("lake_right", &I_icon_lake_right_11x31, 11, 31); + } + else if (strcmp(name, "lake_top") == 0) + { + return icon_generic_alloc("lake_top", &I_icon_lake_top_31x12px, 31, 12); + } + else if (strcmp(name, "lake_top_left") == 0) + { + return icon_generic_alloc("lake_top_left", &I_icon_lake_top_left_24x22px, 24, 22); + } + else if (strcmp(name, "lake_top_right") == 0) + { + return icon_generic_alloc("lake_top_right", &I_icon_lake_top_right_24x22px, 24, 22); + } + else if (strcmp(name, "rock_large") == 0) + { + return icon_generic_alloc("rock_large", &I_icon_rock_large_18x19px, 18, 19); + } + else if (strcmp(name, "rock_medium") == 0) + { + return icon_generic_alloc("rock_medium", &I_icon_rock_medium_16x14px, 16, 14); + } + else if (strcmp(name, "rock_small") == 0) + { + return icon_generic_alloc("rock_small", &I_icon_rock_small_10x8px, 10, 8); + } + + // If no match is found + FURI_LOG_E("Game", "Icon not found: %s", name); + return NULL; +} + +const char *icon_get_id(const Icon *icon) +{ + // if (icon == &I_icon_earth_15x16) + // { + // return "earth"; + // } + // else if (icon == &I_icon_home_15x16) + // { + // return "home"; + // } + if (icon == &I_icon_house_48x32px) + { + return "house"; + } + // else if (icon == &I_icon_house_3d_34x45px) + // { + // return "house_3d"; + // } + // else if (icon == &I_icon_info_15x16) + // { + // return "info"; + // } + else if (icon == &I_icon_man_7x16) + { + return "man"; + } + else if (icon == &I_icon_plant_16x16) + { + return "plant"; + } + // else if (icon == &I_icon_plant_fern_18x16px) + // { + // return "plant_fern"; + // } + // else if (icon == &I_icon_plant_pointy_13x16px) + // { + // return "plant_pointy"; + // } + else if (icon == &I_icon_tree_16x16) + { + return "tree"; + } + // else if (icon == &I_icon_tree_29x30px) + // { + // return "tree_29x30"; + // } + // else if (icon == &I_icon_tree_48x48px) + // { + // return "tree_48x48"; + // } + else if (icon == &I_icon_woman_9x16) + { + return "woman"; + } + // else if (icon == &I_icon_chest_closed_16x13px) + // { + // return "chest_closed"; + // } + // else if (icon == &I_icon_chest_open_16x16px) + // { + // return "chest_open"; + // } + else if (icon == &I_icon_fence_16x8px) + { + return "fence"; + } + else if (icon == &I_icon_fence_end_16x8px) + { + return "fence_end"; + } + // else if (icon == &I_icon_fence_vertical_end_6x8px) + // { + // return "fence_vertical_end"; + // } + // else if (icon == &I_icon_fence_vertical_start_6x15px) + // { + // return "fence_vertical_start"; + // } + else if (icon == &I_icon_flower_16x16) + { + return "flower"; + } + else if (icon == &I_icon_lake_bottom_31x12px) + { + return "lake_bottom"; + } + else if (icon == &I_icon_lake_bottom_left_24x22px) + { + return "lake_bottom_left"; + } + else if (icon == &I_icon_lake_bottom_right_24x22px) + { + return "lake_bottom_right"; + } + else if (icon == &I_icon_lake_left_11x31px) + { + return "lake_left"; + } + else if (icon == &I_icon_lake_right_11x31) + { + return "lake_right"; + } + else if (icon == &I_icon_lake_top_31x12px) + { + return "lake_top"; + } + else if (icon == &I_icon_lake_top_left_24x22px) + { + return "lake_top_left"; + } + else if (icon == &I_icon_lake_top_right_24x22px) + { + return "lake_top_right"; + } + else if (icon == &I_icon_rock_large_18x19px) + { + return "rock_large"; + } + else if (icon == &I_icon_rock_medium_16x14px) + { + return "rock_medium"; + } + else if (icon == &I_icon_rock_small_10x8px) + { + return "rock_small"; + } + + // If no match is found + FURI_LOG_E("Game", "Icon ID not found for given icon pointer."); + return NULL; +} diff --git a/flip_world/game/icon.h b/flip_world/game/icon.h new file mode 100644 index 000000000..17424a3c8 --- /dev/null +++ b/flip_world/game/icon.h @@ -0,0 +1,15 @@ +#pragma once +#include "flip_world_icons.h" +#include "game.h" + +typedef struct +{ + char id[32]; + const Icon *icon; + uint8_t width; + uint8_t height; +} IconContext; + +extern const EntityDescription icon_desc; +IconContext *get_icon_context(const char *name); +const char *icon_get_id(const Icon *icon); \ No newline at end of file diff --git a/flip_world/game/level.c b/flip_world/game/level.c new file mode 100644 index 000000000..ce7e691a3 --- /dev/null +++ b/flip_world/game/level.c @@ -0,0 +1,211 @@ +#include +#include +#include +bool allocate_level(GameManager *manager, int index) +{ + GameContext *game_context = game_manager_game_context_get(manager); + + // open the world list from storage, then create a level for each world + char file_path[128]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json"); + FuriString *world_list = flipper_http_load_from_file(file_path); + if (!world_list) + { + FURI_LOG_E("Game", "Failed to load world list"); + game_context->levels[0] = game_manager_add_level(manager, generic_level("town_world_v2", 0)); + game_context->level_count = 1; + return false; + } + FuriString *world_name = get_json_array_value_furi("worlds", index, world_list); + if (!world_name) + { + FURI_LOG_E("Game", "Failed to get world name"); + furi_string_free(world_list); + return false; + } + FURI_LOG_I("Game", "Allocating level %d for world %s", index, furi_string_get_cstr(world_name)); + game_context->levels[index] = game_manager_add_level(manager, generic_level(furi_string_get_cstr(world_name), index)); + furi_string_free(world_name); + furi_string_free(world_list); + return true; +} +static void set_world(Level *level, GameManager *manager, char *id) +{ + char file_path[256]; + snprintf(file_path, sizeof(file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s/%s_json_data.json", + id, id); + + FuriString *json_data_str = flipper_http_load_from_file(file_path); + if (!json_data_str || furi_string_empty(json_data_str)) + { + FURI_LOG_E("Game", "Failed to load json data from file"); + draw_town_world(level); + return; + } + + if (!is_enough_heap(28400)) + { + FURI_LOG_E("Game", "Not enough heap memory.. ending game early."); + GameContext *game_context = game_manager_game_context_get(manager); + game_context->ended_early = true; + game_manager_game_stop(manager); // end game early + furi_string_free(json_data_str); + return; + } + + FURI_LOG_I("Game", "Drawing world"); + if (!draw_json_world_furi(level, json_data_str)) + { + FURI_LOG_E("Game", "Failed to draw world"); + draw_town_world(level); + furi_string_free(json_data_str); + } + else + { + FURI_LOG_I("Game", "Drawing enemies"); + furi_string_free(json_data_str); + snprintf(file_path, sizeof(file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s/%s_enemy_data.json", + id, id); + + FuriString *enemy_data_str = flipper_http_load_from_file(file_path); + if (!enemy_data_str || furi_string_empty(enemy_data_str)) + { + FURI_LOG_E("Game", "Failed to get enemy data"); + draw_town_world(level); + return; + } + + // Loop through the array + for (int i = 0; i < MAX_ENEMIES; i++) + { + FuriString *single_enemy_data = get_json_array_value_furi("enemy_data", i, enemy_data_str); + if (!single_enemy_data || furi_string_empty(single_enemy_data)) + { + // No more enemy elements found + if (single_enemy_data) + furi_string_free(single_enemy_data); + break; + } + + spawn_enemy_json_furi(level, manager, single_enemy_data); + furi_string_free(single_enemy_data); + } + furi_string_free(enemy_data_str); + FURI_LOG_I("Game", "Finished loading world data"); + } +} +static void level_start(Level *level, GameManager *manager, void *context) +{ + if (!level || !context || !manager) + { + FURI_LOG_E("Game", "Level, context, or manager is NULL"); + return; + } + + level_clear(level); + player_spawn(level, manager); + + LevelContext *level_context = context; + if (!level_context) + { + FURI_LOG_E("Game", "Level context is NULL"); + return; + } + + // check if the world exists + if (!world_exists(level_context->id)) + { + FURI_LOG_E("Game", "World does not exist.. downloading now"); + FuriString *world_data = fetch_world(level_context->id); + if (!world_data) + { + FURI_LOG_E("Game", "Failed to fetch world data"); + draw_town_world(level); + return; + } + furi_string_free(world_data); + + set_world(level, manager, level_context->id); + + FURI_LOG_I("Game", "World set."); + } + else + { + FURI_LOG_I("Game", "World exists.. loading now"); + set_world(level, manager, level_context->id); + FURI_LOG_I("Game", "World set."); + } +} + +static LevelContext *level_context_generic; + +static LevelContext *level_generic_alloc(const char *id, int index) +{ + if (level_context_generic == NULL) + { + size_t heap_size = memmgr_get_free_heap(); + if (heap_size < sizeof(LevelContext)) + { + FURI_LOG_E("Game", "Not enough heap to allocate level context"); + return NULL; + } + level_context_generic = malloc(sizeof(LevelContext)); + } + snprintf(level_context_generic->id, sizeof(level_context_generic->id), "%s", id); + level_context_generic->index = index; + return level_context_generic; +} + +static void level_generic_free() +{ + if (level_context_generic != NULL) + { + free(level_context_generic); + level_context_generic = NULL; + } +} + +static void free_level(Level *level, GameManager *manager, void *context) +{ + UNUSED(level); + UNUSED(manager); + UNUSED(context); + level_generic_free(); +} + +static void level_alloc_generic_world(Level *level, GameManager *manager, void *context) +{ + UNUSED(manager); + UNUSED(level); + if (!level_context_generic) + { + FURI_LOG_E("Game", "Generic level context not set"); + return; + } + if (!context) + { + FURI_LOG_E("Game", "Context is NULL"); + return; + } + LevelContext *level_context = context; + snprintf(level_context->id, sizeof(level_context->id), "%s", level_context_generic->id); + level_context->index = level_context_generic->index; +} + +const LevelBehaviour _generic_level = { + .alloc = level_alloc_generic_world, + .free = free_level, + .start = level_start, + .stop = NULL, + .context_size = sizeof(LevelContext), +}; + +const LevelBehaviour *generic_level(const char *id, int index) +{ + // free any old context before allocating a new one + level_generic_free(); + level_context_generic = level_generic_alloc(id, index); + return &_generic_level; +} diff --git a/flip_world/game/level.h b/flip_world/game/level.h new file mode 100644 index 000000000..91bca9df9 --- /dev/null +++ b/flip_world/game/level.h @@ -0,0 +1,11 @@ +#pragma once +#include "game.h" +#include "flip_world.h" +typedef struct +{ + char id[64]; + int index; +} LevelContext; + +const LevelBehaviour *generic_level(const char *id, int index); +bool allocate_level(GameManager *manager, int index); \ No newline at end of file diff --git a/flip_world/game/player.c b/flip_world/game/player.c new file mode 100644 index 000000000..39a9f222c --- /dev/null +++ b/flip_world/game/player.c @@ -0,0 +1,398 @@ +#include +#include +/****** Entities: Player ******/ +static Level *get_next_level(GameManager *manager) +{ + GameContext *game_context = game_manager_game_context_get(manager); + if (!game_context) + { + FURI_LOG_E(TAG, "Failed to get game context"); + return NULL; + } + for (int i = game_context->current_level + 1; i < game_context->level_count; i++) + { + if (!game_context->levels[i]) + { + if (!allocate_level(manager, i)) + { + FURI_LOG_E(TAG, "Failed to allocate level %d", i); + return NULL; + } + } + game_context->current_level = i; + return game_context->levels[i]; + } + FURI_LOG_I(TAG, "No more levels to load"); + return NULL; +} + +void player_spawn(Level *level, GameManager *manager) +{ + if (!level || !manager) + { + FURI_LOG_E(TAG, "Invalid arguments to player_spawn"); + return; + } + + GameContext *game_context = game_manager_game_context_get(manager); + if (!game_context) + { + FURI_LOG_E(TAG, "Failed to get game context"); + return; + } + + game_context->player = level_add_entity(level, &player_desc); + if (!game_context->player) + { + FURI_LOG_E(TAG, "Failed to add player entity to level"); + return; + } + + // Set player position. + entity_pos_set(game_context->player, (Vector){WORLD_WIDTH / 2, WORLD_HEIGHT / 2}); + + // Box is centered in player x and y, and its size + entity_collider_add_rect(game_context->player, 13, 11); + + // Get player context + PlayerContext *player_context = entity_context_get(game_context->player); + if (!player_context) + { + FURI_LOG_E(TAG, "Failed to get player context"); + return; + } + + // player context must be set each level or NULL pointer will be dereferenced + if (!load_player_context(player_context)) + { + FURI_LOG_E(TAG, "Loading player context failed. Initializing default values."); + + // Initialize default player context + player_context->sprite_right = game_manager_sprite_load(manager, "player_right_sword_15x11px.fxbm"); + player_context->sprite_left = game_manager_sprite_load(manager, "player_left_sword_15x11px.fxbm"); + player_context->direction = PLAYER_RIGHT; // default direction + player_context->health = 100; + player_context->strength = 10; + player_context->level = 1; + player_context->xp = 0; + player_context->start_position = entity_pos_get(game_context->player); + player_context->attack_timer = 0.1f; + player_context->elapsed_attack_timer = player_context->attack_timer; + player_context->health_regen = 1; // 1 health per second + player_context->elapsed_health_regen = 0; + player_context->max_health = 100 + ((player_context->level - 1) * 10); // 10 health per level + + // Set player username + if (!load_char("Flip-Social-Username", player_context->username, sizeof(player_context->username))) + { + // If loading username fails, default to "Player" + snprintf(player_context->username, sizeof(player_context->username), "Player"); + } + + game_context->player_context = player_context; + + // Save the initialized context + if (!save_player_context(player_context)) + { + FURI_LOG_E(TAG, "Failed to save player context after initialization"); + } + + return; + } + + // Load player sprite (we'll add this to the JSON later when players can choose their sprite) + player_context->sprite_right = game_manager_sprite_load(manager, "player_right_sword_15x11px.fxbm"); + player_context->sprite_left = game_manager_sprite_load(manager, "player_left_sword_15x11px.fxbm"); + + player_context->start_position = entity_pos_get(game_context->player); + + // Update player stats based on XP using iterative method + // Function to get the current level based on XP iteratively + int get_player_level_iterative(uint32_t xp) + { + int level = 1; + uint32_t xp_required = 100; // Base XP for level 2 + + while (level < 100 && xp >= xp_required) // Maximum level supported + { + level++; + xp_required = (uint32_t)(xp_required * 1.5); // 1.5 growth factor per level + } + + return level; + } + + // Determine the player's level based on XP + player_context->level = get_player_level_iterative(player_context->xp); + + // Update strength and max health based on the new level + player_context->strength = 10 + (player_context->level * 1); // 1 strength per level + player_context->max_health = 100 + ((player_context->level - 1) * 10); // 10 health per level + + // Assign loaded player context to game context + game_context->player_context = player_context; +} + +// code from Derek Jamison +// eventually we'll add dynamic positioning based on how much pitch/roll is detected +// instead of assigning a fixed value +static int player_x_from_pitch(float pitch) +{ + if (pitch > 6.0) + { + return 1; + } + else if (pitch < -8.0) + { + return -1; + } + return 0; +} + +static int player_y_from_roll(float roll) +{ + if (roll > 9.0) + { + return 1; + } + else if (roll < -20.0) + { + return -1; + } + return 0; +} + +static void player_update(Entity *self, GameManager *manager, void *context) +{ + if (!self || !manager || !context) + return; + + PlayerContext *player = (PlayerContext *)context; + InputState input = game_manager_input_get(manager); + Vector pos = entity_pos_get(self); + GameContext *game_context = game_manager_game_context_get(manager); + + // Store previous direction + int prev_dx = player->dx; + int prev_dy = player->dy; + + // Reset movement deltas each frame + player->dx = 0; + player->dy = 0; + + if (game_context->imu_present) + { + player->dx = player_x_from_pitch(-imu_pitch_get(game_context->imu)); + player->dy = player_y_from_roll(-imu_roll_get(game_context->imu)); + + switch (player->dx) + { + case -1: + player->direction = PLAYER_LEFT; + pos.x -= 1; + break; + case 1: + player->direction = PLAYER_RIGHT; + pos.x += 1; + break; + default: + break; + } + + switch (player->dy) + { + case -1: + player->direction = PLAYER_UP; + pos.y -= 1; + break; + case 1: + player->direction = PLAYER_DOWN; + pos.y += 1; + break; + default: + break; + } + } + + // Apply health regeneration + player->elapsed_health_regen += 1.0f / game_context->fps; + if (player->elapsed_health_regen >= 1.0f && player->health < player->max_health) + { + player->health += (player->health_regen + player->health > player->max_health) + ? (player->max_health - player->health) + : player->health_regen; + player->elapsed_health_regen = 0; + } + + // Increment the elapsed_attack_timer for the player + player->elapsed_attack_timer += 1.0f / game_context->fps; + + // Handle movement input + if (input.held & GameKeyUp) + { + pos.y -= 2; + player->dy = -1; + player->direction = PLAYER_UP; + game_context->user_input = GameKeyUp; + } + if (input.held & GameKeyDown) + { + pos.y += 2; + player->dy = 1; + player->direction = PLAYER_DOWN; + game_context->user_input = GameKeyDown; + } + if (input.held & GameKeyLeft) + { + pos.x -= 2; + player->dx = -1; + player->direction = PLAYER_LEFT; + game_context->user_input = GameKeyLeft; + } + if (input.held & GameKeyRight) + { + pos.x += 2; + player->dx = 1; + player->direction = PLAYER_RIGHT; + game_context->user_input = GameKeyRight; + } + + // Clamp the player's position to stay within world bounds + pos.x = CLAMP(pos.x, WORLD_WIDTH - 5, 5); + pos.y = CLAMP(pos.y, WORLD_HEIGHT - 5, 5); + + // Update player position + entity_pos_set(self, pos); + + // switch levels if holding OK + if (input.pressed & GameKeyOk) + { + // if all enemies are dead, allow the "OK" button to switch levels + // otherwise the "OK" button will be used to attack + if (game_context->enemy_count == 0) + { + FURI_LOG_I(TAG, "Switching levels"); + save_player_context(player); + game_manager_next_level_set(manager, get_next_level(manager)); + furi_delay_ms(500); + return; + } + else + { + game_context->user_input = GameKeyOk; + // furi_delay_ms(100); + } + } + + // If the player is not moving, retain the last movement direction + if (player->dx == 0 && player->dy == 0) + { + player->dx = prev_dx; + player->dy = prev_dy; + player->state = PLAYER_IDLE; + game_context->user_input = -1; // reset user input + } + else + { + player->state = PLAYER_MOVING; + } + + // Handle back button to stop the game + if (input.pressed & GameKeyBack) + { + game_manager_game_stop(manager); + } +} + +static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context) +{ + UNUSED(manager); + if (!self || !context || !canvas) + return; + // Get player context + PlayerContext *player = context; + + // Get player position + Vector pos = entity_pos_get(self); + + // Draw background (updates camera_x and camera_y) + draw_background(canvas, pos); + + // Draw player sprite relative to camera, centered on the player's position + canvas_draw_sprite( + canvas, + player->direction == PLAYER_RIGHT ? player->sprite_right : player->sprite_left, + pos.x - camera_x - 5, // Center the sprite horizontally + pos.y - camera_y - 5 // Center the sprite vertically + ); +} + +const EntityDescription player_desc = { + .start = NULL, // called when entity is added to the level + .stop = NULL, // called when entity is removed from the level + .update = player_update, // called every frame + .render = player_render, // called every frame, after update + .collision = NULL, // called when entity collides with another entity + .event = NULL, // called when entity receives an event + .context_size = sizeof(PlayerContext), // size of entity context, will be automatically allocated and freed +}; + +static SpriteContext *sprite_generic_alloc(const char *id, bool is_enemy, uint8_t width, uint8_t height) +{ + SpriteContext *ctx = malloc(sizeof(SpriteContext)); + if (!ctx) + { + FURI_LOG_E("Game", "Failed to allocate SpriteContext"); + return NULL; + } + snprintf(ctx->id, sizeof(ctx->id), "%s", id); + ctx->width = width; + ctx->height = height; + if (!is_enemy) + { + snprintf(ctx->right_file_name, sizeof(ctx->right_file_name), "player_right_%s_%dx%dpx.fxbm", id, width, height); + snprintf(ctx->left_file_name, sizeof(ctx->left_file_name), "player_left_%s_%dx%dpx.fxbm", id, width, height); + } + else + { + snprintf(ctx->right_file_name, sizeof(ctx->right_file_name), "enemy_right_%s_%dx%dpx.fxbm", id, width, height); + snprintf(ctx->left_file_name, sizeof(ctx->left_file_name), "enemy_left_%s_%dx%dpx.fxbm", id, width, height); + } + return ctx; +} + +SpriteContext *get_sprite_context(const char *name) +{ + if (strcmp(name, "axe") == 0) + { + return sprite_generic_alloc("axe", false, 15, 11); + } + else if (strcmp(name, "bow") == 0) + { + return sprite_generic_alloc("bow", false, 13, 11); + } + else if (strcmp(name, "naked") == 0) + { + return sprite_generic_alloc("naked", false, 10, 10); + } + else if (strcmp(name, "sword") == 0) + { + return sprite_generic_alloc("sword", false, 15, 11); + } + else if (strcmp(name, "cyclops") == 0) + { + return sprite_generic_alloc("cyclops", true, 10, 11); + } + else if (strcmp(name, "ghost") == 0) + { + return sprite_generic_alloc("ghost", true, 15, 15); + } + else if (strcmp(name, "ogre") == 0) + { + return sprite_generic_alloc("ogre", true, 10, 13); + } + + // If no match is found + FURI_LOG_E("Game", "Sprite not found: %s", name); + return NULL; +} \ No newline at end of file diff --git a/flip_world/game/player.h b/flip_world/game/player.h new file mode 100644 index 000000000..ec7fd216c --- /dev/null +++ b/flip_world/game/player.h @@ -0,0 +1,76 @@ +#pragma once +#include "engine/engine.h" +#include +#include +#include "engine/sensors/imu.h" + +// Maximum enemies +#define MAX_ENEMIES 2 +#define MAX_LEVELS 10 + +typedef enum +{ + PLAYER_IDLE, + PLAYER_MOVING, + PLAYER_ATTACKING, + PLAYER_ATTACKED, + PLAYER_DEAD, +} PlayerState; + +typedef enum +{ + PLAYER_UP, + PLAYER_DOWN, + PLAYER_LEFT, + PLAYER_RIGHT +} PlayerDirection; + +typedef struct +{ + PlayerDirection direction; // direction the player is facing + PlayerState state; // current state of the player + Vector start_position; // starting position of the player + Sprite *sprite_right; // player sprite looking right + Sprite *sprite_left; // player sprite looking left + int8_t dx; // x direction + int8_t dy; // y direction + uint32_t xp; // experience points + uint32_t level; // player level + uint32_t strength; // player strength + uint32_t health; // player health + uint32_t max_health; // player maximum health + uint32_t health_regen; // player health regeneration rate per second/frame + float elapsed_health_regen; // time elapsed since last health regeneration + float attack_timer; // Cooldown duration between attacks + float elapsed_attack_timer; // Time elapsed since the last attack + char username[32]; // player username +} PlayerContext; + +typedef struct +{ + PlayerContext *player_context; + Level *levels[MAX_LEVELS]; + Entity *enemies[MAX_ENEMIES]; + Entity *player; + GameKey user_input; + float fps; + int level_count; + int enemy_count; + int current_level; + bool ended_early; + Imu *imu; + bool imu_present; +} GameContext; + +typedef struct +{ + char id[16]; + char left_file_name[64]; + char right_file_name[64]; + uint8_t width; + uint8_t height; +} SpriteContext; + +extern const EntityDescription player_desc; +void player_spawn(Level *level, GameManager *manager); +SpriteContext *get_sprite_context(const char *name); diff --git a/flip_world/game/storage.c b/flip_world/game/storage.c new file mode 100644 index 000000000..bd9fd8b57 --- /dev/null +++ b/flip_world/game/storage.c @@ -0,0 +1,1088 @@ +#include + +static bool save_uint32(const char *path_name, uint32_t value) +{ + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%lu", value); + return save_char(path_name, buffer); +} + +// Helper function to save an int8_t +static bool save_int8(const char *path_name, int8_t value) +{ + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%d", value); + return save_char(path_name, buffer); +} + +// Helper function to save a float +static bool save_float(const char *path_name, float value) +{ + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%.6f", (double)value); // Limit to 6 decimal places + return save_char(path_name, buffer); +} +bool save_player_context(PlayerContext *player_context) +{ + if (!player_context) + { + FURI_LOG_E(TAG, "Invalid player context"); + return false; + } + + // Create the directory for saving settings + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + furi_record_close(RECORD_STORAGE); + + // 1. Username (String) + if (!save_char("player/username", player_context->username)) + { + FURI_LOG_E(TAG, "Failed to save player username"); + return false; + } + + // 2. Level (uint32_t) + if (!save_uint32("player/level", player_context->level)) + { + FURI_LOG_E(TAG, "Failed to save player level"); + return false; + } + + // 3. XP (uint32_t) + if (!save_uint32("player/xp", player_context->xp)) + { + FURI_LOG_E(TAG, "Failed to save player xp"); + return false; + } + + // 4. Health (uint32_t) + if (!save_uint32("player/health", player_context->health)) + { + FURI_LOG_E(TAG, "Failed to save player health"); + return false; + } + + // 5. Strength (uint32_t) + if (!save_uint32("player/strength", player_context->strength)) + { + FURI_LOG_E(TAG, "Failed to save player strength"); + return false; + } + + // 6. Max Health (uint32_t) + if (!save_uint32("player/max_health", player_context->max_health)) + { + FURI_LOG_E(TAG, "Failed to save player max health"); + return false; + } + + // 7. Health Regen (uint32_t) + if (!save_uint32("player/health_regen", player_context->health_regen)) + { + FURI_LOG_E(TAG, "Failed to save player health regen"); + return false; + } + + // 8. Elapsed Health Regen (float) + if (!save_float("player/elapsed_health_regen", player_context->elapsed_health_regen)) + { + FURI_LOG_E(TAG, "Failed to save player elapsed health regen"); + return false; + } + + // 9. Attack Timer (float) + if (!save_float("player/attack_timer", player_context->attack_timer)) + { + FURI_LOG_E(TAG, "Failed to save player attack timer"); + return false; + } + + // 10. Elapsed Attack Timer (float) + if (!save_float("player/elapsed_attack_timer", player_context->elapsed_attack_timer)) + { + FURI_LOG_E(TAG, "Failed to save player elapsed attack timer"); + return false; + } + + // 11. Direction (enum PlayerDirection) + { + char direction_str[2]; + switch (player_context->direction) + { + case PLAYER_UP: + strncpy(direction_str, "0", sizeof(direction_str)); + break; + case PLAYER_DOWN: + strncpy(direction_str, "1", sizeof(direction_str)); + break; + case PLAYER_LEFT: + strncpy(direction_str, "2", sizeof(direction_str)); + break; + case PLAYER_RIGHT: + default: + strncpy(direction_str, "3", sizeof(direction_str)); + break; + } + direction_str[1] = '\0'; // Ensure null termination + + if (!save_char("player/direction", direction_str)) + { + FURI_LOG_E(TAG, "Failed to save player direction"); + return false; + } + } + + // 12. State (enum PlayerState) + { + char state_str[2]; + switch (player_context->state) + { + case PLAYER_IDLE: + strncpy(state_str, "0", sizeof(state_str)); + break; + case PLAYER_MOVING: + strncpy(state_str, "1", sizeof(state_str)); + break; + case PLAYER_ATTACKING: + strncpy(state_str, "2", sizeof(state_str)); + break; + case PLAYER_ATTACKED: + strncpy(state_str, "3", sizeof(state_str)); + break; + case PLAYER_DEAD: + strncpy(state_str, "4", sizeof(state_str)); + break; + default: + strncpy(state_str, "5", sizeof(state_str)); // Assuming '5' for unknown states + break; + } + state_str[1] = '\0'; // Ensure null termination + + if (!save_char("player/state", state_str)) + { + FURI_LOG_E(TAG, "Failed to save player state"); + return false; + } + } + + // 13. Start Position X (float) + if (!save_float("player/start_position_x", player_context->start_position.x)) + { + FURI_LOG_E(TAG, "Failed to save player start position x"); + return false; + } + + // 14. Start Position Y (float) + if (!save_float("player/start_position_y", player_context->start_position.y)) + { + FURI_LOG_E(TAG, "Failed to save player start position y"); + return false; + } + + // 15. dx (int8_t) + if (!save_int8("player/dx", player_context->dx)) + { + FURI_LOG_E(TAG, "Failed to save player dx"); + return false; + } + + // 16. dy (int8_t) + if (!save_int8("player/dy", player_context->dy)) + { + FURI_LOG_E(TAG, "Failed to save player dy"); + return false; + } + + return true; +} + +bool save_player_context_api(PlayerContext *player_context) +{ + if (!player_context) + { + FURI_LOG_E(TAG, "Invalid player context"); + return false; + } + + FlipperHTTP *fhttp = flipper_http_alloc(); + if (!fhttp) + { + FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP"); + return false; + } + + // create JSON for all the player context data + FuriString *json = furi_string_alloc(); + if (!json) + { + FURI_LOG_E(TAG, "Failed to allocate JSON string"); + return false; + } + + // opening brace + furi_string_cat_str(json, "{"); + + // 1. Username (String) + furi_string_cat_str(json, "\"username\":\""); + furi_string_cat_str(json, player_context->username); + furi_string_cat_str(json, "\","); + + // 2. Level (uint32_t) + furi_string_cat_str(json, "\"level\":"); + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%lu", player_context->level); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 3. XP (uint32_t) + furi_string_cat_str(json, "\"xp\":"); + snprintf(buffer, sizeof(buffer), "%lu", player_context->xp); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 4. Health (uint32_t) + furi_string_cat_str(json, "\"health\":"); + snprintf(buffer, sizeof(buffer), "%lu", player_context->health); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 5. Strength (uint32_t) + furi_string_cat_str(json, "\"strength\":"); + snprintf(buffer, sizeof(buffer), "%lu", player_context->strength); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 6. Max Health (uint32_t) + furi_string_cat_str(json, "\"max_health\":"); + snprintf(buffer, sizeof(buffer), "%lu", player_context->max_health); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 7. Health Regen (uint32_t) + furi_string_cat_str(json, "\"health_regen\":"); + snprintf(buffer, sizeof(buffer), "%lu", player_context->health_regen); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 8. Elapsed Health Regen (float) + furi_string_cat_str(json, "\"elapsed_health_regen\":"); + snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->elapsed_health_regen); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 9. Attack Timer (float) + furi_string_cat_str(json, "\"attack_timer\":"); + snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->attack_timer); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 10. Elapsed Attack Timer (float) + furi_string_cat_str(json, "\"elapsed_attack_timer\":"); + snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->elapsed_attack_timer); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 11. Direction (enum PlayerDirection) + furi_string_cat_str(json, "\"direction\":"); + switch (player_context->direction) + { + case PLAYER_UP: + furi_string_cat_str(json, "\"up\","); + break; + case PLAYER_DOWN: + furi_string_cat_str(json, "\"down\","); + break; + case PLAYER_LEFT: + furi_string_cat_str(json, "\"left\","); + break; + case PLAYER_RIGHT: + default: + furi_string_cat_str(json, "\"right\","); + break; + } + + // 12. State (enum PlayerState) + furi_string_cat_str(json, "\"state\":"); + switch (player_context->state) + { + case PLAYER_IDLE: + furi_string_cat_str(json, "\"idle\","); + break; + case PLAYER_MOVING: + furi_string_cat_str(json, "\"moving\","); + break; + case PLAYER_ATTACKING: + furi_string_cat_str(json, "\"attacking\","); + break; + case PLAYER_ATTACKED: + furi_string_cat_str(json, "\"attacked\","); + break; + case PLAYER_DEAD: + furi_string_cat_str(json, "\"dead\","); + break; + default: + furi_string_cat_str(json, "\"unknown\","); + break; + } + + // 13. Start Position X (float) + furi_string_cat_str(json, "\"start_position_x\":"); + snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->start_position.x); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 14. Start Position Y (float) + furi_string_cat_str(json, "\"start_position_y\":"); + snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->start_position.y); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 15. dx (int8_t) + furi_string_cat_str(json, "\"dx\":"); + snprintf(buffer, sizeof(buffer), "%d", player_context->dx); + furi_string_cat_str(json, buffer); + furi_string_cat_str(json, ","); + + // 16. dy (int8_t) + furi_string_cat_str(json, "\"dy\":"); + snprintf(buffer, sizeof(buffer), "%d", player_context->dy); + furi_string_cat_str(json, buffer); + + // closing brace + furi_string_cat_str(json, "}"); + + // save the json to API + + // create new JSON with username key (of just username), and game_stats key (of the all of the data) + FuriString *json_data = furi_string_alloc(); + if (!json_data) + { + FURI_LOG_E(TAG, "Failed to allocate JSON string"); + furi_string_free(json); + return false; + } + + furi_string_cat_str(json_data, "{\"username\":\""); + furi_string_cat_str(json_data, player_context->username); + furi_string_cat_str(json_data, "\",\"game_stats\":"); + furi_string_cat(json_data, json); + furi_string_cat_str(json_data, "}"); + + furi_string_free(json); + + // save the json_data to the API + if (!flipper_http_post_request_with_headers(fhttp, "https://www.flipsocial.net/api/user/update-game-stats/", "{\"Content-Type\":\"application/json\"}", furi_string_get_cstr(json_data))) + { + FURI_LOG_E(TAG, "Failed to save player context to API"); + furi_string_free(json_data); + return false; + } + fhttp->state = RECEIVING; + while (fhttp->state != IDLE) + { + furi_delay_ms(100); + } + furi_string_free(json_data); + flipper_http_free(fhttp); + return true; +} + +// Helper function to load an integer +static bool load_number(const char *path_name, int *value) +{ + if (!path_name || !value) + { + FURI_LOG_E(TAG, "Invalid arguments to load_number"); + return false; + } + + char buffer[64]; + + if (!load_char(path_name, buffer, sizeof(buffer))) + { + FURI_LOG_E(TAG, "Failed to load number from path: %s", path_name); + return false; + } + + *value = atoi(buffer); + return true; +} + +// Helper function to load a float +static bool load_float(const char *path_name, float *value) +{ + if (!path_name || !value) + { + FURI_LOG_E(TAG, "Invalid arguments to load_float"); + return false; + } + + char buffer[64]; + if (!load_char(path_name, buffer, sizeof(buffer))) + { + FURI_LOG_E(TAG, "Failed to load float from path: %s", path_name); + return false; + } + + // check if the string is a valid float + char *endptr; + *value = strtof(buffer, &endptr); + if (endptr == buffer) + { + FURI_LOG_E(TAG, "Failed to parse float from path: %s", path_name); + return false; + } + + return true; +} + +// Helper function to load an int8_t +static bool load_int8(const char *path_name, int8_t *value) +{ + if (!path_name || !value) + { + FURI_LOG_E(TAG, "Invalid arguments to load_int8"); + return false; + } + + char buffer[64]; + + if (!load_char(path_name, buffer, sizeof(buffer))) + { + FURI_LOG_E(TAG, "Failed to load int8 from path: %s", path_name); + return false; + } + + long temp = strtol(buffer, NULL, 10); + if (temp < INT8_MIN || temp > INT8_MAX) + { + FURI_LOG_E(TAG, "Value out of range for int8: %ld", temp); + return false; + } + + // check if the string is a valid int8 + char *endptr; + *value = (int8_t)strtol(buffer, &endptr, 10); + if (endptr == buffer) + { + FURI_LOG_E(TAG, "Failed to parse int8 from path: %s", path_name); + return false; + } + return true; +} + +// Helper function to load a uint32_t +static bool load_uint32(const char *path_name, uint32_t *value) +{ + if (!path_name || !value) + { + FURI_LOG_E(TAG, "Invalid arguments to load_uint32"); + return false; + } + + char buffer[64]; + if (!load_char(path_name, buffer, sizeof(buffer))) + { + FURI_LOG_E(TAG, "Failed to load uint32 from path: %s", path_name); + return false; + } + + // check if the string is a valid uint32 + char *endptr; + *value = strtoul(buffer, &endptr, 10); + if (endptr == buffer) + { + FURI_LOG_E(TAG, "Failed to parse uint32 from path: %s", path_name); + return false; + } + + return true; +} + +bool load_player_context(PlayerContext *player_context) +{ + if (!player_context) + { + FURI_LOG_E(TAG, "Invalid player context"); + return false; + } + + // 1. Username (String) + if (!load_char("player/username", player_context->username, sizeof(player_context->username))) + { + FURI_LOG_E(TAG, "No data or parse error for username. Using default: 'Unknown'"); + memset(player_context->username, 0, sizeof(player_context->username)); + strncpy(player_context->username, "Unknown", sizeof(player_context->username) - 1); + } + + // 2. Level (uint32_t) + { + uint32_t temp = 1; // Default + if (!load_char("player/level", (char *)&temp, sizeof(temp))) + { + FURI_LOG_E(TAG, "No data or parse error for level. Using default: 1"); + } + else + { + // char buffer[64]; + if (load_uint32("player/level", &temp)) + { + player_context->level = temp; + } + else + { + FURI_LOG_E(TAG, "Failed to parse level. Using default: 1"); + player_context->level = 1; + } + } + player_context->level = temp; + } + + // 3. XP (uint32_t) + { + uint32_t temp = 0; // Default + if (!load_uint32("player/xp", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for xp. Using default: 0"); + temp = 0; + } + player_context->xp = temp; + } + + // 4. Health (uint32_t) + { + uint32_t temp = 100; // Default + if (!load_uint32("player/health", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for health. Using default: 100"); + temp = 100; + } + player_context->health = temp; + } + + // 5. Strength (uint32_t) + { + uint32_t temp = 10; // Default + if (!load_uint32("player/strength", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for strength. Using default: 10"); + temp = 10; + } + player_context->strength = temp; + } + + // 6. Max Health (uint32_t) + { + uint32_t temp = 100; // Default + if (!load_uint32("player/max_health", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for max_health. Using default: 100"); + temp = 100; + } + player_context->max_health = temp; + } + + // 7. Health Regen (uint32_t) + { + uint32_t temp = 1; // Default + if (!load_uint32("player/health_regen", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for health_regen. Using default: 1"); + temp = 1; + } + player_context->health_regen = temp; + } + + // 8. Elapsed Health Regen (float) + { + float temp = 0.0f; // Default + if (!load_float("player/elapsed_health_regen", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for elapsed_health_regen. Using default: 0.0f"); + temp = 0.0f; + } + player_context->elapsed_health_regen = temp; + } + + // 9. Attack Timer (float) + { + float temp = 0.1f; // Default + if (!load_float("player/attack_timer", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for attack_timer. Using default: 0.1f"); + temp = 0.1f; + } + player_context->attack_timer = temp; + } + + // 10. Elapsed Attack Timer (float) + { + float temp = 0.0f; // Default + if (!load_float("player/elapsed_attack_timer", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for elapsed_attack_timer. Using default: 0.0f"); + temp = 0.0f; + } + player_context->elapsed_attack_timer = temp; + } + + // 11. Direction (enum PlayerDirection) + { + int direction_int = 3; // Default to PLAYER_RIGHT + if (!load_number("player/direction", &direction_int)) + { + FURI_LOG_E(TAG, "No data or parse error for direction. Defaulting to PLAYER_RIGHT"); + direction_int = 3; + } + + switch (direction_int) + { + case 0: + player_context->direction = PLAYER_UP; + break; + case 1: + player_context->direction = PLAYER_DOWN; + break; + case 2: + player_context->direction = PLAYER_LEFT; + break; + case 3: + player_context->direction = PLAYER_RIGHT; + break; + default: + FURI_LOG_E(TAG, "Invalid direction value: %d. Defaulting to PLAYER_RIGHT", direction_int); + player_context->direction = PLAYER_RIGHT; + break; + } + } + + // 12. State (enum PlayerState) + { + int state_int = 0; // Default to PLAYER_IDLE + if (!load_number("player/state", &state_int)) + { + FURI_LOG_E(TAG, "No data or parse error for state. Defaulting to PLAYER_IDLE"); + state_int = 0; + } + + switch (state_int) + { + case 0: + player_context->state = PLAYER_IDLE; + break; + case 1: + player_context->state = PLAYER_MOVING; + break; + case 2: + player_context->state = PLAYER_ATTACKING; + break; + case 3: + player_context->state = PLAYER_ATTACKED; + break; + case 4: + player_context->state = PLAYER_DEAD; + break; + default: + FURI_LOG_E(TAG, "Invalid state value: %d. Defaulting to PLAYER_IDLE", state_int); + player_context->state = PLAYER_IDLE; + break; + } + } + + // 13. Start Position X (float) + { + float temp = 192.0f; // Default + if (!load_float("player/start_position_x", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for start_position_x. Using default: 192.0f"); + temp = 192.0f; + } + player_context->start_position.x = temp; + } + + // 14. Start Position Y (float) + { + float temp = 96.0f; // Default + if (!load_float("player/start_position_y", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for start_position_y. Using default: 96.0f"); + temp = 96.0f; + } + player_context->start_position.y = temp; + } + + // 15. dx (int8_t) + { + int8_t temp = 1; // Default + if (!load_int8("player/dx", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for dx. Using default: 1"); + temp = 1; + } + player_context->dx = temp; + } + + // 16. dy (int8_t) + { + int8_t temp = 0; // Default + if (!load_int8("player/dy", &temp)) + { + FURI_LOG_E(TAG, "No data or parse error for dy. Using default: 0"); + temp = 0; + } + player_context->dy = temp; + } + + return true; +} +// loads from STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player/player_stats.json +// then gets each key-value pair and saves it as it's own file so it can be loaded separately using +// load_player_context +bool set_player_context() +{ + char file_path[256]; + snprintf(file_path, sizeof(file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player/player_stats.json"); + + FuriString *player_stats = flipper_http_load_from_file(file_path); + if (!player_stats) + { + FURI_LOG_E(TAG, "Failed to load player stats from file: %s", file_path); + return false; + } + + // Get the key one-by-one and save it to a separate file + + // 1. Username (String) + FuriString *username = get_json_value_furi("username", player_stats); + if (username) + { + save_char("player/username", furi_string_get_cstr(username)); + furi_string_free(username); + } + + // 2. Level (uint32_t) + FuriString *level = get_json_value_furi("level", player_stats); + if (level) + { + save_uint32("player/level", atoi(furi_string_get_cstr(level))); + furi_string_free(level); + } + + // 3. XP (uint32_t) + FuriString *xp = get_json_value_furi("xp", player_stats); + if (xp) + { + save_uint32("player/xp", atoi(furi_string_get_cstr(xp))); + furi_string_free(xp); + } + + // 4. Health (uint32_t) + FuriString *health = get_json_value_furi("health", player_stats); + if (health) + { + save_uint32("player/health", atoi(furi_string_get_cstr(health))); + furi_string_free(health); + } + + // 5. Strength (uint32_t) + FuriString *strength = get_json_value_furi("strength", player_stats); + if (strength) + { + save_uint32("player/strength", atoi(furi_string_get_cstr(strength))); + furi_string_free(strength); + } + + // 6. Max Health (uint32_t) + FuriString *max_health = get_json_value_furi("max_health", player_stats); + if (max_health) + { + save_uint32("player/max_health", atoi(furi_string_get_cstr(max_health))); + furi_string_free(max_health); + } + + // 7. Health Regen (uint32_t) + FuriString *health_regen = get_json_value_furi("health_regen", player_stats); + if (health_regen) + { + save_uint32("player/health_regen", atoi(furi_string_get_cstr(health_regen))); + furi_string_free(health_regen); + } + + // 8. Elapsed Health Regen (float) + FuriString *elapsed_health_regen = get_json_value_furi("elapsed_health_regen", player_stats); + if (elapsed_health_regen) + { + save_float("player/elapsed_health_regen", strtof(furi_string_get_cstr(elapsed_health_regen), NULL)); + furi_string_free(elapsed_health_regen); + } + + // 9. Attack Timer (float) + FuriString *attack_timer = get_json_value_furi("attack_timer", player_stats); + if (attack_timer) + { + save_float("player/attack_timer", strtof(furi_string_get_cstr(attack_timer), NULL)); + furi_string_free(attack_timer); + } + + // 10. Elapsed Attack Timer (float) + FuriString *elapsed_attack_timer = get_json_value_furi("elapsed_attack_timer", player_stats); + if (elapsed_attack_timer) + { + save_float("player/elapsed_attack_timer", strtof(furi_string_get_cstr(elapsed_attack_timer), NULL)); + furi_string_free(elapsed_attack_timer); + } + + // 11. Direction (enum PlayerDirection) + FuriString *direction = get_json_value_furi("direction", player_stats); + if (direction) + { + save_char("player/direction", furi_string_get_cstr(direction)); + furi_string_free(direction); + } + + // 12. State (enum PlayerState) + FuriString *state = get_json_value_furi("state", player_stats); + if (state) + { + save_char("player/state", furi_string_get_cstr(state)); + furi_string_free(state); + } + + // 13. Start Position X (float) + FuriString *start_position_x = get_json_value_furi("start_position_x", player_stats); + if (start_position_x) + { + save_float("player/start_position_x", strtof(furi_string_get_cstr(start_position_x), NULL)); + furi_string_free(start_position_x); + } + + // 14. Start Position Y (float) + FuriString *start_position_y = get_json_value_furi("start_position_y", player_stats); + if (start_position_y) + { + save_float("player/start_position_y", strtof(furi_string_get_cstr(start_position_y), NULL)); + furi_string_free(start_position_y); + } + + // 15. dx (int8_t) + FuriString *dx = get_json_value_furi("dx", player_stats); + if (dx) + { + save_int8("player/dx", atoi(furi_string_get_cstr(dx))); + furi_string_free(dx); + } + + // 16. dy (int8_t) + FuriString *dy = get_json_value_furi("dy", player_stats); + if (dy) + { + save_int8("player/dy", atoi(furi_string_get_cstr(dy))); + furi_string_free(dy); + } + + furi_string_free(player_stats); + return true; +} + +static inline void furi_string_remove_str(FuriString *string, const char *needle) +{ + furi_string_replace_str(string, needle, "", 0); +} + +static FuriString *enemy_data(const FuriString *world_data) +{ + size_t enemy_data_pos = furi_string_search_str(world_data, "enemy_data", 0); + if (enemy_data_pos == FURI_STRING_FAILURE) + { + FURI_LOG_E("Game", "Failed to find enemy_data in world data"); + + return NULL; + } + + size_t bracket_start = furi_string_search_char(world_data, '[', enemy_data_pos); + if (bracket_start == FURI_STRING_FAILURE) + { + FURI_LOG_E("Game", "Failed to find start of enemy_data array"); + + return NULL; + } + + size_t bracket_end = furi_string_search_char(world_data, ']', bracket_start); + if (bracket_end == FURI_STRING_FAILURE) + { + FURI_LOG_E("Game", "Failed to find end of enemy_data array"); + + return NULL; + } + + FuriString *enemy_data_str = furi_string_alloc(); + if (!enemy_data_str) + { + FURI_LOG_E("Game", "Failed to allocate enemy_data string"); + + return NULL; + } + + furi_string_cat_str(enemy_data_str, "{\"enemy_data\":"); + + { + FuriString *temp_sub = furi_string_alloc(); + + furi_string_set_strn( + temp_sub, + furi_string_get_cstr(world_data) + bracket_start, + (bracket_end + 1) - bracket_start); + + furi_string_cat(enemy_data_str, temp_sub); + furi_string_free(temp_sub); + } + + furi_string_cat_str(enemy_data_str, "}"); + + return enemy_data_str; +} + +static FuriString *json_data(const FuriString *world_data) +{ + size_t json_data_pos = furi_string_search_str(world_data, "json_data", 0); + if (json_data_pos == FURI_STRING_FAILURE) + { + FURI_LOG_E("Game", "Failed to find json_data in world data"); + + return NULL; + } + + size_t bracket_start = furi_string_search_char(world_data, '[', json_data_pos); + if (bracket_start == FURI_STRING_FAILURE) + { + FURI_LOG_E("Game", "Failed to find start of json_data array"); + + return NULL; + } + + size_t bracket_end = furi_string_search_char(world_data, ']', bracket_start); + if (bracket_end == FURI_STRING_FAILURE) + { + FURI_LOG_E("Game", "Failed to find end of json_data array"); + + return NULL; + } + + FuriString *json_data_str = furi_string_alloc(); + if (!json_data_str) + { + FURI_LOG_E("Game", "Failed to allocate json_data string"); + + return NULL; + } + + furi_string_cat_str(json_data_str, "{\"json_data\":"); + + { + FuriString *temp_sub = furi_string_alloc(); + + furi_string_set_strn( + temp_sub, + furi_string_get_cstr(world_data) + bracket_start, + (bracket_end + 1) - bracket_start); + + furi_string_cat(json_data_str, temp_sub); + furi_string_free(temp_sub); + } + + furi_string_cat_str(json_data_str, "}"); + + return json_data_str; +} + +bool separate_world_data(char *id, FuriString *world_data) +{ + if (!id || !world_data) + { + FURI_LOG_E("Game", "Invalid parameters"); + return false; + } + FuriString *file_json_data = json_data(world_data); + if (!file_json_data || furi_string_size(file_json_data) == 0) + { + FURI_LOG_E("Game", "Failed to get json data in separate_world_data"); + return false; + } + + // Save file_json_data to disk + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s", id); + + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + + File *file = storage_file_alloc(storage); + char file_path[256]; + snprintf(file_path, sizeof(file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s/%s_json_data.json", + id, id); + + if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) + { + FURI_LOG_E("Game", "Failed to open file for writing: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + furi_string_free(file_json_data); + return false; + } + + size_t data_size = furi_string_size(file_json_data); + if (storage_file_write(file, furi_string_get_cstr(file_json_data), data_size) != data_size) + { + FURI_LOG_E("Game", "Failed to write json_data"); + } + storage_file_close(file); + + furi_string_replace_at(file_json_data, 0, 1, ""); + furi_string_replace_at(file_json_data, furi_string_size(file_json_data) - 1, 1, ""); + // include the comma at the end of the json_data array + furi_string_cat_str(file_json_data, ","); + furi_string_remove_str(world_data, furi_string_get_cstr(file_json_data)); + furi_string_free(file_json_data); + + FuriString *file_enemy_data = enemy_data(world_data); + if (!file_enemy_data) + { + FURI_LOG_E("Game", "Failed to get enemy data"); + return false; + } + + snprintf(file_path, sizeof(file_path), + STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s/%s_enemy_data.json", + id, id); + + if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) + { + FURI_LOG_E("Game", "Failed to open file for writing: %s", file_path); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + furi_string_free(file_enemy_data); + return false; + } + + data_size = furi_string_size(file_enemy_data); + if (storage_file_write(file, furi_string_get_cstr(file_enemy_data), data_size) != data_size) + { + FURI_LOG_E("Game", "Failed to write enemy_data"); + } + + // Clean up + furi_string_free(file_enemy_data); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + return true; +} diff --git a/flip_world/game/storage.h b/flip_world/game/storage.h new file mode 100644 index 000000000..e682a3ad9 --- /dev/null +++ b/flip_world/game/storage.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include +#include + +bool save_player_context(PlayerContext *player_context); +bool save_player_context_api(PlayerContext *player_context); +bool load_player_context(PlayerContext *player_context); +bool set_player_context(); + +// save the json_data and enemy_data to separate files +bool separate_world_data(char *id, FuriString *world_data); \ No newline at end of file diff --git a/flip_world/game/world.c b/flip_world/game/world.c new file mode 100644 index 000000000..87b8c2a64 --- /dev/null +++ b/flip_world/game/world.c @@ -0,0 +1,214 @@ +#include +#include +#include +void draw_bounds(Canvas *canvas) +{ + // Draw the outer bounds adjusted by camera offset + // we draw this last to ensure users can see the bounds + canvas_draw_frame(canvas, -camera_x, -camera_y, WORLD_WIDTH, WORLD_HEIGHT); +} + +bool draw_json_world_furi(Level *level, const FuriString *json_data) +{ + if (!json_data) + { + FURI_LOG_E("Game", "JSON data is NULL"); + return false; + } + int levels_added = 0; + FURI_LOG_I("Game", "Looping through world data"); + for (int i = 0; i < MAX_WORLD_OBJECTS; i++) + { + FURI_LOG_I("Game", "Looping through world data: %d", i); + FuriString *data = get_json_array_value_furi("json_data", i, json_data); + if (!data) + { + break; + } + + FuriString *icon = get_json_value_furi("icon", data); + FuriString *x = get_json_value_furi("x", data); + FuriString *y = get_json_value_furi("y", data); + FuriString *amount = get_json_value_furi("amount", data); + FuriString *horizontal = get_json_value_furi("horizontal", data); + + if (!icon || !x || !y || !amount || !horizontal) + { + FURI_LOG_E("Game", "Failed Data: %s", furi_string_get_cstr(data)); + + if (data) + furi_string_free(data); + if (icon) + furi_string_free(icon); + if (x) + furi_string_free(x); + if (y) + furi_string_free(y); + if (amount) + furi_string_free(amount); + if (horizontal) + furi_string_free(horizontal); + + level_clear(level); + return false; + } + + int count = atoi(furi_string_get_cstr(amount)); + if (count < 2) + { + // Just one icon + spawn_icon( + level, + furi_string_get_cstr(icon), + atoi(furi_string_get_cstr(x)), + atoi(furi_string_get_cstr(y))); + } + else + { + bool is_horizontal = (furi_string_cmp(horizontal, "true") == 0); + spawn_icon_line( + level, + furi_string_get_cstr(icon), + atoi(furi_string_get_cstr(x)), + atoi(furi_string_get_cstr(y)), + count, + is_horizontal); + } + + furi_string_free(data); + furi_string_free(icon); + furi_string_free(x); + furi_string_free(y); + furi_string_free(amount); + furi_string_free(horizontal); + levels_added++; + } + FURI_LOG_I("Game", "Finished loading world data"); + return levels_added > 0; +} + +void draw_town_world(Level *level) +{ + + // house-fence group 1 + spawn_icon(level, "house", 164, 40); + spawn_icon(level, "fence", 148, 64); + spawn_icon(level, "fence", 164, 64); + spawn_icon(level, "fence_end", 180, 64); + + // house-fence group 4 (the left of group 1) + spawn_icon(level, "house", 110, 40); + spawn_icon(level, "fence", 96, 64); + spawn_icon(level, "fence", 110, 64); + spawn_icon(level, "fence_end", 126, 64); + + // house-fence group 5 (the left of group 4) + spawn_icon(level, "house", 56, 40); + spawn_icon(level, "fence", 40, 64); + spawn_icon(level, "fence", 56, 64); + spawn_icon(level, "fence_end", 72, 64); + + // line of fences on the 8th row (using spawn_icon_line) + spawn_icon_line(level, "fence", 8, 96, 10, true); + + // plants spaced out underneath the fences + spawn_icon_line(level, "plant", 40, 110, 6, true); + spawn_icon_line(level, "flower", 40, 140, 6, true); + + // man and woman + spawn_icon(level, "man", 156, 110); + spawn_icon(level, "woman", 164, 110); + + // lake + // Top row + spawn_icon(level, "lake_top_left", 240, 62); + spawn_icon(level, "lake_top", 264, 57); + spawn_icon(level, "lake_top_right", 295, 62); + + // Middle row + spawn_icon(level, "lake_left", 231, 84); + spawn_icon(level, "lake_right", 304, 84); + + // Bottom row + spawn_icon(level, "lake_bottom_left", 240, 115); + spawn_icon(level, "lake_bottom", 264, 120); + spawn_icon(level, "lake_bottom_right", 295, 115); + + // Spawn two full left/up tree lines + for (int i = 0; i < 2; i++) + { + // Horizontal line of 22 icons + spawn_icon_line(level, "tree", 5, 2 + i * 17, 22, true); + // Vertical line of 11 icons + spawn_icon_line(level, "tree", 5 + i * 17, 2, 11, false); + } + + // Spawn two full down tree lines + for (int i = 9; i < 11; i++) + { + // Horizontal line of 22 icons + spawn_icon_line(level, "tree", 5, 2 + i * 17, 22, true); + } + + // Spawn two full right tree lines + for (int i = 20; i < 22; i++) + { + // Vertical line of 8 icons starting further down (y=50) + spawn_icon_line(level, "tree", 5 + i * 17, 50, 8, false); + } +} + +FuriString *fetch_world(const char *name) +{ + if (!name) + { + FURI_LOG_E("Game", "World name is NULL"); + return NULL; + } + + FlipperHTTP *fhttp = flipper_http_alloc(); + if (!fhttp) + { + FURI_LOG_E("Game", "Failed to allocate HTTP"); + return NULL; + } + + char url[256]; + snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v3/get/world/%s/", name); + snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name); + fhttp->save_received_data = true; + if (!flipper_http_get_request_with_headers(fhttp, url, "{\"Content-Type\": \"application/json\"}")) + { + FURI_LOG_E("Game", "Failed to send HTTP request"); + flipper_http_free(fhttp); + return NULL; + } + 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 request to be received + furi_delay_ms(100); + } + furi_timer_stop(fhttp->get_timeout_timer); + if (fhttp->state != IDLE) + { + FURI_LOG_E("Game", "Failed to receive world data"); + flipper_http_free(fhttp); + return NULL; + } + flipper_http_free(fhttp); + FuriString *returned_data = load_furi_world(name); + if (!returned_data) + { + FURI_LOG_E("Game", "Failed to load world data from file"); + return NULL; + } + if (!separate_world_data((char *)name, returned_data)) + { + FURI_LOG_E("Game", "Failed to separate world data"); + furi_string_free(returned_data); + return NULL; + } + return returned_data; +} \ No newline at end of file diff --git a/flip_world/game/world.h b/flip_world/game/world.h new file mode 100644 index 000000000..725f25c37 --- /dev/null +++ b/flip_world/game/world.h @@ -0,0 +1,17 @@ +#pragma once +#include +// Screen size +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 + +// World size (3x3) +#define WORLD_WIDTH 384 +#define WORLD_HEIGHT 192 + +// Maximum number of world objects +#define MAX_WORLD_OBJECTS 25 // any more than that and we may run out of heap when switching worlds + +void draw_bounds(Canvas *canvas); +void draw_town_world(Level *level); +bool draw_json_world_furi(Level *level, const FuriString *json_data); +FuriString *fetch_world(const char *name); \ No newline at end of file diff --git a/flip_world/jsmn/jsmn.c b/flip_world/jsmn/jsmn.c new file mode 100644 index 000000000..d101530c6 --- /dev/null +++ b/flip_world/jsmn/jsmn.c @@ -0,0 +1,803 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * [License text continues...] + */ + +#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 *get_json(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, const char *json_data) +{ + // Parse the JSON feed + if (json_data != NULL) + { + jsmn_parser parser; + jsmn_init(&parser); + uint32_t max_tokens = json_token_count(json_data); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + return NULL; + } + // 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"); + } + char warning[128]; + snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key); + FURI_LOG_E("JSMM.H", warning); + return NULL; // Return NULL if something goes wrong +} + +// Helper function to skip a token and all its descendants. +// Returns the index of the next token after skipping this one. +// On error or out of bounds, returns -1. +static int skip_token(const jsmntok_t *tokens, int start, int total) +{ + if (start < 0 || start >= total) + return -1; + + int i = start; + if (tokens[i].type == JSMN_OBJECT) + { + // For an object: size is number of key-value pairs + int pairs = tokens[i].size; + i++; // move to first key-value pair + for (int p = 0; p < pairs; p++) + { + // skip key (primitive/string) + i++; + if (i >= total) + return -1; + // skip value (which could be object/array and must be skipped recursively) + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; // i is now just past the object + } + else if (tokens[i].type == JSMN_ARRAY) + { + // For an array: size is number of elements + int elems = tokens[i].size; + i++; // move to first element + for (int e = 0; e < elems; e++) + { + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; // i is now just past the array + } + else + { + // Primitive or string token, just skip it + return i + 1; + } +} + +// Revised get_json_array_value +char *get_json_array_value(char *key, uint32_t index, const char *json_data) +{ + // Always extract the full array each time from the original json_data + char *array_str = get_json_value(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); + return NULL; + } + uint32_t max_tokens = json_token_count(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + free(array_str); + return NULL; + } + + jsmn_parser parser; + jsmn_init(&parser); + 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; + } + + 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; + } + + 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; + } + + if (index >= (uint32_t)tokens[0].size) + { + // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size); + free(tokens); + free(array_str); + return NULL; + } + + // Find the index-th element: start from token[1], which is the first element + int elem_token = 1; + for (uint32_t i = 0; i < index; i++) + { + elem_token = skip_token(tokens, elem_token, ret); + if (elem_token == -1 || elem_token >= ret) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i); + free(tokens); + free(array_str); + return NULL; + } + } + + // Now elem_token should point to the token of the requested element + jsmntok_t element = tokens[elem_token]; + int length = element.end - element.start; + char *value = malloc(length + 1); + if (!value) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element."); + free(tokens); + free(array_str); + return NULL; + } + + strncpy(value, array_str + element.start, length); + value[length] = '\0'; + + 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, int *num_values) +{ + // Retrieve the array string for the given key + char *array_str = get_json_value(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); + return NULL; + } + uint32_t max_tokens = json_token_count(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + free(array_str); + 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; +} + +int json_token_count(const char *json) +{ + if (json == NULL) + { + return JSMN_ERROR_INVAL; + } + + jsmn_parser parser; + jsmn_init(&parser); + + // Pass NULL for tokens and 0 for num_tokens to get the token count only + int ret = jsmn_parse(&parser, json, strlen(json), NULL, 0); + return ret; // If ret >= 0, it represents the number of tokens needed. +} diff --git a/flip_world/jsmn/jsmn.h b/flip_world/jsmn/jsmn.h new file mode 100644 index 000000000..5f96e5596 --- /dev/null +++ b/flip_world/jsmn/jsmn.h @@ -0,0 +1,74 @@ +/* + * 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 +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + /** + * 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*/ + +// Helper function to create a JSON object +char *get_json(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, const char *json_data); + +// Revised get_json_array_value function +char *get_json_array_value(char *key, uint32_t index, const char *json_data); + +// Revised get_json_array_values function with correct token skipping +char **get_json_array_values(char *key, char *json_data, int *num_values); + +int json_token_count(const char *json); +#endif /* JB_JSMN_EDIT */ diff --git a/flip_world/jsmn/jsmn_furi.c b/flip_world/jsmn/jsmn_furi.c new file mode 100644 index 000000000..0eab40c99 --- /dev/null +++ b/flip_world/jsmn/jsmn_furi.c @@ -0,0 +1,736 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * [License text continues...] + */ + +#include + +// Forward declarations of helper functions +static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s); +static int skip_token(const jsmntok_t *tokens, int start, int total); + +/** + * 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) +{ + if (parser->toknext >= num_tokens) + { + return NULL; + } + jsmntok_t *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. + * Now uses FuriString to access characters. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const size_t num_tokens) +{ + size_t len = furi_string_size(js); + int start = parser->pos; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + switch (c) + { +#ifndef JSMN_STRICT + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + break; + } + if (c < 32 || c >= 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; + } + jsmntok_t *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. + * Now uses FuriString to access characters. + */ +static int jsmn_parse_string(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const size_t num_tokens) +{ + size_t len = furi_string_size(js); + int start = parser->pos; + parser->pos++; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + if (c == '\"') + { + if (tokens == NULL) + { + return 0; + } + jsmntok_t *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; + } + + if (c == '\\' && (parser->pos + 1) < len) + { + parser->pos++; + char esc = furi_string_get_char(js, parser->pos); + switch (esc) + { + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + case 'u': + { + parser->pos++; + for (int i = 0; i < 4 && parser->pos < len; i++) + { + char hex = furi_string_get_char(js, parser->pos); + if (!((hex >= '0' && hex <= '9') || + (hex >= 'A' && hex <= 'F') || + (hex >= 'a' && hex <= 'f'))) + { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + } + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Create JSON parser + */ +void jsmn_init_furi(jsmn_parser *parser) +{ + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +/** + * Parse JSON string and fill tokens. + * Now uses FuriString for the input JSON. + */ +int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const unsigned int num_tokens) +{ + size_t len = furi_string_size(js); + int r; + int i; + int count = parser->toknext; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + jsmntype_t type; + + switch (c) + { + case '{': + case '[': + { + count++; + if (tokens == NULL) + { + break; + } + jsmntok_t *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 + 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; + } + { + jsmntok_t *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 + { + jsmntok_t *token; + 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; + } + } + 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, 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 ' ': + // Whitespace - ignore + 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 + 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': + 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 + default: +#endif + r = jsmn_parse_primitive(parser, js, tokens, num_tokens); + if (r < 0) + return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + { + tokens[parser->toksuper].size++; + } + break; +#ifdef JSMN_STRICT + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) + { + for (i = parser->toknext - 1; i >= 0; i--) + { + if (tokens[i].start != -1 && tokens[i].end == -1) + { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +// Helper function to create a JSON object: {"key":"value"} +FuriString *get_json_furi(const FuriString *key, const FuriString *value) +{ + FuriString *result = furi_string_alloc(); + furi_string_printf(result, "{\"%s\":\"%s\"}", + furi_string_get_cstr(key), + furi_string_get_cstr(value)); + return result; // Caller responsible for furi_string_free +} + +// Helper function to compare JSON keys +static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s) +{ + size_t s_len = furi_string_size(s); + size_t tok_len = tok->end - tok->start; + + if (tok->type != JSMN_STRING) + return -1; + if (s_len != tok_len) + return -1; + + FuriString *sub = furi_string_alloc_set(json); + furi_string_mid(sub, tok->start, tok_len); + + int res = furi_string_cmp(sub, s); + furi_string_free(sub); + + return (res == 0) ? 0 : -1; +} + +// Skip a token and its descendants +static int skip_token(const jsmntok_t *tokens, int start, int total) +{ + if (start < 0 || start >= total) + return -1; + + int i = start; + if (tokens[i].type == JSMN_OBJECT) + { + int pairs = tokens[i].size; + i++; + for (int p = 0; p < pairs; p++) + { + i++; // skip key + if (i >= total) + return -1; + i = skip_token(tokens, i, total); // skip value + if (i == -1) + return -1; + } + return i; + } + else if (tokens[i].type == JSMN_ARRAY) + { + int elems = tokens[i].size; + i++; + for (int e = 0; e < elems; e++) + { + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; + } + else + { + return i + 1; + } +} + +/** + * Parse JSON and return the value associated with a given char* key. + */ +FuriString *get_json_value_furi(const char *key, const FuriString *json_data) +{ + if (json_data == NULL) + { + FURI_LOG_E("JSMM.H", "JSON data is NULL"); + return NULL; + } + uint32_t max_tokens = json_token_count_furi(json_data); + if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + return NULL; + } + // Create a temporary FuriString from key + FuriString *key_str = furi_string_alloc(); + furi_string_cat_str(key_str, key); + + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(key_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, json_data, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); + free(tokens); + furi_string_free(key_str); + return NULL; + } + + if (ret < 1 || tokens[0].type != JSMN_OBJECT) + { + FURI_LOG_E("JSMM.H", "Root element is not an object."); + free(tokens); + furi_string_free(key_str); + return NULL; + } + + for (int i = 1; i < ret; i++) + { + if (jsoneq_furi(json_data, &tokens[i], key_str) == 0) + { + int length = tokens[i + 1].end - tokens[i + 1].start; + FuriString *value = furi_string_alloc_set(json_data); + furi_string_mid(value, tokens[i + 1].start, length); + free(tokens); + furi_string_free(key_str); + return value; + } + } + + free(tokens); + furi_string_free(key_str); + char warning[128]; + snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key); + FURI_LOG_E("JSMM.H", warning); + return NULL; +} + +/** + * Return the value at a given index in a JSON array for a given char* key. + */ +FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data) +{ + FuriString *array_str = get_json_value_furi(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key"); + return NULL; + } + uint32_t max_tokens = json_token_count_furi(array_str); + if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (ret < 1 || tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key is not an array."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (index >= (uint32_t)tokens[0].size) + { + // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int elem_token = 1; + for (uint32_t i = 0; i < index; i++) + { + elem_token = skip_token(tokens, elem_token, ret); + if (elem_token == -1 || elem_token >= ret) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i); + free(tokens); + furi_string_free(array_str); + return NULL; + } + } + + jsmntok_t element = tokens[elem_token]; + int length = element.end - element.start; + + FuriString *value = furi_string_alloc_set(array_str); + furi_string_mid(value, element.start, length); + + free(tokens); + furi_string_free(array_str); + + return value; +} + +/** + * Extract all object values from a JSON array associated with a given char* key. + */ +FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values) +{ + *num_values = 0; + // Convert key to FuriString and call get_json_value_furi + FuriString *array_str = get_json_value_furi(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key"); + return NULL; + } + + uint32_t max_tokens = json_token_count_furi(array_str); + if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key is not an array."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int array_size = tokens[0].size; + FuriString **values = (FuriString **)malloc(array_size * sizeof(FuriString *)); + if (values == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int actual_num_values = 0; + int current_token = 1; + 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]; + + int length = element.end - element.start; + FuriString *value = furi_string_alloc_set(array_str); + furi_string_mid(value, element.start, length); + + values[actual_num_values] = value; + actual_num_values++; + + // Skip this element and its descendants + current_token = skip_token(tokens, current_token, ret); + if (current_token == -1) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens after element %d.", i); + break; + } + } + + *num_values = actual_num_values; + if (actual_num_values < array_size) + { + FuriString **reduced_values = (FuriString **)realloc(values, actual_num_values * sizeof(FuriString *)); + if (reduced_values != NULL) + { + values = reduced_values; + } + } + + free(tokens); + furi_string_free(array_str); + return values; +} + +uint32_t json_token_count_furi(const FuriString *json) +{ + if (json == NULL) + { + return JSMN_ERROR_INVAL; + } + + jsmn_parser parser; + jsmn_init_furi(&parser); + + // Pass NULL for tokens and 0 for num_tokens to get the token count only + int ret = jsmn_parse_furi(&parser, json, NULL, 0); + return ret; // If ret >= 0, it represents the number of tokens needed. +} diff --git a/flip_world/jsmn/jsmn_furi.h b/flip_world/jsmn/jsmn_furi.h new file mode 100644 index 000000000..cb01f38ca --- /dev/null +++ b/flip_world/jsmn/jsmn_furi.h @@ -0,0 +1,74 @@ +/* + * 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_FURI_H +#define JSMN_FURI_H + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + + JSMN_API void jsmn_init_furi(jsmn_parser *parser); + JSMN_API int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/* Implementation in jsmn_furi.c */ +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_FURI_H */ + +#ifndef JB_JSMN_FURI_EDIT +#define JB_JSMN_FURI_EDIT + +// Helper function to create a JSON object +FuriString *get_json_furi(const FuriString *key, const FuriString *value); + +// Updated signatures to accept const char* key +FuriString *get_json_value_furi(const char *key, const FuriString *json_data); +FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data); +FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values); + +uint32_t json_token_count_furi(const FuriString *json); +/* Example usage: +char *json = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; +FuriString *json_data = char_to_furi_string(json); +if (!json_data) +{ + FURI_LOG_E(TAG, "Failed to allocate FuriString"); + return -1; +} +FuriString *value = get_json_value_furi("key1", json_data, json_token_count_furi(json_data)); +if (value) +{ + FURI_LOG_I(TAG, "Value: %s", furi_string_get_cstr(value)); + furi_string_free(value); +} +furi_string_free(json_data); +*/ +#endif /* JB_JSMN_EDIT */ diff --git a/flip_world/jsmn/jsmn_h.c b/flip_world/jsmn/jsmn_h.c new file mode 100644 index 000000000..59480e6e6 --- /dev/null +++ b/flip_world/jsmn/jsmn_h.c @@ -0,0 +1,15 @@ +#include +FuriString *char_to_furi_string(const char *str) +{ + FuriString *furi_str = furi_string_alloc(); + if (!furi_str) + { + return NULL; + } + for (size_t i = 0; i < strlen(str); i++) + { + furi_string_push_back(furi_str, str[i]); + } + return furi_str; +} +bool jsmn_memory_check(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); } \ No newline at end of file diff --git a/flip_world/jsmn/jsmn_h.h b/flip_world/jsmn/jsmn_h.h new file mode 100644 index 000000000..97d53e7ff --- /dev/null +++ b/flip_world/jsmn/jsmn_h.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include +#include +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 +{ + JSMN_ERROR_NOMEM = -1, + JSMN_ERROR_INVAL = -2, + JSMN_ERROR_PART = -3 +}; + +typedef struct +{ + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +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; + +typedef struct +{ + char *key; + char *value; +} JSON; + +typedef struct +{ + FuriString *key; + FuriString *value; +} FuriJSON; + +FuriString *char_to_furi_string(const char *str); + +// check memory +bool jsmn_memory_check(size_t heap_size); diff --git a/flip_world/sprites/.gitkeep b/flip_world/sprites/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/flip_world/sprites/enemy_left_cyclops_10x11px.png b/flip_world/sprites/enemy_left_cyclops_10x11px.png new file mode 100644 index 000000000..6c6750e57 Binary files /dev/null and b/flip_world/sprites/enemy_left_cyclops_10x11px.png differ diff --git a/flip_world/sprites/enemy_left_ghost_15x15px.png b/flip_world/sprites/enemy_left_ghost_15x15px.png new file mode 100644 index 000000000..d46913188 Binary files /dev/null and b/flip_world/sprites/enemy_left_ghost_15x15px.png differ diff --git a/flip_world/sprites/enemy_left_ogre_10x13px.png b/flip_world/sprites/enemy_left_ogre_10x13px.png new file mode 100644 index 000000000..07246f780 Binary files /dev/null and b/flip_world/sprites/enemy_left_ogre_10x13px.png differ diff --git a/flip_world/sprites/enemy_right_cyclops_10x11px.png b/flip_world/sprites/enemy_right_cyclops_10x11px.png new file mode 100644 index 000000000..2569b2476 Binary files /dev/null and b/flip_world/sprites/enemy_right_cyclops_10x11px.png differ diff --git a/flip_world/sprites/enemy_right_ghost_15x15px.png b/flip_world/sprites/enemy_right_ghost_15x15px.png new file mode 100644 index 000000000..44ae4ef94 Binary files /dev/null and b/flip_world/sprites/enemy_right_ghost_15x15px.png differ diff --git a/flip_world/sprites/enemy_right_ogre_10x13px.png b/flip_world/sprites/enemy_right_ogre_10x13px.png new file mode 100644 index 000000000..4866d4dab Binary files /dev/null and b/flip_world/sprites/enemy_right_ogre_10x13px.png differ diff --git a/flip_world/sprites/player_left_axe_15x11px.png b/flip_world/sprites/player_left_axe_15x11px.png new file mode 100644 index 000000000..f4afd3ab1 Binary files /dev/null and b/flip_world/sprites/player_left_axe_15x11px.png differ diff --git a/flip_world/sprites/player_left_bow_13x11px.png b/flip_world/sprites/player_left_bow_13x11px.png new file mode 100644 index 000000000..e002cdaa6 Binary files /dev/null and b/flip_world/sprites/player_left_bow_13x11px.png differ diff --git a/flip_world/sprites/player_left_naked_10x10px.png b/flip_world/sprites/player_left_naked_10x10px.png new file mode 100644 index 000000000..9191babb6 Binary files /dev/null and b/flip_world/sprites/player_left_naked_10x10px.png differ diff --git a/flip_world/sprites/player_left_sword_15x11px.png b/flip_world/sprites/player_left_sword_15x11px.png new file mode 100644 index 000000000..2f8ec7f9b Binary files /dev/null and b/flip_world/sprites/player_left_sword_15x11px.png differ diff --git a/flip_world/sprites/player_right_axe_15x11px.png b/flip_world/sprites/player_right_axe_15x11px.png new file mode 100644 index 000000000..e3bc24792 Binary files /dev/null and b/flip_world/sprites/player_right_axe_15x11px.png differ diff --git a/flip_world/sprites/player_right_bow_13x11.png b/flip_world/sprites/player_right_bow_13x11.png new file mode 100644 index 000000000..5bd2da184 Binary files /dev/null and b/flip_world/sprites/player_right_bow_13x11.png differ diff --git a/flip_world/sprites/player_right_naked_10x10px.png b/flip_world/sprites/player_right_naked_10x10px.png new file mode 100644 index 000000000..16d36eb4a Binary files /dev/null and b/flip_world/sprites/player_right_naked_10x10px.png differ diff --git a/flip_world/sprites/player_right_sword_15x11px.png b/flip_world/sprites/player_right_sword_15x11px.png new file mode 100644 index 000000000..94e488c9a Binary files /dev/null and b/flip_world/sprites/player_right_sword_15x11px.png differ diff --git a/key_copier/.gitsubtree b/key_copier/.gitsubtree index 255251890..555eeac2d 100644 --- a/key_copier/.gitsubtree +++ b/key_copier/.gitsubtree @@ -1,2 +1,2 @@ -https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/key_copier 4558d74c9da36abc851edd96a95d18f7d5511a75 +https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/key_copier ffa18100afc128abea7269fcc31d9c237523e7c6 https://github.com/zinongli/KeyCopier main / diff --git a/key_copier/CHANGELOG.md b/key_copier/CHANGELOG.md index 46d7d9888..cfbfc39b0 100644 --- a/key_copier/CHANGELOG.md +++ b/key_copier/CHANGELOG.md @@ -1,3 +1,30 @@ +## 1.1 +## What's Changed +* Support for double sided key and multiple new key formats by @HonestLocksmith +* New formats: +Manufacturer-Format Name-Data Sheet(if applicable) +Arrow-AR4-C2 +Master Lock-M1-C35 +American-AM7-C80 +Yale-Y2-.025 +Yale-Y11-CX55 +Sargent-S22-C44 +National-NA25-C40 +Corbin-CO88-C14 +Lockwood-LW4 +Lockwood-LW5 +National-NA12-C39 +Russwin-RU45-CX6 +Ford-H75-CX101 +Chevrolet-B102 +Dodge-Y159-CX102 +Kawasaki-KA14-CMC50 +Yamaha-YM63-CMC71 +Best (A2)-SFIC-C3 +RV (FIC,GL,Bauer)-RV + +Thank you @HonestLocksmith! + ## 1.0 - Initial release. - Supports measuring, saving, and loading keys with .keycopy extension. diff --git a/key_copier/README.md b/key_copier/README.md index 1539f6b1f..adc5d63c0 100644 --- a/key_copier/README.md +++ b/key_copier/README.md @@ -9,7 +9,9 @@ To measure your key: ## Special Thanks - Thank [@jamisonderek](https://github.com/jamisonderek) for his [Flipper Zero Tutorial repository](https://github.com/jamisonderek/flipper-zero-tutorials) and [YouTube channel](https://github.com/jamisonderek/flipper-zero-tutorials#:~:text=YouTube%3A%20%40MrDerekJamison)! This app is built with his Skeleton App and GPIO Wiegand app as references. +- Thank [@HonestLocksmith](https://github.com/HonestLocksmith) for PR #13 and #20. TONS of new key formats and supports for DOUBLE-SIDED keys are added. We have car keys now! - [Project channel](https://discord.com/channels/1112390971250974782/1264067969634402356) + diff --git a/key_copier/application.fam b/key_copier/application.fam index e7cd91562..9873986c4 100644 --- a/key_copier/application.fam +++ b/key_copier/application.fam @@ -12,6 +12,6 @@ App( fap_category="Tools", fap_icon_assets="assets", fap_description="@README.md", - fap_version="1.0", + fap_version="1.1", fap_author="Torron", ) diff --git a/key_copier/key_copier.c b/key_copier/key_copier.c index 2b857f9a8..a3b51c7cd 100644 --- a/key_copier/key_copier.c +++ b/key_copier/key_copier.c @@ -1,22 +1,23 @@ +#include "key_copier.h" +#include "key_copier_icons.h" +#include "key_formats.h" +#include +#include +#include #include #include #include -#include -#include #include #include -#include #include +#include +#include +#include +#include #include #include -#include -#include #include -#include -#include -#include "key_copier_icons.h" -#include "key_formats.h" -#include "key_copier.h" + #define TAG "KeyCopier" #define BACKLIGHT_ON 1 @@ -166,9 +167,11 @@ static void key_copier_config_enter_callback(void* context) { key_copier_format_change(app->format_item); view_set_previous_callback(view_config_i, key_copier_navigation_submenu_callback); view_dispatcher_remove_view( - app->view_dispatcher, KeyCopierViewConfigure_i); // delete the last one + app->view_dispatcher, + KeyCopierViewConfigure_i); // delete the last one view_dispatcher_add_view(app->view_dispatcher, KeyCopierViewConfigure_i, view_config_i); - view_dispatcher_switch_to_view(app->view_dispatcher, KeyCopierViewConfigure_i); // recreate it + view_dispatcher_switch_to_view(app->view_dispatcher, + KeyCopierViewConfigure_i); // recreate it } static const char* key_name_entry_text = "Enter name"; @@ -250,7 +253,8 @@ static void key_copier_view_save_callback(void* context) { }, redraw); - // Configure the text input. When user enters text and clicks OK, key_copier_file_saver be called. + // Configure the text input. When user enters text and clicks OK, + // key_copier_file_saver be called. bool clear_previous_text = false; text_input_set_result_callback( app->text_input, @@ -316,9 +320,18 @@ static void key_copier_view_measure_draw_callback(Canvas* canvas, void* model) { double drill_radians = (180 - my_format.drill_angle) / 2 / 180 * (double)M_PI; // Convert angle to radians double tangent = tan(drill_radians); - int top_contour_px = (int)round(63 - my_format.uncut_depth_inch / inches_per_px); + int top_contour_px = (int)round(62 - my_format.uncut_depth_inch / inches_per_px); + int bottom_contour_px = 0; + + if(my_format.sides == 2) + bottom_contour_px = + top_contour_px + (int)round(my_format.uncut_depth_inch / inches_per_px); int post_extra_x_px = 0; int pre_extra_x_px = 0; + int bottom_post_extra_x_px = 0; // new + int bottom_pre_extra_x_px = 0; // new + int level_contour_px = + (int)round((my_format.last_pin_inch + my_format.elbow_inch) / inches_per_px); for(int current_pin = 1; current_pin <= my_model->format.pin_num; current_pin += 1) { double current_center_px = my_format.first_pin_inch + (current_pin - 1) * my_format.pin_increment_inch; @@ -347,7 +360,98 @@ static void key_copier_view_measure_draw_callback(Canvas* canvas, void* model) { pin_center_px - pin_half_width_px, top_contour_px + current_depth_px, pin_center_px + pin_half_width_px, - top_contour_px + current_depth_px); // draw pin width horizontal line + top_contour_px + current_depth_px); // draw top pin width horizontal line + + if(my_format.sides == 2) { // new + int last_depth = my_model->depth[current_pin - 2] - my_format.min_depth_ind; + int next_depth = my_model->depth[current_pin] - my_format.min_depth_ind; + int current_depth = my_model->depth[current_pin - 1] - my_format.min_depth_ind; + int current_depth_px = + (int)round(current_depth * my_format.depth_step_inch / inches_per_px); + + // Draw horizontal line for bottom pin + canvas_draw_line( + canvas, + pin_center_px - pin_half_width_px, + bottom_contour_px - current_depth_px, + pin_center_px + pin_half_width_px, + bottom_contour_px - current_depth_px); + + // Handle first pin for bottom + if(current_pin == 1) { + canvas_draw_line( + canvas, + 0, + bottom_contour_px, + pin_center_px - pin_half_width_px - current_depth_px, + bottom_contour_px); + last_depth = 0; + bottom_pre_extra_x_px = max(current_depth_px + pin_half_width_px, 0); + } + + // Handle left side intersection for bottom + if((last_depth + current_depth) > my_format.clearance) { + if(current_pin != 1) { + bottom_pre_extra_x_px = + min(max(pin_step_px - bottom_post_extra_x_px, pin_half_width_px), + pin_step_px - pin_half_width_px); + } + canvas_draw_line( + canvas, + pin_center_px - bottom_pre_extra_x_px, + bottom_contour_px - + max((int)round( + (current_depth_px - (bottom_pre_extra_x_px - pin_half_width_px)) * + tangent), + 0), + pin_center_px - pin_half_width_px, + bottom_contour_px - (int)round(current_depth_px * tangent)); + } else { + int last_depth_px = + (int)round(last_depth * my_format.depth_step_inch / inches_per_px); + int up_slope_start_x_px = pin_center_px - pin_half_width_px - current_depth_px; + canvas_draw_line( + canvas, + pin_center_px - pin_half_width_px - current_depth_px, + bottom_contour_px, + pin_center_px - pin_half_width_px, + bottom_contour_px - (int)round(current_depth_px * tangent)); + canvas_draw_line( + canvas, + min(pin_center_px - pin_step_px + pin_half_width_px + last_depth_px, + up_slope_start_x_px), + bottom_contour_px, + up_slope_start_x_px, + bottom_contour_px); + } + + // Handle right side intersection for bottom + if((current_depth + next_depth) > my_format.clearance) { + double numerator = (double)current_depth; + double denominator = (double)(current_depth + next_depth); + double product = (numerator / denominator) * pin_step_px; + bottom_post_extra_x_px = + (int)min(max(product, pin_half_width_px), pin_step_px - pin_half_width_px); + canvas_draw_line( + canvas, + pin_center_px + pin_half_width_px, + bottom_contour_px - current_depth_px, + pin_center_px + bottom_post_extra_x_px, + bottom_contour_px - + max(current_depth_px - + (int)round((bottom_post_extra_x_px - pin_half_width_px) * tangent), + 0)); + } else { + canvas_draw_line( + canvas, + pin_center_px + pin_half_width_px, + bottom_contour_px - (int)round(current_depth_px * tangent), + pin_center_px + pin_half_width_px + current_depth_px, + bottom_contour_px); + } + } + // new end + int last_depth = my_model->depth[current_pin - 2] - my_format.min_depth_ind; int next_depth = my_model->depth[current_pin] - my_format.min_depth_ind; if(current_pin == 1) { @@ -356,14 +460,25 @@ static void key_copier_view_measure_draw_callback(Canvas* canvas, void* model) { 0, top_contour_px, pin_center_px - pin_half_width_px - current_depth_px, - top_contour_px); + top_contour_px); // draw top shoulder last_depth = 0; pre_extra_x_px = max(current_depth_px + pin_half_width_px, 0); + if(my_format.sides == 2) { + canvas_draw_line( + canvas, + 0, + bottom_contour_px, + pin_center_px - pin_half_width_px - current_depth_px, + bottom_contour_px); // draw bottom shoulder (hidden by level contour) + } else { + canvas_draw_line(canvas, 0, 62, level_contour_px, 62); + } } if(current_pin == my_model->format.pin_num) { next_depth = 0; } - if((last_depth + current_depth) > my_format.clearance) { //yes intersection + if((last_depth + current_depth) > my_format.clearance) { // yes + // intersection if(current_pin != 1) { pre_extra_x_px = @@ -396,7 +511,8 @@ static void key_copier_view_measure_draw_callback(Canvas* canvas, void* model) { down_slope_start_x_px, top_contour_px); } - if((current_depth + next_depth) > my_format.clearance) { //yes intersection + if((current_depth + next_depth) > my_format.clearance) { // yes + // intersection double numerator = (double)current_depth; double denominator = (double)(current_depth + next_depth); double product = (numerator / denominator) * pin_step_px; @@ -421,11 +537,16 @@ static void key_copier_view_measure_draw_callback(Canvas* canvas, void* model) { } } - int level_contour_px = - (int)round((my_format.last_pin_inch + my_format.elbow_inch) / inches_per_px); int elbow_px = (int)round(my_format.elbow_inch / inches_per_px); - canvas_draw_line(canvas, 0, 62, level_contour_px, 62); canvas_draw_line(canvas, level_contour_px, 62, level_contour_px + elbow_px, 62 - elbow_px); + canvas_draw_line(canvas, 0, top_contour_px - 6, 0, top_contour_px); + if(my_format.stop == 2) { + // Draw a line using level_contour_px if stop equals 2 elbow must be firt pin inch + canvas_draw_line(canvas, level_contour_px, top_contour_px, level_contour_px, 63); + // } else { + // Otherwise, draw a default line + // canvas_draw_line(canvas, 0, top_contour_px, 0, 63); // too confusing but may want later + } int slc_pin_px = (int)round( (my_format.first_pin_inch + (my_model->pin_slc - 1) * my_format.pin_increment_inch) / @@ -433,7 +554,7 @@ static void key_copier_view_measure_draw_callback(Canvas* canvas, void* model) { canvas_draw_icon(canvas, slc_pin_px - 2, top_contour_px - 25, &I_arrow_down); furi_string_printf(buffer, "%s", my_format.format_name); - canvas_draw_str(canvas, 110, 10, furi_string_get_cstr(buffer)); + canvas_draw_str(canvas, 100, 10, furi_string_get_cstr(buffer)); furi_string_free(buffer); } @@ -474,13 +595,12 @@ static bool key_copier_view_measure_input_callback(InputEvent* event, void* cont KeyCopierModel * model, { if(model->depth[model->pin_slc - 1] > model->format.min_depth_ind) { - if(model->pin_slc == 1) { //first pin only limited by the next one + if(model->pin_slc == 1) { // first pin only limited by the next one if(model->depth[model->pin_slc] - model->depth[model->pin_slc - 1] < model->format.macs) model->depth[model->pin_slc - 1]--; - } else if( - model->pin_slc == - model->format.pin_num) { //last pin only limited by the previous one + } else if(model->pin_slc == model->format.pin_num) { // last pin only limited by + // the previous one if(model->depth[model->pin_slc - 2] - model->depth[model->pin_slc - 1] < model->format.macs) { @@ -507,13 +627,12 @@ static bool key_copier_view_measure_input_callback(InputEvent* event, void* cont KeyCopierModel * model, { if(model->depth[model->pin_slc - 1] < model->format.max_depth_ind) { - if(model->pin_slc == 1) { //first pin only limited by the next one + if(model->pin_slc == 1) { // first pin only limited by the next one if(model->depth[model->pin_slc - 1] - model->depth[model->pin_slc] < model->format.macs) model->depth[model->pin_slc - 1]++; - } else if( - model->pin_slc == - model->format.pin_num) { //last pin only limited by the previous one + } else if(model->pin_slc == model->format.pin_num) { // last pin only limited by + // the previous one if(model->depth[model->pin_slc - 1] - model->depth[model->pin_slc - 2] < model->format.macs) { @@ -616,7 +735,11 @@ static KeyCopierApp* key_copier_app_alloc() { 0, 128, 64, - "Key Maker App 1.0\nAuthor: @Torron\n\nTo measure your key:\n\n1. Place it on top of the screen.\n\n2. Use the contour to align your key.\n\n3. Adjust each pin's depth until they match. It's easier if you look with one eye closed.\n\nGithub: github.com/zinongli/KeyCopier \n\nSpecial thanks to Derek Jamison's Skeleton App Template."); + "Key Maker App 1.1\nAuthor: @Torron\n\nTo measure your key:\n\n1. Place " + "it on top of the screen.\n\n2. Use the contour to align your key.\n\n3. " + "Adjust each pin's depth until they match. It's easier if you look with " + "one eye closed.\n\nGithub: github.com/zinongli/KeyCopier \n\nSpecial " + "thanks to Derek Jamison's Skeleton App Template."); view_set_previous_callback( widget_get_view(app->widget_about), key_copier_navigation_submenu_callback); view_dispatcher_add_view( diff --git a/key_copier/key_formats.c b/key_copier/key_formats.c index da49fe966..74133f4db 100644 --- a/key_copier/key_formats.c +++ b/key_copier/key_formats.c @@ -29,12 +29,371 @@ const KeyFormat all_formats[] = { .pin_num = 6, .pin_width_inch = 0.031, .elbow_inch = 0.1, - .drill_angle = - 90, // This should actually be 100 but the current resolution will make 100 degrees very ugly and unsuable + .drill_angle = 90, // This should actually be 100 but the current resolution will make + // 100 degrees very ugly and unsuable .uncut_depth_inch = 0.335, .deepest_depth_inch = 0.2, .depth_step_inch = 0.015, .min_depth_ind = 0, .max_depth_ind = 9, .macs = 7, - .clearance = 8}}; + .clearance = 8}, + + {.manufacturer = "Arrow", + .format_name = "AR4", + .format_link = "C2", + .first_pin_inch = 0.265, + .last_pin_inch = 1.040, + .pin_increment_inch = 0.155, + .pin_num = 6, + .pin_width_inch = 0.060, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.312, + .deepest_depth_inch = 0.186, + .depth_step_inch = 0.014, + .min_depth_ind = 0, + .max_depth_ind = 9, + .macs = 6, + .clearance = 7}, + + {.manufacturer = "Master Lock", + .format_name = "M1", + .format_link = "C35", + .first_pin_inch = 0.185, + .last_pin_inch = 0.689, + .pin_increment_inch = 0.126, + .pin_num = 5, + .pin_width_inch = 0.039, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.276, + .deepest_depth_inch = 0.171, + .depth_step_inch = 0.015, + .min_depth_ind = 0, + .max_depth_ind = 7, + .macs = 7, + .clearance = 6}, + + {.manufacturer = "American", + .format_name = "AM7", + .format_link = "C80", + .first_pin_inch = 0.157, + .last_pin_inch = 0.781, + .pin_increment_inch = 0.125, + .pin_num = 6, + .pin_width_inch = 0.039, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.283, + .deepest_depth_inch = 0.173, + .depth_step_inch = 0.016, + .min_depth_ind = 1, + .max_depth_ind = 8, + .macs = 7, + .clearance = 5}, + + {.manufacturer = "Yale", + .format_name = "Y2", + .format_link = "C57", + .first_pin_inch = 0.200, + .last_pin_inch = 1.025, + .pin_increment_inch = 0.165, + .pin_num = 6, + .pin_width_inch = 0.054, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.320, + .deepest_depth_inch = 0.149, + .depth_step_inch = 0.019, + .min_depth_ind = 0, + .max_depth_ind = 9, + .macs = 9, + .clearance = 4}, + + {.manufacturer = "Yale", + .format_name = "Y11", + .format_link = "CX55", + .first_pin_inch = 0.124, + .last_pin_inch = 0.502, + .pin_increment_inch = 0.095, + .pin_num = 5, + .pin_width_inch = 0.039, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.246, + .deepest_depth_inch = 0.167, + .depth_step_inch = 0.020, + .min_depth_ind = 1, + .max_depth_ind = 5, + .macs = 7, + .clearance = 3}, + + {.manufacturer = "Sargent", + .format_name = "S22", + .format_link = "C44", + .first_pin_inch = 0.216, + .last_pin_inch = 0.996, + .pin_increment_inch = 0.156, + .pin_num = 6, + .pin_width_inch = 0.063, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.328, // double check + .deepest_depth_inch = 0.148, + .depth_step_inch = 0.020, + .min_depth_ind = 1, + .max_depth_ind = 10, + .macs = 7, + .clearance = 5}, + + {.manufacturer = "National", + .format_name = "NA25", + .format_link = "C40", + .first_pin_inch = 0.250, + .last_pin_inch = 0.874, + .pin_increment_inch = 0.156, + .pin_num = 5, + .pin_width_inch = 0.039, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.304, + .deepest_depth_inch = 0.191, + .depth_step_inch = 0.012, + .min_depth_ind = 0, + .max_depth_ind = 9, + .macs = 7, + .clearance = 8}, + + {.manufacturer = "Corbin", + .format_name = "CO88", + .format_link = "C14", + .first_pin_inch = 0.250, + .last_pin_inch = 1.030, + .pin_increment_inch = 0.156, + .pin_num = 6, + .pin_width_inch = 0.047, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.343, + .deepest_depth_inch = 0.217, + .depth_step_inch = 0.014, + .min_depth_ind = 1, + .max_depth_ind = 10, + .macs = 7, + .clearance = 8}, + + {.manufacturer = "Lockwood", + .format_name = "LW4", + .format_link = "", + .first_pin_inch = 0.245, + .last_pin_inch = 0.870, + .pin_increment_inch = 0.1562, + .pin_num = 5, + .pin_width_inch = 0.031, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.344, + .deepest_depth_inch = 0.203, + .depth_step_inch = 0.014, + .min_depth_ind = 0, + .max_depth_ind = 9, + .macs = 9, + .clearance = 8}, + + {.manufacturer = "Lockwood", + .format_name = "LW5", + .format_link = "", + .first_pin_inch = 0.245, + .last_pin_inch = 1.0262, + .pin_increment_inch = 0.1562, + .pin_num = 6, + .pin_width_inch = 0.031, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.344, + .deepest_depth_inch = 0.203, + .depth_step_inch = 0.014, + .min_depth_ind = 0, + .max_depth_ind = 9, + .macs = 9, + .clearance = 8}, + + {.manufacturer = "National", + .format_name = "NA12", + .format_link = "C39", + .first_pin_inch = 0.150, + .last_pin_inch = 0.710, + .pin_increment_inch = 0.140, + .pin_num = 5, + .pin_width_inch = 0.039, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.270, + .deepest_depth_inch = 0.157, + .depth_step_inch = 0.013, + .min_depth_ind = 0, + .max_depth_ind = 9, + .macs = 7, + .clearance = 8}, + + {.manufacturer = "Russwin", + .format_name = "RU45", + .format_link = "CX6", + .first_pin_inch = 0.250, + .last_pin_inch = 1.030, + .pin_increment_inch = 0.156, + .pin_num = 6, + .pin_width_inch = 0.053, + .elbow_inch = 0.1, + .drill_angle = 90, + .uncut_depth_inch = 0.343, + .deepest_depth_inch = 0.203, + .depth_step_inch = 0.028, + .min_depth_ind = 1, + .max_depth_ind = 6, + .macs = 5, + .clearance = 3}, + + {.manufacturer = "Ford", + .format_name = "H75", + .sides = 2, + .stop = 2, + .format_link = "CX101", + .first_pin_inch = 0.201, + .last_pin_inch = 0.845, + .pin_increment_inch = 0.092, + .pin_num = 8, + .pin_width_inch = 0.039, + .elbow_inch = 0.201, // this should be equal to first pin inch for tip + // stopped key line + .drill_angle = 90, + .uncut_depth_inch = 0.354, + .deepest_depth_inch = 0.254, + .depth_step_inch = 0.025, + .min_depth_ind = 1, + .max_depth_ind = 5, + .macs = 5, + .clearance = 2}, + + {.manufacturer = "Chevrolet", + .format_name = "B102", + .sides = 2, + .stop = 2, + .format_link = "", + .first_pin_inch = 0.205, + .last_pin_inch = 1.037, + .pin_increment_inch = 0.093, + .pin_num = 10, + .pin_width_inch = 0.039, + .elbow_inch = 0.205, // this should be equal to first pin inch for tip + // stopped key line + .drill_angle = 90, + .uncut_depth_inch = 0.315, + .deepest_depth_inch = 0.161, + .depth_step_inch = 0.026, + .min_depth_ind = 1, + .max_depth_ind = 4, + .macs = 5, + .clearance = 2}, + + {.manufacturer = "Dodge", + .format_name = "Y159", + .sides = 2, + .stop = 2, + .format_link = "CX102", + .first_pin_inch = 0.297, + .last_pin_inch = 0.941, + .pin_increment_inch = 0.092, + .pin_num = 8, + .pin_width_inch = 0.039, + .elbow_inch = 0.297, // this should be equal to first pin inch for tip + // stopped key line + .drill_angle = 90, + .uncut_depth_inch = 0.339, + .deepest_depth_inch = 0.197, + .depth_step_inch = 0.047, + .min_depth_ind = 1, + .max_depth_ind = 4, + .macs = 5, + .clearance = 1}, + + {.manufacturer = "Kawasaki", + .format_name = "KA14", + .sides = 2, + .format_link = "CMC50", + .first_pin_inch = 0.098, + .last_pin_inch = 0.591, + .pin_increment_inch = 0.098, + .pin_num = 6, + .pin_width_inch = 0.039, + .elbow_inch = 0.1, // this should be equal to first pin inch for tip + // stopped key line + .drill_angle = 90, + .uncut_depth_inch = 0.258, + .deepest_depth_inch = 0.198, + .depth_step_inch = 0.020, + .min_depth_ind = 1, + .max_depth_ind = 4, + .macs = 4, + .clearance = 3}, + + {.manufacturer = "Yamaha", + .format_name = "YM63", + .sides = 2, + .format_link = "CMC71", + .first_pin_inch = 0.157, + .last_pin_inch = 0.748, + .pin_increment_inch = 0.098, + .pin_num = 7, + .pin_width_inch = 0.039, + .elbow_inch = 0.1, // this should be equal to first pin inch for tip + // stopped key line + .drill_angle = 90, + .uncut_depth_inch = 0.295, + .deepest_depth_inch = 0.236, + .depth_step_inch = 0.020, + .min_depth_ind = 1, + .max_depth_ind = 4, + .macs = 4, + .clearance = 3}, + + {.manufacturer = "Best (A2)", + .format_name = "SFIC", + .stop = 2, + .format_link = "C3", + .first_pin_inch = 0.250, + .last_pin_inch = 0.998, + .pin_increment_inch = 0.149, + .pin_num = 6, + .pin_width_inch = 0.051, + .elbow_inch = 0.081, // this should be equal to first pin inch for tip + // stopped key line + .drill_angle = 90, + .uncut_depth_inch = 0.318, + .deepest_depth_inch = 0.206, + .depth_step_inch = 0.025, + .min_depth_ind = 0, + .max_depth_ind = 9, + .macs = 5, + .clearance = 3}, + + {.manufacturer = "RV (FIC,GL,Bauer)", + .format_name = "RV", + .sides = 2, + .format_link = "Card", + .first_pin_inch = 0.126, + .last_pin_inch = 0.504, + .pin_increment_inch = 0.094, + .pin_num = 5, + .pin_width_inch = 0.039, + .elbow_inch = 0.126, // this should be equal to first pin inch for tip + // stopped key line + .drill_angle = 90, + .uncut_depth_inch = 0.260, + .deepest_depth_inch = 0.181, + .depth_step_inch = 0.040, + .min_depth_ind = 1, + .max_depth_ind = 3, + .macs = 3, + .clearance = 1}}; diff --git a/key_copier/key_formats.h b/key_copier/key_formats.h index d27876ae2..ce81eed3e 100644 --- a/key_copier/key_formats.h +++ b/key_copier/key_formats.h @@ -1,13 +1,14 @@ #ifndef KEY_FORMATS_H #define KEY_FORMATS_H -#define FORMAT_NUM 2 +#define FORMAT_NUM 21 typedef struct { char* manufacturer; char* format_name; char* format_link; - + int sides; + int stop; double first_pin_inch; double last_pin_inch; double pin_increment_inch; diff --git a/metroflip/.github/workflows/build&push-beta.yml b/metroflip/.github/workflows/build&push-beta.yml new file mode 100644 index 000000000..602480ef5 --- /dev/null +++ b/metroflip/.github/workflows/build&push-beta.yml @@ -0,0 +1,50 @@ +name: Build and Upload FAP to Beta + +on: + workflow_dispatch: + inputs: + version: + description: 'Version number to use for the release' + required: true + default: '1.0.0' + + branches: + - dev + +permissions: + contents: write + +jobs: + build-and-upload: + name: Build and Upload FAP + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + with: + ref: dev + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install UFBT + run: | + python3 -m pip install --upgrade pip + pip install ufbt + + - name: Initialize UFBT Environment + run: | + ufbt update + ufbt vscode_dist + + - name: Build FAP Applications + run: ufbt faps + + - name: Upload Build Outputs to Release + run: | + gh release upload v${{ github.event.inputs.version }} /home/runner/.ufbt/build/metroflip.fap + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/metroflip/.github/workflows/build&push.yml b/metroflip/.github/workflows/build&push.yml new file mode 100644 index 000000000..da8a7ae9b --- /dev/null +++ b/metroflip/.github/workflows/build&push.yml @@ -0,0 +1,46 @@ +name: Build and Upload FAP to Release + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-and-upload: + name: Build and Upload FAP + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Extract Version from Manifest + id: extract_version + run: | + VERSION=$(grep '^version:' manifest.yml | awk '{print $2}') + echo "VERSION=${VERSION}" >> $GITHUB_ENV + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install UFBT + run: | + python3 -m pip install --upgrade pip + pip install ufbt + + - name: Initialize UFBT Environment + run: | + ufbt update + ufbt vscode_dist + + - name: Build FAP Applications + run: ufbt faps + + - name: Upload Build Outputs to Release + run: | + gh release upload v${{ env.VERSION }} /home/runner/.ufbt/build/metroflip.fap + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/metroflip/.github/workflows/main.yml b/metroflip/.github/workflows/main.yml index a3d45dd6c..65f83545d 100644 --- a/metroflip/.github/workflows/main.yml +++ b/metroflip/.github/workflows/main.yml @@ -1,5 +1,4 @@ name: UFBT Build and Test - on: push: branches: @@ -7,36 +6,29 @@ on: pull_request: branches: - main - jobs: build: name: Build and Test Application runs-on: ubuntu-latest - steps: - name: Checkout Repository uses: actions/checkout@v3 - - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.x' - - name: Install UFBT run: | python3 -m pip install --upgrade pip pip install ufbt - - name: Initialize UFBT Environment run: | ufbt update ufbt vscode_dist - - name: Build FAP Applications run: ufbt faps - - name: Upload Build Artifacts uses: actions/upload-artifact@v3 with: name: build-output - path: build/ + path: build/ \ No newline at end of file diff --git a/metroflip/CHANGELOG.md b/metroflip/CHANGELOG.md index f9a42a087..526d7e6eb 100644 --- a/metroflip/CHANGELOG.md +++ b/metroflip/CHANGELOG.md @@ -1,8 +1,27 @@ ## v0.1 - Initial release by luu176 + ## v0.2 - Update Rav-Kav parsing to show more data such as transaction logs -- Add Navigo parser! +- Add Navigo parser! (Paris, France) - Bug fixes + +## v0.3 + +- Added Clipper parser (San Francisco, CA, USA) +- Added Troika parser (Moscow, Russia) +- Added Myki parser (Melbourne (and surrounds), VIC, Australia) +- Added Opal parser (Sydney (and surrounds), NSW, Australia) +- Added ITSO parser (United Kingdom) + +## v0.4 + +- Updated Navigo parser (Paris, France) (thanks to: DocSystem) + - Now use a global Calypso parser, with defined structures + - Fix Busfault and NULL Pointer Dereferences +- Updated all Desfire parsers (opal, itso, myki, etc..) + - Now doesnt crash when you click the back button while reading +- Fix Charliecard parser + diff --git a/metroflip/README.md b/metroflip/README.md index d60ffe3e0..089873d58 100644 --- a/metroflip/README.md +++ b/metroflip/README.md @@ -1,45 +1,104 @@ # 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. +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) +# Discord Community Server + +Please join the server https://discord.gg/NR5hhbAXqS if you have any questions for me. +--- + +![image](screenshots/Menu-Top.png) + +# Setup Instructions + +## Using a pre-built release: Stable (Recommended) or Beta (Newer updates, less stable) +1. Download the appropriate `metroflip.fap` file from the [Releases section](https://github.com/luu176/Metroflip/releases). +2. Drag and drop the `metroflip.fap` file into the `apps` folder on your Flipper Zero's SD card. + +## Manual Build Instructions +To build Metroflip manually, follow these steps: + +1. **Install Git** + Download and install Git on your Windows computer. + Run the first command to download the app: + +**Either**: +Stable Release (recommended): +```git clone https://github.com/luu176/Metroflip.git``` + +**OR**: +Beta (newer updates but not fully tested): +```git clone --single-branch --branch dev https://github.com/luu176/Metroflip.git``` + +2. **Navigate to the Project Folder** +Run the second command to enter the app folder: + +```cd Metroflip``` + +3. **Install Python** +Download and install Python from the [official website](https://www.python.org). + +4. **Install UFBT** +Run the third command to install UFBT: + +```pip install ufbt``` + +5. **Update and Build the Project** +Run the following commands in order to build the app: + +```ufbt update``` +```ufbt fap_metroflip``` + +6. **Connect Your Flipper Zero** +Ensure your Flipper Zero is connected via USB and close the QFlipper application (if it’s open). + +7. **Launch the Build** +Run the final command to launch the app on your flipper: + +```ufbt launch``` + +--- + # 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. -- [x] **Navigo** - - Status: Fully supported. (v0.2) - -## 🚧 In Progress / Needs More Functionality -- [ ] **Rav-Kav** - - Current functionality: Reads balance only. (v0.1) - - To Do: Parse more data from the card (e.g., transaction history, expiration date, etc.). (v0.2) - -## 📝 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. (v0.3) + +| **Card / Agency** | **Country / City** | **Card Type** | +|--------------------|-------------------------------------|-------------------| +| **Bip!** | 🇨🇱 Santiago de Chile, Chile | Mifare Classic | +| **Charliecard** | 🇺🇸 Boston, MA, USA | Mifare Classic | +| **Clipper** | 🇺🇸 San Francisco, CA, USA | Mifare DESFire | +| **ITSO** | 🇬🇧 United Kingdom | Mifare DESFire | +| **Metromoney** | 🇬🇪 Tbilisi, Georgia | Mifare Classic | +| **myki** | 🇦🇺 Melbourne (and surrounds), VIC, Australia | Mifare DESFire | +| **Navigo** | 🇫🇷 Paris, France | Calypso | +| **Opal** | 🇦🇺 Sydney (and surrounds), NSW, Australia | Mifare DESFire | +| **Rav-Kav** | 🇮🇱 Israel | Calypso | +| **Troika** | 🇷🇺 Moscow, Russia | Mifare Classic | + --- -### Credits: +# Credits - **App Author**: [@luu176](https://github.com/luu176) - **Charliecard Parser**: [@zacharyweiss](https://github.com/zacharyweiss) - **Rav-Kav Parser**: [@luu176](https://github.com/luu176) -- **Navigo Parser**: [@luu176](https://github.com/luu176) +- **Navigo Parser**: [@luu176](https://github.com/luu176), [@DocSystem](https://github.com/DocSystem) - **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) +- **Bip! Parser**: [@rbasoalto](https://github.com/rbasoalto), [@gornekich](https://github.com/gornekich) +- **Clipper Parser**: [@ke6jjj](https://github.com/ke6jjj) +- **Troika Parser**: [@gornekich](https://github.com/gornekich) +- **Myki Parser**: [@gornekich](https://github.com/gornekich) +- **Opal Parser**: [@gornekich](https://github.com/gornekich) +- **ITSO Parser**: [@gsp8181](https://github.com/gsp8181), [@hedger](https://github.com/hedger), [@gornekich](https://github.com/gornekich) +- **Info Slaves**: [@equipter](https://github.com/equipter), [TheDingo8MyBaby](https://github.com/TheDingo8MyBaby) + +--- +### Special Thanks +Huge thanks to [@equipter](https://github.com/equipter) for helping out the community! diff --git a/metroflip/api/calypso/calypso_util.c b/metroflip/api/calypso/calypso_util.c new file mode 100644 index 000000000..ffe3c2b05 --- /dev/null +++ b/metroflip/api/calypso/calypso_util.c @@ -0,0 +1,251 @@ +#include +#include +#include "calypso_util.h" + +CalypsoElement make_calypso_final_element( + const char* key, + int size, + const char* label, + CalypsoFinalType final_type) { + CalypsoElement final_element = {}; + + final_element.type = CALYPSO_ELEMENT_TYPE_FINAL; + final_element.final = malloc(sizeof(CalypsoFinalElement)); + final_element.final->size = size; + final_element.final->final_type = final_type; + strncpy(final_element.final->key, key, 36); + strncpy(final_element.final->label, label, 64); + + return final_element; +} + +CalypsoElement make_calypso_bitmap_element(const char* key, int size, CalypsoElement* elements) { + CalypsoElement bitmap_element = {}; + + bitmap_element.type = CALYPSO_ELEMENT_TYPE_BITMAP; + bitmap_element.bitmap = malloc(sizeof(CalypsoBitmapElement)); + bitmap_element.bitmap->size = size; + bitmap_element.bitmap->elements = malloc(size * sizeof(CalypsoElement)); + for(int i = 0; i < size; i++) { + bitmap_element.bitmap->elements[i] = elements[i]; + } + strncpy(bitmap_element.bitmap->key, key, 36); + + return bitmap_element; +} + +void free_calypso_element(CalypsoElement* element) { + if(element->type == CALYPSO_ELEMENT_TYPE_FINAL) { + free(element->final); + } else { + for(int i = 0; i < element->bitmap->size; i++) { + free_calypso_element(&element->bitmap->elements[i]); + } + free(element->bitmap->elements); + free(element->bitmap); + } +} + +void free_calypso_structure(CalypsoApp* structure) { + for(int i = 0; i < structure->elements_size; i++) { + free_calypso_element(&structure->elements[i]); + } + free(structure->elements); + free(structure); +} + +int* get_bit_positions(const char* binary_string, int* count) { + int length = strlen(binary_string); + int* positions = malloc(length * sizeof(int)); + int pos_index = 0; + + for(int i = 0; i < length; i++) { + if(binary_string[length - 1 - i] == '1') { + positions[pos_index++] = i; + } + } + + *count = pos_index; + return positions; +} + +int is_bit_present(int* positions, int count, int bit) { + for(int i = 0; i < count; i++) { + if(positions[i] == bit) { + return 1; + } + } + return 0; +} + +bool is_calypso_subnode_present( + const char* binary_string, + const char* key, + CalypsoBitmapElement* bitmap) { + char bit_slice[bitmap->size + 1]; + strncpy(bit_slice, binary_string, bitmap->size); + bit_slice[bitmap->size] = '\0'; + int count = 0; + int* positions = get_bit_positions(bit_slice, &count); + int offset = bitmap->size; + for(int i = 0; i < count; i++) { + CalypsoElement* element = &bitmap->elements[positions[i]]; + if(element->type == CALYPSO_ELEMENT_TYPE_FINAL) { + if(strcmp(element->final->key, key) == 0) { + free(positions); + return true; + } + offset += element->final->size; + } else { + if(strcmp(element->bitmap->key, key) == 0) { + free(positions); + return true; + } + int sub_binary_string_size = element->bitmap->size; + char bit_slice[sub_binary_string_size + 1]; + strncpy(bit_slice, binary_string, sub_binary_string_size); + bit_slice[sub_binary_string_size] = '\0'; + if(is_calypso_subnode_present(binary_string + offset, key, element->bitmap)) { + free(positions); + return true; + } + offset += element->bitmap->size; + } + } + free(positions); + return false; +} + +bool is_calypso_node_present(const char* binary_string, const char* key, CalypsoApp* structure) { + int offset = 0; + for(int i = 0; i < structure->elements_size; i++) { + if(structure->elements[i].type == CALYPSO_ELEMENT_TYPE_FINAL) { + if(strcmp(structure->elements[i].final->key, key) == 0) { + return true; + } + offset += structure->elements[i].final->size; + } else { + if(strcmp(structure->elements[i].bitmap->key, key) == 0) { + return true; + } + int sub_binary_string_size = structure->elements[i].bitmap->size; + char bit_slice[sub_binary_string_size + 1]; + strncpy(bit_slice, binary_string, sub_binary_string_size); + bit_slice[sub_binary_string_size] = '\0'; + if(is_calypso_subnode_present( + binary_string + offset, key, structure->elements[i].bitmap)) { + return true; + } + offset += structure->elements[i].bitmap->size; + } + } + return false; +} + +int get_calypso_subnode_offset( + const char* binary_string, + const char* key, + CalypsoBitmapElement* bitmap, + bool* found) { + char bit_slice[bitmap->size + 1]; + strncpy(bit_slice, binary_string, bitmap->size); + bit_slice[bitmap->size] = '\0'; + + int count = 0; + int* positions = get_bit_positions(bit_slice, &count); + + int count_offset = bitmap->size; + for(int i = 0; i < count; i++) { + CalypsoElement element = bitmap->elements[positions[i]]; + if(element.type == CALYPSO_ELEMENT_TYPE_FINAL) { + if(strcmp(element.final->key, key) == 0) { + *found = true; + free(positions); + return count_offset; + } + count_offset += element.final->size; + } else { + if(strcmp(element.bitmap->key, key) == 0) { + *found = true; + free(positions); + return count_offset; + } + count_offset += get_calypso_subnode_offset( + binary_string + count_offset, key, element.bitmap, found); + if(*found) { + free(positions); + return count_offset; + } + } + } + free(positions); + return count_offset; +} + +int get_calypso_node_offset(const char* binary_string, const char* key, CalypsoApp* structure) { + int count = 0; + bool found = false; + for(int i = 0; i < structure->elements_size; i++) { + if(structure->elements[i].type == CALYPSO_ELEMENT_TYPE_FINAL) { + if(strcmp(structure->elements[i].final->key, key) == 0) { + return count; + } + count += structure->elements[i].final->size; + } else { + if(strcmp(structure->elements[i].bitmap->key, key) == 0) { + return count; + } + int sub_binary_string_size = structure->elements[i].bitmap->size; + char bit_slice[sub_binary_string_size + 1]; + strncpy(bit_slice, binary_string + count, sub_binary_string_size); + bit_slice[sub_binary_string_size] = '\0'; + count += get_calypso_subnode_offset( + binary_string + count, key, structure->elements[i].bitmap, &found); + if(found) { + return count; + } + } + } + return 0; +} + +int get_calypso_subnode_size(const char* key, CalypsoElement* element) { + if(element->type == CALYPSO_ELEMENT_TYPE_FINAL) { + if(strcmp(element->final->key, key) == 0) { + return element->final->size; + } + } else { + if(strcmp(element->bitmap->key, key) == 0) { + return element->bitmap->size; + } + for(int i = 0; i < element->bitmap->size; i++) { + int size = get_calypso_subnode_size(key, &element->bitmap->elements[i]); + if(size != 0) { + return size; + } + } + } + return 0; +} + +int get_calypso_node_size(const char* key, CalypsoApp* structure) { + for(int i = 0; i < structure->elements_size; i++) { + if(structure->elements[i].type == CALYPSO_ELEMENT_TYPE_FINAL) { + if(strcmp(structure->elements[i].final->key, key) == 0) { + return structure->elements[i].final->size; + } + } else { + if(strcmp(structure->elements[i].bitmap->key, key) == 0) { + return structure->elements[i].bitmap->size; + } + for(int j = 0; j < structure->elements[i].bitmap->size; j++) { + int size = + get_calypso_subnode_size(key, &structure->elements[i].bitmap->elements[j]); + if(size != 0) { + return size; + } + } + } + } + return 0; +} diff --git a/metroflip/api/calypso/calypso_util.h b/metroflip/api/calypso/calypso_util.h new file mode 100644 index 000000000..c941e44f6 --- /dev/null +++ b/metroflip/api/calypso/calypso_util.h @@ -0,0 +1,80 @@ +#include + +#ifndef CALYPSO_UTIL_H +#define CALYPSO_UTIL_H + +typedef enum { + CALYPSO_APP_CONTRACT, +} CalypsoAppType; + +typedef enum { + CALYPSO_FINAL_TYPE_UNKNOWN, + CALYPSO_FINAL_TYPE_NUMBER, + CALYPSO_FINAL_TYPE_DATE, + CALYPSO_FINAL_TYPE_TIME, + CALYPSO_FINAL_TYPE_PAY_METHOD, + CALYPSO_FINAL_TYPE_AMOUNT, + CALYPSO_FINAL_TYPE_SERVICE_PROVIDER, + CALYPSO_FINAL_TYPE_ZONES, + CALYPSO_FINAL_TYPE_TARIFF, + CALYPSO_FINAL_TYPE_NETWORK_ID, + CALYPSO_FINAL_TYPE_TRANSPORT_TYPE, + CALYPSO_FINAL_TYPE_CARD_STATUS, +} CalypsoFinalType; + +typedef enum { + CALYPSO_ELEMENT_TYPE_BITMAP, + CALYPSO_ELEMENT_TYPE_FINAL +} CalypsoElementType; + +typedef struct CalypsoFinalElement_t CalypsoFinalElement; +typedef struct CalypsoBitmapElement_t CalypsoBitmapElement; + +typedef struct { + CalypsoElementType type; + union { + CalypsoFinalElement* final; + CalypsoBitmapElement* bitmap; + }; +} CalypsoElement; + +struct CalypsoFinalElement_t { + char key[36]; + int size; + char label[64]; + CalypsoFinalType final_type; +}; + +struct CalypsoBitmapElement_t { + char key[36]; + int size; + CalypsoElement* elements; +}; + +typedef struct { + CalypsoAppType type; + CalypsoElement* elements; + int elements_size; +} CalypsoApp; + +CalypsoElement make_calypso_final_element( + const char* key, + int size, + const char* label, + CalypsoFinalType final_type); + +CalypsoElement make_calypso_bitmap_element(const char* key, int size, CalypsoElement* elements); + +void free_calypso_structure(CalypsoApp* structure); + +int* get_bit_positions(const char* binary_string, int* count); + +int is_bit_present(int* positions, int count, int bit); + +bool is_calypso_node_present(const char* binary_string, const char* key, CalypsoApp* structure); + +int get_calypso_node_offset(const char* binary_string, const char* key, CalypsoApp* structure); + +int get_calypso_node_size(const char* key, CalypsoApp* structure); + +#endif // CALYPSO_UTIL_H diff --git a/metroflip/api/calypso/cards/navigo.c b/metroflip/api/calypso/cards/navigo.c new file mode 100644 index 000000000..bc37e20b9 --- /dev/null +++ b/metroflip/api/calypso/cards/navigo.c @@ -0,0 +1,379 @@ +#include +#include "navigo.h" + +CalypsoApp* get_navigo_contract_structure() { + CalypsoApp* NavigoContractStructure = malloc(sizeof(CalypsoApp)); + if(!NavigoContractStructure) { + return NULL; + } + + int app_elements_count = 1; + + NavigoContractStructure->type = CALYPSO_APP_CONTRACT; + NavigoContractStructure->elements = malloc(app_elements_count * sizeof(CalypsoElement)); + NavigoContractStructure->elements_size = app_elements_count; + + NavigoContractStructure->elements[0] = make_calypso_bitmap_element( + "Contract", + 20, + (CalypsoElement[]){ + make_calypso_final_element( + "ContractNetworkId", 24, "Identification du réseau", CALYPSO_FINAL_TYPE_UNKNOWN), + + make_calypso_final_element( + "ContractProvider", + 8, + "Identification de l’exploitant", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractTariff", 16, "Code tarif", CALYPSO_FINAL_TYPE_TARIFF), + + make_calypso_final_element( + "ContractSerialNumber", 32, "Numéro TCN", CALYPSO_FINAL_TYPE_UNKNOWN), + + make_calypso_bitmap_element( + "ContractCustomerInfoBitmap", + 2, + (CalypsoElement[]){ + make_calypso_final_element( + "ContractCustomerProfile", + 6, + "Statut du titulaire ou Taux de réduction applicable", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractCustomerNumber", + 32, + "Numéro de client", + CALYPSO_FINAL_TYPE_UNKNOWN), + }), + + make_calypso_bitmap_element( + "ContractPassengerInfoBitmap", + 2, + (CalypsoElement[]){ + make_calypso_final_element( + "ContractPassengerClass", + 8, + "Classe de service des voyageurs", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractPassengerTotal", + 8, + "Nombre total de voyageurs", + CALYPSO_FINAL_TYPE_UNKNOWN), + }), + + make_calypso_final_element( + "ContractVehicleClassAllowed", + 6, + "Classes de véhicule autorisé", + CALYPSO_FINAL_TYPE_UNKNOWN), + + make_calypso_final_element( + "ContractPaymentPointer", + 32, + "Pointeurs sur les événements de paiement", + CALYPSO_FINAL_TYPE_UNKNOWN), + + make_calypso_final_element( + "ContractPayMethod", 11, "Code mode de paiement", CALYPSO_FINAL_TYPE_PAY_METHOD), + + make_calypso_final_element( + "ContractServices", 16, "Services autorisés", CALYPSO_FINAL_TYPE_UNKNOWN), + + make_calypso_final_element( + "ContractPriceAmount", 16, "Montant total", CALYPSO_FINAL_TYPE_AMOUNT), + + make_calypso_final_element( + "ContractPriceUnit", 16, "Code de monnaie", CALYPSO_FINAL_TYPE_UNKNOWN), + + make_calypso_bitmap_element( + "ContractRestrictionBitmap", + 7, + (CalypsoElement[]){ + make_calypso_final_element( + "ContractRestrictStart", 11, "", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractRestrictEnd", 11, "", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractRestrictDay", 8, "", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractRestrictTimeCode", 8, "", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractRestrictCode", + 8, + "Code de restriction", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractRestrictProduct", + 16, + "Produit de restriction", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractRestrictLocation", + 16, + "Référence du lieu de restriction", + CALYPSO_FINAL_TYPE_UNKNOWN), + }), + + make_calypso_bitmap_element( + "ContractValidityInfoBitmap", + 9, + (CalypsoElement[]){ + make_calypso_final_element( + "ContractValidityStartDate", + 14, + "Date de début de validité", + CALYPSO_FINAL_TYPE_DATE), + make_calypso_final_element( + "ContractValidityStartTime", + 11, + "Heure de début de validité", + CALYPSO_FINAL_TYPE_TIME), + make_calypso_final_element( + "ContractValidityEndDate", + 14, + "Date de fin de validité", + CALYPSO_FINAL_TYPE_DATE), + make_calypso_final_element( + "ContractValidityEndTime", + 11, + "Heure de fin de validité", + CALYPSO_FINAL_TYPE_TIME), + make_calypso_final_element( + "ContractValidityDuration", + 8, + "Durée de validité", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractValidityLimiteDate", + 14, + "Date limite de première utilisation", + CALYPSO_FINAL_TYPE_DATE), + make_calypso_final_element( + "ContractValidityZones", + 8, + "Numéros des zones autorisées", + CALYPSO_FINAL_TYPE_ZONES), + make_calypso_final_element( + "ContractValidityJourneys", + 16, + "Nombre de voyages autorisés", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractPeriodJourneys", + 16, + "Nombre de voyages autorisés par période", + CALYPSO_FINAL_TYPE_UNKNOWN), + }), + + make_calypso_bitmap_element( + "ContractJourneyData", + 8, + (CalypsoElement[]){ + make_calypso_final_element( + "ContractJourneyOrigin", + 16, + "Code lieu d’origine", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractJourneyDestination", + 16, + "Code lieu de destination", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractJourneyRouteNumbers", + 16, + "Numéros des lignes autorisées", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractJourneyRouteVariants", + 8, + "Variantes aux numéros des lignes autorisées", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractJourneyRun", 16, "Référence du voyage", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractJourneyVia", 16, "Code lieu du via", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractJourneyDistance", 16, "Distance", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "ContractJourneyInterchanges", + 8, + "Nombre de correspondances autorisées", + CALYPSO_FINAL_TYPE_UNKNOWN), + }), + + make_calypso_bitmap_element( + "ContractSaleData", + 4, + (CalypsoElement[]){ + make_calypso_final_element( + "ContractValiditySaleDate", 14, "Date de vente", CALYPSO_FINAL_TYPE_DATE), + make_calypso_final_element( + "ContractValiditySaleTime", 11, "Heure de vente", CALYPSO_FINAL_TYPE_TIME), + make_calypso_final_element( + "ContractValiditySaleAgent", + 8, + "Identification de l’exploitant de vente", + CALYPSO_FINAL_TYPE_SERVICE_PROVIDER), + make_calypso_final_element( + "ContractValiditySaleDevice", + 16, + "Identification du terminal de vente", + CALYPSO_FINAL_TYPE_UNKNOWN), + }), + + make_calypso_final_element( + "ContractStatus", 8, "État du contrat", CALYPSO_FINAL_TYPE_UNKNOWN), + + make_calypso_final_element( + "ContractLoyaltyPoints", + 16, + "Nombre de points de fidélité", + CALYPSO_FINAL_TYPE_NUMBER), + + make_calypso_final_element( + "ContractAuthenticator", + 16, + "Code de contrôle de l’intégrité des données", + CALYPSO_FINAL_TYPE_UNKNOWN), + + make_calypso_final_element( + "ContractData(0..255)", 0, "Données complémentaires", CALYPSO_FINAL_TYPE_UNKNOWN), + }); + + return NavigoContractStructure; +} + +CalypsoApp* get_navigo_event_structure() { + CalypsoApp* NavigoEventStructure = malloc(sizeof(CalypsoApp)); + if(!NavigoEventStructure) { + return NULL; + } + + int app_elements_count = 3; + + NavigoEventStructure->type = CALYPSO_APP_CONTRACT; + NavigoEventStructure->elements = malloc(app_elements_count * sizeof(CalypsoElement)); + NavigoEventStructure->elements_size = app_elements_count; + + NavigoEventStructure->elements[0] = make_calypso_final_element( + "EventDateStamp", 14, "Date de l’événement", CALYPSO_FINAL_TYPE_DATE); + + NavigoEventStructure->elements[1] = make_calypso_final_element( + "EventTimeStamp", 11, "Heure de l’événement", CALYPSO_FINAL_TYPE_TIME); + + NavigoEventStructure->elements[2] = make_calypso_bitmap_element( + "EventBitmap", + 28, + (CalypsoElement[]){ + make_calypso_final_element( + "EventDisplayData", 8, "Données pour l’affichage", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element("EventNetworkId", 24, "Réseau", CALYPSO_FINAL_TYPE_NUMBER), + make_calypso_final_element( + "EventCode", 8, "Nature de l’événement", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventResult", 8, "Code Résultat", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventServiceProvider", + 8, + "Identité de l’exploitant", + CALYPSO_FINAL_TYPE_SERVICE_PROVIDER), + make_calypso_final_element( + "EventNotokCounter", 8, "Compteur événements anormaux", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventSerialNumber", + 24, + "Numéro de série de l’événement", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventDestination", 16, "Destination de l’usager", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventLocationId", 16, "Lieu de l’événement", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventLocationGate", 8, "Identification du passage", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventDevice", 16, "Identificateur de l’équipement", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventRouteNumber", 16, "Référence de la ligne", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventRouteVariant", + 8, + "Référence d’une variante de la ligne", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventJourneyRun", 16, "Référence de la mission", CALYPSO_FINAL_TYPE_NUMBER), + make_calypso_final_element( + "EventVehicleId", 16, "Identificateur du véhicule", CALYPSO_FINAL_TYPE_NUMBER), + make_calypso_final_element( + "EventVehicleClass", 8, "Type de véhicule utilisé", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventLocationType", + 5, + "Type d’endroit (gare, arrêt de bus), ", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventEmployee", 240, "Code de l’employé impliqué", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventLocationReference", + 16, + "Référence du lieu de l’événement", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventJourneyInterchanges", + 8, + "Nombre de correspondances", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventPeriodJourneys", 16, "Nombre de voyage effectué", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventTotalJourneys", + 16, + "Nombre total de voyage autorisé", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventJourneyDistance", 16, "Distance parcourue", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventPriceAmount", + 16, + "Montant en jeu lors de l’événement", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventPriceUnit", 16, "Unité de montant en jeu", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventContractPointer", + 5, + "Référence du contrat concerné", + CALYPSO_FINAL_TYPE_NUMBER), + make_calypso_final_element( + "EventAuthenticator", 16, "Code de sécurité", CALYPSO_FINAL_TYPE_UNKNOWN), + + make_calypso_bitmap_element( + "EventData", + 5, + (CalypsoElement[]){ + make_calypso_final_element( + "EventDataDateFirstStamp", + 14, + "Date de la première montée", + CALYPSO_FINAL_TYPE_DATE), + make_calypso_final_element( + "EventDataTimeFirstStamp", + 11, + "Heure de la première montée", + CALYPSO_FINAL_TYPE_TIME), + make_calypso_final_element( + "EventDataSimulation", + 1, + "Dernière validation (0=normal, 1=dégradé), ", + CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventDataTrip", 2, "Tronçon", CALYPSO_FINAL_TYPE_UNKNOWN), + make_calypso_final_element( + "EventDataRouteDirection", 2, "Sens", CALYPSO_FINAL_TYPE_UNKNOWN), + }), + }); + + return NavigoEventStructure; +} diff --git a/metroflip/api/calypso/cards/navigo.h b/metroflip/api/calypso/cards/navigo.h new file mode 100644 index 000000000..facef4643 --- /dev/null +++ b/metroflip/api/calypso/cards/navigo.h @@ -0,0 +1,10 @@ +#include "../calypso_util.h" + +#ifndef NAVIGO_STRUCTURES_H +#define NAVIGO_STRUCTURES_H + +CalypsoApp* get_navigo_contract_structure(); + +CalypsoApp* get_navigo_event_structure(); + +#endif // NAVIGO_STRUCTURES_H diff --git a/metroflip/api/mosgortrans/mosgortrans_util.c b/metroflip/api/mosgortrans/mosgortrans_util.c new file mode 100644 index 000000000..6b2d66081 --- /dev/null +++ b/metroflip/api/mosgortrans/mosgortrans_util.c @@ -0,0 +1,1322 @@ +#include "mosgortrans_util.h" + +#define TAG "Metroflip:Scene:Mosgortrans" + +void render_section_header( + FuriString* str, + const char* name, + uint8_t prefix_separator_cnt, + uint8_t suffix_separator_cnt) { + for(uint8_t i = 0; i < prefix_separator_cnt; i++) { + furi_string_cat_printf(str, ":"); + } + furi_string_cat_printf(str, "[ %s ]", name); + for(uint8_t i = 0; i < suffix_separator_cnt; i++) { + furi_string_cat_printf(str, ":"); + } +} + +void from_days_to_datetime(uint32_t days, DateTime* datetime, uint16_t start_year) { + uint32_t timestamp = days * 24 * 60 * 60; + DateTime start_datetime = {0}; + start_datetime.year = start_year - 1; + start_datetime.month = 12; + start_datetime.day = 31; + timestamp += datetime_datetime_to_timestamp(&start_datetime); + datetime_timestamp_to_datetime(timestamp, datetime); +} + +void from_minutes_to_datetime(uint32_t minutes, DateTime* datetime, uint16_t start_year) { + uint32_t timestamp = minutes * 60; + DateTime start_datetime = {0}; + start_datetime.year = start_year - 1; + start_datetime.month = 12; + start_datetime.day = 31; + timestamp += datetime_datetime_to_timestamp(&start_datetime); + datetime_timestamp_to_datetime(timestamp, datetime); +} + +void from_seconds_to_datetime(uint32_t seconds, DateTime* datetime, uint16_t start_year) { + uint32_t timestamp = seconds; + DateTime start_datetime = {0}; + start_datetime.year = start_year - 1; + start_datetime.month = 12; + start_datetime.day = 31; + timestamp += datetime_datetime_to_timestamp(&start_datetime); + datetime_timestamp_to_datetime(timestamp, datetime); +} + +typedef struct { + uint16_t view; //101 + uint16_t type; //102 + uint8_t layout; //111 + uint8_t layout2; //112 + uint16_t blank_type; //121 + uint16_t type_of_extended; //122 + uint8_t extended; //123 + uint8_t benefit_code; //124 + uint32_t number; //201 + uint16_t use_before_date; //202 + uint16_t use_before_date2; //202.2 + uint16_t use_with_date; //205 + uint8_t requires_activation; //301 + uint16_t activate_during; //302 + uint16_t extension_counter; //304 + uint8_t blocked; //303 + uint32_t valid_from_date; //311 + uint16_t valid_to_date; //312 + uint8_t valid_for_days; //313 + uint32_t valid_for_minutes; //314 + uint16_t valid_for_time; //316 + uint16_t valid_for_time2; //316.2 + uint32_t valid_to_time; //317 + uint16_t remaining_trips; //321 + uint8_t remaining_trips1; //321.1 + uint32_t remaining_funds; //322 + uint16_t total_trips; //331 + uint16_t start_trip_date; //402 + uint16_t start_trip_time; //403 + uint32_t start_trip_neg_minutes; //404 + uint32_t start_trip_minutes; //405 + uint8_t start_trip_seconds; //406 + uint8_t minutes_pass; //412 + uint8_t passage_5_minutes; //413 + uint8_t metro_ride_with; //414 + uint8_t transport_type; //421 + uint8_t transport_type_flag; //421.0 + uint8_t transport_type1; //421.1 + uint8_t transport_type2; //421.2 + uint8_t transport_type3; //421.3 + uint8_t transport_type4; //421.4 + uint16_t validator; //422 + uint8_t validator1; //422.1 + uint16_t validator2; //422.2 + uint16_t route; //424 + uint8_t passage_in_metro; //431 + uint8_t transfer_in_metro; //432 + uint8_t passages_ground_transport; //433 + uint8_t fare_trip; //441 + uint16_t crc16; //501.1 + uint16_t crc16_2; //501.2 + uint32_t hash; //502 + uint16_t hash1; //502.1 + uint32_t hash2; //502.2 + uint8_t geozone_a; //GeoZoneA + uint8_t geozone_b; //GeoZoneB + uint8_t company; //Company + uint8_t units; //Units + uint64_t rfu1; //rfu1 + uint16_t rfu2; //rfu2 + uint32_t rfu3; //rfu3 + uint8_t rfu4; //rfu4 + uint8_t rfu5; //rfu5 + uint8_t write_enabled; //write_enabled + uint32_t tech_code; //TechCode + uint8_t interval; //Interval + uint16_t app_code1; //AppCode1 + uint16_t app_code2; //AppCode2 + uint16_t app_code3; //AppCode3 + uint16_t app_code4; //AppCode4 + uint16_t type1; //Type1 + uint16_t type2; //Type2 + uint16_t type3; //Type3 + uint16_t type4; //Type4 + uint8_t zoo; //zoo +} BlockData; + +void parse_layout_2(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 0x38, 16); //202 + data_block->benefit_code = bit_lib_get_bits(block->data, 0x48, 8); //124 + data_block->rfu1 = bit_lib_get_bits_32(block->data, 0x50, 32); //rfu1 + data_block->crc16 = bit_lib_get_bits_16(block->data, 0x70, 16); //501.1 + data_block->blocked = bit_lib_get_bits(block->data, 0x80, 1); //303 + data_block->start_trip_time = bit_lib_get_bits_16(block->data, 0x81, 12); //403 + data_block->start_trip_date = bit_lib_get_bits_16(block->data, 0x8D, 16); //402 + data_block->valid_from_date = bit_lib_get_bits_16(block->data, 0x9D, 16); //311 + data_block->valid_to_date = bit_lib_get_bits_16(block->data, 0xAD, 16); //312 + data_block->start_trip_seconds = bit_lib_get_bits(block->data, 0xDB, 6); //406 + data_block->transport_type1 = bit_lib_get_bits(block->data, 0xC3, 2); //421.1 + data_block->transport_type2 = bit_lib_get_bits(block->data, 0xC5, 2); //421.2 + data_block->transport_type3 = bit_lib_get_bits(block->data, 0xC7, 2); //421.3 + data_block->transport_type4 = bit_lib_get_bits(block->data, 0xC9, 2); //421.4 + data_block->use_with_date = bit_lib_get_bits_16(block->data, 0xBD, 16); //205 + data_block->route = bit_lib_get_bits(block->data, 0xCD, 1); //424 + data_block->validator1 = bit_lib_get_bits_16(block->data, 0xCE, 15); //422.1 + data_block->validator = bit_lib_get_bits_16(block->data, 0xCD, 16); + data_block->total_trips = bit_lib_get_bits_16(block->data, 0xDD, 16); //331 + data_block->write_enabled = bit_lib_get_bits(block->data, 0xED, 1); //write_enabled + data_block->rfu2 = bit_lib_get_bits(block->data, 0xEE, 2); //rfu2 + data_block->crc16_2 = bit_lib_get_bits_16(block->data, 0xF0, 16); //501.2 +} + +void parse_layout_6(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 0x38, 16); //202 + data_block->geozone_a = bit_lib_get_bits(block->data, 0x48, 4); //GeoZoneA + data_block->geozone_b = bit_lib_get_bits(block->data, 0x4C, 4); //GeoZoneB + data_block->blank_type = bit_lib_get_bits_16(block->data, 0x50, 10); //121 + data_block->type_of_extended = bit_lib_get_bits_16(block->data, 0x5A, 10); //122 + data_block->rfu1 = bit_lib_get_bits_16(block->data, 0x64, 12); //rfu1 + data_block->crc16 = bit_lib_get_bits_16(block->data, 0x70, 16); //501.1 + data_block->blocked = bit_lib_get_bits(block->data, 0x80, 1); //303 + data_block->start_trip_time = bit_lib_get_bits_16(block->data, 0x81, 12); //403 + data_block->start_trip_date = bit_lib_get_bits_16(block->data, 0x8D, 16); //402 + data_block->valid_from_date = bit_lib_get_bits_16(block->data, 0x9D, 16); //311 + data_block->valid_to_date = bit_lib_get_bits_16(block->data, 0xAD, 16); //312 + data_block->company = bit_lib_get_bits(block->data, 0xBD, 4); //Company + data_block->validator1 = bit_lib_get_bits(block->data, 0xC1, 4); //422.1 + data_block->remaining_trips = bit_lib_get_bits_16(block->data, 0xC5, 10); //321 + data_block->units = bit_lib_get_bits(block->data, 0xCF, 6); //Units + data_block->validator2 = bit_lib_get_bits_16(block->data, 0xD5, 10); //422.2 + data_block->total_trips = bit_lib_get_bits_16(block->data, 0xDF, 16); //331 + data_block->extended = bit_lib_get_bits(block->data, 0xEF, 1); //123 + data_block->crc16_2 = bit_lib_get_bits_16(block->data, 0xF0, 16); //501.2 +} + +void parse_layout_8(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 0x38, 16); //202 + data_block->rfu1 = bit_lib_get_bits_64(block->data, 0x48, 56); //rfu1 + data_block->valid_from_date = bit_lib_get_bits_16(block->data, 0x80, 16); //311 + data_block->valid_for_days = bit_lib_get_bits(block->data, 0x90, 8); //313 + data_block->requires_activation = bit_lib_get_bits(block->data, 0x98, 1); //301 + data_block->rfu2 = bit_lib_get_bits(block->data, 0x99, 7); //rfu2 + data_block->remaining_trips1 = bit_lib_get_bits(block->data, 0xA0, 8); //321.1 + data_block->remaining_trips = bit_lib_get_bits(block->data, 0xA8, 8); //321 + data_block->validator1 = bit_lib_get_bits(block->data, 0xB0, 2); //422.1 + data_block->validator = bit_lib_get_bits_16(block->data, 0xB1, 15); //422 + data_block->hash = bit_lib_get_bits_32(block->data, 0xC0, 32); //502 + data_block->rfu3 = bit_lib_get_bits_32(block->data, 0xE0, 32); //rfu3 +} + +void parse_layout_A(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->valid_from_date = bit_lib_get_bits_16(block->data, 0x40, 12); //311 + data_block->valid_for_minutes = bit_lib_get_bits_32(block->data, 0x4C, 19); //314 + data_block->requires_activation = bit_lib_get_bits(block->data, 0x5F, 1); //301 + data_block->start_trip_minutes = bit_lib_get_bits_32(block->data, 0x60, 19); //405 + data_block->minutes_pass = bit_lib_get_bits(block->data, 0x77, 7); //412 + data_block->transport_type_flag = bit_lib_get_bits(block->data, 0x7E, 2); //421.0 + data_block->remaining_trips = bit_lib_get_bits(block->data, 0x80, 8); //321 + data_block->validator = bit_lib_get_bits_16(block->data, 0x88, 16); //422 + data_block->transport_type1 = bit_lib_get_bits(block->data, 0x98, 2); //421.1 + data_block->transport_type2 = bit_lib_get_bits(block->data, 0x9A, 2); //421.2 + data_block->transport_type3 = bit_lib_get_bits(block->data, 0x9C, 2); //421.3 + data_block->transport_type4 = bit_lib_get_bits(block->data, 0x9E, 2); //421.4 + data_block->hash = bit_lib_get_bits_32(block->data, 0xC0, 32); //502 +} + +void parse_layout_C(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 0x38, 16); //202 + data_block->rfu1 = bit_lib_get_bits_64(block->data, 0x48, 56); //rfu1 + data_block->valid_from_date = bit_lib_get_bits_16(block->data, 0x80, 16); //311 + data_block->valid_for_days = bit_lib_get_bits(block->data, 0x90, 8); //313 + data_block->requires_activation = bit_lib_get_bits(block->data, 0x98, 1); //301 + data_block->rfu2 = bit_lib_get_bits_16(block->data, 0x99, 13); //rfu2 + data_block->remaining_trips = bit_lib_get_bits_16(block->data, 0xA6, 10); //321 + data_block->validator = bit_lib_get_bits_16(block->data, 0xB0, 16); //422 + data_block->hash = bit_lib_get_bits_32(block->data, 0xC0, 32); //502 + data_block->start_trip_date = bit_lib_get_bits_16(block->data, 0xE0, 16); //402 + data_block->start_trip_time = bit_lib_get_bits_16(block->data, 0xF0, 11); //403 + data_block->transport_type = bit_lib_get_bits(block->data, 0xFB, 2); //421 + data_block->rfu3 = bit_lib_get_bits(block->data, 0xFD, 2); //rfu3 + data_block->transfer_in_metro = bit_lib_get_bits(block->data, 0xFF, 1); //432 +} + +void parse_layout_D(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->rfu1 = bit_lib_get_bits(block->data, 0x38, 8); //rfu1 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 0x40, 16); //202 + data_block->valid_for_time = bit_lib_get_bits_16(block->data, 0x50, 11); //316 + data_block->rfu2 = bit_lib_get_bits(block->data, 0x5B, 5); //rfu2 + data_block->use_before_date2 = bit_lib_get_bits_16(block->data, 0x60, 16); //202.2 + data_block->valid_for_time2 = bit_lib_get_bits_16(block->data, 0x70, 11); //316.2 + data_block->rfu3 = bit_lib_get_bits(block->data, 0x7B, 5); //rfu3 + data_block->valid_from_date = bit_lib_get_bits_16(block->data, 0x80, 16); //311 + data_block->valid_for_days = bit_lib_get_bits(block->data, 0x90, 8); //313 + data_block->requires_activation = bit_lib_get_bits(block->data, 0x98, 1); //301 + data_block->rfu4 = bit_lib_get_bits(block->data, 0x99, 2); //rfu4 + data_block->passage_5_minutes = bit_lib_get_bits(block->data, 0x9B, 5); //413 + data_block->transport_type1 = bit_lib_get_bits(block->data, 0xA0, 2); //421.1 + data_block->passage_in_metro = bit_lib_get_bits(block->data, 0xA2, 1); //431 + data_block->passages_ground_transport = bit_lib_get_bits(block->data, 0xA3, 3); //433 + data_block->remaining_trips = bit_lib_get_bits_16(block->data, 0xA6, 10); //321 + data_block->validator = bit_lib_get_bits_16(block->data, 0xB0, 16); //422 + data_block->hash = bit_lib_get_bits_32(block->data, 0xC0, 32); //502 + data_block->start_trip_date = bit_lib_get_bits_16(block->data, 0xE0, 16); //402 + data_block->start_trip_time = bit_lib_get_bits_16(block->data, 0xF0, 11); //403 + data_block->transport_type2 = bit_lib_get_bits(block->data, 0xFB, 2); //421.2 + data_block->rfu5 = bit_lib_get_bits(block->data, 0xFD, 2); //rfu5 + data_block->transfer_in_metro = bit_lib_get_bits(block->data, 0xFF, 1); //432 +} + +void parse_layout_E1(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->layout2 = bit_lib_get_bits(block->data, 0x38, 5); //112 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 0x3D, 16); //202 + data_block->blank_type = bit_lib_get_bits_16(block->data, 0x4D, 10); //121 + data_block->validator = bit_lib_get_bits_16(block->data, 0x80, 16); //422 + data_block->start_trip_date = bit_lib_get_bits_16(block->data, 0x90, 16); //402 + data_block->start_trip_time = bit_lib_get_bits_16(block->data, 0xA0, 11); //403 + data_block->transport_type1 = bit_lib_get_bits(block->data, 0xAB, 2); //421.1 + data_block->transport_type2 = bit_lib_get_bits(block->data, 0xAD, 2); //421.2 + data_block->transfer_in_metro = bit_lib_get_bits(block->data, 0xB1, 1); //432 + data_block->passage_in_metro = bit_lib_get_bits(block->data, 0xB2, 1); //431 + data_block->passages_ground_transport = bit_lib_get_bits(block->data, 0xB3, 3); //433 + data_block->minutes_pass = bit_lib_get_bits(block->data, 0xB9, 8); //412 + data_block->remaining_funds = bit_lib_get_bits_32(block->data, 0xC4, 19); //322 + data_block->fare_trip = bit_lib_get_bits(block->data, 0xD7, 2); //441 + data_block->blocked = bit_lib_get_bits(block->data, 0x9D, 1); //303 + data_block->zoo = bit_lib_get_bits(block->data, 0xDA, 1); //zoo + data_block->hash = bit_lib_get_bits_32(block->data, 0xE0, 32); //502 +} + +void parse_layout_E2(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->layout2 = bit_lib_get_bits(block->data, 0x38, 5); //112 + data_block->type_of_extended = bit_lib_get_bits_16(block->data, 0x3D, 10); //122 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 0x47, 16); //202 + data_block->blank_type = bit_lib_get_bits_16(block->data, 0x57, 10); //121 + data_block->valid_from_date = bit_lib_get_bits_16(block->data, 0x61, 16); //311 + data_block->activate_during = bit_lib_get_bits_16(block->data, 0x71, 9); //302 + data_block->valid_for_minutes = bit_lib_get_bits_32(block->data, 0x83, 20); //314 + data_block->minutes_pass = bit_lib_get_bits(block->data, 0x9A, 8); //412 + data_block->transport_type = bit_lib_get_bits(block->data, 0xA3, 2); //421 + data_block->passage_in_metro = bit_lib_get_bits(block->data, 0xA5, 1); //431 + data_block->transfer_in_metro = bit_lib_get_bits(block->data, 0xA6, 1); //432 + data_block->remaining_trips = bit_lib_get_bits_16(block->data, 0xA7, 10); //321 + data_block->validator = bit_lib_get_bits_16(block->data, 0xB1, 16); //422 + data_block->start_trip_neg_minutes = bit_lib_get_bits_32(block->data, 0xC4, 20); //404 + data_block->requires_activation = bit_lib_get_bits(block->data, 0xD8, 1); //301 + data_block->blocked = bit_lib_get_bits(block->data, 0xD9, 1); //303 + data_block->extended = bit_lib_get_bits(block->data, 0xDA, 1); //123 + data_block->hash = bit_lib_get_bits_32(block->data, 0xE0, 32); //502 +} + +void parse_layout_E3(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->layout2 = bit_lib_get_bits(block->data, 0x38, 5); //112 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 61, 16); //202 + data_block->blank_type = bit_lib_get_bits_16(block->data, 0x4D, 10); //121 + data_block->remaining_funds = bit_lib_get_bits_32(block->data, 0xBC, 22); //322 + data_block->hash = bit_lib_get_bits_32(block->data, 224, 32); //502 + data_block->validator = bit_lib_get_bits_16(block->data, 0x80, 16); //422 + data_block->start_trip_minutes = bit_lib_get_bits_32(block->data, 0x90, 23); //405 + data_block->fare_trip = bit_lib_get_bits(block->data, 0xD2, 2); //441 + data_block->minutes_pass = bit_lib_get_bits(block->data, 0xAB, 7); //412 + data_block->transport_type_flag = bit_lib_get_bits(block->data, 0xB2, 2); //421.0 + data_block->transport_type1 = bit_lib_get_bits(block->data, 0xB4, 2); //421.1 + data_block->transport_type2 = bit_lib_get_bits(block->data, 0xB6, 2); //421.2 + data_block->transport_type3 = bit_lib_get_bits(block->data, 0xB8, 2); //421.3 + data_block->transport_type4 = bit_lib_get_bits(block->data, 0xBA, 2); //421.4 + data_block->blocked = bit_lib_get_bits(block->data, 0xD4, 1); //303 +} + +void parse_layout_E4(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->layout2 = bit_lib_get_bits(block->data, 0x38, 5); //112 + data_block->type_of_extended = bit_lib_get_bits_16(block->data, 0x3D, 10); //122 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 0x47, 13); //202 + data_block->blank_type = bit_lib_get_bits_16(block->data, 0x54, 10); //121 + data_block->valid_from_date = bit_lib_get_bits_16(block->data, 0x5E, 13); //311 + data_block->activate_during = bit_lib_get_bits_16(block->data, 0x6B, 9); //302 + data_block->extension_counter = bit_lib_get_bits_16(block->data, 0x74, 10); //304 + data_block->valid_for_minutes = bit_lib_get_bits_32(block->data, 0x80, 20); //314 + data_block->minutes_pass = bit_lib_get_bits(block->data, 0x98, 7); //412 + data_block->transport_type_flag = bit_lib_get_bits(block->data, 0x9F, 2); //421.0 + data_block->transport_type1 = bit_lib_get_bits(block->data, 0xA1, 2); //421.1 + data_block->transport_type2 = bit_lib_get_bits(block->data, 0xA3, 2); //421.2 + data_block->transport_type3 = bit_lib_get_bits(block->data, 0xA5, 2); //421.3 + data_block->transport_type4 = bit_lib_get_bits(block->data, 0xA7, 2); //421.4 + data_block->remaining_trips = bit_lib_get_bits_16(block->data, 0xA9, 10); //321 + data_block->validator = bit_lib_get_bits_16(block->data, 0xB3, 16); //422 + data_block->start_trip_neg_minutes = bit_lib_get_bits_32(block->data, 0xC3, 20); //404 + data_block->requires_activation = bit_lib_get_bits(block->data, 0xD7, 1); //301 + data_block->blocked = bit_lib_get_bits(block->data, 0xD8, 1); //303 + data_block->extended = bit_lib_get_bits(block->data, 0xD9, 1); //123 + data_block->hash = bit_lib_get_bits_32(block->data, 0xE0, 32); //502 +} + +void parse_layout_E5(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->layout2 = bit_lib_get_bits(block->data, 0x38, 5); //112 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 0x3D, 13); //202 + data_block->blank_type = bit_lib_get_bits_16(block->data, 0x4A, 10); //121 + data_block->valid_to_time = bit_lib_get_bits_32(block->data, 0x54, 23); //317 + data_block->extension_counter = bit_lib_get_bits_16(block->data, 0x6B, 10); //304 + data_block->start_trip_minutes = bit_lib_get_bits_32(block->data, 0x80, 23); //405 + data_block->metro_ride_with = bit_lib_get_bits(block->data, 0x97, 7); //414 + data_block->minutes_pass = bit_lib_get_bits(block->data, 0x9E, 7); //412 + data_block->remaining_funds = bit_lib_get_bits_32(block->data, 0xA7, 19); //322 + data_block->validator = bit_lib_get_bits_16(block->data, 0xBA, 16); //422 + data_block->blocked = bit_lib_get_bits(block->data, 0xCA, 1); //303 + data_block->route = bit_lib_get_bits_16(block->data, 0xCC, 12); //424 + data_block->passages_ground_transport = bit_lib_get_bits(block->data, 0xD8, 7); //433 + data_block->hash = bit_lib_get_bits_32(block->data, 0xE0, 32); //502 +} + +void parse_layout_E6(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->layout2 = bit_lib_get_bits(block->data, 0x38, 5); //112 + data_block->type_of_extended = bit_lib_get_bits_16(block->data, 0x3D, 10); //122 + data_block->use_before_date = bit_lib_get_bits_16(block->data, 0x47, 13); //202 + data_block->blank_type = bit_lib_get_bits_16(block->data, 0x54, 10); //121 + data_block->valid_from_date = bit_lib_get_bits_32(block->data, 0x5E, 23); //311 + data_block->extension_counter = bit_lib_get_bits_16(block->data, 0x75, 10); //304 + data_block->valid_for_minutes = bit_lib_get_bits_32(block->data, 0x80, 20); //314 + data_block->start_trip_neg_minutes = bit_lib_get_bits_32(block->data, 0x94, 20); //404 + data_block->metro_ride_with = bit_lib_get_bits(block->data, 0xA8, 7); //414 + data_block->minutes_pass = bit_lib_get_bits(block->data, 0xAF, 7); //412 + data_block->remaining_trips = bit_lib_get_bits(block->data, 0xB6, 7); //321 + data_block->validator = bit_lib_get_bits_16(block->data, 0xBD, 16); //422 + data_block->blocked = bit_lib_get_bits(block->data, 0xCD, 1); //303 + data_block->extended = bit_lib_get_bits(block->data, 0xCE, 1); //123 + data_block->route = bit_lib_get_bits_16(block->data, 0xD4, 12); //424 + data_block->hash = bit_lib_get_bits_32(block->data, 0xE0, 32); //502 +} + +void parse_layout_FCB(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->tech_code = bit_lib_get_bits_32(block->data, 0x38, 10); //tech_code + data_block->valid_from_date = bit_lib_get_bits_16(block->data, 0x42, 16); //311 + data_block->valid_to_date = bit_lib_get_bits_16(block->data, 0x52, 16); //312 + data_block->interval = bit_lib_get_bits(block->data, 0x62, 4); //interval + data_block->app_code1 = bit_lib_get_bits_16(block->data, 0x66, 10); //app_code1 + data_block->hash1 = bit_lib_get_bits_16(block->data, 0x70, 16); //502.1 + data_block->type1 = bit_lib_get_bits_16(block->data, 0x80, 10); //type1 + data_block->app_code2 = bit_lib_get_bits_16(block->data, 0x8A, 10); //app_code2 + data_block->type2 = bit_lib_get_bits_16(block->data, 0x94, 10); //type2 + data_block->app_code3 = bit_lib_get_bits_16(block->data, 0x9E, 10); //app_code3 + data_block->type3 = bit_lib_get_bits_16(block->data, 0xA8, 10); //type3 + data_block->app_code4 = bit_lib_get_bits_16(block->data, 0xB2, 10); //app_code4 + data_block->type4 = bit_lib_get_bits_16(block->data, 0xBC, 10); //type4 + data_block->hash2 = bit_lib_get_bits_32(block->data, 0xE0, 32); //502.2 +} + +void parse_layout_F0B(BlockData* data_block, const MfClassicBlock* block) { + data_block->view = bit_lib_get_bits_16(block->data, 0x00, 10); //101 + data_block->type = bit_lib_get_bits_16(block->data, 0x0A, 10); //102 + data_block->number = bit_lib_get_bits_32(block->data, 0x14, 32); //201 + data_block->layout = bit_lib_get_bits(block->data, 0x34, 4); //111 + data_block->tech_code = bit_lib_get_bits_32(block->data, 0x38, 10); //tech_code + data_block->valid_from_date = bit_lib_get_bits_16(block->data, 0x42, 16); //311 + data_block->valid_to_date = bit_lib_get_bits_16(block->data, 0x52, 16); //312 + data_block->hash1 = bit_lib_get_bits_32(block->data, 0x70, 16); //502.1 +} + +void parse_transport_type(BlockData* data_block, FuriString* transport) { + switch(data_block->transport_type_flag) { + case 1: + uint8_t transport_type = + (data_block->transport_type1 || data_block->transport_type2 || + data_block->transport_type3 || data_block->transport_type4); + switch(transport_type) { + case 1: + furi_string_cat(transport, "Metro"); + break; + case 2: + furi_string_cat(transport, "Monorail"); + break; + case 3: + furi_string_cat(transport, "MCC"); + break; + default: + furi_string_cat(transport, "Unknown"); + break; + } + break; + case 2: + furi_string_cat(transport, "Ground"); + break; + default: + furi_string_cat(transport, ""); + break; + } +} + +bool mosgortrans_parse_transport_block(const MfClassicBlock* block, FuriString* result) { + BlockData data_block = {}; + const uint16_t valid_departments[] = {0x106, 0x108, 0x10A, 0x10E, 0x110, 0x117}; + uint16_t transport_department = bit_lib_get_bits_16(block->data, 0, 10); + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + furi_string_cat_printf(result, "Transport department: %x\n", transport_department); + } + bool department_valid = false; + for(uint8_t i = 0; i < 6; i++) { + if(transport_department == valid_departments[i]) { + department_valid = true; + break; + } + } + if(!department_valid) { + return false; + } + FURI_LOG_D(TAG, "Transport department: %x", transport_department); + uint16_t layout_type = bit_lib_get_bits_16(block->data, 52, 4); + if(layout_type == 0xE) { + layout_type = bit_lib_get_bits_16(block->data, 52, 9); + } else if(layout_type == 0xF) { + layout_type = bit_lib_get_bits_16(block->data, 52, 14); + } + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + furi_string_cat_printf(result, "Layout: %x\n", layout_type); + } + FURI_LOG_D(TAG, "Layout type %x", layout_type); + switch(layout_type) { + case 0x02: { + parse_layout_2(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 1992); + furi_string_cat_printf( + result, + "Use before: %02d.%02d.%04d\n", + card_use_before_date_s.day, + card_use_before_date_s.month, + card_use_before_date_s.year); + + if(data_block.valid_from_date == 0 || data_block.valid_to_date == 0) { + furi_string_cat(result, "\e#No ticket"); + return false; + } + //remaining_trips + furi_string_cat_printf(result, "Trips: %d\n", data_block.total_trips); + //valid_from_date + DateTime card_valid_from_date_s = {0}; + from_days_to_datetime(data_block.valid_from_date, &card_valid_from_date_s, 1992); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d\n", + card_valid_from_date_s.day, + card_valid_from_date_s.month, + card_valid_from_date_s.year); + //valid_to_date + DateTime card_valid_to_date_s = {0}; + from_days_to_datetime(data_block.valid_to_date, &card_valid_to_date_s, 1992); + furi_string_cat_printf( + result, + "Valid to: %02d.%02d.%04d\n", + card_valid_to_date_s.day, + card_valid_to_date_s.month, + card_valid_to_date_s.year); + //trip_number + furi_string_cat_printf(result, "Trips: %d\n", data_block.total_trips); + //trip_from + DateTime card_start_trip_minutes_s = {0}; + from_seconds_to_datetime( + data_block.start_trip_date * 24 * 60 * 60 + data_block.start_trip_time * 60 + + data_block.start_trip_seconds, + &card_start_trip_minutes_s, + 1992); + furi_string_cat_printf( + result, + "Trip from: %02d.%02d.%04d %02d:%02d", + card_start_trip_minutes_s.day, + card_start_trip_minutes_s.month, + card_start_trip_minutes_s.year, + card_start_trip_minutes_s.hour, + card_start_trip_minutes_s.minute); + break; + } + case 0x06: { + parse_layout_6(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 1992); + furi_string_cat_printf( + result, + "Use before: %02d.%02d.%04d\n", + card_use_before_date_s.day, + card_use_before_date_s.month, + card_use_before_date_s.year); + //remaining_trips + furi_string_cat_printf(result, "Trips left: %d\n", data_block.remaining_trips); + //valid_from_date + DateTime card_valid_from_date_s = {0}; + from_days_to_datetime(data_block.valid_from_date, &card_valid_from_date_s, 1992); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d\n", + card_valid_from_date_s.day, + card_valid_from_date_s.month, + card_valid_from_date_s.year); + //valid_to_date + DateTime card_valid_to_date_s = {0}; + from_days_to_datetime(data_block.valid_to_date, &card_valid_to_date_s, 1992); + furi_string_cat_printf( + result, + "Valid to: %02d.%02d.%04d\n", + card_valid_to_date_s.day, + card_valid_to_date_s.month, + card_valid_to_date_s.year); + //trip_number + furi_string_cat_printf(result, "Trips: %d\n", data_block.total_trips); + //trip_from + DateTime card_start_trip_minutes_s = {0}; + from_minutes_to_datetime( + (data_block.start_trip_date) * 24 * 60 + data_block.start_trip_time, + &card_start_trip_minutes_s, + 1992); + furi_string_cat_printf( + result, + "Trip from: %02d.%02d.%04d %02d:%02d\n", + card_start_trip_minutes_s.day, + card_start_trip_minutes_s.month, + card_start_trip_minutes_s.year, + card_start_trip_minutes_s.hour, + card_start_trip_minutes_s.minute); + //validator + furi_string_cat_printf( + result, "Validator: %05d", data_block.validator1 * 1024 + data_block.validator2); + break; + } + case 0x08: { + parse_layout_8(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 1992); + //remaining_trips + furi_string_cat_printf(result, "Trips left: %d\n", data_block.remaining_trips); + //valid_from_date + DateTime card_valid_from_date_s = {0}; + from_days_to_datetime(data_block.valid_from_date, &card_valid_from_date_s, 1992); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d\n", + card_valid_from_date_s.day, + card_valid_from_date_s.month, + card_valid_from_date_s.year); + //valid_to_date + DateTime card_valid_to_date_s = {0}; + from_days_to_datetime( + data_block.valid_from_date + data_block.valid_for_days, &card_valid_to_date_s, 1992); + furi_string_cat_printf( + result, + "Valid to: %02d.%02d.%04d", + card_valid_to_date_s.day, + card_valid_to_date_s.month, + card_valid_to_date_s.year); + break; + } + case 0x0A: { + parse_layout_A(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 2016); + furi_string_cat_printf( + result, + "Use before: %02d.%02d.%04d\n", + card_use_before_date_s.day, + card_use_before_date_s.month, + card_use_before_date_s.year); + //remaining_trips + furi_string_cat_printf(result, "Trips left: %d\n", data_block.remaining_trips); + //valid_from_date + DateTime card_valid_from_date_s = {0}; + from_days_to_datetime(data_block.valid_from_date, &card_valid_from_date_s, 2016); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d\n", + card_valid_from_date_s.day, + card_valid_from_date_s.month, + card_valid_from_date_s.year); + //valid_to_date + DateTime card_valid_to_date_s = {0}; + from_minutes_to_datetime( + data_block.valid_from_date * 24 * 60 + data_block.valid_for_minutes - 1, + &card_valid_to_date_s, + 2016); + furi_string_cat_printf( + result, + "Valid to: %02d.%02d.%04d", + card_valid_to_date_s.day, + card_valid_to_date_s.month, + card_valid_to_date_s.year); + //trip_from + if(data_block.start_trip_minutes) { + DateTime card_start_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.valid_from_date * 24 * 60 + data_block.start_trip_minutes, + &card_start_trip_minutes_s, + 2016); + furi_string_cat_printf( + result, + "\nTrip from: %02d.%02d.%04d %02d:%02d", + card_start_trip_minutes_s.day, + card_start_trip_minutes_s.month, + card_start_trip_minutes_s.year, + card_start_trip_minutes_s.hour, + card_start_trip_minutes_s.minute); + } + //trip_switch + if(data_block.minutes_pass) { + DateTime card_start_switch_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.valid_from_date * 24 * 60 + data_block.start_trip_minutes + + data_block.minutes_pass, + &card_start_switch_trip_minutes_s, + 2016); + furi_string_cat_printf( + result, + "\nTrip switch: %02d.%02d.%04d %02d:%02d", + card_start_switch_trip_minutes_s.day, + card_start_switch_trip_minutes_s.month, + card_start_switch_trip_minutes_s.year, + card_start_switch_trip_minutes_s.hour, + card_start_switch_trip_minutes_s.minute); + } + //transport + FuriString* transport = furi_string_alloc(); + parse_transport_type(&data_block, transport); + furi_string_cat_printf(result, "\nTransport: %s", furi_string_get_cstr(transport)); + //validator + if(data_block.validator) { + furi_string_cat_printf(result, "\nValidator: %05d", data_block.validator); + } + furi_string_free(transport); + break; + } + case 0x0C: { + parse_layout_C(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 1992); + //remaining_trips + furi_string_cat_printf(result, "Trips left: %d\n", data_block.remaining_trips); + //valid_from_date + DateTime card_valid_from_date_s = {0}; + from_days_to_datetime(data_block.valid_from_date, &card_valid_from_date_s, 1992); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d\n", + card_valid_from_date_s.day, + card_valid_from_date_s.month, + card_valid_from_date_s.year); + //valid_to_date + DateTime card_valid_to_date_s = {0}; + from_days_to_datetime( + data_block.valid_from_date + data_block.valid_for_days, &card_valid_to_date_s, 1992); + furi_string_cat_printf( + result, + "Valid to: %02d.%02d.%04d\n", + card_valid_to_date_s.day, + card_valid_to_date_s.month, + card_valid_to_date_s.year); + //remaining_trips + furi_string_cat_printf(result, "Trips left: %d", data_block.remaining_trips); + //trip_from + if(data_block.start_trip_date) { // TODO: (-nofl) unused + DateTime card_start_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.start_trip_date * 24 * 60 + data_block.start_trip_time, + &card_start_trip_minutes_s, + 1992); + } + //validator + if(data_block.validator) { + furi_string_cat_printf(result, "\nValidator: %05d", data_block.validator); + } + break; + } + case 0x0D: { + parse_layout_D(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 1992); + furi_string_cat_printf( + result, + "Use before: %02d.%02d.%04d\n", + card_use_before_date_s.day, + card_use_before_date_s.month, + card_use_before_date_s.year); + //remaining_trips + furi_string_cat_printf(result, "Trips left: %d\n", data_block.remaining_trips); + //valid_from_date + DateTime card_valid_from_date_s = {0}; + from_days_to_datetime(data_block.valid_from_date, &card_valid_from_date_s, 1992); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d\n", + card_valid_from_date_s.day, + card_valid_from_date_s.month, + card_valid_from_date_s.year); + //valid_to_date + DateTime card_valid_to_date_s = {0}; + from_days_to_datetime( + data_block.valid_from_date + data_block.valid_for_days, &card_valid_to_date_s, 1992); + furi_string_cat_printf( + result, + "Valid to: %02d.%02d.%04d", + card_valid_to_date_s.day, + card_valid_to_date_s.month, + card_valid_to_date_s.year); + //trip_from + if(data_block.start_trip_date) { // TODO: (-nofl) unused + DateTime card_start_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.start_trip_date * 24 * 60 + data_block.start_trip_time, + &card_start_trip_minutes_s, + 1992); + } + //trip_switch + if(data_block.passage_5_minutes) { // TODO: (-nofl) unused + DateTime card_start_switch_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.start_trip_date * 24 * 60 + data_block.start_trip_time + + data_block.passage_5_minutes, + &card_start_switch_trip_minutes_s, + 1992); + } + //validator + if(data_block.validator) { + furi_string_cat_printf(result, "\nValidator: %05d", data_block.validator); + } + break; + } + case 0xE1: + case 0x1C1: { + parse_layout_E1(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 1992); + furi_string_cat_printf( + result, + "Use before: %02d.%02d.%04d\n", + card_use_before_date_s.day, + card_use_before_date_s.month, + card_use_before_date_s.year); + //remaining_funds + furi_string_cat_printf(result, "Balance: %ld rub\n", data_block.remaining_funds / 100); + //trip_from + if(data_block.start_trip_date) { + DateTime card_start_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.start_trip_date * 24 * 60 + data_block.start_trip_time, + &card_start_trip_minutes_s, + 1992); + furi_string_cat_printf( + result, + "Trip from: %02d.%02d.%04d %02d:%02d\n", + card_start_trip_minutes_s.day, + card_start_trip_minutes_s.month, + card_start_trip_minutes_s.year, + card_start_trip_minutes_s.hour, + card_start_trip_minutes_s.minute); + } + //transport + FuriString* transport = furi_string_alloc(); + switch(data_block.transport_type1) { + case 1: + switch(data_block.transport_type2) { + case 1: + furi_string_cat(transport, "Metro"); + break; + case 2: + furi_string_cat(transport, "Monorail"); + break; + default: + furi_string_cat(transport, "Unknown"); + break; + } + break; + case 2: + furi_string_cat(transport, "Ground"); + break; + case 3: + furi_string_cat(transport, "MCC"); + break; + default: + furi_string_cat(transport, ""); + break; + } + furi_string_cat_printf(result, "Transport: %s", furi_string_get_cstr(transport)); + //validator + if(data_block.validator) { + furi_string_cat_printf(result, "\nValidator: %05d", data_block.validator); + } + furi_string_free(transport); + break; + } + case 0xE2: + case 0x1C2: { + parse_layout_E2(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 1992); + furi_string_cat_printf( + result, + "Use before: %02d.%02d.%04d\n", + card_use_before_date_s.day, + card_use_before_date_s.month, + card_use_before_date_s.year); + //remaining_trips + furi_string_cat_printf(result, "Trips left: %d\n", data_block.remaining_trips); + //valid_from_date + DateTime card_valid_from_date_s = {0}; + from_days_to_datetime(data_block.valid_from_date, &card_valid_from_date_s, 1992); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d", + card_valid_from_date_s.day, + card_valid_from_date_s.month, + card_valid_from_date_s.year); + //valid_to_date + if(data_block.activate_during) { + DateTime card_valid_to_date_s = {0}; + from_days_to_datetime( + data_block.valid_from_date + data_block.activate_during, + &card_valid_to_date_s, + 1992); + furi_string_cat_printf( + result, + "\nValid to: %02d.%02d.%04d", + card_valid_to_date_s.day, + card_valid_to_date_s.month, + card_valid_to_date_s.year); + } else { + DateTime card_valid_to_date_s = {0}; + from_minutes_to_datetime( + data_block.valid_from_date * 24 * 60 + data_block.valid_for_minutes - 1, + &card_valid_to_date_s, + 1992); + furi_string_cat_printf( + result, + "\nValid to: %02d.%02d.%04d", + card_valid_to_date_s.day, + card_valid_to_date_s.month, + card_valid_to_date_s.year); + } + //trip_from + if(data_block.start_trip_neg_minutes) { + DateTime card_start_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.valid_to_date * 24 * 60 + data_block.valid_for_minutes - + data_block.start_trip_neg_minutes, + &card_start_trip_minutes_s, + 1992); //-time + furi_string_cat_printf( + result, + "\nTrip from: %02d.%02d.%04d %02d:%02d", + card_start_trip_minutes_s.day, + card_start_trip_minutes_s.month, + card_start_trip_minutes_s.year, + card_start_trip_minutes_s.hour, + card_start_trip_minutes_s.minute); + } + //trip_switch + if(data_block.minutes_pass) { + DateTime card_start_switch_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.valid_from_date * 24 * 60 + data_block.valid_for_minutes - + data_block.start_trip_neg_minutes + data_block.minutes_pass, + &card_start_switch_trip_minutes_s, + 1992); + furi_string_cat_printf( + result, + "\nTrip switch: %02d.%02d.%04d %02d:%02d", + card_start_switch_trip_minutes_s.day, + card_start_switch_trip_minutes_s.month, + card_start_switch_trip_minutes_s.year, + card_start_switch_trip_minutes_s.hour, + card_start_switch_trip_minutes_s.minute); + } + //transport + FuriString* transport = furi_string_alloc(); + switch(data_block.transport_type) { // TODO: (-nofl) unused + case 1: + furi_string_cat(transport, "Metro"); + break; + case 2: + furi_string_cat(transport, "Monorail"); + break; + case 3: + furi_string_cat(transport, "Ground"); + break; + default: + furi_string_cat(transport, "Unknown"); + break; + } + //validator + if(data_block.validator) { + furi_string_cat_printf(result, "\nValidator: %05d", data_block.validator); + } + furi_string_free(transport); + break; + } + case 0xE3: + case 0x1C3: { + parse_layout_E3(&data_block, block); + // number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + // use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 1992); + furi_string_cat_printf( + result, + "Use before: %02d.%02d.%04d\n", + card_use_before_date_s.day, + card_use_before_date_s.month, + card_use_before_date_s.year); + // remaining_funds + furi_string_cat_printf(result, "Balance: %lu rub\n", data_block.remaining_funds); + // start_trip_minutes + DateTime card_start_trip_minutes_s = {0}; + from_minutes_to_datetime(data_block.start_trip_minutes, &card_start_trip_minutes_s, 2016); + furi_string_cat_printf( + result, + "Trip from: %02d.%02d.%04d %02d:%02d\n", + card_start_trip_minutes_s.day, + card_start_trip_minutes_s.month, + card_start_trip_minutes_s.year, + card_start_trip_minutes_s.hour, + card_start_trip_minutes_s.minute); + // transport + FuriString* transport = furi_string_alloc(); + parse_transport_type(&data_block, transport); + furi_string_cat_printf(result, "Transport: %s\n", furi_string_get_cstr(transport)); + // validator + furi_string_cat_printf(result, "Validator: %05d\n", data_block.validator); + // fare + FuriString* fare = furi_string_alloc(); + switch(data_block.fare_trip) { + case 0: + furi_string_cat(fare, ""); + break; + case 1: + furi_string_cat(fare, "Single"); + break; + case 2: + furi_string_cat(fare, "90 minutes"); + break; + default: + furi_string_cat(fare, "Unknown"); + break; + } + furi_string_cat_printf(result, "Fare: %s", furi_string_get_cstr(fare)); + furi_string_free(fare); + furi_string_free(transport); + break; + } + case 0xE4: + case 0x1C4: { + parse_layout_E4(&data_block, block); + + // number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + // use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 2016); + furi_string_cat_printf( + result, + "Use before: %02d.%02d.%04d\n", + card_use_before_date_s.day, + card_use_before_date_s.month, + card_use_before_date_s.year); + // remaining_funds + furi_string_cat_printf(result, "Balance: %lu rub\n", data_block.remaining_funds); + // valid_from_date + DateTime card_use_from_date_s = {0}; + from_days_to_datetime(data_block.valid_from_date, &card_use_from_date_s, 2016); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d\n", + card_use_from_date_s.day, + card_use_from_date_s.month, + card_use_from_date_s.year); + // valid_to_date + DateTime card_use_to_date_s = {0}; + if(data_block.requires_activation) { + from_days_to_datetime( + data_block.valid_from_date + data_block.activate_during, + &card_use_to_date_s, + 2016); + } else { + from_minutes_to_datetime( + data_block.valid_from_date * 24 * 60 + data_block.valid_for_minutes - 1, + &card_use_to_date_s, + 2016); + } + + furi_string_cat_printf( + result, + "Valid to: %02d.%02d.%04d\n", + card_use_to_date_s.day, + card_use_to_date_s.month, + card_use_to_date_s.year); + // trip_number + // furi_string_cat_printf(result, "Trips left: %d", data_block.remaining_trips); + // trip_from + DateTime card_start_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.valid_from_date * 24 * 60 + data_block.valid_for_minutes - + data_block.start_trip_neg_minutes, + &card_start_trip_minutes_s, + 2016); // TODO: (-nofl) unused + //transport + FuriString* transport = furi_string_alloc(); + parse_transport_type(&data_block, transport); + furi_string_cat_printf(result, "Transport: %s", furi_string_get_cstr(transport)); + // validator + if(data_block.validator) { + furi_string_cat_printf(result, "\nValidator: %05d", data_block.validator); + } + furi_string_free(transport); + break; + } + case 0xE5: + case 0x1C5: { + parse_layout_E5(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 2019); + furi_string_cat_printf( + result, + "Use before: %02d.%02d.%04d\n", + card_use_before_date_s.day, + card_use_before_date_s.month, + card_use_before_date_s.year); + //remaining_funds + furi_string_cat_printf(result, "Balance: %ld rub", data_block.remaining_funds / 100); + //start_trip_minutes + if(data_block.start_trip_minutes) { + DateTime card_start_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.start_trip_minutes, &card_start_trip_minutes_s, 2019); + furi_string_cat_printf( + result, + "\nTrip from: %02d.%02d.%04d %02d:%02d", + card_start_trip_minutes_s.day, + card_start_trip_minutes_s.month, + card_start_trip_minutes_s.year, + card_start_trip_minutes_s.hour, + card_start_trip_minutes_s.minute); + } + //start_m_trip_minutes + if(data_block.metro_ride_with) { + DateTime card_start_m_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.start_trip_minutes + data_block.metro_ride_with, + &card_start_m_trip_minutes_s, + 2019); + furi_string_cat_printf( + result, + "\n(M) from: %02d.%02d.%04d %02d:%02d", + card_start_m_trip_minutes_s.day, + card_start_m_trip_minutes_s.month, + card_start_m_trip_minutes_s.year, + card_start_m_trip_minutes_s.hour, + card_start_m_trip_minutes_s.minute); + } + if(data_block.minutes_pass) { + DateTime card_start_change_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.start_trip_minutes + data_block.minutes_pass, + &card_start_change_trip_minutes_s, + 2019); + furi_string_cat_printf( + result, + "\nTrip edit: %02d.%02d.%04d %02d:%02d", + card_start_change_trip_minutes_s.day, + card_start_change_trip_minutes_s.month, + card_start_change_trip_minutes_s.year, + card_start_change_trip_minutes_s.hour, + card_start_change_trip_minutes_s.minute); + } + //transport + //validator + if(data_block.validator) { + furi_string_cat_printf(result, "\nValidator: %05d", data_block.validator); + } + break; + } + case 0xE6: + case 0x1C6: { + parse_layout_E6(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //use_before_date + DateTime card_use_before_date_s = {0}; + from_days_to_datetime(data_block.use_before_date, &card_use_before_date_s, 2019); + furi_string_cat_printf( + result, + "Use before: %02d.%02d.%04d\n", + card_use_before_date_s.day, + card_use_before_date_s.month, + card_use_before_date_s.year); + //remaining_trips + furi_string_cat_printf(result, "Trips left: %d\n", data_block.remaining_trips); + //valid_from_date + DateTime card_use_from_date_s = {0}; + from_minutes_to_datetime(data_block.valid_from_date, &card_use_from_date_s, 2019); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d\n", + card_use_from_date_s.day, + card_use_from_date_s.month, + card_use_from_date_s.year); + //valid_to_date + DateTime card_use_to_date_s = {0}; + from_minutes_to_datetime( + data_block.valid_from_date + data_block.valid_for_minutes - 1, + &card_use_to_date_s, + 2019); + furi_string_cat_printf( + result, + "Valid to: %02d.%02d.%04d", + card_use_to_date_s.day, + card_use_to_date_s.month, + card_use_to_date_s.year); + //start_trip_minutes + if(data_block.start_trip_neg_minutes) { + DateTime card_start_trip_minutes_s = {0}; + from_minutes_to_datetime( + data_block.valid_from_date + data_block.valid_for_minutes - + data_block.start_trip_neg_minutes, + &card_start_trip_minutes_s, + 2019); //-time + furi_string_cat_printf( + result, + "\nTrip from: %02d.%02d.%04d %02d:%02d", + card_start_trip_minutes_s.day, + card_start_trip_minutes_s.month, + card_start_trip_minutes_s.year, + card_start_trip_minutes_s.hour, + card_start_trip_minutes_s.minute); + } + //start_trip_m_minutes + if(data_block.metro_ride_with) { + DateTime card_start_trip_m_minutes_s = {0}; + from_minutes_to_datetime( + data_block.valid_from_date + data_block.valid_for_minutes - + data_block.start_trip_neg_minutes + data_block.metro_ride_with, + &card_start_trip_m_minutes_s, + 2019); + furi_string_cat_printf( + result, + "\n(M) from: %02d.%02d.%04d %02d:%02d", + card_start_trip_m_minutes_s.day, + card_start_trip_m_minutes_s.month, + card_start_trip_m_minutes_s.year, + card_start_trip_m_minutes_s.hour, + card_start_trip_m_minutes_s.minute); + } + //transport + //validator + if(data_block.validator) { + furi_string_cat_printf(result, "\nValidator: %05d", data_block.validator); + } + break; + } + case 0x3CCB: { + parse_layout_FCB(&data_block, block); + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //valid_from_date + DateTime card_use_from_date_s = {0}; + from_days_to_datetime(data_block.valid_from_date, &card_use_from_date_s, 1992); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d\n", + card_use_from_date_s.day, + card_use_from_date_s.month, + card_use_from_date_s.year); + //valid_to_date + DateTime card_use_to_date_s = {0}; + from_days_to_datetime(data_block.valid_to_date, &card_use_to_date_s, 1992); + furi_string_cat_printf( + result, + "Valid to: %02d.%02d.%04d", + card_use_to_date_s.day, + card_use_to_date_s.month, + card_use_to_date_s.year); + break; + } + case 0x3C0B: { + //number + furi_string_cat_printf(result, "Number: %010lu\n", data_block.number); + //valid_from_date + DateTime card_use_from_date_s = {0}; + from_days_to_datetime(data_block.valid_from_date, &card_use_from_date_s, 1992); + furi_string_cat_printf( + result, + "Valid from: %02d.%02d.%04d\n", + card_use_from_date_s.day, + card_use_from_date_s.month, + card_use_from_date_s.year); + //valid_to_date + DateTime card_use_to_date_s = {0}; + from_days_to_datetime(data_block.valid_to_date, &card_use_to_date_s, 1992); + furi_string_cat_printf( + result, + "Valid to: %02d.%02d.%04d", + card_use_to_date_s.day, + card_use_to_date_s.month, + card_use_to_date_s.year); + break; + } + default: + result = NULL; + return false; + } + + return true; +} diff --git a/metroflip/api/mosgortrans/mosgortrans_util.h b/metroflip/api/mosgortrans/mosgortrans_util.h new file mode 100644 index 000000000..e8cbd7a37 --- /dev/null +++ b/metroflip/api/mosgortrans/mosgortrans_util.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void render_section_header( + FuriString* str, + const char* name, + uint8_t prefix_separator_cnt, + uint8_t suffix_separator_cnt); +bool mosgortrans_parse_transport_block(const MfClassicBlock* block, FuriString* result); + +#ifdef __cplusplus +} +#endif diff --git a/metroflip/app/README.md b/metroflip/app/README.md index 2bdb60b48..534e4576d 100644 --- a/metroflip/app/README.md +++ b/metroflip/app/README.md @@ -10,23 +10,28 @@ This is a list of metro cards and transit systems that are supported. ## Supported Cards - **Rav-Kav** - - Status: Partially supported - **Navigo** - - Status: Fully supported - **Charliecard** - - Status: Fully supported - **Metromoney** - - Status: Fully supported - **Bip!** - - Status: Fully supported +- **Clipper** +- **Troika** +- **Myki** +- **Opal** +- **ITSO** -More coming soon! +More coming soon! -## Credits -- App Author: luu176 -- Charliecard Parser: zacharyweiss -- Rav-Kav Parser: luu176 -- Navigo Parser: luu176 -- Metromoney Parser: Leptopt1los -- Bip! Parser: rbasoalto, gornekich -- Info Slave: equipter +## Credits: +- **App Author**: luu176 +- **Charliecard Parser**: zacharyweiss +- **Rav-Kav Parser**: luu176 +- **Navigo Parser**: luu176, DocSystem +- **Metromoney Parser**: Leptopt1los +- **Bip! Parser**: rbasoaltor & gornekich +- **Clipper Parser**: ke6jjj +- **Troika Parser**: gornekich +- **Myki Parser**: gornekich +- **Opal parser**: gornekich +- **ITSO parser**: gsp8181, hedger, gornekich +- **Info Slaves**: Equip, TheDingo8MyBaby diff --git a/metroflip/application.fam b/metroflip/application.fam index 11b83b9d8..e0337db89 100644 --- a/metroflip/application.fam +++ b/metroflip/application.fam @@ -5,7 +5,7 @@ App( entry_point="metroflip", stack_size=2 * 1024, fap_category="NFC", - fap_version="0.2", + fap_version="0.4", fap_icon="icon.png", fap_description="An implementation of metrodroid on the flipper", fap_author="luu176", diff --git a/metroflip/manifest.yml b/metroflip/manifest.yml index a5dcbbe18..ffa1baa03 100644 --- a/metroflip/manifest.yml +++ b/metroflip/manifest.yml @@ -8,13 +8,15 @@ name: 'Metroflip' screenshots: - 'screenshots/Menu-Top.png' - 'screenshots/Menu-Middle.png' + - 'screenshots/Navigo.png' + - 'screenshots/Navigo2.png' - 'screenshots/Rav-Kav.png' - 'screenshots/App.png' short_description: 'An implementation of Metrodroid on the Flipper Zero' sourcecode: location: - commit_sha: 80b48d2a91698cfc47bc2d01059518eefc5ad4af + commit_sha: a5311068d33a2a49fc2f120940c3a6e4636855af origin: https://github.com/luu176/Metroflip subdir: type: git -version: 0.2 +version: 0.4.0 diff --git a/metroflip/metroflip.c b/metroflip/metroflip.c index f5631e542..45f397681 100644 --- a/metroflip/metroflip.c +++ b/metroflip/metroflip.c @@ -126,6 +126,15 @@ void metroflip_app_blink_stop(Metroflip* metroflip) { notification_message(metroflip->notifications, &metroflip_app_sequence_blink_stop); } +void metroflip_exit_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); + } +} + // Calypso void byte_to_binary(uint8_t byte, char* bits) { diff --git a/metroflip/metroflip_i.h b/metroflip/metroflip_i.h index ef54e440e..2c4060a6c 100644 --- a/metroflip/metroflip_i.h +++ b/metroflip/metroflip_i.h @@ -43,6 +43,8 @@ extern const Icon I_RFIDDolphinReceive_97x61; #include "scenes/metroflip_scene.h" +#include "scenes/navigo_structs.h" + typedef struct { Gui* gui; SceneManager* scene_manager; @@ -71,6 +73,8 @@ typedef struct { char currency[4]; char card_type[32]; + // Navigo specific context + NavigoContext* navigo_context; } Metroflip; enum MetroflipCustomEvent { @@ -122,6 +126,8 @@ void metroflip_app_blink_stop(Metroflip* metroflip); if(!(locked)) submenu_add_item(submenu, label, index, callback, callback_context) #endif +void metroflip_exit_widget_callback(GuiButtonType result, InputType type, void* context); + ///////////////////////////////// Calypso ///////////////////////////////// #define Metroflip_POLLER_MAX_BUFFER_SIZE 1024 diff --git a/metroflip/scenes/metroflip_scene_about.c b/metroflip/scenes/metroflip_scene_about.c index c655a0e4c..0759cc7ec 100644 --- a/metroflip/scenes/metroflip_scene_about.c +++ b/metroflip/scenes/metroflip_scene_about.c @@ -3,15 +3,6 @@ #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; @@ -29,7 +20,7 @@ void metroflip_scene_about_on_enter(void* context) { 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); + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); furi_string_free(str); view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); diff --git a/metroflip/scenes/metroflip_scene_bip.c b/metroflip/scenes/metroflip_scene_bip.c index 7d19ee028..a64d647fd 100644 --- a/metroflip/scenes/metroflip_scene_bip.c +++ b/metroflip/scenes/metroflip_scene_bip.c @@ -272,15 +272,6 @@ static bool 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); @@ -332,7 +323,7 @@ static NfcCommand metroflip_scene_bip_poller_callback(NfcGenericEvent event, voi 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); + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); furi_string_free(parsed_data); view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); diff --git a/metroflip/scenes/metroflip_scene_charliecard.c b/metroflip/scenes/metroflip_scene_charliecard.c index 7ca7719a7..130abe6eb 100644 --- a/metroflip/scenes/metroflip_scene_charliecard.c +++ b/metroflip/scenes/metroflip_scene_charliecard.c @@ -1127,10 +1127,8 @@ static bool charliecard_parse(FuriString* parsed_data, const MfClassicData* data const uint64_t key_a = bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data)); - const uint64_t key_b = - bit_lib_bytes_to_num_be(sec_tr->key_b.data, COUNT_OF(sec_tr->key_b.data)); + if(key_a != charliecard_1k_keys[verify_sector].a) break; - if(key_b != charliecard_1k_keys[verify_sector].b) break; // parse card data const uint32_t card_number = mfg_sector_parse(data); @@ -1183,15 +1181,6 @@ static bool charliecard_parse(FuriString* parsed_data, const MfClassicData* data 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); @@ -1243,7 +1232,7 @@ static NfcCommand 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); + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); furi_string_free(parsed_data); view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); @@ -1251,7 +1240,7 @@ static NfcCommand metroflip_app_blink_stop(app); } else if(mfc_event->type == MfClassicPollerEventTypeFail) { FURI_LOG_I(TAG, "fail"); - command = NfcCommandStop; + command = NfcCommandContinue; } return command; diff --git a/metroflip/scenes/metroflip_scene_clipper.c b/metroflip/scenes/metroflip_scene_clipper.c new file mode 100644 index 000000000..21e78fa45 --- /dev/null +++ b/metroflip/scenes/metroflip_scene_clipper.c @@ -0,0 +1,659 @@ +/* + * clipper.c - Parser for Clipper cards (San Francisco, California). + * + * Based on research, some of which dates to 2007! + * + * Copyright 2024 Jeremy Cooper + * + * 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 + +#define TAG "Metroflip:Scene:Clipper" + +// +// Table of application ids observed in the wild, and their sources. +// +static const struct { + const MfDesfireApplicationId app; + const char* type; +} clipper_types[] = { + // Application advertised on classic, plastic cards. + {.app = {.data = {0x90, 0x11, 0xf2}}, .type = "Card"}, + // Application advertised on a mobile device. + {.app = {.data = {0x91, 0x11, 0xf2}}, .type = "Mobile Device"}, +}; +static const size_t kNumCardTypes = sizeof(clipper_types) / sizeof(clipper_types[0]); + +struct IdMapping_struct { + uint16_t id; + const char* name; +}; +typedef struct IdMapping_struct IdMapping; + +#define COUNT(_array) sizeof(_array) / sizeof(_array[0]) + +// +// Known transportation agencies and their identifiers. +// +static const IdMapping agency_names[] = { + {.id = 0x0001, .name = "AC Transit"}, + {.id = 0x0004, .name = "BART"}, + {.id = 0x0006, .name = "Caltrain"}, + {.id = 0x0008, .name = "CCTA"}, + {.id = 0x000b, .name = "GGT"}, + {.id = 0x000f, .name = "SamTrans"}, + {.id = 0x0011, .name = "VTA"}, + {.id = 0x0012, .name = "Muni"}, + {.id = 0x0019, .name = "GG Ferry"}, + {.id = 0x001b, .name = "SF Bay Ferry"}, +}; +static const size_t kNumAgencies = COUNT(agency_names); + +// +// Known station names for various agencies. +// +static const IdMapping bart_zones[] = { + {.id = 0x0001, .name = "Colma"}, + {.id = 0x0002, .name = "Daly City"}, + {.id = 0x0003, .name = "Balboa Park"}, + {.id = 0x0004, .name = "Glen Park"}, + {.id = 0x0005, .name = "24th St Mission"}, + {.id = 0x0006, .name = "16th St Mission"}, + {.id = 0x0007, .name = "Civic Center/UN Plaza"}, + {.id = 0x0008, .name = "Powell St"}, + {.id = 0x0009, .name = "Montgomery St"}, + {.id = 0x000a, .name = "Embarcadero"}, + {.id = 0x000b, .name = "West Oakland"}, + {.id = 0x000c, .name = "12th St/Oakland City Center"}, + {.id = 0x000d, .name = "19th St/Oakland"}, + {.id = 0x000e, .name = "MacArthur"}, + {.id = 0x000f, .name = "Rockridge"}, + {.id = 0x0010, .name = "Orinda"}, + {.id = 0x0011, .name = "Lafayette"}, + {.id = 0x0012, .name = "Walnut Creek"}, + {.id = 0x0013, .name = "Pleasant Hill/Contra Costa Centre"}, + {.id = 0x0014, .name = "Concord"}, + {.id = 0x0015, .name = "North Concord/Martinez"}, + {.id = 0x0016, .name = "Pittsburg/Bay Point"}, + {.id = 0x0017, .name = "Ashby"}, + {.id = 0x0018, .name = "Downtown Berkeley"}, + {.id = 0x0019, .name = "North Berkeley"}, + {.id = 0x001a, .name = "El Cerrito Plaza"}, + {.id = 0x001b, .name = "El Cerrito Del Norte"}, + {.id = 0x001c, .name = "Richmond"}, + {.id = 0x001d, .name = "Lake Merrit"}, + {.id = 0x001e, .name = "Fruitvale"}, + {.id = 0x001f, .name = "Coliseum"}, + {.id = 0x0021, .name = "San Leandro"}, + {.id = 0x0022, .name = "Hayward"}, + {.id = 0x0023, .name = "South Hayward"}, + {.id = 0x0024, .name = "Union City"}, + {.id = 0x0025, .name = "Fremont"}, + {.id = 0x0026, .name = "Castro Valley"}, + {.id = 0x0027, .name = "Dublin/Pleasanton"}, + {.id = 0x0028, .name = "South San Francisco"}, + {.id = 0x0029, .name = "San Bruno"}, + {.id = 0x002a, .name = "SFO Airport"}, + {.id = 0x002b, .name = "Millbrae"}, + {.id = 0x002c, .name = "West Dublin/Pleasanton"}, + {.id = 0x002d, .name = "OAK Airport"}, + {.id = 0x002e, .name = "Warm Springs/South Fremont"}, + {.id = 0x002f, .name = "Milpitas"}, + {.id = 0x0030, .name = "Berryessa/North San Jose"}, +}; +static const size_t kNumBARTZones = COUNT(bart_zones); + +static const IdMapping muni_zones[] = { + {.id = 0x0000, .name = "City Street"}, + {.id = 0x0005, .name = "Embarcadero"}, + {.id = 0x0006, .name = "Montgomery"}, + {.id = 0x0007, .name = "Powell"}, + {.id = 0x0008, .name = "Civic Center"}, + {.id = 0x0009, .name = "Van Ness"}, // Guessed + {.id = 0x000a, .name = "Church"}, + {.id = 0x000b, .name = "Castro"}, + {.id = 0x000c, .name = "Forest Hill"}, // Guessed + {.id = 0x000d, .name = "West Portal"}, +}; +static const size_t kNumMUNIZones = COUNT(muni_zones); + +static const IdMapping actransit_zones[] = { + {.id = 0x0000, .name = "City Street"}, +}; +static const size_t kNumACTransitZones = COUNT(actransit_zones); + +// Instead of persisting individual Station IDs, Caltrain saves Zone numbers. +// https://www.caltrain.com/stations-zones +static const IdMapping caltrain_zones[] = { + {.id = 0x0001, .name = "Zone 1"}, + {.id = 0x0002, .name = "Zone 2"}, + {.id = 0x0003, .name = "Zone 3"}, + {.id = 0x0004, .name = "Zone 4"}, + {.id = 0x0005, .name = "Zone 5"}, + {.id = 0x0006, .name = "Zone 6"}, +}; + +static const size_t kNumCaltrainZones = COUNT(caltrain_zones); + +// +// Full agency+zone mapping. +// +static const struct { + uint16_t agency_id; + const IdMapping* zone_map; + size_t zone_count; +} agency_zone_map[] = { + {.agency_id = 0x0001, .zone_map = actransit_zones, .zone_count = kNumACTransitZones}, + {.agency_id = 0x0004, .zone_map = bart_zones, .zone_count = kNumBARTZones}, + {.agency_id = 0x0006, .zone_map = caltrain_zones, .zone_count = kNumCaltrainZones}, + {.agency_id = 0x0012, .zone_map = muni_zones, .zone_count = kNumMUNIZones}}; +static const size_t kNumAgencyZoneMaps = COUNT(agency_zone_map); + +// File ids of important files on the card. +static const MfDesfireFileId clipper_ecash_file_id = 2; +static const MfDesfireFileId clipper_histidx_file_id = 6; +static const MfDesfireFileId clipper_identity_file_id = 8; +static const MfDesfireFileId clipper_history_file_id = 14; + +struct ClipperCardInfo_struct { + uint32_t serial_number; + uint16_t counter; + uint16_t last_txn_id; + uint32_t last_updated_tm_1900; + uint16_t last_terminal_id; + int16_t balance_cents; +}; +typedef struct ClipperCardInfo_struct ClipperCardInfo; + +// Forward declarations for helper functions. +static void furi_string_cat_timestamp( + FuriString* str, + const char* date_hdr, + const char* time_hdr, + uint32_t tmst_1900); +static bool get_file_contents( + const MfDesfireApplication* app, + const MfDesfireFileId* id, + MfDesfireFileType type, + size_t min_size, + const uint8_t** out); +static bool decode_id_file(const uint8_t* ef8_data, ClipperCardInfo* info); +static bool decode_cash_file(const uint8_t* ef2_data, ClipperCardInfo* info); +static bool get_map_item(uint16_t id, const IdMapping* map, size_t sz, const char** out); +static bool get_agency_zone_name(uint16_t agency_id, uint16_t zone_id, const char** out); +static void + decode_usd(int16_t amount_cents, bool* out_is_negative, int16_t* out_usd, uint16_t* out_cents); +static bool dump_ride_history( + const uint8_t* index_file, + const uint8_t* history_file, + size_t len, + FuriString* parsed_data); +static bool dump_ride_event(const uint8_t* record, FuriString* parsed_data); + +// Unmarshal a 32-bit integer, big endian, unsigned +static inline uint32_t get_u32be(const uint8_t* field) { + return bit_lib_bytes_to_num_be(field, 4); +} + +// Unmarshal a 16-bit integer, big endian, unsigned +static uint16_t get_u16be(const uint8_t* field) { + return bit_lib_bytes_to_num_be(field, 2); +} + +// Unmarshal a 16-bit integer, big endian, signed, two's-complement +static int16_t get_i16be(const uint8_t* field) { + uint16_t raw = get_u16be(field); + if(raw > 0x7fff) + return -((uint32_t)0x10000 - raw); + else + return raw; +} + +static bool clipper_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); + + bool parsed = false; + + do { + const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire); + + const MfDesfireApplication* app = NULL; + const char* device_description = NULL; + + for(size_t i = 0; i < kNumCardTypes; i++) { + app = mf_desfire_get_application(data, &clipper_types[i].app); + device_description = clipper_types[i].type; + if(app != NULL) break; + } + // If no matching application was found, abort this parser. + if(app == NULL) break; + ClipperCardInfo info; + const uint8_t* id_data; + if(!get_file_contents( + app, &clipper_identity_file_id, MfDesfireFileTypeStandard, 5, &id_data)) + break; + if(!decode_id_file(id_data, &info)) break; + const uint8_t* cash_data; + if(!get_file_contents(app, &clipper_ecash_file_id, MfDesfireFileTypeBackup, 32, &cash_data)) + break; + if(!decode_cash_file(cash_data, &info)) break; + int16_t balance_usd; + uint16_t balance_cents; + bool _balance_is_negative; + decode_usd(info.balance_cents, &_balance_is_negative, &balance_usd, &balance_cents); + furi_string_cat_printf( + parsed_data, + "\e#Clipper\n" + "Serial: %" PRIu32 "\n" + "Balance: $%d.%02u\n" + "Type: %s\n" + "\e#Last Update\n", + info.serial_number, + balance_usd, + balance_cents, + device_description); + if(info.last_updated_tm_1900 != 0) + furi_string_cat_timestamp( + parsed_data, "Date: ", "\nTime: ", info.last_updated_tm_1900); + else + furi_string_cat_str(parsed_data, "Never"); + furi_string_cat_printf( + parsed_data, + "\nTerminal: 0x%04x\n" + "Transaction Id: %u\n" + "Counter: %u\n", + info.last_terminal_id, + info.last_txn_id, + info.counter); + + const uint8_t *history_index, *history; + + if(!get_file_contents( + app, &clipper_histidx_file_id, MfDesfireFileTypeBackup, 16, &history_index)) + break; + if(!get_file_contents( + app, &clipper_history_file_id, MfDesfireFileTypeStandard, 512, &history)) + break; + + if(!dump_ride_history(history_index, history, 512, parsed_data)) break; + + parsed = true; + } while(false); + + return parsed; +} + +static bool get_file_contents( + const MfDesfireApplication* app, + const MfDesfireFileId* id, + MfDesfireFileType type, + size_t min_size, + const uint8_t** out) { + const MfDesfireFileSettings* settings = mf_desfire_get_file_settings(app, id); + if(settings == NULL) return false; + if(settings->type != type) return false; + + const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, id); + + if(file_data == NULL) return false; + + if(simple_array_get_count(file_data->data) < min_size) return false; + + *out = simple_array_cget_data(file_data->data); + + return true; +} + +static bool decode_id_file(const uint8_t* ef8_data, ClipperCardInfo* info) { + // Identity file (8) + // + // Byte view + // + // 0 1 2 3 4 5 6 7 8 + // +----+----.----.----.----+----.----.----+ + // 0x00 | uk | card_id | unknown | + // +----+----.----.----.----+----.----.----+ + // 0x08 | unknown | + // +----.----.----.----.----.----.----.----+ + // 0x10 ... + // + // + // Field Datatype Description + // ----- -------- ----------- + // uk ?8?? Unknown, 8-bit byte + // card_id U32BE Card identifier + // + info->serial_number = bit_lib_bytes_to_num_be(&ef8_data[1], 4); + return true; +} + +static bool decode_cash_file(const uint8_t* ef2_data, ClipperCardInfo* info) { + // ECash file (2) + // + // Byte view + // + // 0 1 2 3 4 5 6 7 8 + // +----.----+----.----+----.----.----.----+ + // 0x00 | unk00 | counter | timestamp_1900 | + // +----.----+----.----+----.----.----.----+ + // 0x08 | term_id | unk01 | + // +----.----+----.----+----.----.----.----+ + // 0x10 | txn_id | balance | unknown | + // +----.----+----.----+----.----.----.----+ + // 0x18 | unknown | + // +---------------------------------------+ + // + // Field Datatype Description + // ----- -------- ----------- + // unk00 U8[2] Unknown bytes + // counter U16BE Unknown, appears to be a counter + // timestamp_1900 U32BE Timestamp of last transaction, in seconds + // since 1900-01-01 GMT. + // unk01 U8[6] Unknown bytes + // txn_id U16BE Id of last transaction. + // balance S16BE Card cash balance, in cents. + // Cards can obtain negative balances in this + // system, so balances are signed integers. + // Maximum card balance is therefore + // $327.67. + // unk02 U8[12] Unknown bytes. + // + info->counter = get_u16be(&ef2_data[2]); + info->last_updated_tm_1900 = get_u32be(&ef2_data[4]); + info->last_terminal_id = get_u16be(&ef2_data[8]); + info->last_txn_id = get_u16be(&ef2_data[0x10]); + info->balance_cents = get_i16be(&ef2_data[0x12]); + return true; +} + +static bool dump_ride_history( + const uint8_t* index_file, + const uint8_t* history_file, + size_t len, + FuriString* parsed_data) { + static const size_t kRideRecordSize = 0x20; + + for(size_t i = 0; i < 16; i++) { + uint8_t record_num = index_file[i]; + if(record_num == 0xff) break; + + size_t record_offset = record_num * kRideRecordSize; + + if(record_offset + kRideRecordSize > len) break; + + const uint8_t* record = &history_file[record_offset]; + if(!dump_ride_event(record, parsed_data)) break; + } + + return true; +} + +static bool dump_ride_event(const uint8_t* record, FuriString* parsed_data) { + // Ride record + // + // 0 1 2 3 4 5 6 7 8 + // +----+----+----.----+----.----+----.----+ + // 0x00 |0x10| ? | agency | ? | fare | + // +----.----+----.----+----.----.----.----+ + // 0x08 | ? | vehicle | time_on | + // +----.----.----.----+----.----+----.----+ + // 0x10 | time_off | zone_on | zone_off| + // +----+----.----.----.----+----+----+----+ + // 0x18 | ? | ? | ? | ? | ? | + // +----+----.----.----.----+----+----+----+ + // + // Field Datatype Description + // ----- -------- ----------- + // agency U16BE Transportation agency identifier. + // Known ids: + // 1 == AC Transit + // 4 == BART + // 18 == SF MUNI + // fare I16BE Fare deducted, in cents. + // vehicle U16BE Vehicle id (0 == not provided) + // time_on U32BE Boarding time, in seconds since 1900-01-01 GMT. + // time_off U32BE Off-boarding time, if present, in seconds + // since 1900-01-01 GMT. Set to zero if no offboard + // has been recorded. + // zone_on U16BE Id of boarding zone or station. Agency-specific. + // zone_off U16BE Id of offboarding zone or station. Agency- + // specific. + if(record[0] != 0x10) return false; + + uint16_t agency_id = get_u16be(&record[2]); + if(agency_id == 0) + // Likely empty record. Skip. + return false; + const char* agency_name; + bool ok = get_map_item(agency_id, agency_names, kNumAgencies, &agency_name); + if(!ok) agency_name = "Unknown"; + + uint16_t vehicle_id = get_u16be(&record[0x0a]); + + int16_t fare_raw_cents = get_i16be(&record[6]); + bool _fare_is_negative; + int16_t fare_usd; + uint16_t fare_cents; + decode_usd(fare_raw_cents, &_fare_is_negative, &fare_usd, &fare_cents); + + uint32_t time_on_raw = get_u32be(&record[0x0c]); + uint32_t time_off_raw = get_u32be(&record[0x10]); + uint16_t zone_id_on = get_u16be(&record[0x14]); + uint16_t zone_id_off = get_u16be(&record[0x16]); + + const char *zone_on, *zone_off; + if(!get_agency_zone_name(agency_id, zone_id_on, &zone_on)) { + zone_on = "Unknown"; + } + if(!get_agency_zone_name(agency_id, zone_id_off, &zone_off)) { + zone_off = "Unknown"; + } + + furi_string_cat_str(parsed_data, "\e#Ride Record\n"); + furi_string_cat_timestamp(parsed_data, "Date: ", "\nTime: ", time_on_raw); + furi_string_cat_printf( + parsed_data, + "\n" + "Fare: $%d.%02u\n" + "Agency: %s (%04x)\n" + "On: %s (%04x)\n", + fare_usd, + fare_cents, + agency_name, + agency_id, + zone_on, + zone_id_on); + if(vehicle_id != 0) { + furi_string_cat_printf(parsed_data, "Vehicle id: %d\n", vehicle_id); + } + if(time_off_raw != 0) { + furi_string_cat_printf(parsed_data, "Off: %s (%04x)\n", zone_off, zone_id_off); + furi_string_cat_timestamp(parsed_data, "Date Off: ", "\nTime Off: ", time_off_raw); + furi_string_cat_str(parsed_data, "\n"); + } + + return true; +} + +static bool get_map_item(uint16_t id, const IdMapping* map, size_t sz, const char** out) { + for(size_t i = 0; i < sz; i++) { + if(map[i].id == id) { + *out = map[i].name; + return true; + } + } + + return false; +} + +static bool get_agency_zone_name(uint16_t agency_id, uint16_t zone_id, const char** out) { + for(size_t i = 0; i < kNumAgencyZoneMaps; i++) { + if(agency_zone_map[i].agency_id == agency_id) { + return get_map_item( + zone_id, agency_zone_map[i].zone_map, agency_zone_map[i].zone_count, out); + } + } + + return false; +} + +// Split a balance/fare amount from raw cents to dollars and cents portion, +// automatically adjusting the cents portion so that it is always positive, +// for easier display. +static void + decode_usd(int16_t amount_cents, bool* out_is_negative, int16_t* out_usd, uint16_t* out_cents) { + *out_usd = amount_cents / 100; + + if(amount_cents >= 0) { + *out_is_negative = false; + *out_cents = amount_cents % 100; + } else { + *out_is_negative = true; + *out_cents = (amount_cents * -1) % 100; + } +} + +// Decode a raw 1900-based timestamp and append a human-readable form to a +// FuriString. +static void furi_string_cat_timestamp( + FuriString* str, + const char* date_hdr, + const char* time_hdr, + uint32_t tmst_1900) { + DateTime tm; + + datetime_timestamp_to_datetime(tmst_1900, &tm); + + FuriString* date_str = furi_string_alloc(); + locale_format_date(date_str, &tm, locale_get_date_format(), "-"); + + FuriString* time_str = furi_string_alloc(); + locale_format_time(time_str, &tm, locale_get_time_format(), true); + + furi_string_cat_printf( + str, + "%s%s%s%s (UTC)", + date_hdr, + furi_string_get_cstr(date_str), + time_hdr, + furi_string_get_cstr(time_str)); + + furi_string_free(date_str); + furi_string_free(time_str); +} + +static NfcCommand metroflip_scene_clipper_poller_callback(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolMfDesfire); + + Metroflip* app = context; + NfcCommand command = NfcCommandContinue; + + FuriString* parsed_data = furi_string_alloc(); + Widget* widget = app->widget; + furi_string_reset(app->text_box_store); + const MfDesfirePollerEvent* mf_desfire_event = event.event_data; + if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) { + nfc_device_set_data( + app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller)); + if(!clipper_parse(app->nfc_device, parsed_data)) { + furi_string_reset(app->text_box_store); + FURI_LOG_I(TAG, "Unknown card type"); + furi_string_printf(parsed_data, "\e#Unknown card\n"); + } + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); + + furi_string_free(parsed_data); + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); + metroflip_app_blink_stop(app); + command = NfcCommandStop; + } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) { + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess); + command = NfcCommandContinue; + } + + return command; +} + +void metroflip_scene_clipper_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, NfcProtocolMfDesfire); + nfc_poller_start(app->poller, metroflip_scene_clipper_poller_callback, app); + + metroflip_app_blink_start(app); +} + +bool metroflip_scene_clipper_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_clipper_on_exit(void* context) { + Metroflip* app = context; + widget_reset(app->widget); + metroflip_app_blink_stop(app); + + if(app->poller) { + nfc_poller_stop(app->poller); + nfc_poller_free(app->poller); + } +} diff --git a/metroflip/scenes/metroflip_scene_config.h b/metroflip/scenes/metroflip_scene_config.h index e4148d07c..d8e0b3c81 100644 --- a/metroflip/scenes/metroflip_scene_config.h +++ b/metroflip/scenes/metroflip_scene_config.h @@ -2,8 +2,13 @@ ADD_SCENE(metroflip, start, Start) ADD_SCENE(metroflip, ravkav, RavKav) ADD_SCENE(metroflip, navigo, Navigo) ADD_SCENE(metroflip, charliecard, CharlieCard) +ADD_SCENE(metroflip, clipper, Clipper) ADD_SCENE(metroflip, metromoney, Metromoney) ADD_SCENE(metroflip, read_success, ReadSuccess) ADD_SCENE(metroflip, bip, Bip) +ADD_SCENE(metroflip, myki, Myki) +ADD_SCENE(metroflip, troika, Troika) +ADD_SCENE(metroflip, opal, Opal) +ADD_SCENE(metroflip, itso, Itso) 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 index 41b9f4f95..9e16ef4be 100644 --- a/metroflip/scenes/metroflip_scene_credits.c +++ b/metroflip/scenes/metroflip_scene_credits.c @@ -3,15 +3,6 @@ #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; @@ -26,15 +17,21 @@ void metroflip_scene_credits_on_enter(void* context) { 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, "Navigo Parser: \n luu176, DocSystem \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"); + furi_string_cat_printf(str, "Clipper Parser:\n ke6jjj\n\n"); + furi_string_cat_printf(str, "Troika Parser:\n gornekich\n\n"); + furi_string_cat_printf(str, "ITSO Parser:\n gsp8181, hedger, gornekich\n\n"); + furi_string_cat_printf(str, "Opal Parser:\n gornekich\n\n"); + furi_string_cat_printf(str, "myki Parser:\n gornekich\n\n"); + furi_string_cat_printf(str, "Info Slaves:\n Equip, TheDingo8MyBaby\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); + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); furi_string_free(str); view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); diff --git a/metroflip/scenes/metroflip_scene_itso.c b/metroflip/scenes/metroflip_scene_itso.c new file mode 100644 index 000000000..a84efacab --- /dev/null +++ b/metroflip/scenes/metroflip_scene_itso.c @@ -0,0 +1,205 @@ +/* itso.c - Parser for ITSO cards (United Kingdom). */ +#include "../metroflip_i.h" +#include + +#include +#include +#include + +#include +#include + +#define TAG "Metroflip:Scene:ITSO" + +static const MfDesfireApplicationId itso_app_id = {.data = {0x16, 0x02, 0xa0}}; +static const MfDesfireFileId itso_file_id = 0x0f; + +int64_t swap_int64(int64_t val) { + val = ((val << 8) & 0xFF00FF00FF00FF00ULL) | ((val >> 8) & 0x00FF00FF00FF00FFULL); + val = ((val << 16) & 0xFFFF0000FFFF0000ULL) | ((val >> 16) & 0x0000FFFF0000FFFFULL); + return (val << 32) | ((val >> 32) & 0xFFFFFFFFULL); +} + +uint64_t swap_uint64(uint64_t val) { + val = ((val << 8) & 0xFF00FF00FF00FF00ULL) | ((val >> 8) & 0x00FF00FF00FF00FFULL); + val = ((val << 16) & 0xFFFF0000FFFF0000ULL) | ((val >> 16) & 0x0000FFFF0000FFFFULL); + return (val << 32) | (val >> 32); +} + +static bool itso_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); + + bool parsed = false; + + do { + const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire); + + const MfDesfireApplication* app = mf_desfire_get_application(data, &itso_app_id); + if(app == NULL) break; + + typedef struct { + uint64_t part1; + uint64_t part2; + uint64_t part3; + uint64_t part4; + } ItsoFile; + + const MfDesfireFileSettings* file_settings = + mf_desfire_get_file_settings(app, &itso_file_id); + + if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard || + file_settings->data.size < sizeof(ItsoFile)) + break; + + const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &itso_file_id); + if(file_data == NULL) break; + + const ItsoFile* itso_file = simple_array_cget_data(file_data->data); + + uint64_t x1 = swap_uint64(itso_file->part1); + uint64_t x2 = swap_uint64(itso_file->part2); + + char cardBuff[32]; + char dateBuff[18]; + + snprintf(cardBuff, sizeof(cardBuff), "%llx%llx", x1, x2); + snprintf(dateBuff, sizeof(dateBuff), "%llx", x2); + + char* cardp = cardBuff + 4; + cardp[18] = '\0'; + + // All itso card numbers are prefixed with "633597" + if(strncmp(cardp, "633597", 6) != 0) break; + + char* datep = dateBuff + 12; + dateBuff[17] = '\0'; + + // DateStamp is defined in BS EN 1545 - Days passed since 01/01/1997 + uint32_t dateStamp; + if(strint_to_uint32(datep, NULL, &dateStamp, 16) != StrintParseNoError) { + return false; + } + uint32_t unixTimestamp = dateStamp * 24 * 60 * 60 + 852076800U; + + furi_string_set(parsed_data, "\e#ITSO Card\n"); + + // Digit count in each space-separated group + static const uint8_t digit_count[] = {6, 4, 4, 4}; + + for(uint32_t i = 0, k = 0; i < COUNT_OF(digit_count); k += digit_count[i++]) { + for(uint32_t j = 0; j < digit_count[i]; ++j) { + furi_string_push_back(parsed_data, cardp[j + k]); + } + furi_string_push_back(parsed_data, ' '); + } + + DateTime timestamp = {0}; + datetime_timestamp_to_datetime(unixTimestamp, ×tamp); + + FuriString* timestamp_str = furi_string_alloc(); + locale_format_date(timestamp_str, ×tamp, locale_get_date_format(), "-"); + + furi_string_cat(parsed_data, "\nExpiry: "); + furi_string_cat(parsed_data, timestamp_str); + + furi_string_free(timestamp_str); + + parsed = true; + } while(false); + + return parsed; +} + +static NfcCommand metroflip_scene_itso_poller_callback(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolMfDesfire); + + Metroflip* app = context; + NfcCommand command = NfcCommandContinue; + + FuriString* parsed_data = furi_string_alloc(); + Widget* widget = app->widget; + furi_string_reset(app->text_box_store); + const MfDesfirePollerEvent* mf_desfire_event = event.event_data; + if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) { + nfc_device_set_data( + app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller)); + if(!itso_parse(app->nfc_device, parsed_data)) { + furi_string_reset(app->text_box_store); + FURI_LOG_I(TAG, "Unknown card type"); + furi_string_printf(parsed_data, "\e#Unknown card\n"); + } + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); + + furi_string_free(parsed_data); + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); + metroflip_app_blink_stop(app); + command = NfcCommandStop; + } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) { + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess); + command = NfcCommandContinue; + } + + return command; +} + +void metroflip_scene_itso_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, NfcProtocolMfDesfire); + nfc_poller_start(app->poller, metroflip_scene_itso_poller_callback, app); + + metroflip_app_blink_start(app); +} + +bool metroflip_scene_itso_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_itso_on_exit(void* context) { + Metroflip* app = context; + widget_reset(app->widget); + metroflip_app_blink_stop(app); + if(app->poller) { + nfc_poller_stop(app->poller); + nfc_poller_free(app->poller); + } +} diff --git a/metroflip/scenes/metroflip_scene_myki.c b/metroflip/scenes/metroflip_scene_myki.c new file mode 100644 index 000000000..32325bef8 --- /dev/null +++ b/metroflip/scenes/metroflip_scene_myki.c @@ -0,0 +1,188 @@ +#include + +#include +#include + +#include "../metroflip_i.h" +#include + +#define TAG "Metroflip:Scene:myki" + +static const MfDesfireApplicationId myki_app_id = {.data = {0x00, 0x11, 0xf2}}; +static const MfDesfireFileId myki_file_id = 0x0f; + +static uint8_t myki_calculate_luhn(uint64_t number) { + // https://en.wikipedia.org/wiki/Luhn_algorithm + // Drop existing check digit to form payload + uint64_t payload = number / 10; + int sum = 0; + int position = 0; + + while(payload > 0) { + int digit = payload % 10; + if(position % 2 == 0) { + digit *= 2; + } + if(digit > 9) { + digit = (digit / 10) + (digit % 10); + } + sum += digit; + payload /= 10; + position++; + } + + return (10 - (sum % 10)) % 10; +} + +static bool myki_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); + + bool parsed = false; + + do { + const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire); + + const MfDesfireApplication* app = mf_desfire_get_application(data, &myki_app_id); + if(app == NULL) break; + + typedef struct { + uint32_t top; + uint32_t bottom; + } mykiFile; + + const MfDesfireFileSettings* file_settings = + mf_desfire_get_file_settings(app, &myki_file_id); + + if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard || + file_settings->data.size < sizeof(mykiFile)) + break; + + const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &myki_file_id); + if(file_data == NULL) break; + + const mykiFile* myki_file = simple_array_cget_data(file_data->data); + + // All myki card numbers are prefixed with "308425" + if(myki_file->top != 308425UL) break; + // Card numbers are always 15 digits in length + if(myki_file->bottom < 10000000UL || myki_file->bottom >= 100000000UL) break; + + uint64_t card_number = myki_file->top * 1000000000ULL + myki_file->bottom * 10UL; + // Stored card number doesn't include check digit + card_number += myki_calculate_luhn(card_number); + + furi_string_set(parsed_data, "\e#myki\nNo.: "); + + // Stylise card number according to the physical card + char card_string[20]; + snprintf(card_string, sizeof(card_string), "%llu", card_number); + + // Digit count in each space-separated group + static const uint8_t digit_count[] = {1, 5, 4, 4, 1}; + + for(uint32_t i = 0, k = 0; i < COUNT_OF(digit_count); k += digit_count[i++]) { + for(uint32_t j = 0; j < digit_count[i]; ++j) { + furi_string_push_back(parsed_data, card_string[j + k]); + } + furi_string_push_back(parsed_data, ' '); + } + + parsed = true; + } while(false); + + return parsed; +} + +static NfcCommand metroflip_scene_myki_poller_callback(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolMfDesfire); + + Metroflip* app = context; + NfcCommand command = NfcCommandContinue; + + FuriString* parsed_data = furi_string_alloc(); + Widget* widget = app->widget; + furi_string_reset(app->text_box_store); + const MfDesfirePollerEvent* mf_desfire_event = event.event_data; + if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) { + nfc_device_set_data( + app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller)); + if(!myki_parse(app->nfc_device, parsed_data)) { + furi_string_reset(app->text_box_store); + FURI_LOG_I(TAG, "Unknown card type"); + furi_string_printf(parsed_data, "\e#Unknown card\n"); + } + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); + + furi_string_free(parsed_data); + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); + metroflip_app_blink_stop(app); + command = NfcCommandStop; + } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) { + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess); + command = NfcCommandContinue; + } + + return command; +} + +void metroflip_scene_myki_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, NfcProtocolMfDesfire); + nfc_poller_start(app->poller, metroflip_scene_myki_poller_callback, app); + + metroflip_app_blink_start(app); +} + +bool metroflip_scene_myki_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_myki_on_exit(void* context) { + Metroflip* app = context; + widget_reset(app->widget); + metroflip_app_blink_stop(app); + if(app->poller) { + nfc_poller_stop(app->poller); + nfc_poller_free(app->poller); + } +} diff --git a/metroflip/scenes/metroflip_scene_navigo.c b/metroflip/scenes/metroflip_scene_navigo.c index 841d7a3a4..67c790a68 100644 --- a/metroflip/scenes/metroflip_scene_navigo.c +++ b/metroflip/scenes/metroflip_scene_navigo.c @@ -1,6 +1,7 @@ #include "../metroflip_i.h" #include #include +#include #include #include "navigo.h" @@ -8,15 +9,584 @@ #define TAG "Metroflip:Scene:Navigo" -void metroflip_navigo_widget_callback(GuiButtonType result, InputType type, void* context) { +int select_new_app( + int new_app, + BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + Iso14443_4bPoller* iso14443_4b_poller, + Metroflip* app, + MetroflipPollerEventType* stage) { + select_app[6] = new_app; + + bit_buffer_reset(tx_buffer); + bit_buffer_append_bytes(tx_buffer, select_app, sizeof(select_app)); + int 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); + return error; + } + return 0; +} + +int read_new_file( + int new_file, + BitBuffer* tx_buffer, + BitBuffer* rx_buffer, + Iso14443_4bPoller* iso14443_4b_poller, + Metroflip* app, + MetroflipPollerEventType* stage) { + read_file[2] = new_file; + bit_buffer_reset(tx_buffer); + bit_buffer_append_bytes(tx_buffer, read_file, sizeof(read_file)); + Iso14443_4bError 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); + return error; + } + return 0; +} + +int check_response( + BitBuffer* rx_buffer, + Metroflip* app, + MetroflipPollerEventType* stage, + size_t* response_length) { + *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 profile app/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); + return 1; + } + return 0; +} + +const char* get_country(int country_num) { + switch(country_num) { + case 250: + return "France"; + default: + return "Unknown"; + } +} + +const char* get_network(int network_num) { + switch(network_num) { + case 901: + return "Ile-de-France Mobilites"; + default: + return "Unknown"; + } +} + +const char* get_transport_type(int type) { + switch(type) { + case BUS_URBAIN: + return "Bus Urbain"; + case BUS_INTERURBAIN: + return "Bus Interurbain"; + case METRO: + return "Metro"; + case TRAM: + return "Tram"; + case TRAIN: + return "Train"; + case PARKING: + return "Parking"; + default: + return "Unknown"; + } +} + +const char* get_service_provider(int provider) { + switch(provider) { + case 2: + return "SNCF"; + case 3: + return "RATP"; + case 4: + return "IDF Mobilites"; + case 10: + return "IDF Mobilites"; + case 115: + return "CSO (VEOLIA)"; + case 116: + return "R'Bus (VEOLIA)"; + case 156: + return "Phebus"; + case 175: + return "RATP (Veolia Transport Nanterre)"; + default: + return "Unknown"; + } +} + +const char* get_transition_type(int transition) { + switch(transition) { + case 1: + return "Validation - entree"; + case 2: + return "Validation - sortie"; + case 4: + return "Controle volant (a bord)"; + case 5: + return "Validation de test"; + case 6: + return "Validation en correspondance - entree"; + case 7: + return "Validation en correspondance - sortie"; + case 9: + return "Annulation de validation"; + case 10: + return "Validation - entree"; + case 13: + return "Distribution"; + case 15: + return "Invalidation"; + default: { + return "Unknown"; + } + } +} + +const char* get_navigo_type(int type) { + switch(type) { + case NAVIGO_EASY: + return "Navigo Easy"; + case NAVIGO_DECOUVERTE: + return "Navigo Decouverte"; + case NAVIGO_STANDARD: + return "Navigo Standard"; + case NAVIGO_INTEGRAL: + return "Navigo Integral"; + case IMAGINE_R: + return "Imagine R"; + default: + return "Navigo Inconnu"; + } +} + +const char* get_tariff(int tariff) { + switch(tariff) { + case 0x0002: + return "Navigo Annuel"; + case 0x0004: + return "Imagine R Junior"; + case 0x0005: + return "Imagine R Etudiant"; + case 0x000D: + return "Navigo Jeunes Week-end"; + case 0x1000: + return "Navigo Liberte+"; + case 0x5000: + return "Navigo Easy"; + default: + return "Inconnu"; + } +} + +const char* get_pay_method(int pay_method) { + switch(pay_method) { + case 0x30: + return "Apple Pay"; + case 0x80: + return "Debit PME"; + case 0x90: + return "Espece"; + case 0xA0: + return "Cheque mobilite"; + case 0xB3: + return "Carte de paiement"; + case 0xA4: + return "Cheque"; + case 0xA5: + return "Cheque vacance"; + case 0xB7: + return "Telepaiement"; + case 0xD0: + return "Telereglement"; + case 0xD7: + return "Bon de caisse, Versement prealable, Bon d’echange, Bon Voyage"; + case 0xD9: + return "Bon de reduction"; + default: + return "Inconnu"; + } +} + +const char* get_metro_station(int station_group_id, int station_id) { + // Use NAVIGO_H constants + if(station_group_id < 32 && station_id < 16) { + return METRO_STATION_LIST[station_group_id][station_id]; + } + // cast station_group_id-station_id to a string + char* station = malloc(12 * sizeof(char)); + if(!station) { + return "Unknown"; + } + snprintf(station, 10, "%d-%d", station_group_id, station_id); + return station; +} + +const char* get_train_line(int station_group_id) { + if(station_group_id < 77) { + return TRAIN_LINES_LIST[station_group_id]; + } + return "Unknown"; +} + +const char* get_train_station(int station_group_id, int station_id) { + if(station_group_id < 77 && station_id < 19) { + return TRAIN_STATION_LIST[station_group_id][station_id]; + } + // cast station_group_id-station_id to a string + char* station = malloc(12 * sizeof(char)); + if(!station) { + return "Unknown"; + } + snprintf(station, 10, "%d-%d", station_group_id, station_id); + return station; +} + +void show_event_info(NavigoCardEvent* event, FuriString* parsed_data) { + if(event->transport_type == BUS_URBAIN || event->transport_type == BUS_INTERURBAIN || + event->transport_type == METRO || event->transport_type == TRAM) { + if(event->route_number_available) { + if(event->transport_type == METRO && event->route_number == 103) { + furi_string_cat_printf( + parsed_data, + "%s 3 bis\n%s\n", + get_transport_type(event->transport_type), + get_transition_type(event->transition)); + } else { + furi_string_cat_printf( + parsed_data, + "%s %d\n%s\n", + get_transport_type(event->transport_type), + event->route_number, + get_transition_type(event->transition)); + } + } else { + furi_string_cat_printf( + parsed_data, + "%s\n%s\n", + get_transport_type(event->transport_type), + get_transition_type(event->transition)); + } + furi_string_cat_printf( + parsed_data, "Transporteur : %s\n", get_service_provider(event->service_provider)); + if(event->transport_type == METRO) { + furi_string_cat_printf( + parsed_data, + "Station : %s\nSecteur : %s\n", + get_metro_station(event->station_group_id, event->station_id), + get_metro_station(event->station_group_id, 0)); + } else { + furi_string_cat_printf( + parsed_data, "ID Station : %d-%d\n", event->station_group_id, event->station_id); + } + if(event->location_gate_available) { + furi_string_cat_printf(parsed_data, "Passage : %d\n", event->location_gate); + } + if(event->device_available) { + if(event->transport_type == BUS_URBAIN || event->transport_type == BUS_INTERURBAIN) { + const char* side = event->side == 0 ? "droit" : "gauche"; + furi_string_cat_printf(parsed_data, "Porte : %d\nCote %s\n", event->door, side); + } else { + furi_string_cat_printf(parsed_data, "Equipement : %d\n", event->device); + } + } + if(event->mission_available) { + furi_string_cat_printf(parsed_data, "Mission : %d\n", event->mission); + } + if(event->vehicle_id_available) { + furi_string_cat_printf(parsed_data, "Vehicule : %d\n", event->vehicle_id); + } + if(event->used_contract_available) { + furi_string_cat_printf(parsed_data, "Contrat : %d\n", event->used_contract); + } + locale_format_datetime_cat(parsed_data, &event->date, true); + furi_string_cat_printf(parsed_data, "\n"); + } else if(event->transport_type == TRAIN) { + if(event->route_number_available) { + furi_string_cat_printf( + parsed_data, + "RER %c\n%s\n", + (65 + event->route_number - 17), + get_transition_type(event->transition)); + } else { + furi_string_cat_printf( + parsed_data, + "%s %s\n%s\n", + get_transport_type(event->transport_type), + get_train_line(event->station_group_id), + get_transition_type(event->transition)); + } + furi_string_cat_printf( + parsed_data, "Transporteur : %s\n", get_service_provider(event->service_provider)); + furi_string_cat_printf( + parsed_data, + "Station : %s\n", + get_train_station(event->station_group_id, event->station_id)); + if(event->route_number_available) { + furi_string_cat_printf(parsed_data, "Route : %d\n", event->route_number); + } + if(event->location_gate_available) { + furi_string_cat_printf(parsed_data, "Passage : %d\n", event->location_gate); + } + if(event->device_available) { + furi_string_cat_printf(parsed_data, "Equipement : %d\n", event->device); + } + if(event->mission_available) { + furi_string_cat_printf(parsed_data, "Mission : %d\n", event->mission); + } + if(event->vehicle_id_available) { + furi_string_cat_printf(parsed_data, "Vehicule : %d\n", event->vehicle_id); + } + if(event->used_contract_available) { + furi_string_cat_printf(parsed_data, "Contrat : %d\n", event->used_contract); + } + locale_format_datetime_cat(parsed_data, &event->date, true); + furi_string_cat_printf(parsed_data, "\n"); + } else { + furi_string_cat_printf( + parsed_data, + "%s - %s\n", + get_transport_type(event->transport_type), + get_transition_type(event->transition)); + furi_string_cat_printf( + parsed_data, "Transporteur : %s\n", get_service_provider(event->service_provider)); + furi_string_cat_printf( + parsed_data, "ID Station : %d-%d\n", event->station_group_id, event->station_id); + if(event->location_gate_available) { + furi_string_cat_printf(parsed_data, "Passage : %d\n", event->location_gate); + } + if(event->device_available) { + furi_string_cat_printf(parsed_data, "Equipement : %d\n", event->device); + } + if(event->mission_available) { + furi_string_cat_printf(parsed_data, "Mission : %d\n", event->mission); + } + if(event->vehicle_id_available) { + furi_string_cat_printf(parsed_data, "Vehicule : %d\n", event->vehicle_id); + } + if(event->used_contract_available) { + furi_string_cat_printf(parsed_data, "Contrat : %d\n", event->used_contract); + } + locale_format_datetime_cat(parsed_data, &event->date, true); + furi_string_cat_printf(parsed_data, "\n"); + } +} + +void show_contract_info(NavigoCardContract* contract, int ticket_count, FuriString* parsed_data) { + furi_string_cat_printf(parsed_data, "Type : %s\n", get_tariff(contract->tariff)); + if(contract->serial_number_available) { + furi_string_cat_printf(parsed_data, "Numero TCN : %d\n", contract->serial_number); + } + if(contract->pay_method_available) { + furi_string_cat_printf( + parsed_data, "Methode de paiement : %s\n", get_pay_method(contract->pay_method)); + } + if(contract->price_amount_available) { + furi_string_cat_printf(parsed_data, "Montant : %.2f EUR\n", contract->price_amount); + } + if(contract->tariff == 0x5000) { + furi_string_cat_printf(parsed_data, "Titres restants : %d\n", ticket_count); + } + if(contract->end_date_available) { + furi_string_cat_printf(parsed_data, "Valide\ndu : "); + locale_format_datetime_cat(parsed_data, &contract->start_date, false); + furi_string_cat_printf(parsed_data, "\nau : "); + locale_format_datetime_cat(parsed_data, &contract->end_date, false); + furi_string_cat_printf(parsed_data, "\n"); + } else { + furi_string_cat_printf(parsed_data, "Valide a partir du\n"); + locale_format_datetime_cat(parsed_data, &contract->start_date, false); + furi_string_cat_printf(parsed_data, "\n"); + } + if(contract->zones_available) { + furi_string_cat_printf(parsed_data, "Zones "); + for(int i = 0; i < 5; i++) { + if(contract->zones[i] != 0) { + furi_string_cat_printf(parsed_data, "%d", i + 1); + if(i < 4) { + furi_string_cat_printf(parsed_data, ", "); + } + } + } + furi_string_cat_printf(parsed_data, "\n"); + } + furi_string_cat_printf(parsed_data, "Vendu le : "); + locale_format_datetime_cat(parsed_data, &contract->sale_date, false); + furi_string_cat_printf(parsed_data, "\n"); + furi_string_cat_printf( + parsed_data, + "Agent de vente : %s (%d)\n", + get_service_provider(contract->sale_agent), + contract->sale_agent); + furi_string_cat_printf(parsed_data, "Terminal de vente : %d\n", contract->sale_device); + furi_string_cat_printf(parsed_data, "Etat : %d\n", contract->status); + furi_string_cat_printf(parsed_data, "Code authenticite : %d\n", contract->authenticator); +} + +void show_environment_info(NavigoCardEnv* environment, FuriString* parsed_data) { + furi_string_cat_printf( + parsed_data, "Version de l'application : %d\n", environment->app_version); + if(environment->country_num == 250) { + furi_string_cat_printf(parsed_data, "Pays : France\n"); + } else { + furi_string_cat_printf(parsed_data, "Pays : %d\n", environment->country_num); + } + if(environment->network_num == 901) { + furi_string_cat_printf(parsed_data, "Reseau : Ile-de-France Mobilites\n"); + } else { + furi_string_cat_printf(parsed_data, "Reseau : %d\n", environment->network_num); + } + furi_string_cat_printf(parsed_data, "Fin de validite:\n"); + locale_format_datetime_cat(parsed_data, &environment->end_dt, false); + furi_string_cat_printf(parsed_data, "\n"); +} + +void update_page_info(void* context, FuriString* parsed_data) { + Metroflip* app = context; + NavigoContext* ctx = app->navigo_context; + if(ctx->page_id == 0) { + furi_string_cat_printf( + parsed_data, "\e#%s :\n", get_navigo_type(ctx->card->holder.card_status)); + furi_string_cat_printf(parsed_data, "\e#Contrat 1 :\n"); + show_contract_info(&ctx->card->contracts[0], ctx->card->ticket_count, parsed_data); + } else if(ctx->page_id == 1) { + furi_string_cat_printf( + parsed_data, "\e#%s :\n", get_navigo_type(ctx->card->holder.card_status)); + furi_string_cat_printf(parsed_data, "\e#Contrat 2 :\n"); + show_contract_info(&ctx->card->contracts[1], ctx->card->ticket_count, parsed_data); + } else if(ctx->page_id == 2) { + furi_string_cat_printf(parsed_data, "\e#Environnement :\n"); + show_environment_info(&ctx->card->environment, parsed_data); + } else if(ctx->page_id == 3) { + furi_string_cat_printf(parsed_data, "\e#Event 1 :\n"); + show_event_info(&ctx->card->events[0], parsed_data); + } else if(ctx->page_id == 4) { + furi_string_cat_printf(parsed_data, "\e#Event 2 :\n"); + show_event_info(&ctx->card->events[1], parsed_data); + } else if(ctx->page_id == 5) { + furi_string_cat_printf(parsed_data, "\e#Event 3 :\n"); + show_event_info(&ctx->card->events[2], parsed_data); + } +} + +void update_widget_elements(void* context) { + Metroflip* app = context; + NavigoContext* ctx = app->navigo_context; + Widget* widget = app->widget; + if(ctx->page_id < 5) { + widget_add_button_element( + widget, GuiButtonTypeRight, "Next", metroflip_next_button_widget_callback, context); + } else { + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_next_button_widget_callback, context); + } + if(ctx->page_id > 0) { + widget_add_button_element( + widget, GuiButtonTypeLeft, "Back", metroflip_back_button_widget_callback, context); + } +} + +void metroflip_back_button_widget_callback(GuiButtonType result, InputType type, void* context) { Metroflip* app = context; + NavigoContext* ctx = app->navigo_context; UNUSED(result); - if(type == InputTypeShort) { - scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart); + Widget* widget = app->widget; + + if(type == InputTypePress) { + widget_reset(widget); + + FURI_LOG_I(TAG, "Page ID: %d -> %d", ctx->page_id, ctx->page_id - 1); + + if(ctx->page_id > 0) { + if(ctx->page_id == 2 && ctx->card->contracts[1].tariff == 0) { + ctx->page_id -= 1; + } + ctx->page_id -= 1; + } + + FuriString* parsed_data = furi_string_alloc(); + + // Ensure no nested mutexes + furi_mutex_acquire(ctx->mutex, FuriWaitForever); + update_page_info(app, parsed_data); + furi_mutex_release(ctx->mutex); + + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); + // widget_add_icon_element(widget, 0, 0, &I_RFIDDolphinReceive_97x61); + + // Ensure no nested mutexes + furi_mutex_acquire(ctx->mutex, FuriWaitForever); + update_widget_elements(app); + furi_mutex_release(ctx->mutex); + + furi_string_free(parsed_data); + } +} + +void metroflip_next_button_widget_callback(GuiButtonType result, InputType type, void* context) { + Metroflip* app = context; + NavigoContext* ctx = app->navigo_context; + UNUSED(result); + + Widget* widget = app->widget; + + if(type == InputTypePress) { + widget_reset(widget); + + FURI_LOG_I(TAG, "Page ID: %d -> %d", ctx->page_id, ctx->page_id + 1); + + if(ctx->page_id < 5) { + if(ctx->page_id == 0 && ctx->card->contracts[1].tariff == 0) { + ctx->page_id += 1; + } + ctx->page_id += 1; + } else { + ctx->page_id = 0; + scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, MetroflipSceneStart); + return; + } + + FuriString* parsed_data = furi_string_alloc(); + + // Ensure no nested mutexes + furi_mutex_acquire(ctx->mutex, FuriWaitForever); + update_page_info(app, parsed_data); + furi_mutex_release(ctx->mutex); + + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); + + // Ensure no nested mutexes + furi_mutex_acquire(ctx->mutex, FuriWaitForever); + update_widget_elements(app); + furi_mutex_release(ctx->mutex); + + furi_string_free(parsed_data); } } +void delay(int milliseconds) { + furi_thread_flags_wait(0, FuriFlagWaitAny, milliseconds); +} + static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event, void* context) { furi_assert(event.protocol == NfcProtocolIso14443_4b); NfcCommand next_command = NfcCommandContinue; @@ -36,6 +606,11 @@ static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event, if(iso14443_4b_event->type == Iso14443_4bPollerEventTypeReady) { if(stage == MetroflipPollerEventTypeStart) { + // Start Flipper vibration + NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); + notification_message(notification, &sequence_set_vibro_on); + delay(50); + notification_message(notification, &sequence_reset_vibro); nfc_device_set_data( app->nfc_device, NfcProtocolIso14443_4b, nfc_poller_get_data(app->poller)); @@ -43,140 +618,278 @@ static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event, size_t response_length = 0; do { + // Initialize the card data + NavigoCardData* card = malloc(sizeof(NavigoCardData)); + // Select app for contracts - select_app[6] = 32; - bit_buffer_reset(tx_buffer); - bit_buffer_append_bytes(tx_buffer, select_app, sizeof(select_app)); - 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); + error = + select_new_app(0x20, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage); + if(error != 0) { break; } // Check the response after selecting app - 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 profile app 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); + if(check_response(rx_buffer, app, &stage, &response_length) != 0) { break; } - // read file 1 - read_file[2] = 1; - bit_buffer_reset(tx_buffer); - bit_buffer_append_bytes(tx_buffer, read_file, sizeof(read_file)); - 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); + // Prepare calypso structure + CalypsoApp* NavigoContractStructure = get_navigo_contract_structure(); + if(!NavigoContractStructure) { + FURI_LOG_E(TAG, "Failed to load Navigo Contract structure"); 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; - } - char bit_representation[response_length * 8 + 1]; - bit_representation[0] = '\0'; - for(size_t i = 0; i < response_length; i++) { - char bits[9]; - uint8_t byte = bit_buffer_get_byte(rx_buffer, i); - byte_to_binary(byte, bits); - strlcat(bit_representation, bits, sizeof(bit_representation)); + // Now send the read command for contracts + for(size_t i = 1; i < 3; i++) { + error = + read_new_file(i, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage); + if(error != 0) { + break; + } + + // Check the response after reading the file + if(check_response(rx_buffer, app, &stage, &response_length) != 0) { + break; + } + + char bit_representation[response_length * 8 + 1]; + bit_representation[0] = '\0'; + for(size_t i = 0; i < response_length; i++) { + char bits[9]; + uint8_t byte = bit_buffer_get_byte(rx_buffer, i); + byte_to_binary(byte, bits); + strlcat(bit_representation, bits, sizeof(bit_representation)); + } + bit_representation[response_length * 8] = '\0'; + + /* int count = 0; + int start = 0, end = NavigoContractStructure->elements[0].bitmap->size; + char bit_slice[end - start + 1]; + strncpy(bit_slice, bit_representation + start, end - start); + bit_slice[end - start] = '\0'; + int* positions = get_bit_positions(bit_slice, &count); + + FURI_LOG_I(TAG, "Contract %d bit positions: %d", i, count); + + // print positions + for(int i = 0; i < count; i++) { + char* key = + (NavigoContractStructure->elements[0] + .bitmap->elements[positions[i]] + .type == CALYPSO_ELEMENT_TYPE_FINAL ? + NavigoContractStructure->elements[0] + .bitmap->elements[positions[i]] + .final->key : + NavigoContractStructure->elements[0] + .bitmap->elements[positions[i]] + .bitmap->key); + int offset = get_calypso_node_offset( + bit_representation, key, NavigoContractStructure); + FURI_LOG_I( + TAG, "Position: %d, Key: %s, Offset: %d", positions[i], key, offset); + } */ + + // 2. ContractTariff + const char* contract_key = "ContractTariff"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + card->contracts[i - 1].tariff = + bit_slice_to_dec(bit_representation, start, end); + } + + // 3. ContractSerialNumber + contract_key = "ContractSerialNumber"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + card->contracts[i - 1].serial_number = + bit_slice_to_dec(bit_representation, start, end); + card->contracts[i - 1].serial_number_available = true; + } + + // 8. ContractPayMethod + contract_key = "ContractPayMethod"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + card->contracts[i - 1].pay_method = + bit_slice_to_dec(bit_representation, start, end); + card->contracts[i - 1].pay_method_available = true; + } + + // 10. ContractPriceAmount + contract_key = "ContractPriceAmount"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + card->contracts[i - 1].price_amount = + bit_slice_to_dec(bit_representation, start, end) / 100.0; + card->contracts[i - 1].price_amount_available = true; + } + + // 13.0. ContractValidityStartDate + contract_key = "ContractValidityStartDate"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + float decimal_value = + bit_slice_to_dec(bit_representation, start, end) * 24 * 3600; + uint64_t start_validity_timestamp = (decimal_value + (float)epoch) + 3600; + datetime_timestamp_to_datetime( + start_validity_timestamp, &card->contracts[i - 1].start_date); + } + + // 13.2. ContractValidityEndDate + contract_key = "ContractValidityEndDate"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + float decimal_value = + bit_slice_to_dec(bit_representation, start, end) * 24 * 3600; + uint64_t end_validity_timestamp = (decimal_value + (float)epoch) + 3600; + datetime_timestamp_to_datetime( + end_validity_timestamp, &card->contracts[i - 1].end_date); + card->contracts[i - 1].end_date_available = true; + } + + // 13.6. ContractValidityZones + contract_key = "ContractValidityZones"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int start = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + // binary form is 00011111 for zones 5, 4, 3, 2, 1 + for(int j = 0; j < 5; j++) { + card->contracts[i - 1].zones[j] = + bit_slice_to_dec(bit_representation, start + 3 + j, start + 3 + j); + } + card->contracts[i - 1].zones_available = true; + } + + // 13.7. ContractValidityJourneys -- pas sûr de le mettre lui + + // 15.0. ContractValiditySaleDate + contract_key = "ContractValiditySaleDate"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + float decimal_value = + bit_slice_to_dec(bit_representation, start, end) * 24 * 3600; + uint64_t sale_timestamp = (decimal_value + (float)epoch) + 3600; + datetime_timestamp_to_datetime( + sale_timestamp, &card->contracts[i - 1].sale_date); + } + + // 15.2. ContractValiditySaleAgent + contract_key = "ContractValiditySaleAgent"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + card->contracts[i - 1].sale_agent = + bit_slice_to_dec(bit_representation, start, end); + } else { + card->contracts[i - 1].sale_agent = -1; + } + + // 15.3. ContractValiditySaleDevice + contract_key = "ContractValiditySaleDevice"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + card->contracts[i - 1].sale_device = + bit_slice_to_dec(bit_representation, start, end); + } + + // 16. ContractStatus -- 0x1 ou 0xff + contract_key = "ContractStatus"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + card->contracts[i - 1].status = + bit_slice_to_dec(bit_representation, start, end); + } + + // 18. ContractAuthenticator + contract_key = "ContractAuthenticator"; + if(is_calypso_node_present( + bit_representation, contract_key, NavigoContractStructure)) { + int positionOffset = get_calypso_node_offset( + bit_representation, contract_key, NavigoContractStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(contract_key, NavigoContractStructure) - 1; + card->contracts[i - 1].authenticator = + bit_slice_to_dec(bit_representation, start, end); + } } - bit_representation[response_length * 8] = '\0'; - int start = 55, end = 70; - float decimal_value = bit_slice_to_dec(bit_representation, start, end); - float balance = decimal_value / 100; - furi_string_printf(parsed_data, "\e#Navigo:\n\n"); - furi_string_cat_printf(parsed_data, "\e#Contract 1:\n"); - furi_string_cat_printf(parsed_data, "Balance: %.2f EUR\n", (double)balance); - start = 80, end = 93; - decimal_value = bit_slice_to_dec(bit_representation, start, end); - uint64_t start_date_timestamp = (decimal_value * 24 * 3600) + (float)epoch + 3600; - DateTime start_dt = {0}; - datetime_timestamp_to_datetime(start_date_timestamp, &start_dt); - furi_string_cat_printf(parsed_data, "\nStart Date:\n"); - locale_format_datetime_cat(parsed_data, &start_dt, false); - furi_string_cat_printf(parsed_data, "\n"); + + // Free the calypso structure + free_calypso_structure(NavigoContractStructure); // Select app for environment - select_app[6] = 1; - bit_buffer_reset(tx_buffer); - bit_buffer_append_bytes(tx_buffer, select_app, sizeof(select_app)); - 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); + error = select_new_app(0x1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage); + if(error != 0) { break; } // Check the response after selecting app - 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 profile app 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); + if(check_response(rx_buffer, app, &stage, &response_length) != 0) { break; } // read file 1 - read_file[2] = 1; - bit_buffer_reset(tx_buffer); - bit_buffer_append_bytes(tx_buffer, read_file, sizeof(read_file)); - 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); + error = read_new_file(1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage); + if(error != 0) { 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); + if(check_response(rx_buffer, app, &stage, &response_length) != 0) { break; } + char environment_bit_representation[response_length * 8 + 1]; environment_bit_representation[0] = '\0'; for(size_t i = 0; i < response_length; i++) { @@ -188,76 +901,112 @@ static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event, bits, sizeof(environment_bit_representation)); } + FURI_LOG_I( + TAG, "Environment bit_representation: %s", environment_bit_representation); + int start = 0; + int end = 5; + card->environment.app_version = + bit_slice_to_dec(environment_bit_representation, start, end); + start = 13; + end = 16; + card->environment.country_num = + bit_slice_to_dec(environment_bit_representation, start, end) * 100 + + bit_slice_to_dec(environment_bit_representation, start + 4, end + 4) * 10 + + bit_slice_to_dec(environment_bit_representation, start + 8, end + 8); + start = 25; + end = 28; + card->environment.network_num = + bit_slice_to_dec(environment_bit_representation, start, end) * 100 + + bit_slice_to_dec(environment_bit_representation, start + 4, end + 4) * 10 + + bit_slice_to_dec(environment_bit_representation, start + 8, end + 8); start = 45; end = 58; - decimal_value = bit_slice_to_dec(environment_bit_representation, start, end); + float decimal_value = bit_slice_to_dec(environment_bit_representation, start, end); uint64_t end_validity_timestamp = (decimal_value * 24 * 3600) + (float)epoch + 3600; - DateTime end_dt = {0}; - datetime_timestamp_to_datetime(end_validity_timestamp, &end_dt); - furi_string_cat_printf(parsed_data, "\nEnd Validity:\n"); - locale_format_datetime_cat(parsed_data, &end_dt, false); - furi_string_cat_printf(parsed_data, "\n\n"); + datetime_timestamp_to_datetime(end_validity_timestamp, &card->environment.end_dt); + + start = 95; + end = 98; + card->holder.card_status = + bit_slice_to_dec(environment_bit_representation, start, end); + + start = 99; + end = 104; + card->holder.commercial_id = + bit_slice_to_dec(environment_bit_representation, start, end); + + // Select app for counters (remaining tickets on Navigo Easy) + error = + select_new_app(0x2A, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage); + if(error != 0) { + break; + } + + // Check the response after selecting app + if(check_response(rx_buffer, app, &stage, &response_length) != 0) { + break; + } + + // read file 1 + error = read_new_file(1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage); + if(error != 0) { + break; + } + + // Check the response after reading the file + if(check_response(rx_buffer, app, &stage, &response_length) != 0) { + break; + } + + char counter_bit_representation[response_length * 8 + 1]; + counter_bit_representation[0] = '\0'; + for(size_t i = 0; i < response_length; i++) { + char bits[9]; + uint8_t byte = bit_buffer_get_byte(rx_buffer, i); + byte_to_binary(byte, bits); + strlcat(counter_bit_representation, bits, sizeof(counter_bit_representation)); + } + FURI_LOG_I(TAG, "Counter bit_representation: %s", counter_bit_representation); + + // Ticket count + start = 2; + end = 5; + card->ticket_count = bit_slice_to_dec(counter_bit_representation, start, end); // Select app for events - select_app[6] = 16; - bit_buffer_reset(tx_buffer); - bit_buffer_append_bytes(tx_buffer, select_app, sizeof(select_app)); - 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); + error = + select_new_app(0x10, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage); + if(error != 0) { break; } // Check the response after selecting app - 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 events app 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); + if(check_response(rx_buffer, app, &stage, &response_length) != 0) { break; } - furi_string_cat_printf(parsed_data, "\e#Events:\n"); - // Now send the read command + // Load the calypso structure for events + CalypsoApp* NavigoEventStructure = get_navigo_event_structure(); + if(!NavigoEventStructure) { + FURI_LOG_E(TAG, "Failed to load Navigo Event structure"); + break; + } + + // furi_string_cat_printf(parsed_data, "\e#Events :\n"); + // Now send the read command for events for(size_t i = 1; i < 4; i++) { - read_file[2] = i; - bit_buffer_reset(tx_buffer); - bit_buffer_append_bytes(tx_buffer, read_file, sizeof(read_file)); 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); + read_new_file(i, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage); + if(error != 0) { 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); + if(check_response(rx_buffer, app, &stage, &response_length) != 0) { break; } + char event_bit_representation[response_length * 8 + 1]; event_bit_representation[0] = '\0'; for(size_t i = 0; i < response_length; i++) { @@ -266,51 +1015,198 @@ static NfcCommand metroflip_scene_navigo_poller_callback(NfcGenericEvent event, byte_to_binary(byte, bits); strlcat(event_bit_representation, bits, sizeof(event_bit_representation)); } - furi_string_cat_printf(parsed_data, "\nEvent 0%d:\n", i); - int start = 53, end = 60; + + // furi_string_cat_printf(parsed_data, "Event 0%d :\n", i); + /* int count = 0; + int start = 25, end = 52; + char bit_slice[end - start + 2]; + strncpy(bit_slice, event_bit_representation + start, end - start + 1); + bit_slice[end - start + 1] = '\0'; + int* positions = get_bit_positions(bit_slice, &count); + FURI_LOG_I(TAG, "Positions: "); + for(int i = 0; i < count; i++) { + FURI_LOG_I(TAG, "%d ", positions[i]); + } */ + + // 2. EventCode + const char* event_key = "EventCode"; + if(is_calypso_node_present( + event_bit_representation, event_key, NavigoEventStructure)) { + int positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + int start = positionOffset, + end = positionOffset + + get_calypso_node_size(event_key, NavigoEventStructure) - 1; + int decimal_value = bit_slice_to_dec(event_bit_representation, start, end); + card->events[i - 1].transport_type = decimal_value >> 4; + card->events[i - 1].transition = decimal_value & 15; + } + + // 4. EventServiceProvider + event_key = "EventServiceProvider"; + if(is_calypso_node_present( + event_bit_representation, event_key, NavigoEventStructure)) { + int positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + start = positionOffset, + end = positionOffset + + get_calypso_node_size(event_key, NavigoEventStructure) - 1; + card->events[i - 1].service_provider = + bit_slice_to_dec(event_bit_representation, start, end); + } + + // 8. EventLocationId + event_key = "EventLocationId"; + if(is_calypso_node_present( + event_bit_representation, event_key, NavigoEventStructure)) { + int positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + start = positionOffset, + end = positionOffset + + get_calypso_node_size(event_key, NavigoEventStructure) - 1; + int decimal_value = bit_slice_to_dec(event_bit_representation, start, end); + card->events[i - 1].station_group_id = decimal_value >> 9; + card->events[i - 1].station_id = (decimal_value >> 4) & 31; + } + + // 9. EventLocationGate + event_key = "EventLocationGate"; + if(is_calypso_node_present( + event_bit_representation, event_key, NavigoEventStructure)) { + int positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + start = positionOffset, + end = positionOffset + + get_calypso_node_size(event_key, NavigoEventStructure) - 1; + card->events[i - 1].location_gate = + bit_slice_to_dec(event_bit_representation, start, end); + card->events[i - 1].location_gate_available = true; + } + + // 10. EventDevice + event_key = "EventDevice"; + if(is_calypso_node_present( + event_bit_representation, event_key, NavigoEventStructure)) { + int positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + start = positionOffset, + end = positionOffset + + get_calypso_node_size(event_key, NavigoEventStructure) - 1; + int decimal_value = bit_slice_to_dec(event_bit_representation, start, end); + card->events[i - 1].device = decimal_value >> 8; + card->events[i - 1].door = card->events[i - 1].device / 2 + 1; + card->events[i - 1].side = card->events[i - 1].device % 2; + card->events[i - 1].device_available = true; + } + + // 11. EventRouteNumber + event_key = "EventRouteNumber"; + if(is_calypso_node_present( + event_bit_representation, event_key, NavigoEventStructure)) { + int positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + start = positionOffset, + end = positionOffset + + get_calypso_node_size(event_key, NavigoEventStructure) - 1; + card->events[i - 1].route_number = + bit_slice_to_dec(event_bit_representation, start, end); + card->events[i - 1].route_number_available = true; + } + + // 13. EventJourneyRun + event_key = "EventJourneyRun"; + if(is_calypso_node_present( + event_bit_representation, event_key, NavigoEventStructure)) { + int positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + start = positionOffset, + end = positionOffset + + get_calypso_node_size(event_key, NavigoEventStructure) - 1; + card->events[i - 1].mission = + bit_slice_to_dec(event_bit_representation, start, end); + card->events[i - 1].mission_available = true; + } + + // 14. EventVehicleId + event_key = "EventVehicleId"; + if(is_calypso_node_present( + event_bit_representation, event_key, NavigoEventStructure)) { + int positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + start = positionOffset, + end = positionOffset + + get_calypso_node_size(event_key, NavigoEventStructure) - 1; + card->events[i - 1].vehicle_id = + bit_slice_to_dec(event_bit_representation, start, end); + card->events[i - 1].vehicle_id_available = true; + } + + // 25. EventContractPointer + event_key = "EventContractPointer"; + if(is_calypso_node_present( + event_bit_representation, event_key, NavigoEventStructure)) { + int positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + start = positionOffset, + end = positionOffset + + get_calypso_node_size(event_key, NavigoEventStructure) - 1; + card->events[i - 1].used_contract = + bit_slice_to_dec(event_bit_representation, start, end); + card->events[i - 1].used_contract_available = true; + } + + // EventDateStamp + event_key = "EventDateStamp"; + int positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + start = positionOffset, + end = positionOffset + get_calypso_node_size(event_key, NavigoEventStructure) - + 1; int decimal_value = bit_slice_to_dec(event_bit_representation, start, end); - int transport_type = decimal_value >> 4; - int transition = decimal_value & 15; - furi_string_cat_printf( - parsed_data, - "%s - %s\n", - TRANSPORT_LIST[transport_type], - TRANSITION_LIST[transition]); - start = 69, end = 84; - decimal_value = bit_slice_to_dec(event_bit_representation, start, end); - int line_id = (decimal_value >> 9) - 1; - int station_id = ((decimal_value >> 4) & 31) - 1; - furi_string_cat_printf( - parsed_data, - "Line: %s\nStation: %s\n", - METRO_LIST[line_id].name, - METRO_LIST[line_id].stations[station_id]); - start = 61, end = 68; - decimal_value = bit_slice_to_dec(event_bit_representation, start, end); - furi_string_cat_printf( - parsed_data, "Provider: %s\n", SERVICE_PROVIDERS[decimal_value]); - start = 0, end = 13; - decimal_value = bit_slice_to_dec(event_bit_representation, start, end); uint64_t date_timestamp = (decimal_value * 24 * 3600) + epoch + 3600; - DateTime dt = {0}; - datetime_timestamp_to_datetime(date_timestamp, &dt); - furi_string_cat_printf(parsed_data, "Time: "); - locale_format_datetime_cat(parsed_data, &dt, false); - start = 14, end = 24; + datetime_timestamp_to_datetime(date_timestamp, &card->events[i - 1].date); + + // EventTimeStamp + event_key = "EventTimeStamp"; + positionOffset = get_calypso_node_offset( + event_bit_representation, event_key, NavigoEventStructure); + start = positionOffset, + end = positionOffset + get_calypso_node_size(event_key, NavigoEventStructure) - + 1; decimal_value = bit_slice_to_dec(event_bit_representation, start, end); - furi_string_cat_printf( - parsed_data, - " %02d:%02d:%02d\n\n", - ((decimal_value * 60) / 3600), - (((decimal_value * 60) % 3600) / 60), - (((decimal_value * 60) % 3600) % 60)); + card->events[i - 1].date.hour = (decimal_value * 60) / 3600; + card->events[i - 1].date.minute = ((decimal_value * 60) % 3600) / 60; + card->events[i - 1].date.second = ((decimal_value * 60) % 3600) % 60; } + // Free the calypso structure + free_calypso_structure(NavigoEventStructure); + + UNUSED(TRANSITION_LIST); + UNUSED(TRANSPORT_LIST); + UNUSED(SERVICE_PROVIDERS); + widget_add_text_scroll_element( widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); - widget_add_button_element( - widget, GuiButtonTypeRight, "Exit", metroflip_navigo_widget_callback, app); + NavigoContext* context = malloc(sizeof(NavigoContext)); + context->card = card; + context->page_id = 0; + context->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + app->navigo_context = context; + + // Ensure no nested mutexes + furi_mutex_acquire(context->mutex, FuriWaitForever); + update_page_info(app, parsed_data); + furi_mutex_release(context->mutex); + + widget_add_text_scroll_element( + widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); + + // Ensure no nested mutexes + furi_mutex_acquire(context->mutex, FuriWaitForever); + update_widget_elements(app); + furi_mutex_release(context->mutex); furi_string_free(parsed_data); view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); @@ -386,4 +1282,12 @@ void metroflip_scene_navigo_on_exit(void* context) { // Clear view popup_reset(app->popup); + + if(app->navigo_context) { + NavigoContext* ctx = app->navigo_context; + free(ctx->card); + furi_mutex_free(ctx->mutex); + free(ctx); + app->navigo_context = NULL; + } } diff --git a/metroflip/scenes/metroflip_scene_opal.c b/metroflip/scenes/metroflip_scene_opal.c new file mode 100644 index 000000000..2e77f979b --- /dev/null +++ b/metroflip/scenes/metroflip_scene_opal.c @@ -0,0 +1,309 @@ +/* + * opal.c - Parser for Opal card (Sydney, Australia). + * + * Copyright 2023 Michael Farrell + * + * This will only read "standard" MIFARE DESFire-based Opal cards. Free travel + * cards (including School Opal cards, veteran, vision-impaired persons and + * TfNSW employees' cards) and single-trip tickets are MIFARE Ultralight C + * cards and not supported. + * + * Reference: https://github.com/metrodroid/metrodroid/wiki/Opal + * + * Note: The card values are all little-endian (like Flipper), but the above + * reference was originally written based on Java APIs, which are big-endian. + * This implementation presumes a little-endian system. + * + * 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 "../metroflip_i.h" +#include + +#include +#include + +#include +#include + +#define TAG "Metroflip:Scene:Opal" + +static const MfDesfireApplicationId opal_app_id = {.data = {0x31, 0x45, 0x53}}; + +static const MfDesfireFileId opal_file_id = 0x07; + +static const char* opal_modes[5] = + {"Rail / Metro", "Ferry / Light Rail", "Bus", "Unknown mode", "Manly Ferry"}; + +static const char* opal_usages[14] = { + "New / Unused", + "Tap on: new journey", + "Tap on: transfer from same mode", + "Tap on: transfer from other mode", + NULL, // Manly Ferry: new journey + NULL, // Manly Ferry: transfer from ferry + NULL, // Manly Ferry: transfer from other + "Tap off: distance fare", + "Tap off: flat fare", + "Automated tap off: failed to tap off", + "Tap off: end of trip without start", + "Tap off: reversal", + "Tap on: rejected", + "Unknown usage", +}; + +// Opal file 0x7 structure. Assumes a little-endian CPU. +typedef struct FURI_PACKED { + uint32_t serial : 32; + uint8_t check_digit : 4; + bool blocked : 1; + uint16_t txn_number : 16; + int32_t balance : 21; + uint16_t days : 15; + uint16_t minutes : 11; + uint8_t mode : 3; + uint16_t usage : 4; + bool auto_topup : 1; + uint8_t weekly_journeys : 4; + uint16_t checksum : 16; +} OpalFile; + +static_assert(sizeof(OpalFile) == 16, "OpalFile"); + +// Converts an Opal timestamp to DateTime. +// +// Opal measures days since 1980-01-01 and minutes since midnight, and presumes +// all days are 1440 minutes. +static void opal_days_minutes_to_datetime(uint16_t days, uint16_t minutes, DateTime* out) { + out->year = 1980; + out->month = 1; + // 1980-01-01 is a Tuesday + out->weekday = ((days + 1) % 7) + 1; + out->hour = minutes / 60; + out->minute = minutes % 60; + out->second = 0; + + // What year is it? + for(;;) { + const uint16_t num_days_in_year = datetime_get_days_per_year(out->year); + if(days < num_days_in_year) break; + days -= num_days_in_year; + out->year++; + } + + // 1-index the day of the year + days++; + + for(;;) { + // What month is it? + const bool is_leap = datetime_is_leap_year(out->year); + const uint8_t num_days_in_month = datetime_get_days_per_month(is_leap, out->month); + if(days <= num_days_in_month) break; + days -= num_days_in_month; + out->month++; + } + + out->day = days; +} + +static bool opal_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); + + const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire); + + bool parsed = false; + + do { + const MfDesfireApplication* app = mf_desfire_get_application(data, &opal_app_id); + if(app == NULL) break; + + const MfDesfireFileSettings* file_settings = + mf_desfire_get_file_settings(app, &opal_file_id); + if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard || + file_settings->data.size != sizeof(OpalFile)) + break; + + const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &opal_file_id); + if(file_data == NULL) break; + + const OpalFile* opal_file = simple_array_cget_data(file_data->data); + + const uint8_t serial2 = opal_file->serial / 10000000; + const uint16_t serial3 = (opal_file->serial / 1000) % 10000; + const uint16_t serial4 = (opal_file->serial % 1000); + + if(opal_file->check_digit > 9) break; + + // Negative balance. Make this a positive value again and record the + // sign separately, because then we can handle balances of -99..-1 + // cents, as the "dollars" division below would result in a positive + // zero value. + const bool is_negative_balance = (opal_file->balance < 0); + const char* sign = is_negative_balance ? "-" : ""; + const int32_t balance = is_negative_balance ? labs(opal_file->balance) : //-V1081 + opal_file->balance; + const uint8_t balance_cents = balance % 100; + const int32_t balance_dollars = balance / 100; + + DateTime timestamp; + opal_days_minutes_to_datetime(opal_file->days, opal_file->minutes, ×tamp); + + // Usages 4..6 associated with the Manly Ferry, which correspond to + // usages 1..3 for other modes. + const bool is_manly_ferry = (opal_file->usage >= 4) && (opal_file->usage <= 6); + + // 3..7 are "reserved", but we use 4 to indicate the Manly Ferry. + const uint8_t mode = is_manly_ferry ? 4 : opal_file->mode; + const uint8_t usage = is_manly_ferry ? opal_file->usage - 3 : opal_file->usage; + + const char* mode_str = opal_modes[mode > 4 ? 3 : mode]; + const char* usage_str = opal_usages[usage > 12 ? 13 : usage]; + + furi_string_printf( + parsed_data, + "\e#Opal: $%s%ld.%02hu\nNo.: 3085 22%02hhu %04hu %03hu%01hhu\n%s, %s\n", + sign, + balance_dollars, + balance_cents, + serial2, + serial3, + serial4, + opal_file->check_digit, + mode_str, + usage_str); + + FuriString* timestamp_str = furi_string_alloc(); + + locale_format_date(timestamp_str, ×tamp, locale_get_date_format(), "-"); + furi_string_cat(parsed_data, timestamp_str); + furi_string_cat(parsed_data, " at "); + + locale_format_time(timestamp_str, ×tamp, locale_get_time_format(), false); + furi_string_cat(parsed_data, timestamp_str); + + furi_string_free(timestamp_str); + + furi_string_cat_printf( + parsed_data, + "\nWeekly journeys: %hhu, Txn #%hu\n", + opal_file->weekly_journeys, + opal_file->txn_number); + + if(opal_file->auto_topup) { + furi_string_cat_str(parsed_data, "Auto-topup enabled\n"); + } + + if(opal_file->blocked) { + furi_string_cat_str(parsed_data, "Card blocked\n"); + } + + parsed = true; + } while(false); + + return parsed; +} + +static NfcCommand metroflip_scene_opal_poller_callback(NfcGenericEvent event, void* context) { + furi_assert(event.protocol == NfcProtocolMfDesfire); + + Metroflip* app = context; + NfcCommand command = NfcCommandContinue; + + FuriString* parsed_data = furi_string_alloc(); + Widget* widget = app->widget; + furi_string_reset(app->text_box_store); + const MfDesfirePollerEvent* mf_desfire_event = event.event_data; + if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) { + nfc_device_set_data( + app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller)); + if(!opal_parse(app->nfc_device, parsed_data)) { + furi_string_reset(app->text_box_store); + FURI_LOG_I(TAG, "Unknown card type"); + furi_string_printf(parsed_data, "\e#Unknown card\n"); + } + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); + + furi_string_free(parsed_data); + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); + metroflip_app_blink_stop(app); + command = NfcCommandStop; + } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) { + view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess); + command = NfcCommandContinue; + } + + return command; +} + +void metroflip_scene_opal_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, NfcProtocolMfDesfire); + nfc_poller_start(app->poller, metroflip_scene_opal_poller_callback, app); + + metroflip_app_blink_start(app); +} + +bool metroflip_scene_opal_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_opal_on_exit(void* context) { + Metroflip* app = context; + widget_reset(app->widget); + metroflip_app_blink_stop(app); + if(app->poller) { + nfc_poller_stop(app->poller); + nfc_poller_free(app->poller); + } +} diff --git a/metroflip/scenes/metroflip_scene_ravkav.c b/metroflip/scenes/metroflip_scene_ravkav.c index 0f7aef4a7..11e66c5f8 100644 --- a/metroflip/scenes/metroflip_scene_ravkav.c +++ b/metroflip/scenes/metroflip_scene_ravkav.c @@ -4,15 +4,6 @@ #define TAG "Metroflip:Scene:RavKav" -void metroflip_ravkav_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_ravkav_poller_callback(NfcGenericEvent event, void* context) { furi_assert(event.protocol == NfcProtocolIso14443_4b); NfcCommand next_command = NfcCommandContinue; @@ -274,7 +265,7 @@ static NfcCommand metroflip_scene_ravkav_poller_callback(NfcGenericEvent event, widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); widget_add_button_element( - widget, GuiButtonTypeRight, "Exit", metroflip_ravkav_widget_callback, app); + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); furi_string_free(parsed_data); view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); diff --git a/metroflip/scenes/metroflip_scene_read_success.c b/metroflip/scenes/metroflip_scene_read_success.c index d4f426098..1a6d7415f 100644 --- a/metroflip/scenes/metroflip_scene_read_success.c +++ b/metroflip/scenes/metroflip_scene_read_success.c @@ -3,15 +3,6 @@ #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; @@ -37,7 +28,7 @@ void metroflip_scene_read_success_on_enter(void* context) { 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); + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); furi_string_free(str); view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); diff --git a/metroflip/scenes/metroflip_scene_start.c b/metroflip/scenes/metroflip_scene_start.c index 9d004bb69..eb1f5744e 100644 --- a/metroflip/scenes/metroflip_scene_start.c +++ b/metroflip/scenes/metroflip_scene_start.c @@ -24,6 +24,21 @@ void metroflip_scene_start_on_enter(void* context) { metroflip_scene_start_submenu_callback, app); + submenu_add_item( + submenu, "Clipper", MetroflipSceneClipper, metroflip_scene_start_submenu_callback, app); + + submenu_add_item( + submenu, "myki", MetroflipSceneMyki, metroflip_scene_start_submenu_callback, app); + + submenu_add_item( + submenu, "Troika", MetroflipSceneTroika, metroflip_scene_start_submenu_callback, app); + + submenu_add_item( + submenu, "Opal", MetroflipSceneOpal, metroflip_scene_start_submenu_callback, app); + + submenu_add_item( + submenu, "ITSO", MetroflipSceneItso, metroflip_scene_start_submenu_callback, app); + submenu_add_item( submenu, "Metromoney", diff --git a/metroflip/scenes/metroflip_scene_troika.c b/metroflip/scenes/metroflip_scene_troika.c new file mode 100644 index 000000000..c7308b45a --- /dev/null +++ b/metroflip/scenes/metroflip_scene_troika.c @@ -0,0 +1,311 @@ +#include +#include "../metroflip_i.h" + +#include +#include +#include +#include "../api/mosgortrans/mosgortrans_util.h" + +#include +#include +#include +#include +#include +#include + +#define TAG "Metroflip:Scene:Troika" + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +typedef struct { + const MfClassicKeyPair* keys; + uint32_t data_sector; +} TroikaCardConfig; + +static const MfClassicKeyPair troika_1k_keys[] = { + {.a = 0xa0a1a2a3a4a5, .b = 0xfbf225dc5d58}, + {.a = 0xa82607b01c0d, .b = 0x2910989b6880}, + {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99}, + {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99}, + {.a = 0x73068f118c13, .b = 0x2b7f3253fac5}, + {.a = 0xfbc2793d540b, .b = 0xd3a297dc2698}, + {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99}, + {.a = 0xae3d65a3dad4, .b = 0x0f1c63013dba}, + {.a = 0xa73f5dc1d333, .b = 0xe35173494a81}, + {.a = 0x69a32f1c2f19, .b = 0x6b8bd9860763}, + {.a = 0x9becdf3d9273, .b = 0xf8493407799d}, + {.a = 0x08b386463229, .b = 0x5efbaecef46b}, + {.a = 0xcd4c61c26e3d, .b = 0x31c7610de3b0}, + {.a = 0xa82607b01c0d, .b = 0x2910989b6880}, + {.a = 0x0e8f64340ba4, .b = 0x4acec1205d75}, + {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99}, +}; + +static const MfClassicKeyPair troika_4k_keys[] = { + {.a = 0xEC29806D9738, .b = 0xFBF225DC5D58}, //1 + {.a = 0xA0A1A2A3A4A5, .b = 0x7DE02A7F6025}, //2 + {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //3 + {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //4 + {.a = 0x73068F118C13, .b = 0x2B7F3253FAC5}, //5 + {.a = 0xFBC2793D540B, .b = 0xD3A297DC2698}, //6 + {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //7 + {.a = 0xAE3D65A3DAD4, .b = 0x0F1C63013DBA}, //8 + {.a = 0xA73F5DC1D333, .b = 0xE35173494A81}, //9 + {.a = 0x69A32F1C2F19, .b = 0x6B8BD9860763}, //10 + {.a = 0x9BECDF3D9273, .b = 0xF8493407799D}, //11 + {.a = 0x08B386463229, .b = 0x5EFBAECEF46B}, //12 + {.a = 0xCD4C61C26E3D, .b = 0x31C7610DE3B0}, //13 + {.a = 0xA82607B01C0D, .b = 0x2910989B6880}, //14 + {.a = 0x0E8F64340BA4, .b = 0x4ACEC1205D75}, //15 + {.a = 0x2AA05ED1856F, .b = 0xEAAC88E5DC99}, //16 + {.a = 0x6B02733BB6EC, .b = 0x7038CD25C408}, //17 + {.a = 0x403D706BA880, .b = 0xB39D19A280DF}, //18 + {.a = 0xC11F4597EFB5, .b = 0x70D901648CB9}, //19 + {.a = 0x0DB520C78C1C, .b = 0x73E5B9D9D3A4}, //20 + {.a = 0x3EBCE0925B2F, .b = 0x372CC880F216}, //21 + {.a = 0x16A27AF45407, .b = 0x9868925175BA}, //22 + {.a = 0xABA208516740, .b = 0xCE26ECB95252}, //23 + {.a = 0xCD64E567ABCD, .b = 0x8F79C4FD8A01}, //24 + {.a = 0x764CD061F1E6, .b = 0xA74332F74994}, //25 + {.a = 0x1CC219E9FEC1, .b = 0xB90DE525CEB6}, //26 + {.a = 0x2FE3CB83EA43, .b = 0xFBA88F109B32}, //27 + {.a = 0x07894FFEC1D6, .b = 0xEFCB0E689DB3}, //28 + {.a = 0x04C297B91308, .b = 0xC8454C154CB5}, //29 + {.a = 0x7A38E3511A38, .b = 0xAB16584C972A}, //30 + {.a = 0x7545DF809202, .b = 0xECF751084A80}, //31 + {.a = 0x5125974CD391, .b = 0xD3EAFB5DF46D}, //32 + {.a = 0x7A86AA203788, .b = 0xE41242278CA2}, //33 + {.a = 0xAFCEF64C9913, .b = 0x9DB96DCA4324}, //34 + {.a = 0x04EAA462F70B, .b = 0xAC17B93E2FAE}, //35 + {.a = 0xE734C210F27E, .b = 0x29BA8C3E9FDA}, //36 + {.a = 0xD5524F591EED, .b = 0x5DAF42861B4D}, //37 + {.a = 0xE4821A377B75, .b = 0xE8709E486465}, //38 + {.a = 0x518DC6EEA089, .b = 0x97C64AC98CA4}, //39 + {.a = 0xBB52F8CCE07F, .b = 0x6B6119752C70}, //40 +}; + +static bool troika_get_card_config(TroikaCardConfig* config, MfClassicType type) { + bool success = true; + + if(type == MfClassicType1k) { + config->data_sector = 11; + config->keys = troika_1k_keys; + } else if(type == MfClassicType4k) { + config->data_sector = 8; // Further testing needed + config->keys = troika_4k_keys; + } else { + success = false; + } + + return success; +} + +static bool troika_parse(FuriString* parsed_data, const MfClassicData* data) { + bool parsed = false; + + do { + // Verify card type + TroikaCardConfig cfg = {}; + if(!troika_get_card_config(&cfg, data->type)) break; + + // Verify key + const MfClassicSectorTrailer* sec_tr = + mf_classic_get_sector_trailer_by_sector(data, cfg.data_sector); + + const uint64_t key = + bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data)); + if(key != cfg.keys[cfg.data_sector].a) break; + + FuriString* metro_result = furi_string_alloc(); + FuriString* ground_result = furi_string_alloc(); + FuriString* tat_result = furi_string_alloc(); + + bool is_metro_data_present = + mosgortrans_parse_transport_block(&data->block[32], metro_result); + bool is_ground_data_present = + mosgortrans_parse_transport_block(&data->block[28], ground_result); + bool is_tat_data_present = mosgortrans_parse_transport_block(&data->block[16], tat_result); + + furi_string_cat_printf(parsed_data, "\e#Troyka card\n"); + if(is_metro_data_present && !furi_string_empty(metro_result)) { + render_section_header(parsed_data, "Metro", 22, 21); + furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(metro_result)); + } + + if(is_ground_data_present && !furi_string_empty(ground_result)) { + render_section_header(parsed_data, "Ediny", 22, 22); + furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(ground_result)); + } + + if(is_tat_data_present && !furi_string_empty(tat_result)) { + render_section_header(parsed_data, "TAT", 24, 23); + furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(tat_result)); + } + + furi_string_free(tat_result); + furi_string_free(ground_result); + furi_string_free(metro_result); + + parsed = is_metro_data_present || is_ground_data_present || is_tat_data_present; + } while(false); + + return parsed; +} + +bool checked = false; + +static NfcCommand metroflip_scene_troika_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}; + MfClassicKeyType key_type = MfClassicKeyTypeA; + bit_lib_num_to_bytes_be(troika_1k_keys[app->sec_num].a, COUNT_OF(key.data), key.data); + if(!checked) { + 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; + app->sec_num++; + checked = true; + } + 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); + if(mfc_data->type == MfClassicType1k) { + bit_lib_num_to_bytes_be(troika_1k_keys[app->sec_num].a, COUNT_OF(key.data), key.data); + + 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_data->type == MfClassicType4k) { + bit_lib_num_to_bytes_be(troika_4k_keys[app->sec_num].a, COUNT_OF(key.data), key.data); + + 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 == 40) { + mfc_event->data->read_sector_request_data.key_provided = false; + app->sec_num = 0; + } + app->sec_num++; + } + } else if(mfc_event->type == MfClassicPollerEventTypeSuccess) { + const MfClassicData* mfc_data = nfc_device_get_data(app->nfc_device, NfcProtocolMfClassic); + FuriString* parsed_data = furi_string_alloc(); + Widget* widget = app->widget; + if(!troika_parse(parsed_data, mfc_data)) { + furi_string_reset(app->text_box_store); + FURI_LOG_I(TAG, "Unknown card type"); + furi_string_printf(parsed_data, "\e#Unknown card\n"); + } + widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data)); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app); + + furi_string_free(parsed_data); + view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget); + metroflip_app_blink_stop(app); + command = NfcCommandStop; + } else if(mfc_event->type == MfClassicPollerEventTypeFail) { + FURI_LOG_I(TAG, "fail"); + command = NfcCommandStop; + } + + return command; +} + +void metroflip_scene_troika_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_troika_poller_callback, app); + + metroflip_app_blink_start(app); +} + +bool metroflip_scene_troika_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_troika_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/navigo.h b/metroflip/scenes/navigo.h index 42206f83c..eb91df1e5 100644 --- a/metroflip/scenes/navigo.h +++ b/metroflip/scenes/navigo.h @@ -1,305 +1,732 @@ +#include +#include +#include "../api/calypso/calypso_util.h" +#include "../api/calypso/cards/navigo.h" +#include +#include + #ifndef METRO_LIST_H #define METRO_LIST_H -typedef struct { - const char* name; - const char* stations[14]; -} MetroLine; - #ifndef NAVIGO_H #define NAVIGO_H +void metroflip_back_button_widget_callback(GuiButtonType result, InputType type, void* context); +void metroflip_next_button_widget_callback(GuiButtonType result, InputType type, void* context); + // Service Providers -static const char* SERVICE_PROVIDERS[] = {[2] = "SNCF", [3] = "RATP"}; +static const char* SERVICE_PROVIDERS[] = { + [2] = "SNCF", + [3] = "RATP", + [115] = "CSO (VEOLIA)", + [116] = "R'Bus (VEOLIA)", + [156] = "Phebus", + [175] = "RATP (Veolia Transport Nanterre)"}; // Transport Types static const char* TRANSPORT_LIST[] = { - [1] = "Urban Bus", - [2] = "Interurban Bus", + [1] = "Bus Urbain", + [2] = "Bus Interurbain", [3] = "Metro", [4] = "Tram", [5] = "Train", [8] = "Parking"}; +typedef enum { + BUS_URBAIN = 1, + BUS_INTERURBAIN = 2, + METRO = 3, + TRAM = 4, + TRAIN = 5, + PARKING = 8 +} TRANSPORT_TYPE; + +typedef enum { + NAVIGO_EASY = 0, + NAVIGO_DECOUVERTE = 1, + NAVIGO_STANDARD = 2, + NAVIGO_INTEGRAL = 6, + IMAGINE_R = 14 +} CARD_STATUS; + // Transition Types static const char* TRANSITION_LIST[] = { - [1] = "Entry", - [2] = "Exit", - [4] = "Inspection", - [6] = "Interchange (entry)", - [7] = "Interchange (exit)"}; + [1] = "Validation en entree", + [2] = "Validation en sortie", + [4] = "Controle volant (a bord)", + [5] = "Validation de test", + [6] = "Validation en correspondance (entree)", + [7] = "Validation en correspondance (sortie)", + [9] = "Annulation de validation", + [10] = "Validation en entree", + [13] = "Distribution", + [15] = "Invalidation"}; #endif // NAVIGO_H -const MetroLine METRO_LIST[] = { - [0] = - {"Cite", - {"Saint-Michel", - "Odeon", - "Cluny - La Sorbonne", - "Maubert - Mutualite", - "Luxembourg", - "Chatelet", - "Les Halles", - "Les Halles", - "Louvre - Rivoli", - "Pont Neuf", - "Cite", - "Hotel de Ville"}}, - [1] = - {"Rennes", - {"Cambronne", - "Sevres - Lecourbe", - "Segur", - "Saint-Francois-Xavier", - "Duroc", - "Vaneau", - "Sevres - Babylone", - "Rue du Bac", - "Rennes", - "Saint-Sulpice", - "Mabillon", - "Saint-Germain-des-Pres"}}, - [2] = - {"Villette", - {"Porte de la Villette", - "Aubervilliers - Pantin - Quatre Chemins", - "Fort d'Aubervilliers", - "La Courneuve - 8 Mai 1945", - "Hoche", - "Eglise de Pantin", - "Bobigny - Pantin - Raymond Queneau", - "Bobigny - Pablo Picasso"}}, - [3] = - {"Montparnasse", - {"Pernety", - "Plaisance", - "Gaite", - "Edgar Quinet", - "Vavin", - "Montparnasse - Bienvenue", - "Saint-Placide", - "Notre-Dame-des-Champs"}}, - [4] = - {"Nation", - {"Robespierre", - "Porte de Montreuil", - "Maraichers", - "Buzenval", - "Rue des Boulets", - "Porte de Vincennes", - "Picpus", - "Nation", - "Avron", - "Alexandre Dumas"}}, - [5] = - {"Saint-Lazare", - {"Malesherbes", - "Monceau", - "Villiers", - "Quatre-Septembre", - "Opera", - "Auber", - "Havre - Caumartin", - "Saint-Lazare", - "Saint-Lazare", - "Saint-Augustin", - "Europe", - "Liege"}}, - [6] = - {"Auteuil", - {"Porte de Saint-Cloud", - "Porte d'Auteuil", - "Eglise d'Auteuil", - "Michel-Ange - Auteuil", - "Michel-Ange - Molitor", - "Chardon-Lagache", - "Mirabeau", - "Exelmans", - "Jasmin"}}, - [7] = - {"Republique", - {"Rambuteau", - "Arts et Metiers", - "Jacques Bonsergent", - "Goncourt", - "Temple", - "Republique", - "Oberkampf", - "Parmentier", - "Filles du Calvaire", - "Saint-Sebastien - Froissart", - "Richard-Lenoir", - "Saint-Ambroise"}}, - [8] = - {"Austerlitz", - {"Quai de la Gare", - "Chevaleret", - "Saint-Marcel", - "Gare d'Austerlitz", - "Gare de Lyon", - "Quai de la Rapee"}}, - [9] = - {"Invalides", - {"Champs-Elysees - Clemenceau", - "Concorde", - "Madeleine", - "Bir-Hakeim", - "Ecole Militaire", - "La Tour-Maubourg", - "Invalides", - "Saint-Denis - Universite", - "Varenne", - "Assemblee nationale", - "Solferino"}}, - [10] = - {"Sentier", - {"Tuileries", - "Palais Royal - Musee du Louvre", - "Pyramides", - "Bourse", - "Grands Boulevards", - "Richelieu - Drouot", - "Bonne Nouvelle", - "Strasbourg - Saint-Denis", - "Chateau d'Eau", - "Sentier", - "Reaumur - Sebastopol", - "Etienne Marcel"}}, - [11] = - {"Ile Saint-Louis", - {"Faidherbe - Chaligny", - "Reuilly - Diderot", - "Montgallet", - "Censier - Daubenton", - "Place Monge", - "Cardinal Lemoine", - "Jussieu", - "Sully - Morland", - "Pont Marie", - "Saint-Paul", - "Bastille", - "Chemin Vert", - "Breguet - Sabin", - "Ledru-Rollin"}}, - [12] = - {"Daumesnil", - {"Porte Doree", - "Porte de Charenton", - "Bercy", - "Dugommier", - "Michel Bizot", - "Daumesnil", - "Bel-Air"}}, - [13] = - {"Italie", - {"Porte de Choisy", - "Porte d'Italie", - "Cite universitaire", - "Maison Blanche", - "Tolbiac", - "Nationale", - "Campo-Formio", - "Les Gobelins", - "Place d'Italie", - "Corvisart"}}, - [14] = - {"Denfert", - {"Cour Saint-Emilion", - "Porte d'Orleans", - "Bibliotheque Francois Mitterrand", - "Mouton-Duvernet", - "Alesia", - "Olympiades", - "Glaciere", - "Saint-Jacques", - "Raspail", - "Denfert-Rochereau"}}, - [15] = - {"Felix Faure", - {"Falguiere", - "Pasteur", - "Volontaires", - "Vaugirard", - "Convention", - "Porte de Versailles", - "Balard", - "Lourmel", - "Boucicaut", - "Felix Faure", - "Charles Michels", - "Javel - Andre Citroen"}}, +static const char* METRO_STATION_LIST[32][16] = + {[1] = + {[0] = "Cite", + [1] = "Saint-Michel", + [4] = "Odeon", + [5] = "Cluny - La Sorbonne", + [6] = "Maubert - Mutualite", + [7] = "Luxembourg", + [8] = "Châtelet", + [9] = "Les Halles", + [10] = "Les Halles", + [12] = "Louvre - Rivoli", + [13] = "Pont Neuf", + [14] = "Cite", + [15] = "Hotel de Ville"}, + [2] = + {[0] = "Rennes", + [2] = "Cambronne", + [3] = "Sevres - Lecourbe", + [4] = "Segur", + [6] = "Saint-François-Xavier", + [7] = "Duroc", + [8] = "Vaneau", + [9] = "Sevres - Babylone", + [10] = "Rue du Bac", + [11] = "Rennes", + [12] = "Saint-Sulpice", + [14] = "Mabillon", + [15] = "Saint-Germain-des-Pres"}, + [3] = + {[0] = "Villette", + [4] = "Porte de la Villette", + [5] = "Aubervilliers - Pantin - Quatre Chemins", + [6] = "Fort d'Aubervilliers", + [7] = "La Courneuve - 8 Mai 1945", + [9] = "Hoche", + [10] = "Eglise de Pantin", + [11] = "Bobigny - Pantin - Raymond Queneau", + [12] = "Bobigny - Pablo Picasso"}, + [4] = + {[0] = "Montparnasse", + [2] = "Pernety", + [3] = "Plaisance", + [4] = "Gaite", + [6] = "Edgar Quinet", + [7] = "Vavin", + [8] = "Montparnasse - Bienvenue", + [12] = "Saint-Placide", + [14] = "Notre-Dame-des-Champs"}, + [5] = + {[0] = "Nation", + [2] = "Robespierre", + [3] = "Porte de Montreuil", + [4] = "Maraichers", + [5] = "Buzenval", + [6] = "Rue des Boulets", + [7] = "Porte de Vincennes", + [9] = "Picpus", + [10] = "Nation", + [12] = "Avron", + [13] = "Alexandre Dumas"}, + [6] = + {[0] = "Saint-Lazare", + [1] = "Malesherbes", + [2] = "Monceau", + [3] = "Villiers", + [4] = "Quatre-Septembre", + [5] = "Opera", + [6] = "Auber", + [7] = "Havre - Caumartin", + [8] = "Saint-Lazare", + [9] = "Saint-Lazare", + [10] = "Saint-Augustin", + [12] = "Europe", + [13] = "Liege"}, + [7] = + {[0] = "Auteuil", + [3] = "Porte de Saint-Cloud", + [7] = "Porte d'Auteuil", + [8] = "eglise d'Auteuil", + [9] = "Michel-Ange - Auteuil", + [10] = "Michel-Ange - Molitor", + [11] = "Chardon-Lagache", + [12] = "Mirabeau", + [14] = "Exelmans", + [15] = "Jasmin"}, + [8] = + {[0] = "Republique", + [1] = "Rambuteau", + [3] = "Arts et Metiers", + [4] = "Jacques Bonsergent", + [5] = "Goncourt", + [6] = "Temple", + [7] = "Republique", + [10] = "Oberkampf", + [11] = "Parmentier", + [12] = "Filles du Calvaire", + [13] = "Saint-Sebastien - Froissart", + [14] = "Richard-Lenoir", + [15] = "Saint-Ambroise"}, + [9] = + {[0] = "Austerlitz", + [1] = "Quai de la Gare", + [2] = "Chevaleret", + [4] = "Saint-Marcel", + [7] = "Gare d'Austerlitz", + [8] = "Gare de Lyon", + [10] = "Quai de la Rapee"}, + [10] = + {[0] = "Invalides", + [1] = "Champs-elysees - Clemenceau", + [2] = "Concorde", + [3] = "Madeleine", + [4] = "Bir-Hakeim", + [7] = "ecole Militaire", + [8] = "La Tour-Maubourg", + [9] = "Invalides", + [11] = "Saint-Denis - Universite", + [12] = "Varenne", + [13] = "Assemblee nationale", + [14] = "Solferino"}, + [11] = + {[0] = "Sentier", + [1] = "Tuileries", + [2] = "Palais Royal - Musee du Louvre", + [3] = "Pyramides", + [4] = "Bourse", + [6] = "Grands Boulevards", + [7] = "Richelieu - Drouot", + [8] = "Bonne Nouvelle", + [10] = "Strasbourg - Saint-Denis", + [11] = "Château d'Eau", + [13] = "Sentier", + [14] = "Reaumur - Sebastopol", + [15] = "etienne Marcel"}, + [12] = + {[0] = "ile Saint-Louis", + [1] = "Faidherbe - Chaligny", + [2] = "Reuilly - Diderot", + [3] = "Montgallet", + [4] = "Censier - Daubenton", + [5] = "Place Monge", + [6] = "Cardinal Lemoine", + [7] = "Jussieu", + [8] = "Sully - Morland", + [9] = "Pont Marie", + [10] = "Saint-Paul", + [12] = "Bastille", + [13] = "Chemin Vert", + [14] = "Breguet - Sabin", + [15] = "Ledru-Rollin"}, + [13] = + {[0] = "Daumesnil", + [1] = "Porte Doree", + [3] = "Porte de Charenton", + [7] = "Bercy", + [8] = "Dugommier", + [10] = "Michel Bizot", + [11] = "Daumesnil", + [12] = "Bel-Air"}, + [14] = + {[0] = "Italie", + [2] = "Porte de Choisy", + [3] = "Porte d'Italie", + [4] = "Cite universitaire", + [9] = "Maison Blanche", + [10] = "Tolbiac", + [11] = "Nationale", + [12] = "Campo-Formio", + [13] = "Les Gobelins", + [14] = "Place d'Italie", + [15] = "Corvisart"}, + [15] = + {[0] = "Denfert", + [1] = "Cour Saint-Emilion", + [2] = "Porte d'Orleans", + [3] = "Bibliotheque François Mitterrand", + [4] = "Mouton-Duvernet", + [5] = "Alesia", + [6] = "Olympiades", + [8] = "Glaciere", + [9] = "Saint-Jacques", + [10] = "Raspail", + [14] = "Denfert-Rochereau"}, + [16] = + {[0] = "Felix Faure", + [1] = "Falguiere", + [2] = "Pasteur", + [3] = "Volontaires", + [4] = "Vaugirard", + [5] = "Convention", + [6] = "Porte de Versailles", + [9] = "Balard", + [10] = "Lourmel", + [11] = "Boucicaut", + [12] = "Felix Faure", + [13] = "Charles Michels", + [14] = "Javel - Andre Citroen"}, + [17] = + {[0] = "Passy", + [2] = "Porte Dauphine", + [4] = "La Motte-Picquet - Grenelle", + [5] = "Commerce", + [6] = "Avenue emile Zola", + [7] = "Dupleix", + [8] = "Passy", + [9] = "Ranelagh", + [11] = "La Muette", + [13] = "Rue de la Pompe", + [14] = "Boissiere", + [15] = "Trocadero"}, + [18] = + {[0] = "Etoile", + [1] = "Iena", + [3] = "Alma - Marceau", + [4] = "Miromesnil", + [5] = "Saint-Philippe du Roule", + [7] = "Franklin D. Roosevelt", + [8] = "George V", + [9] = "Kleber", + [10] = "Victor Hugo", + [11] = "Argentine", + [12] = "Charles de Gaulle - Itoile", + [14] = "Ternes", + [15] = "Courcelles"}, + [19] = + {[0] = "Clichy - Saint Ouen", + [1] = "Mairie de Clichy", + [2] = "Gabriel Peri", + [3] = "Les Agnettes", + [4] = "Asnieres - Gennevilliers - Les Courtilles", + [9] = "La Chapelle)", + [10] = "Garibaldi", + [11] = "Mairie de Saint-Ouen", + [13] = "Carrefour Pleyel", + [14] = "Saint-Denis - Porte de Paris", + [15] = "Basilique de Saint-Denis"}, + [20] = + {[0] = "Montmartre", + [1] = "Porte de Clignancourt", + [6] = "Porte de la Chapelle", + [7] = "Marx Dormoy", + [9] = "Marcadet - Poissonniers", + [10] = "Simplon", + [11] = "Jules Joffrin", + [12] = "Lamarck - Caulaincourt"}, + [21] = + {[0] = "Lafayette", + [1] = "Chaussee d'Antin - La Fayette", + [2] = "Le Peletier", + [3] = "Cadet", + [4] = "Château Rouge", + [7] = "Barbes - Rochechouart", + [8] = "Gare du Nord", + [9] = "Gare de l'Est", + [10] = "Poissonniere", + [11] = "Château-Landon"}, + [22] = + {[0] = "Buttes Chaumont", + [1] = "Porte de Pantin", + [2] = "Ourcq", + [4] = "Corentin Cariou", + [6] = "Crimee", + [8] = "Riquet", + [9] = "La Chapelle", + [10] = "Louis Blanc", + [11] = "Stalingrad", + [12] = "Jaures", + [13] = "Laumiere", + [14] = "Bolivar", + [15] = "Colonel Fabien"}, + [23] = + {[0] = "Belleville", + [2] = "Porte des Lilas", + [3] = "Mairie des Lilas", + [4] = "Porte de Bagnolet", + [5] = "Gallieni", + [8] = "Place des Fetes", + [9] = "Botzaris", + [10] = "Danube", + [11] = "Pre Saint-Gervais", + [13] = "Buttes Chaumont", + [14] = "Jourdain", + [15] = "Telegraphe"}, + [24] = + {[0] = "Pere Lachaise", + [1] = "Voltaire", + [2] = "Charonne", + [4] = "Pere Lachaise", + [5] = "Menilmontant", + [6] = "Rue Saint-Maur", + [7] = "Philippe Auguste", + [8] = "Saint-Fargeau", + [9] = "Pelleport", + [10] = "Gambetta", + [12] = "Belleville", + [13] = "Couronnes", + [14] = "Pyrenees"}, + [25] = + {[0] = "Charenton", + [2] = "Croix de Chavaux", + [3] = "Mairie de Montreuil", + [4] = "Maisons-Alfort - Les Juilliottes", + [5] = "Creteil - L'echat", + [6] = "Creteil - Universite", + [7] = "Creteil - Prefecture", + [8] = "Saint-Mande", + [10] = "Berault", + [11] = "Château de Vincennes", + [12] = "Liberte", + [13] = "Charenton - ecoles", + [14] = "ecole veterinaire de Maisons-Alfort", + [15] = "Maisons-Alfort - Stade"}, + [26] = + {[0] = "Ivry - Villejuif", + [3] = "Porte d'Ivry", + [4] = "Pierre et Marie Curie", + [5] = "Mairie d'Ivry", + [6] = "Le Kremlin-Bicetre", + [7] = "Villejuif - Leo Lagrange", + [8] = "Villejuif - Paul Vaillant-Couturier", + [9] = "Villejuif - Louis Aragon"}, + [27] = + {[0] = "Vanves", + [2] = "Porte de Vanves", + [7] = "Malakoff - Plateau de Vanves", + [8] = "Malakoff - Rue etienne Dolet", + [9] = "Châtillon - Montrouge"}, + [28] = + {[0] = "Issy", + [2] = "Corentin Celton", + [3] = "Mairie d'Issy", + [8] = "Marcel Sembat", + [9] = "Billancourt", + [10] = "Pont de Sevres"}, + [29] = + {[0] = "Levallois", + [4] = "Boulogne - Jean Jaures", + [5] = "Boulogne - Pont de Saint-Cloud", + [8] = "Les Sablons", + [9] = "Pont de Neuilly", + [10] = "Esplanade de la Defense", + [11] = "La Defense", + [12] = "Porte de Champerret", + [13] = "Louise Michel", + [14] = "Anatole France", + [15] = "Pont de Levallois - Becon"}, + [30] = + {[0] = "Pereire", + [1] = "Porte Maillot", + [4] = "Wagram", + [5] = "Pereire", + [8] = "Brochant", + [9] = "Porte de Clichy", + [12] = "Guy Moquet", + [13] = "Porte de Saint-Ouen"}, + [31] = { + [0] = "Pigalle", + [2] = "Funiculaire de Montmartre (station inferieure)", + [3] = "Funiculaire de Montmartre (station superieure)", + [4] = "Anvers", + [5] = "Abbesses", + [6] = "Pigalle", + [7] = "Blanche", + [8] = "Trinite - d'Estienne d'Orves", + [9] = "Notre-Dame-de-Lorette", + [10] = "Saint-Georges", + [12] = "Rome", + [13] = "Place de Clichy", + [14] = "La Fourche"}}; + +static const char* TRAIN_LINES_LIST[77] = { + [1] = "RER B", [3] = "RER B", [6] = "RER A", [14] = "RER B", + [15] = "RER B", [16] = "RER A", [17] = "RER A", [18] = "RER B", + [20] = "Transilien P", [21] = "Transilien P", [22] = "T4", [23] = "Transilien P", + [26] = "RER A", [28] = "RER B", [30] = "Transilien L", [31] = "Transilien L", + [32] = "Transilien J", [33] = "RER A", [35] = "Transilien J", [40] = "RER D", + [41] = "RER C", [42] = "RER C", [43] = "Transilien R", [44] = "Transilien R", + [45] = "RER D", [50] = "Transilien H", [51] = "Transilien K", [52] = "RER D", + [53] = "Transilien H", [54] = "Transilien J", [55] = "RER C", [56] = "Transilien H", + [57] = "Transilien H", [60] = "Transilien N", [61] = "Transilien N", [63] = "RER C", + [64] = "RER C", [65] = "Transilien V", [70] = "RER B", [72] = "Transilien J", + [73] = "Transilien J", [75] = "RER C", [76] = "RER C"}; + +static const char* TRAIN_STATION_LIST[77][19] = { + [1] = {[0] = "Châtelet-Les Halles", [1] = "Châtelet-Les Halles", [7] = "Luxembourg"}, + [3] = {[0] = "Saint-Michel Notre-Dame"}, + [6] = {[0] = "Auber", [6] = "Auber"}, + [14] = {[4] = "Cite Universitaire"}, + [15] = {[12] = "Port Royal"}, [16] = - {"Passy", - {"Porte Dauphine", - "La Motte-Picquet - Grenelle", - "Commerce", - "Avenue Emile Zola", - "Dupleix", - "Passy", - "Ranelagh", - "La Muette", - "Rue de la Pompe", - "Boissiere", - "Trocadero"}}, + {[1] = "Nation", + [2] = "Fontenay-sous-Bois | Vincennes", + [3] = "Joinville-le-Pont | Nogent-sur-Marne", + [4] = "Saint-Maur Creteil", + [5] = "Le Parc de Saint-Maur", + [6] = "Champigny", + [7] = "La Varenne-Chennevieres", + [8] = "Boissy-Saint-Leger | Sucy Bonneuil"}, [17] = - {"Etoile", - {"Iena", - "Alma - Marceau", - "Miromesnil", - "Saint-Philippe du Roule", - "Franklin D. Roosevelt", - "George V", - "Kleber", - "Victor Hugo", - "Argentine", - "Charles de Gaulle - Etoile", - "Ternes", - "Courcelles"}}, + {[1] = "Charles de Gaulle-Etoile", + [4] = "La Defense (Grande Arche)", + [5] = "Nanterre-Ville", + [6] = "Rueil-Malmaison", + [8] = "Chatou-Croissy", + [9] = "Le Vesinet-Centre | Le Vesinet-Le Pecq | Saint-Germain-en-Laye"}, [18] = - {"Clichy - Saint Ouen", - {"Mairie de Clichy", - "Gabriel Peri", - "Les Agnettes", - "Asnieres - Gennevilliers - Les Courtilles", - "La Chapelle", - "Garibaldi", - "Mairie de Saint-Ouen", - "Carrefour Pleyel", - "Saint-Denis - Porte de Paris", - "Basilique de Saint-Denis"}}, - [19] = - {"Montmartre", - {"Porte de Clignancourt", - "Porte de la Chapelle", - "Marx Dormoy", - "Marcadet - Poissonniers", - "Simplon", - "Jules Joffrin", - "Lamarck - Caulaincourt"}}, + {[0] = "Denfert-Rochereau", + [1] = "Gentilly", + [2] = "Arcueil-Cachan | Laplace", + [3] = "Bagneux | Bourg-la-Reine", + [4] = "La Croix-de-Berny | Parc de Sceaux", + [5] = "Antony | Fontaine-Michalon | Les Baconnets", + [6] = "Massy-Palaiseau | Massy-Verrieres", + [7] = "Palaiseau Villebon | Palaiseau", + [8] = "Lozere", + [9] = "Le Guichet | Orsay-Ville", + [10] = + "Bures-sur-Yvette | Courcelle-sur-Yvette | Gif-sur-Yvette | La Hacquiniere | Saint-Remy-les-Chevreuse"}, [20] = - {"Lafayette", - {"Chaussee d'Antin - La Fayette", - "Le Peletier", - "Cadet", - "Chateau Rouge", - "Barbes - Rochechouart", - "Gare du Nord", - "Gare de l'Est", - "Poissonniere", - "Chateau-Landon"}}, - [21] = { - "Buttes Chaumont", - {"Porte de Pantin", - "Ourcq", - "Corentin Cariou", - "Crimee", - "Riquet", - "La Chapelle", - "Belleville", - "Botzaris", - "Pelleport", - "Place des Fetes", - "Cimetiere du Pere Lachaise"}}}; + {[1] = "Gare de l'Est", + [4] = "Pantin", + [5] = "Noisy-le-Sec", + [6] = "Bondy", + [7] = "Gagny | Le Raincy Villemomble Montfermeil", + [9] = "Chelles Gournay | Le Chenay Gagny", + [10] = "Vaires Torcy", + [11] = "Lagny-Thorigny", + [13] = "Esbly", + [14] = "Meaux", + [15] = "Changis-Saint-Jean | Isles-Armentieres Congis | Lizy-sur-Ourcq | Trilport", + [16] = "Crouy-sur-Ourcq | La Ferte-sous-Jouarre | Nanteuil Saacy"}, + [21] = + {[5] = "Rosny-Bois-Perrier | Rosny-sous-Bois | Val de Fontenay", + [6] = "Nogent Le-Perreux", + [7] = "Les Boullereaux Champigny", + [8] = "Villiers-sur-Marne Plessis-Trevise", + [9] = "Les Yvris Noisy-le-Grand", + [10] = "Emerainville Pontault-Combault | Roissy-en-Brie", + [11] = "Ozoir-la-Ferriere", + [12] = "Gretz-Armainvilliers | Tournan", + [15] = + "Courquetaine | Faremoutiers Pommeuse | Guerard La-Celle-sur-Morin | Liverdy en Brie | Marles-en-Brie | Mormant | Mortcerf | Mouroux | Ozouer le voulgis | Verneuil-l'Etang | Villepatour - Presles | Yebles - Guignes | Yebles", + [16] = + "Chailly Boissy-le-Châtel | Chauffry | Coulommiers | Jouy-sur-Morin Le-Marais | Nangis | Saint-Remy-la-Vanne | Saint-Simeon", + [17] = + "Champbenoist-Poigny | La Ferte-Gaucher | Longueville | Provins | Sainte-Colombe-Septveilles", + [18] = "Flamboin | Meilleray | Villiers St Georges"}, + [22] = + {[7] = + "Allee de la Tour-Rendez-Vous | La Remise-a-Jorelle | Les Coquetiers | Les Pavillons-sous-Bois", + [8] = "Gargan", + [9] = "Freinville Sevran | L'Abbaye"}, + [23] = + {[13] = "Couilly Saint-Germain Quincy | Les Champs-Forts | Montry Conde", + [14] = "Crecy-en-Brie La Chapelle | Villiers-Montbarbin"}, + [26] = + {[5] = "Val de Fontenay", + [6] = "Bry-sur-Marne | Neuilly-Plaisance", + [7] = "Noisy-le-Grand (Mont d'Est)", + [8] = "Noisy-Champs", + [10] = "Lognes | Noisiel | Torcy", + [11] = "Bussy-Saint-Georges", + [12] = "Val d'europe", + [13] = "Marne-la-Vallee Chessy"}, + [28] = {[4] = "Fontenay-aux-Roses | Robinson | Sceaux"}, + [30] = + {[1] = "Gare Saint-Lazare", + [3] = "Pont Cardinet", + [4] = + "Asnieres | Becon-les-Bruyeres | Clichy Levallois | Courbevoie | La Defense (Grande Arche)", + [5] = "Puteaux | Suresnes Mont-Valerien", + [7] = "Garches Marne-la-Coquette | Le Val d'Or | Saint-Cloud", + [8] = "Vaucresson", + [9] = "Bougival | La Celle-Saint-Cloud | Louveciennes | Marly-le-Roi", + [10] = "L'Etang-la-Ville | Saint-Nom-la-Breteche Foret de Marly"}, + [31] = + {[7] = "Chaville-Rive Droite | Sevres Ville-d'Avray | Viroflay-Rive Droite", + [8] = "Montreuil | Versailles-Rive Droite"}, + [32] = + {[5] = "La Garenne-Colombes | Les Vallees | Nanterre-Universite", + [7] = "Houilles Carrieres-sur-Seine | Sartrouville", + [9] = "Maisons-Laffitte", + [10] = "Poissy", + [11] = "Villennes-sur-Seine", + [12] = "Les Clairieres de Verneuil | Vernouillet Verneuil", + [13] = "Aubergenville-Elisabethville | Les Mureaux", + [14] = "Epone Mezieres", + [16] = "Bonnieres | Mantes-Station | Mantes-la-Jolie | Port-Villez | Rosny-sur-Seine"}, + [33] = + {[10] = "Acheres-Grand-Cormier | Acheres-Ville", + [11] = "Cergy-Prefecture | Neuville-Universite", + [12] = "Cergy-Saint-Christophe | Cergy-le-Haut"}, + [35] = + {[4] = "Bois-Colombes", + [5] = "Colombes | Le Stade", + [6] = "Argenteuil | Argenteuil", + [8] = "Cormeilles-en-Parisis | Val d'Argenteuil | Val d'Argenteuil", + [9] = "Herblay | La Frette Montigny", + [10] = "Conflans-Fin d'Oise | Conflans-Sainte-Honorine", + [11] = "Andresy | Chanteloup-les-Vignes | Maurecourt", + [12] = "Triel-sur-Seine | Vaux-sur-Seine", + [13] = "Meulan Hadricourt | Thun-le-Paradis", + [14] = "Gargenville | Juziers", + [15] = "Issou Porcheville | Limay", + [16] = "Breval | Menerville"}, + [40] = + {[1] = "Gare de Lyon", + [5] = "Le Vert de Maisons | Maisons-Alfort Alfortville", + [6] = "Villeneuve-Prairie", + [7] = "Villeneuve-Triage", + [8] = "Villeneuve-Saint-Georges", + [9] = "Juvisy | Vigneux-sur-Seine", + [10] = "Ris-Orangis | Viry-Châtillon", + [11] = "Evry Val de Seine | Grand-Bourg", + [12] = "Corbeil-Essonnes | Mennecy | Moulin-Galant", + [13] = "Ballancourt | Fontenay le Vicomte", + [14] = "La Ferte-Alais", + [16] = "Boutigny | Maisse", + [17] = "Boigneville | Buno-Gironville"}, + [41] = + {[0] = "Musee d'Orsay | Saint-Michel Notre-Dame", + [1] = "Gare d'Austerlitz", + [2] = "Bibliotheque-Francois Mitterrand", + [4] = "Ivry-sur-Seine | Vitry-sur-Seine", + [5] = "Choisy-le-Roi | Les Ardoines", + [7] = "Villeneuve-le-Roi", + [8] = "Ablon", + [9] = "Athis-Mons"}, + [42] = + {[9] = "Epinay-sur-Orge | Savigny-sur-Orge", + [10] = "Sainte-Genevieve-des-Bois", + [11] = "Saint-Michel-sur-Orge", + [12] = "Bretigny-sur-Orge | Marolles-en-Hurepoix", + [13] = "Bouray | Lardy", + [14] = "Chamarande | Etampes | Etrechy", + [16] = "Saint-Martin d'Etampes", + [17] = "Guillerval"}, + [43] = + {[9] = "Montgeron Crosne | Yerres", + [10] = "Brunoy", + [11] = "Boussy-Saint-Antoine | Combs-la-Ville Quincy", + [12] = "Lieusaint Moissy", + [13] = "Cesson | Savigny-le-Temple Nandy", + [15] = "Le Mee | Melun", + [16] = "Chartrettes | Fontaine-le-Port | Livry-sur-Seine", + [17] = + "Champagne-sur-Seine | Hericy | La Grande Paroisse | Vernou-sur-Seine | Vulaines-sur-Seine Samoreau"}, + [44] = + {[12] = "Essonnes-Robinson | Villabe", + [13] = "Coudray-Montceaux | Le Plessis-Chenet-IBM | Saint-Fargeau", + [14] = "Boissise-le-Roi | Ponthierry Pringy", + [15] = "Vosves", + [16] = "Bois-le-Roi", + [17] = + "Bagneaux-sur-Loing | Bourron-Marlotte Grez | Fontainebleau-Avon | Montereau | Montigny-sur-Loing | Moret Veneux-les-Sablons | Nemours Saint-Pierre | Saint-Mammes | Souppes | Thomery"}, + [45] = + {[10] = "Grigny-Centre", + [11] = "Evry Courcouronnes | Orangis Bois de l'Epine", + [12] = "Le Bras-de-Fer - Evry Genopole"}, + [50] = + {[0] = "Haussmann-Saint-Lazare", + [1] = "Gare du Nord | Magenta | Paris-Nord", + [5] = "Epinay-Villetaneuse | Saint-Denis | Sevres-Rive Gauche", + [6] = "La Barre-Ormesson", + [7] = "Champ de Courses d'Enghien | Enghien-les-Bains", + [8] = "Ermont-Eaubonne | Ermont-Halte | Gros-Noyer Saint-Prix", + [9] = "Saint-Leu-La-Foret | Taverny | Vaucelles", + [10] = "Bessancourt | Frepillon | Mery", + [11] = "Meriel | Valmondois", + [12] = "Bruyeres-sur-Oise | Champagne-sur-Oise | L'Isle-Adam Parmain | Persan Beaumont"}, + [51] = + {[4] = "La Courneuve-Aubervilliers | La Plaine-Stade de France", + [5] = "Le Bourget", + [7] = "Blanc-Mesnil | Drancy", + [8] = "Aulnay-sous-Bois", + [9] = "Sevran Livry | Vert-Galant", + [10] = "Villeparisis", + [11] = "Compans | Mitry-Claye", + [12] = "Dammartin Juilly Saint-Mard | Thieux Nantouillet"}, + [52] = + {[5] = "Stade de France-Saint-Denis", + [6] = "Pierrefitte Stains", + [7] = "Garges-Sarcelles", + [8] = "Villiers-le-Bel (Gonesse - Arnouville)", + [10] = "Goussainville | Les Noues | Louvres", + [11] = "La Borne-Blanche | Survilliers-Fosses"}, + [53] = + {[6] = "Deuil Montmagny", + [7] = "Groslay", + [8] = "Sarcelles Saint-Brice", + [9] = "Domont | Ecouen Ezanville", + [10] = "Bouffemont Moisselles | Montsoult Maffliers", + [11] = "Belloy-Saint-Martin | Luzarches | Seugy | Viarmes | Villaines"}, + [54] = + {[8] = "Cernay", + [9] = "Franconville Plessis-Bouchard | Montigny-Beauchamp", + [10] = "Pierrelaye", + [11] = "Pontoise | Saint-Ouen-l'Aumone-Liesse", + [12] = "Boissy-l'Aillerie | Osny", + [15] = "Chars | Montgeroult Courcelles | Santeuil Le Perchay | Us"}, + [55] = + {[0] = + "Avenue Foch | Avenue Henri-Martin | Boulainvilliers | Kennedy Radio-France | Neuilly-Porte Maillot (Palais des congres)", + [1] = "Pereire-Levallois", + [2] = "Porte de Clichy", + [3] = "Saint-Ouen", + [4] = "Les Gresillons", + [5] = "Gennevilliers", + [6] = "Epinay-sur-Seine", + [7] = "Saint-Gratien"}, + [56] = {[11] = "Auvers-sur-Oise | Chaponval | Epluches | Pont Petit"}, + [57] = {[11] = "Presles Courcelles", [12] = "Nointel Mours"}, + [60] = + {[1] = "Gare Montparnasse", + [4] = "Clamart | Vanves Malakoff", + [5] = "Bellevue | Bievres | Meudon", + [6] = "Chaville-Rive Gauche | Chaville-Velizy | Viroflay-Rive Gauche", + [7] = "Versailles-Chantiers", + [10] = "Saint-Cyr", + [11] = "Saint-Quentin-en-Yvelines - Montigny le Bretonneux | Trappes", + [12] = "Coignieres | La Verriere", + [13] = "Les Essarts-le-Roi", + [14] = "Le Perray | Rambouillet", + [15] = "Gazeran"}, + [61] = + {[10] = "Fontenay-le-Fleury", + [11] = "Villepreux Les-Clayes", + [12] = "Plaisir Grignon | Plaisir Les-Clayes", + [13] = "Beynes | Mareil-sur-Mauldre | Maule | Nezel Aulnay", + [15] = + "Garancieres La-Queue | Montfort-l'Amaury Mere | Orgerus Behoust | Tacoigneres Richebourg | Villiers Neauphle Pontchartrain", + [16] = "Houdan"}, + [63] = {[7] = "Porchefontaine | Versailles-Rive Gauche"}, + [64] = + {[0] = "Invalides | Pont de l'alma", + [1] = "Champ de Mars-Tour Eiffel", + [2] = "Javel", + [3] = "Boulevard Victor - Pont du Garigliano | Issy-Val de Seine | Issy", + [5] = "Meudon-Val-Fleury"}, + [65] = + {[8] = "Jouy-en-Josas | Petit-Jouy-les-Loges", + [9] = "Vauboyen", + [10] = "Igny", + [11] = "Massy-Palaiseau", + [12] = "Longjumeau", + [13] = "Chilly-Mazarin", + [14] = "Gravigny-Balizy | Petit-Vaux"}, + [70] = + {[9] = "Parc des Expositions | Sevran-Beaudottes | Villepinte", + [10] = "Aeroport Charles de Gaulle"}, + [72] = {[7] = "Sannois"}, + [73] = {[11] = "Eragny Neuville | Saint-Ouen-l'Aumone (Eglise)"}, + [75] = + {[7] = "Les Saules | Orly-Ville", + [9] = "Pont de Rungis Aeroport d'Orly | Rungis-La Fraternelle", + [10] = "Chemin d'Antony", + [12] = "Massy-Verrieres | Arpajon"}, + [76] = + {[12] = "Egly | La Norville Saint-Germain-les-Arpajon", + [13] = "Breuillet Bruyeres-le-Châtel | Breuillet-Village | Saint-Cheron", + [14] = "Sermaise", + [15] = "Dourdan | Dourdan-la-Foret"}, +}; #endif // METRO_LIST_H diff --git a/metroflip/scenes/navigo_structs.h b/metroflip/scenes/navigo_structs.h new file mode 100644 index 000000000..89b487d25 --- /dev/null +++ b/metroflip/scenes/navigo_structs.h @@ -0,0 +1,73 @@ +#include +#include +#include + +typedef struct { + int transport_type; + int transition; + int service_provider; + int station_group_id; + int station_id; + int location_gate; + bool location_gate_available; + int device; + int door; + int side; + bool device_available; + int route_number; + bool route_number_available; + int mission; + bool mission_available; + int vehicle_id; + bool vehicle_id_available; + int used_contract; + bool used_contract_available; + DateTime date; +} NavigoCardEvent; + +typedef struct { + int app_version; + int country_num; + int network_num; + DateTime end_dt; +} NavigoCardEnv; + +typedef struct { + int card_status; + int commercial_id; +} NavigoCardHolder; + +typedef struct { + int tariff; + int serial_number; + bool serial_number_available; + int pay_method; + bool pay_method_available; + double price_amount; + bool price_amount_available; + DateTime start_date; + DateTime end_date; + bool end_date_available; + int zones[5]; + bool zones_available; + DateTime sale_date; + int sale_agent; + int sale_device; + int status; + int authenticator; +} NavigoCardContract; + +typedef struct { + NavigoCardEnv environment; + NavigoCardHolder holder; + NavigoCardContract contracts[2]; + NavigoCardEvent events[3]; + int ticket_count; +} NavigoCardData; + +typedef struct { + NavigoCardData* card; + int page_id; + // mutex + FuriMutex* mutex; +} NavigoContext; diff --git a/metroflip/screenshots/Menu-Top.png b/metroflip/screenshots/Menu-Top.png index 092232b0e..27fc1d04b 100644 Binary files a/metroflip/screenshots/Menu-Top.png and b/metroflip/screenshots/Menu-Top.png differ diff --git a/metroflip/screenshots/Navigo.png b/metroflip/screenshots/Navigo.png new file mode 100644 index 000000000..587f88026 Binary files /dev/null and b/metroflip/screenshots/Navigo.png differ diff --git a/metroflip/screenshots/Navigo2.png b/metroflip/screenshots/Navigo2.png new file mode 100644 index 000000000..2cdad900f Binary files /dev/null and b/metroflip/screenshots/Navigo2.png differ diff --git a/mp_flipper/.gitsubtree b/mp_flipper/.gitsubtree index fad379fbc..007b0ada5 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 17bec3e26b8250c59acebd4fa52b6b08f68152d7 +https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/mp_flipper c450eaa657997d2a8776df11b837e22a69fabd77 https://github.com/ofabel/mp-flipper master / diff --git a/mp_flipper/CHANGELOG.md b/mp_flipper/CHANGELOG.md index d250fbdc5..4b27798c0 100644 --- a/mp_flipper/CHANGELOG.md +++ b/mp_flipper/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.7.0] + +### Changed + +* The `SPEAKER_NOTE_*` constants are replaced by an attribute delegator function to save space. + +### Fixed + +* [#6](https://github.com/ofabel/mp-flipper/issues/6): Update to latest SDK version. + ## [1.6.0] - 2024-11-17 ### Added @@ -162,7 +172,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.6.0...dev +[Unreleased]: https://github.com/ofabel/mp-flipper/compare/v1.7.0...dev +[1.7.0]: https://github.com/ofabel/mp-flipper/compare/v1.6.0...v1.7.0 [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 diff --git a/mp_flipper/application.fam b/mp_flipper/application.fam index 0a1b4a4c2..b366bbf56 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.6", + fap_version="1.7", fap_description="Compile and execute MicroPython scripts", fap_icon="icon.png", fap_icon_assets="images", diff --git a/mp_flipper/docs/CHANGELOG.md b/mp_flipper/docs/CHANGELOG.md index 9b5fd11cd..dc2de258c 100644 --- a/mp_flipper/docs/CHANGELOG.md +++ b/mp_flipper/docs/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.7 + +* Replaced speaker note constants with an attribute delegator function to save space. +* Update to latest SDK version. + +## 1.6 + +* Added extra functions for the **random** module. + ## 1.5 * Added **io** module for basic file system operations. diff --git a/mp_flipper/docs/pages/conf.py b/mp_flipper/docs/pages/conf.py index 63a2dba98..90b15cb02 100644 --- a/mp_flipper/docs/pages/conf.py +++ b/mp_flipper/docs/pages/conf.py @@ -32,12 +32,12 @@ def copy_dict(source, target): now = datetime.datetime.now() -project = "uPython" -copyright = str(now.year) + ", Oliver Fabel" -author = "Oliver Fabel" -release = "1.5.0" -version = "1.5" -language = "en" +project = 'uPython' +copyright = str(now.year) + ', Oliver Fabel' +author = 'Oliver Fabel' +release = '1.7.0' +version = '1.7' +language = 'en' extensions = ["sphinx.ext.autodoc", "myst_parser"] source_suffix = {".rst": "restructuredtext", ".md": "markdown"} diff --git a/mp_flipper/docs/pages/reference.rst b/mp_flipper/docs/pages/reference.rst index f08753d2c..ed04e962b 100644 --- a/mp_flipper/docs/pages/reference.rst +++ b/mp_flipper/docs/pages/reference.rst @@ -45,6 +45,13 @@ Full control over the built-in speaker module. Musical Notes ~~~~~~~~~~~~~ +Constant values for all musical notes between C\ :sub:`0` and B\ :sub:`8`. + +.. warning:: + + You won't be able to find these constants in the REPL using autocompletion from version 1.7.0 onwards. + But the constants are still available. You just have to type the full name by hand. + .. for octave in range(9): for name in ['C', 'CS', 'D', 'DS', 'E', 'F', 'FS', 'G', 'GS', 'A', 'AS', 'B']: diff --git a/mp_flipper/flipperzero/_speaker.py b/mp_flipper/flipperzero/_speaker.py index b0af3c4fa..ef5dbceb6 100644 --- a/mp_flipper/flipperzero/_speaker.py +++ b/mp_flipper/flipperzero/_speaker.py @@ -12,8 +12,10 @@ for name in note_names: print("SPEAKER_NOTE_%s%s: float" % (name, octave)) print('\'\'\'') - print('The musical note %s\\ :sub:`0` as frequency in `Hz`.\n' % (name if len(name) == 1 else (name[0]+'#'))) + print('The musical note %s\\ :sub:`%s` as frequency in `Hz`.\n' % (name if len(name) == 1 else (name[0]+'#'), octave)) print('.. versionadded:: 1.2.0') + print('.. versionchanged:: 1.7.0') + print(' The constant is replaced by a delegator function.') print('\'\'\'\n') """ @@ -22,756 +24,972 @@ The musical note C\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_CS0: float """ The musical note C#\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_D0: float """ The musical note D\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_DS0: float """ The musical note D#\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_E0: float """ The musical note E\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_F0: float """ The musical note F\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_FS0: float """ The musical note F#\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_G0: float """ The musical note G\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_GS0: float """ The musical note G#\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_A0: float """ The musical note A\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_AS0: float """ The musical note A#\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_B0: float """ The musical note B\ :sub:`0` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_C1: float -""" -The musical note C\ :sub:`0` as frequency in `Hz`. +''' +The musical note C\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_CS1: float -""" -The musical note C#\ :sub:`0` as frequency in `Hz`. +''' +The musical note C#\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_D1: float -""" -The musical note D\ :sub:`0` as frequency in `Hz`. +''' +The musical note D\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_DS1: float -""" -The musical note D#\ :sub:`0` as frequency in `Hz`. +''' +The musical note D#\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_E1: float -""" -The musical note E\ :sub:`0` as frequency in `Hz`. +''' +The musical note E\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_F1: float -""" -The musical note F\ :sub:`0` as frequency in `Hz`. +''' +The musical note F\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_FS1: float -""" -The musical note F#\ :sub:`0` as frequency in `Hz`. +''' +The musical note F#\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_G1: float -""" -The musical note G\ :sub:`0` as frequency in `Hz`. +''' +The musical note G\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_GS1: float -""" -The musical note G#\ :sub:`0` as frequency in `Hz`. +''' +The musical note G#\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_A1: float -""" -The musical note A\ :sub:`0` as frequency in `Hz`. +''' +The musical note A\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_AS1: float -""" -The musical note A#\ :sub:`0` as frequency in `Hz`. +''' +The musical note A#\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_B1: float -""" -The musical note B\ :sub:`0` as frequency in `Hz`. +''' +The musical note B\ :sub:`1` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_C2: float -""" -The musical note C\ :sub:`0` as frequency in `Hz`. +''' +The musical note C\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_CS2: float -""" -The musical note C#\ :sub:`0` as frequency in `Hz`. +''' +The musical note C#\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_D2: float -""" -The musical note D\ :sub:`0` as frequency in `Hz`. +''' +The musical note D\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_DS2: float -""" -The musical note D#\ :sub:`0` as frequency in `Hz`. +''' +The musical note D#\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_E2: float -""" -The musical note E\ :sub:`0` as frequency in `Hz`. +''' +The musical note E\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_F2: float -""" -The musical note F\ :sub:`0` as frequency in `Hz`. +''' +The musical note F\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_FS2: float -""" -The musical note F#\ :sub:`0` as frequency in `Hz`. +''' +The musical note F#\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_G2: float -""" -The musical note G\ :sub:`0` as frequency in `Hz`. +''' +The musical note G\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_GS2: float -""" -The musical note G#\ :sub:`0` as frequency in `Hz`. +''' +The musical note G#\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_A2: float -""" -The musical note A\ :sub:`0` as frequency in `Hz`. +''' +The musical note A\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_AS2: float -""" -The musical note A#\ :sub:`0` as frequency in `Hz`. +''' +The musical note A#\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_B2: float -""" -The musical note B\ :sub:`0` as frequency in `Hz`. +''' +The musical note B\ :sub:`2` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_C3: float -""" -The musical note C\ :sub:`0` as frequency in `Hz`. +''' +The musical note C\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_CS3: float -""" -The musical note C#\ :sub:`0` as frequency in `Hz`. +''' +The musical note C#\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_D3: float -""" -The musical note D\ :sub:`0` as frequency in `Hz`. +''' +The musical note D\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_DS3: float -""" -The musical note D#\ :sub:`0` as frequency in `Hz`. +''' +The musical note D#\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_E3: float -""" -The musical note E\ :sub:`0` as frequency in `Hz`. +''' +The musical note E\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_F3: float -""" -The musical note F\ :sub:`0` as frequency in `Hz`. +''' +The musical note F\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_FS3: float -""" -The musical note F#\ :sub:`0` as frequency in `Hz`. +''' +The musical note F#\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_G3: float -""" -The musical note G\ :sub:`0` as frequency in `Hz`. +''' +The musical note G\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_GS3: float -""" -The musical note G#\ :sub:`0` as frequency in `Hz`. +''' +The musical note G#\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_A3: float -""" -The musical note A\ :sub:`0` as frequency in `Hz`. +''' +The musical note A\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_AS3: float -""" -The musical note A#\ :sub:`0` as frequency in `Hz`. +''' +The musical note A#\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_B3: float -""" -The musical note B\ :sub:`0` as frequency in `Hz`. +''' +The musical note B\ :sub:`3` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_C4: float -""" -The musical note C\ :sub:`0` as frequency in `Hz`. +''' +The musical note C\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_CS4: float -""" -The musical note C#\ :sub:`0` as frequency in `Hz`. +''' +The musical note C#\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_D4: float -""" -The musical note D\ :sub:`0` as frequency in `Hz`. +''' +The musical note D\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_DS4: float -""" -The musical note D#\ :sub:`0` as frequency in `Hz`. +''' +The musical note D#\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_E4: float -""" -The musical note E\ :sub:`0` as frequency in `Hz`. +''' +The musical note E\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_F4: float -""" -The musical note F\ :sub:`0` as frequency in `Hz`. +''' +The musical note F\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_FS4: float -""" -The musical note F#\ :sub:`0` as frequency in `Hz`. +''' +The musical note F#\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_G4: float -""" -The musical note G\ :sub:`0` as frequency in `Hz`. +''' +The musical note G\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_GS4: float -""" -The musical note G#\ :sub:`0` as frequency in `Hz`. +''' +The musical note G#\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_A4: float -""" -The musical note A\ :sub:`0` as frequency in `Hz`. +''' +The musical note A\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_AS4: float -""" -The musical note A#\ :sub:`0` as frequency in `Hz`. +''' +The musical note A#\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_B4: float -""" -The musical note B\ :sub:`0` as frequency in `Hz`. +''' +The musical note B\ :sub:`4` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_C5: float -""" -The musical note C\ :sub:`0` as frequency in `Hz`. +''' +The musical note C\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_CS5: float -""" -The musical note C#\ :sub:`0` as frequency in `Hz`. +''' +The musical note C#\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_D5: float -""" -The musical note D\ :sub:`0` as frequency in `Hz`. +''' +The musical note D\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_DS5: float -""" -The musical note D#\ :sub:`0` as frequency in `Hz`. +''' +The musical note D#\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_E5: float -""" -The musical note E\ :sub:`0` as frequency in `Hz`. +''' +The musical note E\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_F5: float -""" -The musical note F\ :sub:`0` as frequency in `Hz`. +''' +The musical note F\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_FS5: float -""" -The musical note F#\ :sub:`0` as frequency in `Hz`. +''' +The musical note F#\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_G5: float -""" -The musical note G\ :sub:`0` as frequency in `Hz`. +''' +The musical note G\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_GS5: float -""" -The musical note G#\ :sub:`0` as frequency in `Hz`. +''' +The musical note G#\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_A5: float -""" -The musical note A\ :sub:`0` as frequency in `Hz`. +''' +The musical note A\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_AS5: float -""" -The musical note A#\ :sub:`0` as frequency in `Hz`. +''' +The musical note A#\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_B5: float -""" -The musical note B\ :sub:`0` as frequency in `Hz`. +''' +The musical note B\ :sub:`5` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_C6: float -""" -The musical note C\ :sub:`0` as frequency in `Hz`. +''' +The musical note C\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_CS6: float -""" -The musical note C#\ :sub:`0` as frequency in `Hz`. +''' +The musical note C#\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_D6: float -""" -The musical note D\ :sub:`0` as frequency in `Hz`. +''' +The musical note D\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_DS6: float -""" -The musical note D#\ :sub:`0` as frequency in `Hz`. +''' +The musical note D#\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_E6: float -""" -The musical note E\ :sub:`0` as frequency in `Hz`. +''' +The musical note E\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_F6: float -""" -The musical note F\ :sub:`0` as frequency in `Hz`. +''' +The musical note F\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_FS6: float -""" -The musical note F#\ :sub:`0` as frequency in `Hz`. +''' +The musical note F#\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_G6: float -""" -The musical note G\ :sub:`0` as frequency in `Hz`. +''' +The musical note G\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_GS6: float -""" -The musical note G#\ :sub:`0` as frequency in `Hz`. +''' +The musical note G#\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_A6: float -""" -The musical note A\ :sub:`0` as frequency in `Hz`. +''' +The musical note A\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_AS6: float -""" -The musical note A#\ :sub:`0` as frequency in `Hz`. +''' +The musical note A#\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_B6: float -""" -The musical note B\ :sub:`0` as frequency in `Hz`. +''' +The musical note B\ :sub:`6` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_C7: float -""" -The musical note C\ :sub:`0` as frequency in `Hz`. +''' +The musical note C\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_CS7: float -""" -The musical note C#\ :sub:`0` as frequency in `Hz`. +''' +The musical note C#\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_D7: float -""" -The musical note D\ :sub:`0` as frequency in `Hz`. +''' +The musical note D\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_DS7: float -""" -The musical note D#\ :sub:`0` as frequency in `Hz`. +''' +The musical note D#\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_E7: float -""" -The musical note E\ :sub:`0` as frequency in `Hz`. +''' +The musical note E\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_F7: float -""" -The musical note F\ :sub:`0` as frequency in `Hz`. +''' +The musical note F\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_FS7: float -""" -The musical note F#\ :sub:`0` as frequency in `Hz`. +''' +The musical note F#\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_G7: float -""" -The musical note G\ :sub:`0` as frequency in `Hz`. +''' +The musical note G\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_GS7: float -""" -The musical note G#\ :sub:`0` as frequency in `Hz`. +''' +The musical note G#\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_A7: float -""" -The musical note A\ :sub:`0` as frequency in `Hz`. +''' +The musical note A\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_AS7: float -""" -The musical note A#\ :sub:`0` as frequency in `Hz`. +''' +The musical note A#\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_B7: float -""" -The musical note B\ :sub:`0` as frequency in `Hz`. +''' +The musical note B\ :sub:`7` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_C8: float -""" -The musical note C\ :sub:`0` as frequency in `Hz`. +''' +The musical note C\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_CS8: float -""" -The musical note C#\ :sub:`0` as frequency in `Hz`. +''' +The musical note C#\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_D8: float -""" -The musical note D\ :sub:`0` as frequency in `Hz`. +''' +The musical note D\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_DS8: float -""" -The musical note D#\ :sub:`0` as frequency in `Hz`. +''' +The musical note D#\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_E8: float -""" -The musical note E\ :sub:`0` as frequency in `Hz`. +''' +The musical note E\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_F8: float -""" -The musical note F\ :sub:`0` as frequency in `Hz`. +''' +The musical note F\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_FS8: float -""" -The musical note F#\ :sub:`0` as frequency in `Hz`. +''' +The musical note F#\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_G8: float -""" -The musical note G\ :sub:`0` as frequency in `Hz`. +''' +The musical note G\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_GS8: float -""" -The musical note G#\ :sub:`0` as frequency in `Hz`. +''' +The musical note G#\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_A8: float -""" -The musical note A\ :sub:`0` as frequency in `Hz`. +''' +The musical note A\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_AS8: float -""" -The musical note A#\ :sub:`0` as frequency in `Hz`. +''' +The musical note A#\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_NOTE_B8: float -""" -The musical note B\ :sub:`0` as frequency in `Hz`. +''' +The musical note B\ :sub:`8` as frequency in `Hz`. .. versionadded:: 1.2.0 -""" +.. versionchanged:: 1.7.0 + The constant is replaced by a delegator function. +''' SPEAKER_VOLUME_MIN: float """ diff --git a/mp_flipper/lib/micropython/.gitsubtree b/mp_flipper/lib/micropython/.gitsubtree index 45c3d9f9d..ab3de22dd 100644 --- a/mp_flipper/lib/micropython/.gitsubtree +++ b/mp_flipper/lib/micropython/.gitsubtree @@ -1 +1 @@ -https://github.com/ofabel/mp-flipper 242fff05599d97faafab563a827dd86791c0cbee / +https://github.com/ofabel/mp-flipper 80f0dc2f91ea779215e6506eaa9abcacb484994f / diff --git a/mp_flipper/lib/micropython/genhdr/moduledefs.h b/mp_flipper/lib/micropython/genhdr/moduledefs.h index 9f7d01f8b..a2cf22269 100644 --- a/mp_flipper/lib/micropython/genhdr/moduledefs.h +++ b/mp_flipper/lib/micropython/genhdr/moduledefs.h @@ -41,3 +41,8 @@ extern const struct _mp_obj_module_t flipperzero_module; MODULE_DEF_RANDOM \ MODULE_DEF_TIME \ // MICROPY_REGISTERED_EXTENSIBLE_MODULES + +extern void flipperzero_module_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest); +#define MICROPY_MODULE_DELEGATIONS \ + { MP_ROM_PTR(&flipperzero_module), flipperzero_module_attr }, \ +// MICROPY_MODULE_DELEGATIONS diff --git a/mp_flipper/lib/micropython/genhdr/mpversion.h b/mp_flipper/lib/micropython/genhdr/mpversion.h index c0a76c5d6..54a7bfca4 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-11-15" +#define MICROPY_BUILD_DATE "2025-01-19" diff --git a/mp_flipper/lib/micropython/genhdr/qstrdefs.generated.h b/mp_flipper/lib/micropython/genhdr/qstrdefs.generated.h index 5601148ed..93e67cf8c 100644 --- a/mp_flipper/lib/micropython/genhdr/qstrdefs.generated.h +++ b/mp_flipper/lib/micropython/genhdr/qstrdefs.generated.h @@ -226,114 +226,6 @@ QDEF1(MP_QSTR_NONE, 79, 4, "NONE") QDEF1(MP_QSTR_SEEK_CUR, 134, 8, "SEEK_CUR") QDEF1(MP_QSTR_SEEK_END, 237, 8, "SEEK_END") QDEF1(MP_QSTR_SEEK_SET, 128, 8, "SEEK_SET") -QDEF1(MP_QSTR_SPEAKER_NOTE_A0, 95, 15, "SPEAKER_NOTE_A0") -QDEF1(MP_QSTR_SPEAKER_NOTE_A1, 94, 15, "SPEAKER_NOTE_A1") -QDEF1(MP_QSTR_SPEAKER_NOTE_A2, 93, 15, "SPEAKER_NOTE_A2") -QDEF1(MP_QSTR_SPEAKER_NOTE_A3, 92, 15, "SPEAKER_NOTE_A3") -QDEF1(MP_QSTR_SPEAKER_NOTE_A4, 91, 15, "SPEAKER_NOTE_A4") -QDEF1(MP_QSTR_SPEAKER_NOTE_A5, 90, 15, "SPEAKER_NOTE_A5") -QDEF1(MP_QSTR_SPEAKER_NOTE_A6, 89, 15, "SPEAKER_NOTE_A6") -QDEF1(MP_QSTR_SPEAKER_NOTE_A7, 88, 15, "SPEAKER_NOTE_A7") -QDEF1(MP_QSTR_SPEAKER_NOTE_A8, 87, 15, "SPEAKER_NOTE_A8") -QDEF1(MP_QSTR_SPEAKER_NOTE_AS0, 140, 16, "SPEAKER_NOTE_AS0") -QDEF1(MP_QSTR_SPEAKER_NOTE_AS1, 141, 16, "SPEAKER_NOTE_AS1") -QDEF1(MP_QSTR_SPEAKER_NOTE_AS2, 142, 16, "SPEAKER_NOTE_AS2") -QDEF1(MP_QSTR_SPEAKER_NOTE_AS3, 143, 16, "SPEAKER_NOTE_AS3") -QDEF1(MP_QSTR_SPEAKER_NOTE_AS4, 136, 16, "SPEAKER_NOTE_AS4") -QDEF1(MP_QSTR_SPEAKER_NOTE_AS5, 137, 16, "SPEAKER_NOTE_AS5") -QDEF1(MP_QSTR_SPEAKER_NOTE_AS6, 138, 16, "SPEAKER_NOTE_AS6") -QDEF1(MP_QSTR_SPEAKER_NOTE_AS7, 139, 16, "SPEAKER_NOTE_AS7") -QDEF1(MP_QSTR_SPEAKER_NOTE_AS8, 132, 16, "SPEAKER_NOTE_AS8") -QDEF1(MP_QSTR_SPEAKER_NOTE_B0, 60, 15, "SPEAKER_NOTE_B0") -QDEF1(MP_QSTR_SPEAKER_NOTE_B1, 61, 15, "SPEAKER_NOTE_B1") -QDEF1(MP_QSTR_SPEAKER_NOTE_B2, 62, 15, "SPEAKER_NOTE_B2") -QDEF1(MP_QSTR_SPEAKER_NOTE_B3, 63, 15, "SPEAKER_NOTE_B3") -QDEF1(MP_QSTR_SPEAKER_NOTE_B4, 56, 15, "SPEAKER_NOTE_B4") -QDEF1(MP_QSTR_SPEAKER_NOTE_B5, 57, 15, "SPEAKER_NOTE_B5") -QDEF1(MP_QSTR_SPEAKER_NOTE_B6, 58, 15, "SPEAKER_NOTE_B6") -QDEF1(MP_QSTR_SPEAKER_NOTE_B7, 59, 15, "SPEAKER_NOTE_B7") -QDEF1(MP_QSTR_SPEAKER_NOTE_B8, 52, 15, "SPEAKER_NOTE_B8") -QDEF1(MP_QSTR_SPEAKER_NOTE_C0, 29, 15, "SPEAKER_NOTE_C0") -QDEF1(MP_QSTR_SPEAKER_NOTE_C1, 28, 15, "SPEAKER_NOTE_C1") -QDEF1(MP_QSTR_SPEAKER_NOTE_C2, 31, 15, "SPEAKER_NOTE_C2") -QDEF1(MP_QSTR_SPEAKER_NOTE_C3, 30, 15, "SPEAKER_NOTE_C3") -QDEF1(MP_QSTR_SPEAKER_NOTE_C4, 25, 15, "SPEAKER_NOTE_C4") -QDEF1(MP_QSTR_SPEAKER_NOTE_C5, 24, 15, "SPEAKER_NOTE_C5") -QDEF1(MP_QSTR_SPEAKER_NOTE_C6, 27, 15, "SPEAKER_NOTE_C6") -QDEF1(MP_QSTR_SPEAKER_NOTE_C7, 26, 15, "SPEAKER_NOTE_C7") -QDEF1(MP_QSTR_SPEAKER_NOTE_C8, 21, 15, "SPEAKER_NOTE_C8") -QDEF1(MP_QSTR_SPEAKER_NOTE_CS0, 14, 16, "SPEAKER_NOTE_CS0") -QDEF1(MP_QSTR_SPEAKER_NOTE_CS1, 15, 16, "SPEAKER_NOTE_CS1") -QDEF1(MP_QSTR_SPEAKER_NOTE_CS2, 12, 16, "SPEAKER_NOTE_CS2") -QDEF1(MP_QSTR_SPEAKER_NOTE_CS3, 13, 16, "SPEAKER_NOTE_CS3") -QDEF1(MP_QSTR_SPEAKER_NOTE_CS4, 10, 16, "SPEAKER_NOTE_CS4") -QDEF1(MP_QSTR_SPEAKER_NOTE_CS5, 11, 16, "SPEAKER_NOTE_CS5") -QDEF1(MP_QSTR_SPEAKER_NOTE_CS6, 8, 16, "SPEAKER_NOTE_CS6") -QDEF1(MP_QSTR_SPEAKER_NOTE_CS7, 9, 16, "SPEAKER_NOTE_CS7") -QDEF1(MP_QSTR_SPEAKER_NOTE_CS8, 6, 16, "SPEAKER_NOTE_CS8") -QDEF1(MP_QSTR_SPEAKER_NOTE_D0, 250, 15, "SPEAKER_NOTE_D0") -QDEF1(MP_QSTR_SPEAKER_NOTE_D1, 251, 15, "SPEAKER_NOTE_D1") -QDEF1(MP_QSTR_SPEAKER_NOTE_D2, 248, 15, "SPEAKER_NOTE_D2") -QDEF1(MP_QSTR_SPEAKER_NOTE_D3, 249, 15, "SPEAKER_NOTE_D3") -QDEF1(MP_QSTR_SPEAKER_NOTE_D4, 254, 15, "SPEAKER_NOTE_D4") -QDEF1(MP_QSTR_SPEAKER_NOTE_D5, 255, 15, "SPEAKER_NOTE_D5") -QDEF1(MP_QSTR_SPEAKER_NOTE_D6, 252, 15, "SPEAKER_NOTE_D6") -QDEF1(MP_QSTR_SPEAKER_NOTE_D7, 253, 15, "SPEAKER_NOTE_D7") -QDEF1(MP_QSTR_SPEAKER_NOTE_D8, 242, 15, "SPEAKER_NOTE_D8") -QDEF1(MP_QSTR_SPEAKER_NOTE_DS0, 137, 16, "SPEAKER_NOTE_DS0") -QDEF1(MP_QSTR_SPEAKER_NOTE_DS1, 136, 16, "SPEAKER_NOTE_DS1") -QDEF1(MP_QSTR_SPEAKER_NOTE_DS2, 139, 16, "SPEAKER_NOTE_DS2") -QDEF1(MP_QSTR_SPEAKER_NOTE_DS3, 138, 16, "SPEAKER_NOTE_DS3") -QDEF1(MP_QSTR_SPEAKER_NOTE_DS4, 141, 16, "SPEAKER_NOTE_DS4") -QDEF1(MP_QSTR_SPEAKER_NOTE_DS5, 140, 16, "SPEAKER_NOTE_DS5") -QDEF1(MP_QSTR_SPEAKER_NOTE_DS6, 143, 16, "SPEAKER_NOTE_DS6") -QDEF1(MP_QSTR_SPEAKER_NOTE_DS7, 142, 16, "SPEAKER_NOTE_DS7") -QDEF1(MP_QSTR_SPEAKER_NOTE_DS8, 129, 16, "SPEAKER_NOTE_DS8") -QDEF1(MP_QSTR_SPEAKER_NOTE_E0, 219, 15, "SPEAKER_NOTE_E0") -QDEF1(MP_QSTR_SPEAKER_NOTE_E1, 218, 15, "SPEAKER_NOTE_E1") -QDEF1(MP_QSTR_SPEAKER_NOTE_E2, 217, 15, "SPEAKER_NOTE_E2") -QDEF1(MP_QSTR_SPEAKER_NOTE_E3, 216, 15, "SPEAKER_NOTE_E3") -QDEF1(MP_QSTR_SPEAKER_NOTE_E4, 223, 15, "SPEAKER_NOTE_E4") -QDEF1(MP_QSTR_SPEAKER_NOTE_E5, 222, 15, "SPEAKER_NOTE_E5") -QDEF1(MP_QSTR_SPEAKER_NOTE_E6, 221, 15, "SPEAKER_NOTE_E6") -QDEF1(MP_QSTR_SPEAKER_NOTE_E7, 220, 15, "SPEAKER_NOTE_E7") -QDEF1(MP_QSTR_SPEAKER_NOTE_E8, 211, 15, "SPEAKER_NOTE_E8") -QDEF1(MP_QSTR_SPEAKER_NOTE_F0, 184, 15, "SPEAKER_NOTE_F0") -QDEF1(MP_QSTR_SPEAKER_NOTE_F1, 185, 15, "SPEAKER_NOTE_F1") -QDEF1(MP_QSTR_SPEAKER_NOTE_F2, 186, 15, "SPEAKER_NOTE_F2") -QDEF1(MP_QSTR_SPEAKER_NOTE_F3, 187, 15, "SPEAKER_NOTE_F3") -QDEF1(MP_QSTR_SPEAKER_NOTE_F4, 188, 15, "SPEAKER_NOTE_F4") -QDEF1(MP_QSTR_SPEAKER_NOTE_F5, 189, 15, "SPEAKER_NOTE_F5") -QDEF1(MP_QSTR_SPEAKER_NOTE_F6, 190, 15, "SPEAKER_NOTE_F6") -QDEF1(MP_QSTR_SPEAKER_NOTE_F7, 191, 15, "SPEAKER_NOTE_F7") -QDEF1(MP_QSTR_SPEAKER_NOTE_F8, 176, 15, "SPEAKER_NOTE_F8") -QDEF1(MP_QSTR_SPEAKER_NOTE_FS0, 11, 16, "SPEAKER_NOTE_FS0") -QDEF1(MP_QSTR_SPEAKER_NOTE_FS1, 10, 16, "SPEAKER_NOTE_FS1") -QDEF1(MP_QSTR_SPEAKER_NOTE_FS2, 9, 16, "SPEAKER_NOTE_FS2") -QDEF1(MP_QSTR_SPEAKER_NOTE_FS3, 8, 16, "SPEAKER_NOTE_FS3") -QDEF1(MP_QSTR_SPEAKER_NOTE_FS4, 15, 16, "SPEAKER_NOTE_FS4") -QDEF1(MP_QSTR_SPEAKER_NOTE_FS5, 14, 16, "SPEAKER_NOTE_FS5") -QDEF1(MP_QSTR_SPEAKER_NOTE_FS6, 13, 16, "SPEAKER_NOTE_FS6") -QDEF1(MP_QSTR_SPEAKER_NOTE_FS7, 12, 16, "SPEAKER_NOTE_FS7") -QDEF1(MP_QSTR_SPEAKER_NOTE_FS8, 3, 16, "SPEAKER_NOTE_FS8") -QDEF1(MP_QSTR_SPEAKER_NOTE_G0, 153, 15, "SPEAKER_NOTE_G0") -QDEF1(MP_QSTR_SPEAKER_NOTE_G1, 152, 15, "SPEAKER_NOTE_G1") -QDEF1(MP_QSTR_SPEAKER_NOTE_G2, 155, 15, "SPEAKER_NOTE_G2") -QDEF1(MP_QSTR_SPEAKER_NOTE_G3, 154, 15, "SPEAKER_NOTE_G3") -QDEF1(MP_QSTR_SPEAKER_NOTE_G4, 157, 15, "SPEAKER_NOTE_G4") -QDEF1(MP_QSTR_SPEAKER_NOTE_G5, 156, 15, "SPEAKER_NOTE_G5") -QDEF1(MP_QSTR_SPEAKER_NOTE_G6, 159, 15, "SPEAKER_NOTE_G6") -QDEF1(MP_QSTR_SPEAKER_NOTE_G7, 158, 15, "SPEAKER_NOTE_G7") -QDEF1(MP_QSTR_SPEAKER_NOTE_G8, 145, 15, "SPEAKER_NOTE_G8") -QDEF1(MP_QSTR_SPEAKER_NOTE_GS0, 10, 16, "SPEAKER_NOTE_GS0") -QDEF1(MP_QSTR_SPEAKER_NOTE_GS1, 11, 16, "SPEAKER_NOTE_GS1") -QDEF1(MP_QSTR_SPEAKER_NOTE_GS2, 8, 16, "SPEAKER_NOTE_GS2") -QDEF1(MP_QSTR_SPEAKER_NOTE_GS3, 9, 16, "SPEAKER_NOTE_GS3") -QDEF1(MP_QSTR_SPEAKER_NOTE_GS4, 14, 16, "SPEAKER_NOTE_GS4") -QDEF1(MP_QSTR_SPEAKER_NOTE_GS5, 15, 16, "SPEAKER_NOTE_GS5") -QDEF1(MP_QSTR_SPEAKER_NOTE_GS6, 12, 16, "SPEAKER_NOTE_GS6") -QDEF1(MP_QSTR_SPEAKER_NOTE_GS7, 13, 16, "SPEAKER_NOTE_GS7") -QDEF1(MP_QSTR_SPEAKER_NOTE_GS8, 2, 16, "SPEAKER_NOTE_GS8") QDEF1(MP_QSTR_SPEAKER_VOLUME_MAX, 66, 18, "SPEAKER_VOLUME_MAX") QDEF1(MP_QSTR_SPEAKER_VOLUME_MIN, 92, 18, "SPEAKER_VOLUME_MIN") QDEF1(MP_QSTR_TRACE, 196, 5, "TRACE") diff --git a/mp_flipper/lib/micropython/mp_flipper_modflipperzero.c b/mp_flipper/lib/micropython/mp_flipper_modflipperzero.c index c1bfae113..a98416d83 100644 --- a/mp_flipper/lib/micropython/mp_flipper_modflipperzero.c +++ b/mp_flipper/lib/micropython/mp_flipper_modflipperzero.c @@ -7,6 +7,7 @@ #include "py/obj.h" #include "py/stream.h" #include "py/runtime.h" +#include #include "mp_flipper_modflipperzero.h" @@ -66,130 +67,6 @@ typedef struct _mp_obj_float_t { mp_float_t value; } mp_obj_float_t; -/* -Python script for notes generation - -# coding: utf-8 -# Python script for notes generation - -from typing import List - -note_names: List = ['C', 'CS', 'D', 'DS', 'E', 'F', 'FS', 'G', 'GS', 'A', 'AS', 'B'] - -for octave in range(9): - for name in note_names: - print("static const struct _mp_obj_float_t flipperzero_speaker_note_%s%s_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_%s%s};" % (name.lower(),octave,name,octave)) -*/ - -static const struct _mp_obj_float_t flipperzero_speaker_note_c0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_C0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_cs0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_CS0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_d0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_D0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_ds0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_DS0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_e0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_E0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_f0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_F0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_fs0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_FS0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_g0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_G0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_gs0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_GS0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_a0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_A0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_as0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_AS0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_b0_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_B0}; -static const struct _mp_obj_float_t flipperzero_speaker_note_c1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_C1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_cs1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_CS1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_d1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_D1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_ds1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_DS1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_e1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_E1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_f1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_F1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_fs1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_FS1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_g1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_G1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_gs1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_GS1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_a1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_A1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_as1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_AS1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_b1_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_B1}; -static const struct _mp_obj_float_t flipperzero_speaker_note_c2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_C2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_cs2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_CS2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_d2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_D2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_ds2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_DS2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_e2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_E2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_f2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_F2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_fs2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_FS2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_g2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_G2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_gs2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_GS2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_a2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_A2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_as2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_AS2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_b2_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_B2}; -static const struct _mp_obj_float_t flipperzero_speaker_note_c3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_C3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_cs3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_CS3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_d3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_D3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_ds3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_DS3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_e3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_E3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_f3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_F3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_fs3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_FS3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_g3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_G3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_gs3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_GS3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_a3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_A3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_as3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_AS3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_b3_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_B3}; -static const struct _mp_obj_float_t flipperzero_speaker_note_c4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_C4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_cs4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_CS4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_d4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_D4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_ds4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_DS4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_e4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_E4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_f4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_F4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_fs4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_FS4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_g4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_G4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_gs4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_GS4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_a4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_A4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_as4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_AS4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_b4_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_B4}; -static const struct _mp_obj_float_t flipperzero_speaker_note_c5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_C5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_cs5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_CS5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_d5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_D5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_ds5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_DS5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_e5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_E5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_f5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_F5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_fs5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_FS5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_g5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_G5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_gs5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_GS5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_a5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_A5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_as5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_AS5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_b5_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_B5}; -static const struct _mp_obj_float_t flipperzero_speaker_note_c6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_C6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_cs6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_CS6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_d6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_D6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_ds6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_DS6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_e6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_E6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_f6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_F6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_fs6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_FS6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_g6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_G6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_gs6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_GS6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_a6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_A6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_as6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_AS6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_b6_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_B6}; -static const struct _mp_obj_float_t flipperzero_speaker_note_c7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_C7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_cs7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_CS7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_d7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_D7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_ds7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_DS7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_e7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_E7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_f7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_F7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_fs7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_FS7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_g7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_G7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_gs7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_GS7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_a7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_A7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_as7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_AS7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_b7_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_B7}; -static const struct _mp_obj_float_t flipperzero_speaker_note_c8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_C8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_cs8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_CS8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_d8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_D8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_ds8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_DS8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_e8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_E8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_f8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_F8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_fs8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_FS8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_g8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_G8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_gs8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_GS8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_a8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_A8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_as8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_AS8}; -static const struct _mp_obj_float_t flipperzero_speaker_note_b8_obj = {{&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_NOTE_B8}; - static const struct _mp_obj_float_t flipperzero_speaker_volume_min_obj = { {&mp_type_float}, (mp_float_t)MP_FLIPPER_SPEAKER_VOLUME_MIN}; @@ -744,6 +621,60 @@ MP_DEFINE_CONST_OBJ_TYPE( locals_dict, &flipperzero_uart_connection_locals_dict); +static const char* notes = "CCDDEFFGGAAB"; +static const float base_frequency = 16.3515979; +static const float const_factor = 1.05946309436; + +static inline float get_frequency_by_note(const uint8_t octave, const char note, const bool is_sharp) { + float frequency = base_frequency; + + for(size_t i = 0; i < octave * 12; i++) { + frequency *= const_factor; + } + + for(size_t j = 0; j < 12; j++) { + if(notes[j] == note) { + frequency *= (is_sharp ? const_factor : 1.0); + + return frequency; + } + + frequency *= const_factor; + } + + return -1.0; +} + +void flipperzero_module_attr(mp_obj_t self_in, qstr attr, mp_obj_t* dest) { + if(dest[0] == MP_OBJ_NULL) { + // load attribute + + const char* attribute = qstr_str(attr); + + if(strstr(attribute, "SPEAKER_NOTE_") == &attribute[0]) { + size_t len = strlen(attribute); + uint8_t octave = attribute[len - 1] - '0'; + bool is_sharp = attribute[len - 2] == 'S'; + size_t note_index = len - (is_sharp ? 3 : 2); + uint8_t i_note = UINT8_MAX; + + float frequency = get_frequency_by_note(octave, attribute[note_index], is_sharp); + + if(octave > 8 || frequency < 0.0) { + dest[0] = mp_const_none; + } else { + dest[0] = mp_obj_new_float(frequency); + } + + return; + } + } else if(dest[1] == MP_OBJ_NULL) { + // delete attribute + } else { + // store attribute + } +} + static const mp_rom_map_elem_t flipperzero_module_globals_table[] = { // light {MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_flipperzero)}, @@ -757,132 +688,9 @@ static const mp_rom_map_elem_t flipperzero_module_globals_table[] = { {MP_ROM_QSTR(MP_QSTR_light_blink_stop), MP_ROM_PTR(&flipperzero_light_blink_stop_obj)}, // vibro {MP_ROM_QSTR(MP_QSTR_vibro_set), MP_ROM_PTR(&flipperzero_vibro_set_obj)}, - /* -Python script for notes generation - -# coding: utf-8 -# Python script for notes generation - -from typing import List - -note_names: List = ['C', 'CS', 'D', 'DS', 'E', 'F', 'FS', 'G', 'GS', 'A', 'AS', 'B'] - -for octave in range(9): - for name in note_names: - print("{MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_%s%s), MP_ROM_PTR(&flipperzero_speaker_note_%s%s_obj)}," % (name,octave,name.lower(),octave)) -*/ - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_C0), MP_ROM_PTR(&flipperzero_speaker_note_c0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_CS0), MP_ROM_PTR(&flipperzero_speaker_note_cs0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_D0), MP_ROM_PTR(&flipperzero_speaker_note_d0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_DS0), MP_ROM_PTR(&flipperzero_speaker_note_ds0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_E0), MP_ROM_PTR(&flipperzero_speaker_note_e0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_F0), MP_ROM_PTR(&flipperzero_speaker_note_f0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_FS0), MP_ROM_PTR(&flipperzero_speaker_note_fs0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_G0), MP_ROM_PTR(&flipperzero_speaker_note_g0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_GS0), MP_ROM_PTR(&flipperzero_speaker_note_gs0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_A0), MP_ROM_PTR(&flipperzero_speaker_note_a0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_AS0), MP_ROM_PTR(&flipperzero_speaker_note_as0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_B0), MP_ROM_PTR(&flipperzero_speaker_note_b0_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_C1), MP_ROM_PTR(&flipperzero_speaker_note_c1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_CS1), MP_ROM_PTR(&flipperzero_speaker_note_cs1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_D1), MP_ROM_PTR(&flipperzero_speaker_note_d1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_DS1), MP_ROM_PTR(&flipperzero_speaker_note_ds1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_E1), MP_ROM_PTR(&flipperzero_speaker_note_e1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_F1), MP_ROM_PTR(&flipperzero_speaker_note_f1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_FS1), MP_ROM_PTR(&flipperzero_speaker_note_fs1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_G1), MP_ROM_PTR(&flipperzero_speaker_note_g1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_GS1), MP_ROM_PTR(&flipperzero_speaker_note_gs1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_A1), MP_ROM_PTR(&flipperzero_speaker_note_a1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_AS1), MP_ROM_PTR(&flipperzero_speaker_note_as1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_B1), MP_ROM_PTR(&flipperzero_speaker_note_b1_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_C2), MP_ROM_PTR(&flipperzero_speaker_note_c2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_CS2), MP_ROM_PTR(&flipperzero_speaker_note_cs2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_D2), MP_ROM_PTR(&flipperzero_speaker_note_d2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_DS2), MP_ROM_PTR(&flipperzero_speaker_note_ds2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_E2), MP_ROM_PTR(&flipperzero_speaker_note_e2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_F2), MP_ROM_PTR(&flipperzero_speaker_note_f2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_FS2), MP_ROM_PTR(&flipperzero_speaker_note_fs2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_G2), MP_ROM_PTR(&flipperzero_speaker_note_g2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_GS2), MP_ROM_PTR(&flipperzero_speaker_note_gs2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_A2), MP_ROM_PTR(&flipperzero_speaker_note_a2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_AS2), MP_ROM_PTR(&flipperzero_speaker_note_as2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_B2), MP_ROM_PTR(&flipperzero_speaker_note_b2_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_C3), MP_ROM_PTR(&flipperzero_speaker_note_c3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_CS3), MP_ROM_PTR(&flipperzero_speaker_note_cs3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_D3), MP_ROM_PTR(&flipperzero_speaker_note_d3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_DS3), MP_ROM_PTR(&flipperzero_speaker_note_ds3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_E3), MP_ROM_PTR(&flipperzero_speaker_note_e3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_F3), MP_ROM_PTR(&flipperzero_speaker_note_f3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_FS3), MP_ROM_PTR(&flipperzero_speaker_note_fs3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_G3), MP_ROM_PTR(&flipperzero_speaker_note_g3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_GS3), MP_ROM_PTR(&flipperzero_speaker_note_gs3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_A3), MP_ROM_PTR(&flipperzero_speaker_note_a3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_AS3), MP_ROM_PTR(&flipperzero_speaker_note_as3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_B3), MP_ROM_PTR(&flipperzero_speaker_note_b3_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_C4), MP_ROM_PTR(&flipperzero_speaker_note_c4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_CS4), MP_ROM_PTR(&flipperzero_speaker_note_cs4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_D4), MP_ROM_PTR(&flipperzero_speaker_note_d4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_DS4), MP_ROM_PTR(&flipperzero_speaker_note_ds4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_E4), MP_ROM_PTR(&flipperzero_speaker_note_e4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_F4), MP_ROM_PTR(&flipperzero_speaker_note_f4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_FS4), MP_ROM_PTR(&flipperzero_speaker_note_fs4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_G4), MP_ROM_PTR(&flipperzero_speaker_note_g4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_GS4), MP_ROM_PTR(&flipperzero_speaker_note_gs4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_A4), MP_ROM_PTR(&flipperzero_speaker_note_a4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_AS4), MP_ROM_PTR(&flipperzero_speaker_note_as4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_B4), MP_ROM_PTR(&flipperzero_speaker_note_b4_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_C5), MP_ROM_PTR(&flipperzero_speaker_note_c5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_CS5), MP_ROM_PTR(&flipperzero_speaker_note_cs5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_D5), MP_ROM_PTR(&flipperzero_speaker_note_d5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_DS5), MP_ROM_PTR(&flipperzero_speaker_note_ds5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_E5), MP_ROM_PTR(&flipperzero_speaker_note_e5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_F5), MP_ROM_PTR(&flipperzero_speaker_note_f5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_FS5), MP_ROM_PTR(&flipperzero_speaker_note_fs5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_G5), MP_ROM_PTR(&flipperzero_speaker_note_g5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_GS5), MP_ROM_PTR(&flipperzero_speaker_note_gs5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_A5), MP_ROM_PTR(&flipperzero_speaker_note_a5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_AS5), MP_ROM_PTR(&flipperzero_speaker_note_as5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_B5), MP_ROM_PTR(&flipperzero_speaker_note_b5_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_C6), MP_ROM_PTR(&flipperzero_speaker_note_c6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_CS6), MP_ROM_PTR(&flipperzero_speaker_note_cs6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_D6), MP_ROM_PTR(&flipperzero_speaker_note_d6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_DS6), MP_ROM_PTR(&flipperzero_speaker_note_ds6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_E6), MP_ROM_PTR(&flipperzero_speaker_note_e6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_F6), MP_ROM_PTR(&flipperzero_speaker_note_f6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_FS6), MP_ROM_PTR(&flipperzero_speaker_note_fs6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_G6), MP_ROM_PTR(&flipperzero_speaker_note_g6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_GS6), MP_ROM_PTR(&flipperzero_speaker_note_gs6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_A6), MP_ROM_PTR(&flipperzero_speaker_note_a6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_AS6), MP_ROM_PTR(&flipperzero_speaker_note_as6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_B6), MP_ROM_PTR(&flipperzero_speaker_note_b6_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_C7), MP_ROM_PTR(&flipperzero_speaker_note_c7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_CS7), MP_ROM_PTR(&flipperzero_speaker_note_cs7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_D7), MP_ROM_PTR(&flipperzero_speaker_note_d7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_DS7), MP_ROM_PTR(&flipperzero_speaker_note_ds7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_E7), MP_ROM_PTR(&flipperzero_speaker_note_e7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_F7), MP_ROM_PTR(&flipperzero_speaker_note_f7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_FS7), MP_ROM_PTR(&flipperzero_speaker_note_fs7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_G7), MP_ROM_PTR(&flipperzero_speaker_note_g7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_GS7), MP_ROM_PTR(&flipperzero_speaker_note_gs7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_A7), MP_ROM_PTR(&flipperzero_speaker_note_a7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_AS7), MP_ROM_PTR(&flipperzero_speaker_note_as7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_B7), MP_ROM_PTR(&flipperzero_speaker_note_b7_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_C8), MP_ROM_PTR(&flipperzero_speaker_note_c8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_CS8), MP_ROM_PTR(&flipperzero_speaker_note_cs8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_D8), MP_ROM_PTR(&flipperzero_speaker_note_d8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_DS8), MP_ROM_PTR(&flipperzero_speaker_note_ds8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_E8), MP_ROM_PTR(&flipperzero_speaker_note_e8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_F8), MP_ROM_PTR(&flipperzero_speaker_note_f8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_FS8), MP_ROM_PTR(&flipperzero_speaker_note_fs8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_G8), MP_ROM_PTR(&flipperzero_speaker_note_g8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_GS8), MP_ROM_PTR(&flipperzero_speaker_note_gs8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_A8), MP_ROM_PTR(&flipperzero_speaker_note_a8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_AS8), MP_ROM_PTR(&flipperzero_speaker_note_as8_obj)}, - {MP_ROM_QSTR(MP_QSTR_SPEAKER_NOTE_B8), MP_ROM_PTR(&flipperzero_speaker_note_b8_obj)}, - + // speaker {MP_ROM_QSTR(MP_QSTR_SPEAKER_VOLUME_MIN), MP_ROM_PTR(&flipperzero_speaker_volume_min_obj)}, {MP_ROM_QSTR(MP_QSTR_SPEAKER_VOLUME_MAX), MP_ROM_PTR(&flipperzero_speaker_volume_max_obj)}, - {MP_ROM_QSTR(MP_QSTR_speaker_start), MP_ROM_PTR(&flipperzero_speaker_start_obj)}, {MP_ROM_QSTR(MP_QSTR_speaker_set_volume), MP_ROM_PTR(&flipperzero_speaker_set_volume_obj)}, {MP_ROM_QSTR(MP_QSTR_speaker_stop), MP_ROM_PTR(&flipperzero_speaker_stop_obj)}, @@ -987,6 +795,7 @@ const mp_obj_module_t flipperzero_module = { }; MP_REGISTER_MODULE(MP_QSTR_flipperzero, flipperzero_module); +MP_REGISTER_MODULE_DELEGATION(flipperzero_module, flipperzero_module_attr); void mp_flipper_on_input(uint16_t button, uint16_t type) { if(mp_flipper_on_input_callback != NULL) { diff --git a/mp_flipper/lib/micropython/mp_flipper_modflipperzero.h b/mp_flipper/lib/micropython/mp_flipper_modflipperzero.h index 555a50667..6d23d5fa5 100644 --- a/mp_flipper/lib/micropython/mp_flipper_modflipperzero.h +++ b/mp_flipper/lib/micropython/mp_flipper_modflipperzero.h @@ -42,134 +42,6 @@ void mp_flipper_light_blink_stop(); void mp_flipper_vibro(bool state); -/* -Python script for notes generation - -# coding: utf-8 -# Python script for notes generation - -from typing import List - -note_names: List = ['C', 'CS', 'D', 'DS', 'E', 'F', 'FS', 'G', 'GS', 'A', 'AS', 'B'] -base_note: float = 16.3515979 -cf: float = 2 ** (1.0 / 12) - -note: float = base_note -for octave in range(9): - for name in note_names: - print(f"#define MP_FLIPPER_SPEAKER_NOTE_{name}{octave} MICROPY_FLOAT_CONST({round(note, 2)})") - note = note * cf -*/ - -#define MP_FLIPPER_SPEAKER_NOTE_C0 MICROPY_FLOAT_CONST(16.35) -#define MP_FLIPPER_SPEAKER_NOTE_CS0 MICROPY_FLOAT_CONST(17.32) -#define MP_FLIPPER_SPEAKER_NOTE_D0 MICROPY_FLOAT_CONST(18.35) -#define MP_FLIPPER_SPEAKER_NOTE_DS0 MICROPY_FLOAT_CONST(19.45) -#define MP_FLIPPER_SPEAKER_NOTE_E0 MICROPY_FLOAT_CONST(20.6) -#define MP_FLIPPER_SPEAKER_NOTE_F0 MICROPY_FLOAT_CONST(21.83) -#define MP_FLIPPER_SPEAKER_NOTE_FS0 MICROPY_FLOAT_CONST(23.12) -#define MP_FLIPPER_SPEAKER_NOTE_G0 MICROPY_FLOAT_CONST(24.5) -#define MP_FLIPPER_SPEAKER_NOTE_GS0 MICROPY_FLOAT_CONST(25.96) -#define MP_FLIPPER_SPEAKER_NOTE_A0 MICROPY_FLOAT_CONST(27.5) -#define MP_FLIPPER_SPEAKER_NOTE_AS0 MICROPY_FLOAT_CONST(29.14) -#define MP_FLIPPER_SPEAKER_NOTE_B0 MICROPY_FLOAT_CONST(30.87) -#define MP_FLIPPER_SPEAKER_NOTE_C1 MICROPY_FLOAT_CONST(32.7) -#define MP_FLIPPER_SPEAKER_NOTE_CS1 MICROPY_FLOAT_CONST(34.65) -#define MP_FLIPPER_SPEAKER_NOTE_D1 MICROPY_FLOAT_CONST(36.71) -#define MP_FLIPPER_SPEAKER_NOTE_DS1 MICROPY_FLOAT_CONST(38.89) -#define MP_FLIPPER_SPEAKER_NOTE_E1 MICROPY_FLOAT_CONST(41.2) -#define MP_FLIPPER_SPEAKER_NOTE_F1 MICROPY_FLOAT_CONST(43.65) -#define MP_FLIPPER_SPEAKER_NOTE_FS1 MICROPY_FLOAT_CONST(46.25) -#define MP_FLIPPER_SPEAKER_NOTE_G1 MICROPY_FLOAT_CONST(49.0) -#define MP_FLIPPER_SPEAKER_NOTE_GS1 MICROPY_FLOAT_CONST(51.91) -#define MP_FLIPPER_SPEAKER_NOTE_A1 MICROPY_FLOAT_CONST(55.0) -#define MP_FLIPPER_SPEAKER_NOTE_AS1 MICROPY_FLOAT_CONST(58.27) -#define MP_FLIPPER_SPEAKER_NOTE_B1 MICROPY_FLOAT_CONST(61.74) -#define MP_FLIPPER_SPEAKER_NOTE_C2 MICROPY_FLOAT_CONST(65.41) -#define MP_FLIPPER_SPEAKER_NOTE_CS2 MICROPY_FLOAT_CONST(69.3) -#define MP_FLIPPER_SPEAKER_NOTE_D2 MICROPY_FLOAT_CONST(73.42) -#define MP_FLIPPER_SPEAKER_NOTE_DS2 MICROPY_FLOAT_CONST(77.78) -#define MP_FLIPPER_SPEAKER_NOTE_E2 MICROPY_FLOAT_CONST(82.41) -#define MP_FLIPPER_SPEAKER_NOTE_F2 MICROPY_FLOAT_CONST(87.31) -#define MP_FLIPPER_SPEAKER_NOTE_FS2 MICROPY_FLOAT_CONST(92.5) -#define MP_FLIPPER_SPEAKER_NOTE_G2 MICROPY_FLOAT_CONST(98.0) -#define MP_FLIPPER_SPEAKER_NOTE_GS2 MICROPY_FLOAT_CONST(103.83) -#define MP_FLIPPER_SPEAKER_NOTE_A2 MICROPY_FLOAT_CONST(110.0) -#define MP_FLIPPER_SPEAKER_NOTE_AS2 MICROPY_FLOAT_CONST(116.54) -#define MP_FLIPPER_SPEAKER_NOTE_B2 MICROPY_FLOAT_CONST(123.47) -#define MP_FLIPPER_SPEAKER_NOTE_C3 MICROPY_FLOAT_CONST(130.81) -#define MP_FLIPPER_SPEAKER_NOTE_CS3 MICROPY_FLOAT_CONST(138.59) -#define MP_FLIPPER_SPEAKER_NOTE_D3 MICROPY_FLOAT_CONST(146.83) -#define MP_FLIPPER_SPEAKER_NOTE_DS3 MICROPY_FLOAT_CONST(155.56) -#define MP_FLIPPER_SPEAKER_NOTE_E3 MICROPY_FLOAT_CONST(164.81) -#define MP_FLIPPER_SPEAKER_NOTE_F3 MICROPY_FLOAT_CONST(174.61) -#define MP_FLIPPER_SPEAKER_NOTE_FS3 MICROPY_FLOAT_CONST(185.0) -#define MP_FLIPPER_SPEAKER_NOTE_G3 MICROPY_FLOAT_CONST(196.0) -#define MP_FLIPPER_SPEAKER_NOTE_GS3 MICROPY_FLOAT_CONST(207.65) -#define MP_FLIPPER_SPEAKER_NOTE_A3 MICROPY_FLOAT_CONST(220.0) -#define MP_FLIPPER_SPEAKER_NOTE_AS3 MICROPY_FLOAT_CONST(233.08) -#define MP_FLIPPER_SPEAKER_NOTE_B3 MICROPY_FLOAT_CONST(246.94) -#define MP_FLIPPER_SPEAKER_NOTE_C4 MICROPY_FLOAT_CONST(261.63) -#define MP_FLIPPER_SPEAKER_NOTE_CS4 MICROPY_FLOAT_CONST(277.18) -#define MP_FLIPPER_SPEAKER_NOTE_D4 MICROPY_FLOAT_CONST(293.66) -#define MP_FLIPPER_SPEAKER_NOTE_DS4 MICROPY_FLOAT_CONST(311.13) -#define MP_FLIPPER_SPEAKER_NOTE_E4 MICROPY_FLOAT_CONST(329.63) -#define MP_FLIPPER_SPEAKER_NOTE_F4 MICROPY_FLOAT_CONST(349.23) -#define MP_FLIPPER_SPEAKER_NOTE_FS4 MICROPY_FLOAT_CONST(369.99) -#define MP_FLIPPER_SPEAKER_NOTE_G4 MICROPY_FLOAT_CONST(392.0) -#define MP_FLIPPER_SPEAKER_NOTE_GS4 MICROPY_FLOAT_CONST(415.3) -#define MP_FLIPPER_SPEAKER_NOTE_A4 MICROPY_FLOAT_CONST(440.0) -#define MP_FLIPPER_SPEAKER_NOTE_AS4 MICROPY_FLOAT_CONST(466.16) -#define MP_FLIPPER_SPEAKER_NOTE_B4 MICROPY_FLOAT_CONST(493.88) -#define MP_FLIPPER_SPEAKER_NOTE_C5 MICROPY_FLOAT_CONST(523.25) -#define MP_FLIPPER_SPEAKER_NOTE_CS5 MICROPY_FLOAT_CONST(554.37) -#define MP_FLIPPER_SPEAKER_NOTE_D5 MICROPY_FLOAT_CONST(587.33) -#define MP_FLIPPER_SPEAKER_NOTE_DS5 MICROPY_FLOAT_CONST(622.25) -#define MP_FLIPPER_SPEAKER_NOTE_E5 MICROPY_FLOAT_CONST(659.26) -#define MP_FLIPPER_SPEAKER_NOTE_F5 MICROPY_FLOAT_CONST(698.46) -#define MP_FLIPPER_SPEAKER_NOTE_FS5 MICROPY_FLOAT_CONST(739.99) -#define MP_FLIPPER_SPEAKER_NOTE_G5 MICROPY_FLOAT_CONST(783.99) -#define MP_FLIPPER_SPEAKER_NOTE_GS5 MICROPY_FLOAT_CONST(830.61) -#define MP_FLIPPER_SPEAKER_NOTE_A5 MICROPY_FLOAT_CONST(880.0) -#define MP_FLIPPER_SPEAKER_NOTE_AS5 MICROPY_FLOAT_CONST(932.33) -#define MP_FLIPPER_SPEAKER_NOTE_B5 MICROPY_FLOAT_CONST(987.77) -#define MP_FLIPPER_SPEAKER_NOTE_C6 MICROPY_FLOAT_CONST(1046.5) -#define MP_FLIPPER_SPEAKER_NOTE_CS6 MICROPY_FLOAT_CONST(1108.73) -#define MP_FLIPPER_SPEAKER_NOTE_D6 MICROPY_FLOAT_CONST(1174.66) -#define MP_FLIPPER_SPEAKER_NOTE_DS6 MICROPY_FLOAT_CONST(1244.51) -#define MP_FLIPPER_SPEAKER_NOTE_E6 MICROPY_FLOAT_CONST(1318.51) -#define MP_FLIPPER_SPEAKER_NOTE_F6 MICROPY_FLOAT_CONST(1396.91) -#define MP_FLIPPER_SPEAKER_NOTE_FS6 MICROPY_FLOAT_CONST(1479.98) -#define MP_FLIPPER_SPEAKER_NOTE_G6 MICROPY_FLOAT_CONST(1567.98) -#define MP_FLIPPER_SPEAKER_NOTE_GS6 MICROPY_FLOAT_CONST(1661.22) -#define MP_FLIPPER_SPEAKER_NOTE_A6 MICROPY_FLOAT_CONST(1760.0) -#define MP_FLIPPER_SPEAKER_NOTE_AS6 MICROPY_FLOAT_CONST(1864.66) -#define MP_FLIPPER_SPEAKER_NOTE_B6 MICROPY_FLOAT_CONST(1975.53) -#define MP_FLIPPER_SPEAKER_NOTE_C7 MICROPY_FLOAT_CONST(2093.0) -#define MP_FLIPPER_SPEAKER_NOTE_CS7 MICROPY_FLOAT_CONST(2217.46) -#define MP_FLIPPER_SPEAKER_NOTE_D7 MICROPY_FLOAT_CONST(2349.32) -#define MP_FLIPPER_SPEAKER_NOTE_DS7 MICROPY_FLOAT_CONST(2489.02) -#define MP_FLIPPER_SPEAKER_NOTE_E7 MICROPY_FLOAT_CONST(2637.02) -#define MP_FLIPPER_SPEAKER_NOTE_F7 MICROPY_FLOAT_CONST(2793.83) -#define MP_FLIPPER_SPEAKER_NOTE_FS7 MICROPY_FLOAT_CONST(2959.96) -#define MP_FLIPPER_SPEAKER_NOTE_G7 MICROPY_FLOAT_CONST(3135.96) -#define MP_FLIPPER_SPEAKER_NOTE_GS7 MICROPY_FLOAT_CONST(3322.44) -#define MP_FLIPPER_SPEAKER_NOTE_A7 MICROPY_FLOAT_CONST(3520.0) -#define MP_FLIPPER_SPEAKER_NOTE_AS7 MICROPY_FLOAT_CONST(3729.31) -#define MP_FLIPPER_SPEAKER_NOTE_B7 MICROPY_FLOAT_CONST(3951.07) -#define MP_FLIPPER_SPEAKER_NOTE_C8 MICROPY_FLOAT_CONST(4186.01) -#define MP_FLIPPER_SPEAKER_NOTE_CS8 MICROPY_FLOAT_CONST(4434.92) -#define MP_FLIPPER_SPEAKER_NOTE_D8 MICROPY_FLOAT_CONST(4698.64) -#define MP_FLIPPER_SPEAKER_NOTE_DS8 MICROPY_FLOAT_CONST(4978.03) -#define MP_FLIPPER_SPEAKER_NOTE_E8 MICROPY_FLOAT_CONST(5274.04) -#define MP_FLIPPER_SPEAKER_NOTE_F8 MICROPY_FLOAT_CONST(5587.65) -#define MP_FLIPPER_SPEAKER_NOTE_FS8 MICROPY_FLOAT_CONST(5919.91) -#define MP_FLIPPER_SPEAKER_NOTE_G8 MICROPY_FLOAT_CONST(6271.93) -#define MP_FLIPPER_SPEAKER_NOTE_GS8 MICROPY_FLOAT_CONST(6644.88) -#define MP_FLIPPER_SPEAKER_NOTE_A8 MICROPY_FLOAT_CONST(7040.0) -#define MP_FLIPPER_SPEAKER_NOTE_AS8 MICROPY_FLOAT_CONST(7458.62) -#define MP_FLIPPER_SPEAKER_NOTE_B8 MICROPY_FLOAT_CONST(7902.13) - #define MP_FLIPPER_SPEAKER_VOLUME_MIN MICROPY_FLOAT_CONST(0.0) #define MP_FLIPPER_SPEAKER_VOLUME_MAX MICROPY_FLOAT_CONST(1.0) diff --git a/mp_flipper/pyproject.toml b/mp_flipper/pyproject.toml index eb9c3bb3d..6637d4a83 100644 --- a/mp_flipper/pyproject.toml +++ b/mp_flipper/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "flipperzero" -version = "1.6.0" +version = "1.7.0" authors = [ { name = "Oliver Fabel" }, ] diff --git a/mp_flipper/upython.c b/mp_flipper/upython.c index fcd507a16..77a76b94b 100644 --- a/mp_flipper/upython.c +++ b/mp_flipper/upython.c @@ -10,7 +10,9 @@ volatile Action action = ActionNone; FuriString* file_path = NULL; volatile FuriThreadStdoutWriteCallback stdout_callback = NULL; -static void write_to_log_output(const char* data, size_t size) { +static void write_to_log_output(const char* data, size_t size, void* context) { + UNUSED(context); + furi_log_tx((const uint8_t*)data, size); } @@ -41,7 +43,7 @@ int32_t upython(void* args) { case ActionRepl: break; case ActionExec: - furi_thread_set_stdout_callback(stdout_callback); + furi_thread_set_stdout_callback(stdout_callback, NULL); upython_file_execute(file_path); @@ -49,7 +51,7 @@ int32_t upython(void* args) { action = ActionNone; - furi_thread_set_stdout_callback(stdout_callback = NULL); + furi_thread_set_stdout_callback(stdout_callback = NULL, NULL); break; case ActionExit: diff --git a/mp_flipper/upython_cli.c b/mp_flipper/upython_cli.c index d41f25a82..0447d3460 100644 --- a/mp_flipper/upython_cli.c +++ b/mp_flipper/upython_cli.c @@ -5,7 +5,9 @@ static FuriStreamBuffer* stdout_buffer = NULL; -static void write_to_stdout_buffer(const char* data, size_t size) { +static void write_to_stdout_buffer(const char* data, size_t size, void* context) { + UNUSED(context); + furi_stream_buffer_send(stdout_buffer, data, size, 0); } diff --git a/mtp/.github/FUNDING.yml b/mtp/.github/FUNDING.yml new file mode 100644 index 000000000..6b78f39e6 --- /dev/null +++ b/mtp/.github/FUNDING.yml @@ -0,0 +1 @@ +github: Alex4386 diff --git a/mtp/.gitignore b/mtp/.gitignore index 81a8981f7..dd6a4750d 100644 --- a/mtp/.gitignore +++ b/mtp/.gitignore @@ -1,6 +1,7 @@ dist/* .vscode .clang-format +.clangd .editorconfig .env .ufbt diff --git a/mtp/src/scenes/mtp/device_props.c b/mtp/src/scenes/mtp/device_props.c new file mode 100644 index 000000000..e1fa16737 --- /dev/null +++ b/mtp/src/scenes/mtp/device_props.c @@ -0,0 +1,223 @@ +#include "device_props.h" +#include "utils.h" +#include "usb_desc.h" +#include "mtp_support.h" +#include + +int GetDevicePropValueInternal(uint32_t prop_code, uint8_t* buffer) { + uint8_t* ptr = buffer; + uint16_t length; + + switch(prop_code) { + case 0xd402: { + const char* deviceName = furi_hal_version_get_name_ptr(); + if(deviceName == NULL) { + deviceName = "Flipper Zero"; + } + + WriteMTPString(ptr, deviceName, &length); + ptr += length; + break; + } + case 0x5001: { + // Battery level + Power* power = furi_record_open(RECORD_POWER); + FURI_LOG_I("MTP", "Getting battery level"); + if(power == NULL) { + *(uint8_t*)ptr = 0x00; + } else { + PowerInfo info; + power_get_info(power, &info); + + FURI_LOG_I("MTP", "Battery level: %d", info.charge); + + *(uint8_t*)ptr = info.charge; + furi_record_close(RECORD_POWER); + } + ptr += sizeof(uint8_t); + break; + } + default: + // Unsupported property + break; + } + + return ptr - buffer; +} + +int GetDevicePropDescInternal(uint32_t prop_code, uint8_t* buffer) { + uint8_t* ptr = buffer; + uint16_t length; + + switch(prop_code) { + case 0xd402: + // Device friendly name + *(uint16_t*)ptr = prop_code; + ptr += 2; + + // type is string + *(uint16_t*)ptr = 0xffff; + ptr += 2; + + // read-only + *(uint8_t*)ptr = 0x00; + ptr += 1; + + length = GetDevicePropValueInternal(prop_code, ptr); + ptr += length; + + length = GetDevicePropValueInternal(prop_code, ptr); + ptr += length; + + // no-form + *(uint8_t*)ptr = 0x00; + ptr += 1; + break; + case 0x5001: + // Device friendly name + *(uint16_t*)ptr = prop_code; + ptr += 2; + + // type is uint8 + *(uint16_t*)ptr = 0x0002; + ptr += 2; + + // read-only + *(uint8_t*)ptr = 0x00; + ptr += 1; + + length = GetDevicePropValueInternal(prop_code, ptr); + ptr += length; + + length = GetDevicePropValueInternal(prop_code, ptr); + ptr += length; + + // no-form + *(uint8_t*)ptr = 0x00; + ptr += 1; + break; + + default: + // Unsupported property + break; + } + + return ptr - buffer; +} + +void GetDevicePropValue(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code) { + uint8_t* response = malloc(sizeof(uint8_t) * MTP_BUFFER_SIZE); + int length = GetDevicePropValueInternal(prop_code, response); + send_mtp_response_buffer( + mtp, MTP_TYPE_DATA, MTP_OP_GET_DEVICE_PROP_VALUE, transaction_id, response, length); + send_mtp_response_buffer(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL, 0); + free(response); +} + +void GetDevicePropDesc(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code) { + uint8_t* response = malloc(sizeof(uint8_t) * MTP_BUFFER_SIZE); + int length = GetDevicePropDescInternal(prop_code, response); + send_mtp_response_buffer( + mtp, MTP_TYPE_DATA, MTP_OP_GET_DEVICE_PROP_DESC, transaction_id, response, length); + send_mtp_response_buffer(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL, 0); + free(response); +} + +int BuildDeviceInfo(uint8_t* buffer) { + uint8_t* ptr = buffer; + uint16_t length; + + // Standard version + *(uint16_t*)ptr = 100; + ptr += sizeof(uint16_t); + + // Vendor extension ID + *(uint32_t*)ptr = MTP_VENDOR_EXTENSION_ID; + ptr += sizeof(uint32_t); + + // Vendor extension version + *(uint16_t*)ptr = MTP_VENDOR_EXTENSION_VERSION; + ptr += sizeof(uint16_t); + + // Vendor extension description + WriteMTPString(ptr, "microsoft.com: 1.0;", &length); + ptr += length; + + // Functional mode + FURI_LOG_I("MTP", "MTP Functional Mode"); + *(uint16_t*)ptr = MTP_FUNCTIONAL_MODE; + ptr += sizeof(uint16_t); + + // Operations supported + FURI_LOG_I("MTP", "Writing Operation Supported"); + length = sizeof(supported_operations) / sizeof(uint16_t); + *(uint32_t*)ptr = length; // Number of supported operations + FURI_LOG_I("MTP", "Supported Operations: %d", length); + ptr += sizeof(uint32_t); + + for(int i = 0; i < length; i++) { + FURI_LOG_I("MTP", "Operation: %04x", supported_operations[i]); + *(uint16_t*)ptr = supported_operations[i]; + ptr += sizeof(uint16_t); + } + + // Supported events (example, add as needed) + FURI_LOG_I("MTP", "Supported Events"); + *(uint32_t*)ptr = 0; // Number of supported events + ptr += sizeof(uint32_t); + + FURI_LOG_I("MTP", "Supported Device Properties"); + length = sizeof(supported_device_properties) / sizeof(uint16_t); + *(uint32_t*)ptr = length; // Number of supported device properties + ptr += sizeof(uint32_t); + + for(int i = 0; i < length; i++) { + FURI_LOG_I("MTP", "Supported Device Props"); + *(uint16_t*)ptr = supported_device_properties[i]; + ptr += sizeof(uint16_t); + } + + // Supported capture formats (example, add as needed) + *(uint32_t*)ptr = 0; // Number of supported capture formats + ptr += sizeof(uint32_t); + + // Supported playback formats (example, add as needed) + FURI_LOG_I("MTP", "Supported Playback Formats"); + length = sizeof(supported_playback_formats) / sizeof(uint16_t); + *(uint32_t*)ptr = length; // Number of supported playback formats + ptr += sizeof(uint32_t); + + for(int i = 0; i < length; i++) { + *(uint16_t*)ptr = supported_playback_formats[i]; + ptr += sizeof(uint16_t); + } + + // Manufacturer + WriteMTPString(ptr, USB_MANUFACTURER_STRING, &length); + ptr += length; + + // Model + WriteMTPString(ptr, USB_DEVICE_MODEL, &length); + ptr += length; + + // Device version + const Version* ver = furi_hal_version_get_firmware_version(); + WriteMTPString(ptr, version_get_version(ver), &length); + ptr += length; + + // Serial number + char* serial = malloc(sizeof(char) * furi_hal_version_uid_size() * 2 + 1); + const uint8_t* uid = furi_hal_version_uid(); + for(size_t i = 0; i < furi_hal_version_uid_size(); i++) { + serial[i * 2] = byte_to_hex(uid[i] >> 4); + serial[i * 2 + 1] = byte_to_hex(uid[i] & 0x0F); + } + serial[furi_hal_version_uid_size() * 2] = '\0'; + + WriteMTPString(ptr, serial, &length); + ptr += length; + + free(serial); + + return ptr - buffer; +} diff --git a/mtp/src/scenes/mtp/device_props.h b/mtp/src/scenes/mtp/device_props.h new file mode 100644 index 000000000..75aa746d0 --- /dev/null +++ b/mtp/src/scenes/mtp/device_props.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include "main.h" +#include "mtp.h" + +// Device property operations +void GetDevicePropValue(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code); +void GetDevicePropDesc(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code); +int GetDevicePropValueInternal(uint32_t prop_code, uint8_t* buffer); +int GetDevicePropDescInternal(uint32_t prop_code, uint8_t* buffer); +int BuildDeviceInfo(uint8_t* buffer); diff --git a/mtp/src/scenes/mtp/mtp.c b/mtp/src/scenes/mtp/mtp.c index a54745323..b480bb170 100644 --- a/mtp/src/scenes/mtp/mtp.c +++ b/mtp/src/scenes/mtp/mtp.c @@ -3,61 +3,13 @@ #include #include "main.h" #include "mtp.h" +#include "mtp_ops.h" +#include "mtp_support.h" +#include "device_props.h" +#include "storage_ops.h" #include "usb_desc.h" #include "utils.h" -#define BLOCK_SIZE 65536 - -// Supported operations (example, add as needed) -uint16_t supported_operations[] = { - MTP_OP_GET_DEVICE_INFO, - MTP_OP_OPEN_SESSION, - MTP_OP_CLOSE_SESSION, - MTP_OP_GET_STORAGE_IDS, - MTP_OP_GET_STORAGE_INFO, - MTP_OP_GET_NUM_OBJECTS, - MTP_OP_GET_OBJECT_HANDLES, - MTP_OP_GET_OBJECT_INFO, - MTP_OP_GET_OBJECT, - MTP_OP_SEND_OBJECT_INFO, - MTP_OP_SEND_OBJECT, - MTP_OP_DELETE_OBJECT, - MTP_OP_GET_DEVICE_PROP_DESC, - MTP_OP_GET_DEVICE_PROP_VALUE, - MTP_OP_GET_OBJECT_PROPS_SUPPORTED, - MTP_OP_SET_OBJECT_PROP_VALUE, - MTP_OP_MOVE_OBJECT, - MTP_OP_POWER_DOWN, -}; - -uint16_t supported_object_props[] = { - MTP_PROP_STORAGE_ID, - MTP_PROP_OBJECT_FORMAT, - MTP_PROP_OBJECT_FILE_NAME, -}; - -// Supported device properties (example, add as needed) -uint16_t supported_device_properties[] = { - 0xd402, // Device friendly name - 0x5001, // Battery level -}; - -uint16_t supported_playback_formats[] = { - MTP_FORMAT_UNDEFINED, - MTP_FORMAT_ASSOCIATION, -}; - -void merge_path(char* target, char* base, char* name) { - // implement this way since strcat is unavailable - - char* ptr = target; - strcpy(target, base); - ptr += strlen(base); - strcpy(ptr, "/"); - ptr++; - strcpy(ptr, name); -} - // temporary storage for buffer MTPDataPersistence persistence; @@ -124,7 +76,7 @@ void handle_mtp_data_packet(AppMTP* mtp, uint8_t* buffer, int32_t length, bool c switch(header->op) { case MTP_OP_SEND_OBJECT_INFO: case MTP_OP_SET_OBJECT_PROP_VALUE: { - persistence.global_buffer = malloc(sizeof(uint8_t) * 256); + persistence.global_buffer = malloc(sizeof(uint8_t) * MTP_BUFFER_SIZE); persistence.buffer_offset = 0; break; } @@ -163,20 +115,34 @@ void handle_mtp_data_packet(AppMTP* mtp, uint8_t* buffer, int32_t length, bool c } File* file = persistence.current_file; - // no need since the file is already opened - // storage_file_seek(file, offset, SEEK_SET); - storage_file_write(file, ptr, bytes_length); + // Write with error handling + uint16_t bytes_written = storage_file_write(file, ptr, bytes_length); + if(bytes_written != bytes_length) { + FURI_LOG_E("MTP", "Write failed: %d/%d bytes written", bytes_written, bytes_length); + send_mtp_response(mtp, 3, MTP_RESP_STORE_FULL, persistence.transaction_id, NULL); + storage_file_close(file); + storage_file_free(file); + persistence.current_file = NULL; + return; + } + + persistence.buffer_offset += bytes_written; + persistence.left_bytes -= bytes_written; - persistence.buffer_offset += bytes_length; - persistence.left_bytes -= bytes_length; + // Sync periodically based on configured interval + if(persistence.buffer_offset % MTP_FILE_SYNC_INTERVAL == 0) { + storage_file_sync(file); + FURI_LOG_I("MTP", "Progress: %ld bytes remaining", persistence.left_bytes); + } if(persistence.left_bytes <= 0) { + storage_file_sync(file); send_mtp_response(mtp, 3, MTP_RESP_OK, persistence.transaction_id, NULL); storage_file_close(file); storage_file_free(file); - persistence.current_file = NULL; + FURI_LOG_I("MTP", "File transfer completed successfully"); } } else if(persistence.op == MTP_OP_SET_OBJECT_PROP_VALUE) { uint32_t handle = persistence.params[0]; @@ -195,7 +161,7 @@ void handle_mtp_data_packet(AppMTP* mtp, uint8_t* buffer, int32_t length, bool c print_bytes("MTP FileName", ptr, length - sizeof(struct MTPHeader)); char* name = ReadMTPString(ptr); - char* full_path = malloc(sizeof(char) * 256); + char* full_path = malloc(sizeof(char) * MTP_PATH_SIZE); merge_path(full_path, get_base_path_from_storage_id(persistence.params[2]), name); @@ -297,7 +263,7 @@ void handle_mtp_data_complete(AppMTP* mtp) { } } - char* full_path = malloc(sizeof(char) * 256); + char* full_path = malloc(sizeof(char) * MTP_PATH_SIZE); merge_path(full_path, base_path, name); FURI_LOG_I("MTP", "Format: %04x", info->format); @@ -380,7 +346,7 @@ void handle_mtp_command(AppMTP* mtp, struct MTPContainer* container) { break; case MTP_OP_GET_STORAGE_INFO: { FURI_LOG_I("MTP", "GetStorageInfo operation"); - uint8_t* info = malloc(sizeof(uint8_t) * 256); + uint8_t* info = malloc(sizeof(uint8_t) * MTP_BUFFER_SIZE); int length = GetStorageInfo(mtp, container->params[0], info); send_mtp_response_buffer( mtp, @@ -406,7 +372,7 @@ void handle_mtp_command(AppMTP* mtp, struct MTPContainer* container) { 0); break; } else { - uint8_t* buffer = malloc(sizeof(uint8_t) * 256); + uint8_t* buffer = malloc(sizeof(uint8_t) * MTP_BUFFER_SIZE); int length = GetObjectHandles(mtp, container->params[0], container->params[2], buffer); send_mtp_response_buffer( mtp, @@ -423,7 +389,7 @@ void handle_mtp_command(AppMTP* mtp, struct MTPContainer* container) { break; case MTP_OP_GET_OBJECT_INFO: { FURI_LOG_I("MTP", "GetObjectInfo operation"); - uint8_t* buffer = malloc(sizeof(uint8_t) * 512); + uint8_t* buffer = malloc(sizeof(uint8_t) * MTP_BUFFER_SIZE); int length = GetObjectInfo(mtp, container->params[0], buffer); if(length < 0) { @@ -451,7 +417,7 @@ void handle_mtp_command(AppMTP* mtp, struct MTPContainer* container) { } case MTP_OP_GET_OBJECT_PROPS_SUPPORTED: { FURI_LOG_I("MTP", "GetObjectPropsSupported operation"); - uint8_t* buffer = malloc(sizeof(uint8_t) * 256); + uint8_t* buffer = malloc(sizeof(uint8_t) * MTP_BUFFER_SIZE); uint32_t count = sizeof(supported_object_props) / sizeof(uint16_t); memcpy(buffer, &count, sizeof(uint32_t)); memcpy(buffer + sizeof(uint32_t), supported_object_props, sizeof(uint16_t) * count); @@ -481,12 +447,12 @@ void handle_mtp_command(AppMTP* mtp, struct MTPContainer* container) { case MTP_OP_GET_DEVICE_PROP_VALUE: FURI_LOG_I("MTP", "GetDevicePropValue operation"); - send_device_prop_value(mtp, container->header.transaction_id, container->params[0]); + GetDevicePropValue(mtp, container->header.transaction_id, container->params[0]); // Process the GetDevicePropValue operation break; case MTP_OP_GET_DEVICE_PROP_DESC: FURI_LOG_I("MTP", "GetDevicePropDesc operation"); - send_device_prop_desc(mtp, container->header.transaction_id, container->params[0]); + GetDevicePropDesc(mtp, container->header.transaction_id, container->params[0]); // Process the GetDevicePropDesc operation break; // Handle bulk transfer specific operations @@ -562,7 +528,7 @@ void send_storage_ids(AppMTP* mtp, uint32_t transaction_id) { } void send_device_info(AppMTP* mtp, uint32_t transaction_id) { - uint8_t* response = malloc(sizeof(uint8_t) * 256); + uint8_t* response = malloc(sizeof(uint8_t) * MTP_BUFFER_SIZE); int length = BuildDeviceInfo(response); send_mtp_response_buffer( mtp, MTP_TYPE_DATA, MTP_OP_GET_DEVICE_INFO, transaction_id, response, length); @@ -570,446 +536,9 @@ void send_device_info(AppMTP* mtp, uint32_t transaction_id) { free(response); } -void send_device_prop_value(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code) { - uint8_t* response = malloc(sizeof(uint8_t) * 256); - int length = GetDevicePropValue(prop_code, response); - send_mtp_response_buffer( - mtp, MTP_TYPE_DATA, MTP_OP_GET_DEVICE_PROP_VALUE, transaction_id, response, length); - send_mtp_response_buffer(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL, 0); - free(response); -} - -void send_device_prop_desc(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code) { - uint8_t* response = malloc(sizeof(uint8_t) * 256); - int length = GetDevicePropDesc(prop_code, response); - send_mtp_response_buffer( - mtp, MTP_TYPE_DATA, MTP_OP_GET_DEVICE_PROP_DESC, transaction_id, response, length); - send_mtp_response_buffer(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL, 0); - free(response); -} - -char* get_path_from_handle(AppMTP* mtp, uint32_t handle) { - FileHandle* current = mtp->handles; - while(current != NULL) { - if(current->handle == handle) { - return current->path; - } - current = current->next; - } - return NULL; -} - -uint32_t issue_object_handle(AppMTP* mtp, char* path) { - int handle = 1; - int length = strlen(path); - char* path_store = malloc(sizeof(char) * (length + 1)); - strcpy(path_store, path); - - if(mtp->handles == NULL) { - mtp->handles = malloc(sizeof(FileHandle)); - mtp->handles->handle = handle; - mtp->handles->path = path_store; - mtp->handles->next = NULL; - return handle; - } - - FileHandle* current = mtp->handles; - if(strcmp(current->path, path) == 0) { - return current->handle; - } - - while(current->next != NULL) { - if(strcmp(current->path, path) == 0) { - return current->handle; - } - current = current->next; - handle++; - } - - current->next = malloc(sizeof(FileHandle)); - current = current->next; - handle++; - - current->handle = handle; - current->path = path_store; - current->next = NULL; - return handle; -} - -uint32_t update_object_handle_path(AppMTP* mtp, uint32_t handle, char* path) { - FileHandle* current = mtp->handles; - while(current != NULL) { - if(current->handle == handle) { - free(current->path); - current->path = path; - return handle; - } - current = current->next; - } - return 0; -} - -char* get_base_path_from_storage_id(uint32_t storage_id) { - if(storage_id == INTERNAL_STORAGE_ID) { - return STORAGE_INT_PATH_PREFIX; - } else if(storage_id == EXTERNAL_STORAGE_ID) { - return STORAGE_EXT_PATH_PREFIX; - } - return NULL; -} - -int list_and_issue_handles( - AppMTP* mtp, - uint32_t storage_id, - uint32_t association, - uint32_t* handles) { - Storage* storage = mtp->storage; - char* base_path = get_base_path_from_storage_id(storage_id); - if(base_path == NULL) { - base_path = ""; - } - - File* dir = storage_file_alloc(storage); - if(association == 0xffffffff) { - // count the objects in the root directory - storage_dir_open(dir, base_path); - } else { - char* path = get_path_from_handle(mtp, association); - FURI_LOG_I("MTP", "Association path: %s", path); - if(path == NULL) { - return 0; - } - storage_dir_open(dir, path); - base_path = path; - } - - int count = 0; - FileInfo fileinfo; - char* file_name = malloc(sizeof(char) * 256); - char* full_path = malloc(sizeof(char) * 256); - while(storage_dir_read(dir, &fileinfo, file_name, 256)) { - if(file_info_is_dir(&fileinfo)) { - FURI_LOG_I("MTP", "Found directory: %s", file_name); - } else { - FURI_LOG_I("MTP", "Found file: %s", file_name); - } - - merge_path(full_path, base_path, file_name); - FURI_LOG_I("MTP", "Full path: %s", full_path); - - uint32_t handle = issue_object_handle(mtp, full_path); - if(handles != NULL) { - handles[count] = handle; - } - count++; - } - - FURI_LOG_I("MTP", "Getting number of objects in storage %ld", storage_id); - FURI_LOG_I("MTP", "Base path: %s", base_path); - FURI_LOG_I("MTP", "Association: %ld", association); - FURI_LOG_I("MTP", "Number of objects: %d", count); - - storage_dir_close(dir); - storage_file_free(dir); - free(file_name); - free(full_path); - return count; -} - -struct GetObjectContext { - File* file; -}; - -int GetObject_callback(void* ctx, uint8_t* buffer, int length) { - struct GetObjectContext* obj_ctx = (struct GetObjectContext*)ctx; - return storage_file_read(obj_ctx->file, buffer, length); -} - -void GetObject(AppMTP* mtp, uint32_t transaction_id, uint32_t handle) { - char* path = get_path_from_handle(mtp, handle); - if(path == NULL) { - send_mtp_response( - mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); - return; - } - - FURI_LOG_I("MTP", "Getting object: %s", path); - - Storage* storage = mtp->storage; - File* file = storage_file_alloc(storage); - if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { - FURI_LOG_E("MTP", "Failed to open file: %s", path); - send_mtp_response( - mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); - return; - } - - uint32_t size = storage_file_size(file); - - struct GetObjectContext ctx = { - .file = file, - }; - - send_mtp_response_stream( - mtp, MTP_TYPE_DATA, MTP_OP_GET_OBJECT, transaction_id, &ctx, GetObject_callback, size); - - send_mtp_response(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL); - - storage_file_close(file); - storage_file_free(file); -} - -int GetObjectInfo(AppMTP* mtp, uint32_t handle, uint8_t* buffer) { - ObjectInfoHeader* header = (ObjectInfoHeader*)buffer; - uint8_t* ptr = buffer + sizeof(ObjectInfoHeader); - - char* path = get_path_from_handle(mtp, handle); - if(path == NULL) { - return -1; - } - - FURI_LOG_I("MTP", "Getting object info for handle %ld", handle); - FURI_LOG_I("MTP", "Path: %s", path); - - header->protection_status = 0; - - Storage* storage = mtp->storage; - File* file = storage_file_alloc(storage); - uint16_t length; - - FileInfo fileinfo; - FS_Error err = storage_common_stat(storage, path, &fileinfo); - if(err != FSE_OK) { - FURI_LOG_E("MTP", "Failed to get file info: %s", filesystem_api_error_get_desc(err)); - return -1; - } - - if(memcmp(path, STORAGE_INT_PATH_PREFIX, strlen(STORAGE_INT_PATH_PREFIX)) == 0) { - FURI_LOG_I("MTP", "Object in Internal storage"); - header->storage_id = INTERNAL_STORAGE_ID; - } else if(memcmp(path, STORAGE_EXT_PATH_PREFIX, strlen(STORAGE_EXT_PATH_PREFIX)) == 0) { - FURI_LOG_I("MTP", "Object in External storage"); - header->storage_id = EXTERNAL_STORAGE_ID; - } else { - return -1; - } - - if(file_info_is_dir(&fileinfo)) { - FURI_LOG_I("MTP", "Directory"); - - header->format = MTP_FORMAT_ASSOCIATION; - header->association_type = 0x0001; // Generic folder - } else { - FURI_LOG_I("MTP", "Undefined type"); - - header->format = MTP_FORMAT_UNDEFINED; - } - - header->compressed_size = fileinfo.size; - - header->thumb_format = 0; - header->thumb_compressed_size = 0; - header->thumb_pix_width = 0; - header->thumb_pix_height = 0; - header->image_pix_width = 0; - header->image_pix_height = 0; - header->image_bit_depth = 0; - - /* - // is on root directory (/int or /ext) - if(strchr(path + 1, '/') == NULL) { - header->parent_object = 0; - } else { - char* parent_path = malloc(sizeof(char) * 256); - strcpy(parent_path, path); - char* last_slash = strrchr(parent_path, '/'); - *last_slash = '\0'; - header->parent_object = issue_object_handle(mtp, parent_path); - - free(parent_path); - } - */ - - char* file_name = strrchr(path, '/'); - FURI_LOG_I("MTP", "File name: %s", file_name + 1); - - WriteMTPString(ptr, file_name + 1, &length); - ptr += length; - - // get created - WriteMTPString(ptr, "20240608T010702", &length); - ptr += length; - - // get last modified - WriteMTPString(ptr, "20240608T010702", &length); - ptr += length; - - // get keywords - WriteMTPString(ptr, "", &length); - ptr += length; - - storage_file_free(file); - - return ptr - buffer; -} - -bool CheckMTPStringHasUnicode(uint8_t* buffer) { - uint8_t* base = buffer; - int len = *base; - - uint16_t* ptr = (uint16_t*)(base + 1); - - for(int i = 0; i < len; i++) { - if(ptr[i] > 0x7F) { - return true; - } - } - - return false; -} - -char* ReadMTPString(uint8_t* buffer) { - int len16 = *(uint8_t*)buffer; - if(len16 == 0) { - return ""; - } - - char* str = malloc(sizeof(char) * len16); - - uint8_t* base = buffer + 1; - uint16_t* ptr = (uint16_t*)base; - - for(int i = 0; i < len16; i++) { - str[i] = *ptr++; - } - - return str; -} - int GetNumObjects(AppMTP* mtp, uint32_t storage_id, uint32_t association) { return list_and_issue_handles(mtp, storage_id, association, NULL); } - -int GetObjectHandles(AppMTP* mtp, uint32_t storage_id, uint32_t association, uint8_t* buffer) { - uint8_t* ptr = buffer; - uint16_t length; - - UNUSED(length); - - // For now, just return a single object handle - int count = GetNumObjects(mtp, storage_id, association); - - *(uint32_t*)ptr = count; - ptr += sizeof(uint32_t); - - uint32_t* handles = (uint32_t*)ptr; - length = list_and_issue_handles(mtp, storage_id, association, handles); - ptr += length * sizeof(uint32_t); - - return ptr - buffer; -} - -int GetDevicePropValue(uint32_t prop_code, uint8_t* buffer) { - uint8_t* ptr = buffer; - uint16_t length; - - switch(prop_code) { - case 0xd402: { - const char* deviceName = furi_hal_version_get_name_ptr(); - if(deviceName == NULL) { - deviceName = "Flipper Zero"; - } - - WriteMTPString(ptr, deviceName, &length); - ptr += length; - break; - } - case 0x5001: { - // Battery level - Power* power = furi_record_open(RECORD_POWER); - FURI_LOG_I("MTP", "Getting battery level"); - if(power == NULL) { - *(uint8_t*)ptr = 0x00; - } else { - PowerInfo info; - power_get_info(power, &info); - - FURI_LOG_I("MTP", "Battery level: %d", info.charge); - - *(uint8_t*)ptr = info.charge; - furi_record_close(RECORD_POWER); - } - ptr += sizeof(uint8_t); - break; - } - default: - // Unsupported property - break; - } - - return ptr - buffer; -} - -int GetDevicePropDesc(uint32_t prop_code, uint8_t* buffer) { - uint8_t* ptr = buffer; - uint16_t length; - - switch(prop_code) { - case 0xd402: - // Device friendly name - *(uint16_t*)ptr = prop_code; - ptr += 2; - - // type is string - *(uint16_t*)ptr = 0xffff; - ptr += 2; - - // read-only - *(uint8_t*)ptr = 0x00; - ptr += 1; - - length = GetDevicePropValue(prop_code, ptr); - ptr += length; - - length = GetDevicePropValue(prop_code, ptr); - ptr += length; - - // no-form - *(uint8_t*)ptr = 0x00; - ptr += 1; - break; - case 0x5001: - // Device friendly name - *(uint16_t*)ptr = prop_code; - ptr += 2; - - // type is uint8 - *(uint16_t*)ptr = 0x0002; - ptr += 2; - - // read-only - *(uint8_t*)ptr = 0x00; - ptr += 1; - - length = GetDevicePropValue(prop_code, ptr); - ptr += length; - - length = GetDevicePropValue(prop_code, ptr); - ptr += length; - - // no-form - *(uint8_t*)ptr = 0x00; - ptr += 1; - break; - - default: - // Unsupported property - break; - } - - return ptr - buffer; -} - void send_mtp_response_stream( AppMTP* mtp, uint16_t resp_type, @@ -1155,415 +684,3 @@ int mtp_handle_class_control(AppMTP* mtp, usbd_device* dev, usbd_ctlreq* req) { return value; } -int BuildDeviceInfo(uint8_t* buffer) { - uint8_t* ptr = buffer; - uint16_t length; - - // Standard version - *(uint16_t*)ptr = 100; - ptr += sizeof(uint16_t); - - // Vendor extension ID - *(uint32_t*)ptr = MTP_VENDOR_EXTENSION_ID; - ptr += sizeof(uint32_t); - - // Vendor extension version - *(uint16_t*)ptr = MTP_VENDOR_EXTENSION_VERSION; - ptr += sizeof(uint16_t); - - // Vendor extension description - WriteMTPString(ptr, "microsoft.com: 1.0;", &length); - ptr += length; - - // Functional mode - FURI_LOG_I("MTP", "MTP Functional Mode"); - *(uint16_t*)ptr = MTP_FUNCTIONAL_MODE; - ptr += sizeof(uint16_t); - - // Operations supported - FURI_LOG_I("MTP", "Writing Operation Supported"); - length = sizeof(supported_operations) / sizeof(uint16_t); - *(uint32_t*)ptr = length; // Number of supported operations - FURI_LOG_I("MTP", "Supported Operations: %d", length); - ptr += sizeof(uint32_t); - - for(int i = 0; i < length; i++) { - FURI_LOG_I("MTP", "Operation: %04x", supported_operations[i]); - *(uint16_t*)ptr = supported_operations[i]; - ptr += sizeof(uint16_t); - } - - // Supported events (example, add as needed) - FURI_LOG_I("MTP", "Supported Events"); - *(uint32_t*)ptr = 0; // Number of supported events - ptr += sizeof(uint32_t); - - FURI_LOG_I("MTP", "Supported Device Properties"); - length = sizeof(supported_device_properties) / sizeof(uint16_t); - *(uint32_t*)ptr = length; // Number of supported device properties - ptr += sizeof(uint32_t); - - for(int i = 0; i < length; i++) { - FURI_LOG_I("MTP", "Supported Device Props"); - *(uint16_t*)ptr = supported_device_properties[i]; - ptr += sizeof(uint16_t); - } - - // Supported capture formats (example, add as needed) - *(uint32_t*)ptr = 0; // Number of supported capture formats - ptr += sizeof(uint32_t); - - // Supported playback formats (example, add as needed) - FURI_LOG_I("MTP", "Supported Playback Formats"); - length = sizeof(supported_playback_formats) / sizeof(uint16_t); - *(uint32_t*)ptr = length; // Number of supported playback formats - ptr += sizeof(uint32_t); - - for(int i = 0; i < length; i++) { - *(uint16_t*)ptr = supported_playback_formats[i]; - ptr += sizeof(uint16_t); - } - - // Manufacturer - WriteMTPString(ptr, USB_MANUFACTURER_STRING, &length); - ptr += length; - - // Model - WriteMTPString(ptr, USB_DEVICE_MODEL, &length); - ptr += length; - - // Device version - const Version* ver = furi_hal_version_get_firmware_version(); - WriteMTPString(ptr, version_get_version(ver), &length); - ptr += length; - - // Serial number - char* serial = malloc(sizeof(char) * furi_hal_version_uid_size() * 2 + 1); - const uint8_t* uid = furi_hal_version_uid(); - for(size_t i = 0; i < furi_hal_version_uid_size(); i++) { - serial[i * 2] = byte_to_hex(uid[i] >> 4); - serial[i * 2 + 1] = byte_to_hex(uid[i] & 0x0F); - } - serial[furi_hal_version_uid_size() * 2] = '\0'; - - WriteMTPString(ptr, serial, &length); - ptr += length; - - free(serial); - - return ptr - 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); - - // 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 = 0; // No storage. - return; - } - - storage_ids[0] = EXTERNAL_STORAGE_ID; - *count = 1; -} - -int GetStorageInfo(AppMTP* mtp, uint32_t storage_id, uint8_t* buf) { - MTPStorageInfoHeader* info = (MTPStorageInfoHeader*)buf; - uint8_t* ptr = buf + sizeof(MTPStorageInfoHeader); - uint16_t length; - - info->free_space_in_objects = 20ul; - info->filesystem_type = 0x0002; // Generic hierarchical - info->access_capability = 0x0000; // Read-write - FURI_LOG_I("MTP", "Getting storage info for storage ID %04lx", storage_id); - - if(storage_id == INTERNAL_STORAGE_ID) { - // Fill in details for internal storage - info->storage_type = 0x0003; // Fixed RAM - - // Fill in details for internal storage - uint64_t total_space; - uint64_t free_space; - FS_Error err = storage_common_fs_info( - mtp->storage, STORAGE_INT_PATH_PREFIX, &total_space, &free_space); - if(err != FSE_OK) { - info->max_capacity = 0; - info->free_space_in_bytes = 0; - } else { - info->max_capacity = total_space / BLOCK_SIZE; - info->free_space_in_bytes = free_space / BLOCK_SIZE; - } - - WriteMTPString(ptr, "Internal Storage", &length); - ptr += length; - - WriteMTPString(ptr, "INT_STORAGE", &length); - ptr += length; - } else if(storage_id == EXTERNAL_STORAGE_ID) { - SDInfo sd_info; - FS_Error err = storage_sd_info(mtp->storage, &sd_info); - - // Fill in details for internal storage - info->storage_type = 0x0004; // Removable RAM - - if(err != FSE_OK) { - info->max_capacity = 0; - info->free_space_in_bytes = 0; - - FURI_LOG_E("MTP", "SD Card not found"); - } else { - // Fill in details for external storage - info->max_capacity = (uint64_t)sd_info.kb_total * 1024 / BLOCK_SIZE; - info->free_space_in_bytes = (uint64_t)sd_info.kb_free * 1024 / BLOCK_SIZE; - } - - WriteMTPString(ptr, "SD Card", &length); - ptr += length; - - WriteMTPString(ptr, "SD_CARD", &length); - ptr += length; - } - - // try to convert into big endian??? - //info->max_capacity = __builtin_bswap64(info->max_capacity); - //info->free_space_in_bytes = __builtin_bswap64(info->free_space_in_bytes); - - return ptr - buf; -} - -// Microsoft-style UTF-16LE string: -void WriteMTPString(uint8_t* buffer, const char* str, uint16_t* length) { - uint8_t* ptr = buffer; - uint8_t str_len = strlen(str); - - FURI_LOG_I("MTP", "Writing MTP string: %s", str); - FURI_LOG_I("MTP", "String length: %d", str_len); - - // extra handling for empty string - if(str_len == 0) { - *ptr = 0x00; - - // that's it! - *length = 1; - return; - } - - *ptr = str_len + 1; // Length byte (number of characters including the null terminator) - ptr++; - while(*str) { - *ptr++ = *str++; - *ptr++ = 0x00; // UTF-16LE encoding (add null byte for each character) - } - *ptr++ = 0x00; // Null terminator (UTF-16LE) - *ptr++ = 0x00; - - FURI_LOG_I("MTP", "String byte length: %d", ptr - buffer); - *length = ptr - buffer; -} - -void WriteMTPBEString(uint8_t* buffer, const char* str, uint16_t* length) { - uint8_t* ptr = buffer; - uint8_t str_len = strlen(str); - *ptr++ = str_len + 1; // Length byte (number of characters including the null terminator) - while(*str) { - *ptr++ = 0x00; // UTF-16BE encoding (add null byte for each character) - *ptr++ = *str++; - } - *ptr++ = 0x00; // Null terminator (UTF-16LE) - *ptr++ = 0x00; - *length = ptr - buffer; -} - -bool DeleteObject(AppMTP* mtp, uint32_t handle) { - UNUSED(mtp); - FURI_LOG_I("MTP", "Deleting object %ld", handle); - - char* path = get_path_from_handle(mtp, handle); - if(path == NULL) { - return false; - } - - FileInfo fileinfo; - FS_Error err = storage_common_stat(mtp->storage, path, &fileinfo); - - if(err == FSE_OK) { - if(file_info_is_dir(&fileinfo)) { - if(!storage_simply_remove_recursive(mtp->storage, path)) { - return false; - } - } else { - if(storage_common_remove(mtp->storage, path) != FSE_OK) { - return false; - } - } - - return true; - } - - return false; -} - -void MoveObject( - AppMTP* mtp, - uint32_t transaction_id, - uint32_t handle, - uint32_t storage_id, - uint32_t parent) { - UNUSED(storage_id); - - FURI_LOG_I("MTP", "Moving object %ld to storage %ld, parent %ld", handle, storage_id, parent); - char* path = get_path_from_handle(mtp, handle); - if(path == NULL) { - send_mtp_response( - mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); - return; - } - - char* parentPath; - - if(parent != 0) { - parentPath = get_path_from_handle(mtp, parent); - if(parentPath == NULL) { - send_mtp_response( - mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); - return; - } - } else { - parentPath = get_base_path_from_storage_id(storage_id); - } - - Storage* storage = mtp->storage; - - char* filename = strrchr(path, '/'); - // remove the beginning slash - char* realFilename = filename + 1; - - char* newPath = malloc(sizeof(char) * 256); - merge_path(newPath, parentPath, realFilename); - - FURI_LOG_I("MTP", "Moving object: %s to %s", path, newPath); - - if(storage_common_rename(storage, path, newPath) != FSE_OK) { - FURI_LOG_E("MTP", "Failed to move object: %s", path); - send_mtp_response( - mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); - return; - } - - FURI_LOG_I("MTP", "Object moved successfully"); - send_mtp_response(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL); - - update_object_handle_path(mtp, handle, newPath); - free(path); - - return; -} - -int GetObjectPropValueInternal(AppMTP* mtp, const char* path, uint32_t prop_code, uint8_t* buffer) { - UNUSED(mtp); - uint8_t* ptr = buffer; - uint16_t length; - - switch(prop_code) { - case MTP_PROP_STORAGE_ID: { - FURI_LOG_I("MTP", "Getting storage ID for object: %s", path); - - *(uint32_t*)ptr = prop_code; - ptr += sizeof(uint32_t); - - *(uint32_t*)ptr = 0x0006; - ptr += sizeof(uint32_t); - - *(uint8_t*)ptr = 0x00; - ptr += sizeof(uint8_t); - - if(memcmp(path, STORAGE_INT_PATH_PREFIX, strlen(STORAGE_INT_PATH_PREFIX)) == 0) { - *(uint32_t*)ptr = INTERNAL_STORAGE_ID; - } else if(memcmp(path, STORAGE_EXT_PATH_PREFIX, strlen(STORAGE_EXT_PATH_PREFIX)) == 0) { - *(uint32_t*)ptr = EXTERNAL_STORAGE_ID; - } else { - *(uint32_t*)ptr = 0; - } - ptr += sizeof(uint32_t); - - *(uint8_t*)ptr = 0x00; - break; - } - case MTP_PROP_OBJECT_FORMAT: { - FURI_LOG_I("MTP", "Getting object format for object: %s", path); - - *(uint32_t*)ptr = prop_code; - ptr += sizeof(uint32_t); - - *(uint32_t*)ptr = 0x0006; - ptr += sizeof(uint32_t); - - *(uint8_t*)ptr = 0x00; - ptr += sizeof(uint8_t); - - *(uint16_t*)ptr = MTP_FORMAT_UNDEFINED; - ptr += sizeof(uint16_t); - - *(uint8_t*)ptr = 0x00; - break; - } - case MTP_PROP_OBJECT_FILE_NAME: { - FURI_LOG_I("MTP", "Getting object file name for object: %s", path); - - *(uint32_t*)ptr = prop_code; - ptr += sizeof(uint32_t); - - *(uint32_t*)ptr = 0xffff; - ptr += sizeof(uint32_t); - - *(uint8_t*)ptr = 0x01; - ptr += sizeof(uint8_t); - - char* file_name = strrchr(path, '/'); - WriteMTPString(ptr, file_name + 1, &length); - ptr += length; - - *(uint8_t*)ptr = 0x00; - break; - } - } - - return ptr - buffer; -} - -void GetObjectPropValue(AppMTP* mtp, uint32_t transaction_id, uint32_t handle, uint32_t prop_code) { - FURI_LOG_I( - "MTP", "Getting object property value for handle %ld, prop code %04lx", handle, prop_code); - - char* path = get_path_from_handle(mtp, handle); - if(path == NULL) { - send_mtp_response( - mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); - return; - } - - uint8_t* buffer = malloc(sizeof(uint8_t) * 256); - int length = GetObjectPropValueInternal(mtp, path, prop_code, buffer); - if(length < 0) { - send_mtp_response( - mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); - free(buffer); - return; - } - - send_mtp_response_buffer( - mtp, MTP_TYPE_DATA, MTP_OP_GET_OBJECT_PROP_VALUE, transaction_id, buffer, length); - send_mtp_response(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL); - free(buffer); -} diff --git a/mtp/src/scenes/mtp/mtp.h b/mtp/src/scenes/mtp/mtp.h index f1149a36a..52debf8a7 100644 --- a/mtp/src/scenes/mtp/mtp.h +++ b/mtp/src/scenes/mtp/mtp.h @@ -1,5 +1,10 @@ #pragma once +#include "main.h" + +// Block size for storage operations +#define BLOCK_SIZE 65536 + // MTP Device Serial #define MTP_DEVICE_FALLBACK_SERIAL \ "HakureiReimu" // If you found this, Thank you for checking my (shitty) code! @@ -84,6 +89,14 @@ #define MTP_FORMAT_UNDEFINED 0x3000 #define MTP_FORMAT_ASSOCIATION 0x3001 +// Buffer size configuration +#define MTP_BUFFER_SIZE 1024 // General purpose buffer size +#define MTP_PATH_SIZE 256 // Maximum path length +#define MTP_NAME_SIZE 256 // Maximum filename length + +// File transfer configuration +#define MTP_FILE_SYNC_INTERVAL (1024 * 1024) // Sync interval in bytes (1MB) + typedef struct { uint32_t handle; char name[256]; @@ -149,15 +162,8 @@ struct MTPContainer { uint32_t params[5]; // 12 }; -extern uint16_t supported_operations[]; -extern uint16_t supported_device_properties[]; -extern uint16_t supported_playback_formats[]; - void OpenSession(AppMTP* mtp, uint32_t session_id); void CloseSession(AppMTP* mtp); -void GetStorageIDs(AppMTP* mtp, uint32_t* storage_ids, uint32_t* count); -int GetStorageInfo(AppMTP* mtp, uint32_t storage_id, uint8_t* buf); -bool DeleteObject(AppMTP* mtp, uint32_t handle); void mtp_handle_bulk(AppMTP* mtp, uint8_t* buffer, uint32_t length); void handle_mtp_command(AppMTP* mtp, struct MTPContainer* container); @@ -185,38 +191,19 @@ void send_mtp_response_stream( void* callback_context, int (*callback)(void* ctx, uint8_t* buffer, int length), uint32_t length); -int BuildDeviceInfo(uint8_t* buffer); -bool CheckMTPStringHasUnicode(uint8_t* buffer); -char* ReadMTPString(uint8_t* buffer); -void WriteMTPString(uint8_t* buffer, const char* str, uint16_t* length); void send_device_info(AppMTP* mtp, uint32_t transaction_id); void send_storage_ids(AppMTP* mtp, uint32_t transaction_id); -void send_device_prop_value(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code); -void send_device_prop_desc(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code); -int GetDevicePropValue(uint32_t prop_code, uint8_t* buffer); -int GetDevicePropDesc(uint32_t prop_code, uint8_t* buffer); -int GetObjectHandles(AppMTP* mtp, uint32_t storage_id, uint32_t association, uint8_t* buffer); -int GetObjectInfo(AppMTP* mtp, uint32_t handle, uint8_t* buffer); -void GetObject(AppMTP* mtp, uint32_t transaction_id, uint32_t handle); -char* get_base_path_from_storage_id(uint32_t storage_id); -char* get_path_from_handle(AppMTP* mtp, uint32_t handle); -uint32_t issue_object_handle(AppMTP* mtp, char* path); void handle_mtp_data_complete(AppMTP* mtp); -uint32_t update_object_handle_path(AppMTP* mtp, uint32_t handle, char* path); - -void MoveObject( - AppMTP* mtp, - uint32_t transaction_id, - uint32_t handle, - uint32_t storage_id, - uint32_t parent); - -void GetObjectPropValue(AppMTP* mtp, uint32_t transaction_id, uint32_t handle, uint32_t prop_code); struct MTPResponseBufferContext { uint8_t* buffer; uint32_t size; uint32_t sent; }; + +// Object context for file operations +struct GetObjectContext { + File* file; +}; diff --git a/mtp/src/scenes/mtp/mtp_ops.c b/mtp/src/scenes/mtp/mtp_ops.c new file mode 100644 index 000000000..9251dc0b1 --- /dev/null +++ b/mtp/src/scenes/mtp/mtp_ops.c @@ -0,0 +1,397 @@ +#include "mtp_ops.h" +#include "utils.h" + +void GetStorageIDs(AppMTP* mtp, uint32_t* storage_ids, uint32_t* count) { + SDInfo sd_info; + FS_Error err = storage_sd_info(mtp->storage, &sd_info); + + // Check if SD card is present + if(err != FSE_OK) { + FURI_LOG_E("MTP", "SD Card not found"); + *count = 0; // No storage. + return; + } + + storage_ids[0] = EXTERNAL_STORAGE_ID; + *count = 1; +} + +int GetStorageInfo(AppMTP* mtp, uint32_t storage_id, uint8_t* buf) { + MTPStorageInfoHeader* info = (MTPStorageInfoHeader*)buf; + uint8_t* ptr = buf + sizeof(MTPStorageInfoHeader); + uint16_t length; + + info->free_space_in_objects = 20ul; + info->filesystem_type = 0x0002; // Generic hierarchical + info->access_capability = 0x0000; // Read-write + FURI_LOG_I("MTP", "Getting storage info for storage ID %04lx", storage_id); + + if(storage_id == INTERNAL_STORAGE_ID) { + // Fill in details for internal storage + info->storage_type = 0x0003; // Fixed RAM + + // Fill in details for internal storage + uint64_t total_space; + uint64_t free_space; + FS_Error err = storage_common_fs_info( + mtp->storage, STORAGE_INT_PATH_PREFIX, &total_space, &free_space); + if(err != FSE_OK) { + info->max_capacity = 0; + info->free_space_in_bytes = 0; + } else { + info->max_capacity = total_space / BLOCK_SIZE; + info->free_space_in_bytes = free_space / BLOCK_SIZE; + } + + WriteMTPString(ptr, "Internal Storage", &length); + ptr += length; + + WriteMTPString(ptr, "INT_STORAGE", &length); + ptr += length; + } else if(storage_id == EXTERNAL_STORAGE_ID) { + SDInfo sd_info; + FS_Error err = storage_sd_info(mtp->storage, &sd_info); + + // Fill in details for internal storage + info->storage_type = 0x0004; // Removable RAM + + if(err != FSE_OK) { + info->max_capacity = 0; + info->free_space_in_bytes = 0; + + FURI_LOG_E("MTP", "SD Card not found"); + } else { + // Fill in details for external storage + info->max_capacity = (uint64_t)sd_info.kb_total * 1024 / BLOCK_SIZE; + info->free_space_in_bytes = (uint64_t)sd_info.kb_free * 1024 / BLOCK_SIZE; + } + + WriteMTPString(ptr, "SD Card", &length); + ptr += length; + + WriteMTPString(ptr, "SD_CARD", &length); + ptr += length; + } + + return ptr - buf; +} + +int GetObjectInfo(AppMTP* mtp, uint32_t handle, uint8_t* buffer) { + ObjectInfoHeader* header = (ObjectInfoHeader*)buffer; + uint8_t* ptr = buffer + sizeof(ObjectInfoHeader); + + char* path = get_path_from_handle(mtp, handle); + if(path == NULL) { + return -1; + } + + FURI_LOG_I("MTP", "Getting object info for handle %ld", handle); + FURI_LOG_I("MTP", "Path: %s", path); + + header->protection_status = 0; + + Storage* storage = mtp->storage; + File* file = storage_file_alloc(storage); + uint16_t length; + + FileInfo fileinfo; + FS_Error err = storage_common_stat(storage, path, &fileinfo); + if(err != FSE_OK) { + FURI_LOG_E("MTP", "Failed to get file info: %s", filesystem_api_error_get_desc(err)); + return -1; + } + + if(memcmp(path, STORAGE_INT_PATH_PREFIX, strlen(STORAGE_INT_PATH_PREFIX)) == 0) { + FURI_LOG_I("MTP", "Object in Internal storage"); + header->storage_id = INTERNAL_STORAGE_ID; + } else if(memcmp(path, STORAGE_EXT_PATH_PREFIX, strlen(STORAGE_EXT_PATH_PREFIX)) == 0) { + FURI_LOG_I("MTP", "Object in External storage"); + header->storage_id = EXTERNAL_STORAGE_ID; + } else { + return -1; + } + + if(file_info_is_dir(&fileinfo)) { + FURI_LOG_I("MTP", "Directory"); + + header->format = MTP_FORMAT_ASSOCIATION; + header->association_type = 0x0001; // Generic folder + } else { + FURI_LOG_I("MTP", "Undefined type"); + + header->format = MTP_FORMAT_UNDEFINED; + } + + header->compressed_size = fileinfo.size; + + header->thumb_format = 0; + header->thumb_compressed_size = 0; + header->thumb_pix_width = 0; + header->thumb_pix_height = 0; + header->image_pix_width = 0; + header->image_pix_height = 0; + header->image_bit_depth = 0; + + char* file_name = strrchr(path, '/'); + FURI_LOG_I("MTP", "File name: %s", file_name + 1); + + WriteMTPString(ptr, file_name + 1, &length); + ptr += length; + + // get created + WriteMTPString(ptr, "20240608T010702", &length); + ptr += length; + + // get last modified + WriteMTPString(ptr, "20240608T010702", &length); + ptr += length; + + // get keywords + WriteMTPString(ptr, "", &length); + ptr += length; + + storage_file_free(file); + + return ptr - buffer; +} + +int GetObject_callback(void* ctx, uint8_t* buffer, int length) { + struct GetObjectContext* obj_ctx = (struct GetObjectContext*)ctx; + return storage_file_read(obj_ctx->file, buffer, length); +} + +void GetObject(AppMTP* mtp, uint32_t transaction_id, uint32_t handle) { + char* path = get_path_from_handle(mtp, handle); + if(path == NULL) { + send_mtp_response( + mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); + return; + } + + FURI_LOG_I("MTP", "Getting object: %s", path); + + Storage* storage = mtp->storage; + File* file = storage_file_alloc(storage); + if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { + FURI_LOG_E("MTP", "Failed to open file: %s", path); + send_mtp_response( + mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); + return; + } + + uint32_t size = storage_file_size(file); + + struct GetObjectContext ctx = { + .file = file, + }; + + send_mtp_response_stream( + mtp, MTP_TYPE_DATA, MTP_OP_GET_OBJECT, transaction_id, &ctx, GetObject_callback, size); + + send_mtp_response(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL); + + storage_file_close(file); + storage_file_free(file); +} + +int GetObjectHandles(AppMTP* mtp, uint32_t storage_id, uint32_t association, uint8_t* buffer) { + uint8_t* ptr = buffer; + uint16_t length; + + UNUSED(length); + + // For now, just return a single object handle + int count = list_and_issue_handles(mtp, storage_id, association, NULL); + + *(uint32_t*)ptr = count; + ptr += sizeof(uint32_t); + + uint32_t* handles = (uint32_t*)ptr; + length = list_and_issue_handles(mtp, storage_id, association, handles); + ptr += length * sizeof(uint32_t); + + return ptr - buffer; +} + +bool DeleteObject(AppMTP* mtp, uint32_t handle) { + UNUSED(mtp); + FURI_LOG_I("MTP", "Deleting object %ld", handle); + + char* path = get_path_from_handle(mtp, handle); + if(path == NULL) { + return false; + } + + FileInfo fileinfo; + FS_Error err = storage_common_stat(mtp->storage, path, &fileinfo); + + if(err == FSE_OK) { + if(file_info_is_dir(&fileinfo)) { + if(!storage_simply_remove_recursive(mtp->storage, path)) { + return false; + } + } else { + if(storage_common_remove(mtp->storage, path) != FSE_OK) { + return false; + } + } + + return true; + } + + return false; +} + +void MoveObject( + AppMTP* mtp, + uint32_t transaction_id, + uint32_t handle, + uint32_t storage_id, + uint32_t parent) { + UNUSED(storage_id); + + FURI_LOG_I("MTP", "Moving object %ld to storage %ld, parent %ld", handle, storage_id, parent); + char* path = get_path_from_handle(mtp, handle); + if(path == NULL) { + send_mtp_response( + mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); + return; + } + + char* parentPath; + + if(parent != 0) { + parentPath = get_path_from_handle(mtp, parent); + if(parentPath == NULL) { + send_mtp_response( + mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); + return; + } + } else { + parentPath = get_base_path_from_storage_id(storage_id); + } + + Storage* storage = mtp->storage; + + char* filename = strrchr(path, '/'); + // remove the beginning slash + char* realFilename = filename + 1; + + char* newPath = malloc(sizeof(char) * 256); + merge_path(newPath, parentPath, realFilename); + + FURI_LOG_I("MTP", "Moving object: %s to %s", path, newPath); + + if(storage_common_rename(storage, path, newPath) != FSE_OK) { + FURI_LOG_E("MTP", "Failed to move object: %s", path); + send_mtp_response( + mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); + return; + } + + FURI_LOG_I("MTP", "Object moved successfully"); + send_mtp_response(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL); + + update_object_handle_path(mtp, handle, newPath); + free(path); + + return; +} + +int GetObjectPropValueInternal(AppMTP* mtp, const char* path, uint32_t prop_code, uint8_t* buffer) { + UNUSED(mtp); + uint8_t* ptr = buffer; + uint16_t length; + + switch(prop_code) { + case MTP_PROP_STORAGE_ID: { + FURI_LOG_I("MTP", "Getting storage ID for object: %s", path); + + *(uint32_t*)ptr = prop_code; + ptr += sizeof(uint32_t); + + *(uint32_t*)ptr = 0x0006; + ptr += sizeof(uint32_t); + + *(uint8_t*)ptr = 0x00; + ptr += sizeof(uint8_t); + + if(memcmp(path, STORAGE_INT_PATH_PREFIX, strlen(STORAGE_INT_PATH_PREFIX)) == 0) { + *(uint32_t*)ptr = INTERNAL_STORAGE_ID; + } else if(memcmp(path, STORAGE_EXT_PATH_PREFIX, strlen(STORAGE_EXT_PATH_PREFIX)) == 0) { + *(uint32_t*)ptr = EXTERNAL_STORAGE_ID; + } else { + *(uint32_t*)ptr = 0; + } + ptr += sizeof(uint32_t); + + *(uint8_t*)ptr = 0x00; + break; + } + case MTP_PROP_OBJECT_FORMAT: { + FURI_LOG_I("MTP", "Getting object format for object: %s", path); + + *(uint32_t*)ptr = prop_code; + ptr += sizeof(uint32_t); + + *(uint32_t*)ptr = 0x0006; + ptr += sizeof(uint32_t); + + *(uint8_t*)ptr = 0x00; + ptr += sizeof(uint8_t); + + *(uint16_t*)ptr = MTP_FORMAT_UNDEFINED; + ptr += sizeof(uint16_t); + + *(uint8_t*)ptr = 0x00; + break; + } + case MTP_PROP_OBJECT_FILE_NAME: { + FURI_LOG_I("MTP", "Getting object file name for object: %s", path); + + *(uint32_t*)ptr = prop_code; + ptr += sizeof(uint32_t); + + *(uint32_t*)ptr = 0xffff; + ptr += sizeof(uint32_t); + + *(uint8_t*)ptr = 0x01; + ptr += sizeof(uint8_t); + + char* file_name = strrchr(path, '/'); + WriteMTPString(ptr, file_name + 1, &length); + ptr += length; + + *(uint8_t*)ptr = 0x00; + break; + } + } + + return ptr - buffer; +} + +void GetObjectPropValue(AppMTP* mtp, uint32_t transaction_id, uint32_t handle, uint32_t prop_code) { + FURI_LOG_I( + "MTP", "Getting object property value for handle %ld, prop code %04lx", handle, prop_code); + + char* path = get_path_from_handle(mtp, handle); + if(path == NULL) { + send_mtp_response( + mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); + return; + } + + uint8_t* buffer = malloc(sizeof(uint8_t) * 256); + int length = GetObjectPropValueInternal(mtp, path, prop_code, buffer); + if(length < 0) { + send_mtp_response( + mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL); + free(buffer); + return; + } + + send_mtp_response_buffer( + mtp, MTP_TYPE_DATA, MTP_OP_GET_OBJECT_PROP_VALUE, transaction_id, buffer, length); + send_mtp_response(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL); + free(buffer); +} diff --git a/mtp/src/scenes/mtp/mtp_ops.h b/mtp/src/scenes/mtp/mtp_ops.h new file mode 100644 index 000000000..6b048accc --- /dev/null +++ b/mtp/src/scenes/mtp/mtp_ops.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include "main.h" +#include "mtp.h" +#include "storage_ops.h" + +// MTP Operations (Actions) +void GetStorageIDs(AppMTP* mtp, uint32_t* storage_ids, uint32_t* count); +int GetStorageInfo(AppMTP* mtp, uint32_t storage_id, uint8_t* buf); +int GetObjectInfo(AppMTP* mtp, uint32_t handle, uint8_t* buffer); +void GetObject(AppMTP* mtp, uint32_t transaction_id, uint32_t handle); +int GetObjectHandles(AppMTP* mtp, uint32_t storage_id, uint32_t association, uint8_t* buffer); +void GetObjectPropValue(AppMTP* mtp, uint32_t transaction_id, uint32_t handle, uint32_t prop_code); +bool DeleteObject(AppMTP* mtp, uint32_t handle); +void MoveObject( + AppMTP* mtp, + uint32_t transaction_id, + uint32_t handle, + uint32_t storage_id, + uint32_t parent); diff --git a/mtp/src/scenes/mtp/mtp_support.h b/mtp/src/scenes/mtp/mtp_support.h new file mode 100644 index 000000000..e1abcfbd9 --- /dev/null +++ b/mtp/src/scenes/mtp/mtp_support.h @@ -0,0 +1,31 @@ +#pragma once + +static const uint16_t supported_operations[] = { + MTP_OP_GET_DEVICE_INFO, + MTP_OP_OPEN_SESSION, + MTP_OP_CLOSE_SESSION, + MTP_OP_GET_STORAGE_IDS, + MTP_OP_GET_STORAGE_INFO, + MTP_OP_GET_NUM_OBJECTS, + MTP_OP_GET_OBJECT_HANDLES, + MTP_OP_GET_OBJECT_INFO, + MTP_OP_GET_OBJECT, + MTP_OP_DELETE_OBJECT, + MTP_OP_SEND_OBJECT_INFO, + MTP_OP_SEND_OBJECT, + MTP_OP_GET_DEVICE_PROP_DESC, + MTP_OP_GET_DEVICE_PROP_VALUE, + MTP_OP_MOVE_OBJECT}; + +static const uint16_t supported_object_props[] = { + MTP_PROP_STORAGE_ID, + MTP_PROP_OBJECT_FORMAT, + MTP_PROP_OBJECT_FILE_NAME, +}; + +static const uint16_t supported_device_properties[] = { + 0xd402, // Device Friendly Name + 0x5001 // Battery Level +}; + +static const uint16_t supported_playback_formats[] = {MTP_FORMAT_UNDEFINED, MTP_FORMAT_ASSOCIATION}; diff --git a/mtp/src/scenes/mtp/storage_ops.c b/mtp/src/scenes/mtp/storage_ops.c new file mode 100644 index 000000000..6ee1ed69d --- /dev/null +++ b/mtp/src/scenes/mtp/storage_ops.c @@ -0,0 +1,130 @@ +#include "storage_ops.h" +#include "utils.h" + +char* get_base_path_from_storage_id(uint32_t storage_id) { + if(storage_id == INTERNAL_STORAGE_ID) { + return STORAGE_INT_PATH_PREFIX; + } else if(storage_id == EXTERNAL_STORAGE_ID) { + return STORAGE_EXT_PATH_PREFIX; + } + return NULL; +} + +char* get_path_from_handle(AppMTP* mtp, uint32_t handle) { + FileHandle* current = mtp->handles; + while(current != NULL) { + if(current->handle == handle) { + return current->path; + } + current = current->next; + } + return NULL; +} + +uint32_t issue_object_handle(AppMTP* mtp, char* path) { + int handle = 1; + int length = strlen(path); + char* path_store = malloc(sizeof(char) * (length + 1)); + strcpy(path_store, path); + + if(mtp->handles == NULL) { + mtp->handles = malloc(sizeof(FileHandle)); + mtp->handles->handle = handle; + mtp->handles->path = path_store; + mtp->handles->next = NULL; + return handle; + } + + FileHandle* current = mtp->handles; + if(strcmp(current->path, path) == 0) { + return current->handle; + } + + while(current->next != NULL) { + if(strcmp(current->path, path) == 0) { + return current->handle; + } + current = current->next; + handle++; + } + + current->next = malloc(sizeof(FileHandle)); + current = current->next; + handle++; + + current->handle = handle; + current->path = path_store; + current->next = NULL; + return handle; +} + +uint32_t update_object_handle_path(AppMTP* mtp, uint32_t handle, char* path) { + FileHandle* current = mtp->handles; + while(current != NULL) { + if(current->handle == handle) { + free(current->path); + current->path = path; + return handle; + } + current = current->next; + } + return 0; +} + +int list_and_issue_handles( + AppMTP* mtp, + uint32_t storage_id, + uint32_t association, + uint32_t* handles) { + Storage* storage = mtp->storage; + char* base_path = get_base_path_from_storage_id(storage_id); + if(base_path == NULL) { + base_path = ""; + } + + File* dir = storage_file_alloc(storage); + if(association == 0xffffffff) { + // count the objects in the root directory + storage_dir_open(dir, base_path); + } else { + char* path = get_path_from_handle(mtp, association); + FURI_LOG_I("MTP", "Association path: %s", path); + if(path == NULL) { + return 0; + } + storage_dir_open(dir, path); + base_path = path; + } + + int count = 0; + FileInfo fileinfo; + char* file_name = malloc(sizeof(char) * 256); + char* full_path = malloc(sizeof(char) * 256); + while(storage_dir_read(dir, &fileinfo, file_name, 256)) { + if(file_info_is_dir(&fileinfo)) { + FURI_LOG_I("MTP", "Found directory: %s", file_name); + } else { + FURI_LOG_I("MTP", "Found file: %s", file_name); + } + + merge_path(full_path, base_path, file_name); + FURI_LOG_I("MTP", "Full path: %s", full_path); + + uint32_t handle = issue_object_handle(mtp, full_path); + if(handles != NULL) { + handles[count] = handle; + } + count++; + } + + FURI_LOG_I("MTP", "Getting number of objects in storage %ld", storage_id); + FURI_LOG_I("MTP", "Base path: %s", base_path); + FURI_LOG_I("MTP", "Association: %ld", association); + FURI_LOG_I("MTP", "Number of objects: %d", count); + + storage_dir_close(dir); + storage_file_free(dir); + free(file_name); + free(full_path); + return count; +} diff --git a/mtp/src/scenes/mtp/storage_ops.h b/mtp/src/scenes/mtp/storage_ops.h new file mode 100644 index 000000000..8b55e36f4 --- /dev/null +++ b/mtp/src/scenes/mtp/storage_ops.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "main.h" +#include "mtp.h" + +// Storage utility functions +char* get_base_path_from_storage_id(uint32_t storage_id); +char* get_path_from_handle(AppMTP* mtp, uint32_t handle); +uint32_t issue_object_handle(AppMTP* mtp, char* path); +uint32_t update_object_handle_path(AppMTP* mtp, uint32_t handle, char* path); +int list_and_issue_handles( + AppMTP* mtp, + uint32_t storage_id, + uint32_t association, + uint32_t* handles); diff --git a/mtp/src/scenes/mtp/utils.c b/mtp/src/scenes/mtp/utils.c index cca0d3bcd..7af41d158 100644 --- a/mtp/src/scenes/mtp/utils.c +++ b/mtp/src/scenes/mtp/utils.c @@ -40,3 +40,74 @@ void print_bytes(char* tag, uint8_t* bytes, size_t len) { free(line); FURI_LOG_I("MTP", "End of dump - TAG: %s", tag); } + +void merge_path(char* target, char* base, char* name) { + char* ptr = target; + strcpy(target, base); + ptr += strlen(base); + strcpy(ptr, "/"); + ptr++; + strcpy(ptr, name); +} + +bool CheckMTPStringHasUnicode(uint8_t* buffer) { + uint8_t* base = buffer; + int len = *base; + + uint16_t* ptr = (uint16_t*)(base + 1); + + for(int i = 0; i < len; i++) { + if(ptr[i] > 0x7F) { + return true; + } + } + + return false; +} + +char* ReadMTPString(uint8_t* buffer) { + int len16 = *(uint8_t*)buffer; + if(len16 == 0) { + return ""; + } + + char* str = malloc(sizeof(char) * len16); + + uint8_t* base = buffer + 1; + uint16_t* ptr = (uint16_t*)base; + + for(int i = 0; i < len16; i++) { + str[i] = *ptr++; + } + + return str; +} + +void WriteMTPString(uint8_t* buffer, const char* str, uint16_t* length) { + uint8_t* ptr = buffer; + uint8_t str_len = strlen(str); + + FURI_LOG_I("MTP", "Writing MTP string: %s", str); + FURI_LOG_I("MTP", "String length: %d", str_len); + + // extra handling for empty string + if(str_len == 0) { + *ptr = 0x00; + + // that's it! + *length = 1; + return; + } + + *ptr = str_len + 1; // Length byte (number of characters including the null terminator) + ptr++; + while(*str) { + *ptr++ = *str++; + *ptr++ = 0x00; // UTF-16LE encoding (add null byte for each character) + } + *ptr++ = 0x00; // Null terminator (UTF-16LE) + *ptr++ = 0x00; + + FURI_LOG_I("MTP", "String byte length: %d", ptr - buffer); + *length = ptr - buffer; +} diff --git a/mtp/src/scenes/mtp/utils.h b/mtp/src/scenes/mtp/utils.h index faf428083..9ab30d1a8 100644 --- a/mtp/src/scenes/mtp/utils.h +++ b/mtp/src/scenes/mtp/utils.h @@ -1,5 +1,10 @@ #pragma once + #include char byte_to_hex(uint8_t byte); void print_bytes(char* tag, uint8_t* bytes, size_t len); +void merge_path(char* target, char* base, char* name); +bool CheckMTPStringHasUnicode(uint8_t* buffer); +char* ReadMTPString(uint8_t* buffer); +void WriteMTPString(uint8_t* buffer, const char* str, uint16_t* length); diff --git a/oscilloscope/.gitsubtree b/oscilloscope/.gitsubtree index 6ad94692e..b82adbd89 100644 --- a/oscilloscope/.gitsubtree +++ b/oscilloscope/.gitsubtree @@ -1,2 +1,2 @@ -https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/flipperscope 4558d74c9da36abc851edd96a95d18f7d5511a75 +https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/flipperscope ffa18100afc128abea7269fcc31d9c237523e7c6 https://github.com/anfractuosity/flipperscope main / diff --git a/oscilloscope/README.md b/oscilloscope/README.md index 44d48c58e..3c6abe346 100644 --- a/oscilloscope/README.md +++ b/oscilloscope/README.md @@ -24,6 +24,7 @@ Also see [Derek Jamison's demonstration](https://www.youtube.com/watch?v=iC5fBGw * Measures frequency of waveform in hertz * Measures voltage: min, max, Vpp +* FFT option provides simple spectrum analyser ![Signal Generator](photos/sig.jpg) @@ -31,7 +32,12 @@ Also see [Derek Jamison's demonstration](https://www.youtube.com/watch?v=iC5fBGw ![Rigol](photos/rigol.jpg) -![Flipper Zero running flipperscope](photos/volt.jpg) +![Flipper Zero running flipperscope - showing voltage measurements](photos/volt.jpg) + +In the following photo attached a MAX9814 electret microphone module to the flipper zero and used the spectrum +analyser functionality, with an FFT window size of 1024 and played a 3kHz sine wave tone from a computer. + +![Flipper Zero running flipperscope - showing spectrum analyser](photos/fft.jpg) ## Processing captures @@ -62,8 +68,6 @@ plt.show() * Customisable input pin * Trigger type mode -* FFT -* ... ## Inspiration @@ -71,3 +75,4 @@ plt.show() * STM32 DMA example * VREFBUF information - https://community.st.com/s/question/0D53W00001awIlMSAU/enable-and-use-vrefbuf-for-adc-measures * Relocating vector table - https://community.nxp.com/t5/i-MX-Processors/Relocate-vector-table-to-ITCM/m-p/1302304 +* Uses FFT algorithm from - https://www.algorithm-archive.org/contents/cooley_tukey/cooley_tukey.html diff --git a/oscilloscope/application.fam b/oscilloscope/application.fam index f4f61deff..cf4de899a 100644 --- a/oscilloscope/application.fam +++ b/oscilloscope/application.fam @@ -9,6 +9,6 @@ App( fap_category="GPIO", fap_icon="scope_10px.png", fap_icon_assets="icons", - fap_version="0.3", + fap_version="0.4", fap_description="Oscilloscope application - apply signal to pin 16/PC0, with a voltage ranging from 0V to 2.5V and ground to pin 18/GND", ) diff --git a/oscilloscope/docs/CHANGELOG.md b/oscilloscope/docs/CHANGELOG.md index 6f636124c..8df06448d 100644 --- a/oscilloscope/docs/CHANGELOG.md +++ b/oscilloscope/docs/CHANGELOG.md @@ -1,3 +1,7 @@ +## v0.4 + +Add simple spectrum analyser and basic software scaling support + ## v0.3 Fix compilation issue diff --git a/oscilloscope/docs/README.md b/oscilloscope/docs/README.md index 67c9ae37e..61fb93027 100644 --- a/oscilloscope/docs/README.md +++ b/oscilloscope/docs/README.md @@ -14,3 +14,8 @@ Oscilloscope application - apply signal to pin 16/PC0, with a voltage ranging fr * Setup screen also enables you to choose the capture mode, to save samples to the SD card (currently 128 samples). You can parse this data using the Python script in the flipperscope repo. + +* Setup screen allows you to choose an FFT option, to display a simple spectrum analyser. You can alter the size of +the FFT window also. + +* Setup screen allows you to scale the size of a signal via software. diff --git a/oscilloscope/photos/fft.jpg b/oscilloscope/photos/fft.jpg new file mode 100644 index 000000000..be3b1901d Binary files /dev/null and b/oscilloscope/photos/fft.jpg differ diff --git a/oscilloscope/scenes/scope_scene_about.c b/oscilloscope/scenes/scope_scene_about.c index 8a30a32cb..4e03c47b6 100644 --- a/oscilloscope/scenes/scope_scene_about.c +++ b/oscilloscope/scenes/scope_scene_about.c @@ -13,6 +13,7 @@ void scope_scene_about_on_enter(void* context) { FuriString* temp_str; temp_str = furi_string_alloc(); furi_string_printf(temp_str, "\e#%s\n", "Information"); + furi_string_cat_printf(temp_str, "Version: %s\n\n", FAP_VERSION); furi_string_cat_printf( temp_str, "Provide signal to pin 16/PC0, with a voltage ranging from 0V to 2.5V and ground to pin 18/GND.\n\n"); diff --git a/oscilloscope/scenes/scope_scene_run.c b/oscilloscope/scenes/scope_scene_run.c index 46f05d89d..6f27ac65f 100644 --- a/oscilloscope/scenes/scope_scene_run.c +++ b/oscilloscope/scenes/scope_scene_run.c @@ -1,3 +1,6 @@ +#include +#include + #include #include #include @@ -65,28 +68,30 @@ const uint32_t MSIRangeTable[16UL] = { 0UL}; /* 0UL values are incorrect cases */ char* time; // Current time period text +float scale; // Current scale double freq; // Current samplerate uint8_t pause = 0; // Whether we want to pause output or not enum measureenum type; // Type of measurement we are performing int toggle = 0; // Used for toggling output GPIO, only used in testing +uint32_t adc_buffer; // ADC buffer size +int16_t* index_crossings; // Indexes of zero crossings +float* data; // Shift data across virtual zero line +float* crossings; +float complex* fft_data; // Real data, transformed to complex data via FFT +float* fft_power; // Power data from FFT void Error_Handler() { while(1) { } } -__IO uint16_t - aADCxConvertedData[ADC_CONVERTED_DATA_BUFFER_SIZE]; // Array that ADC data is copied to, via DMA -__IO uint16_t aADCxConvertedData_Voltage_mVoltA - [ADC_CONVERTED_DATA_BUFFER_SIZE]; // Data is converted to range from 0 to 2500 -__IO uint16_t aADCxConvertedData_Voltage_mVoltB - [ADC_CONVERTED_DATA_BUFFER_SIZE]; // Data is converted to range from 0 to 2500 +uint16_t* aADCxConvertedData; // Array that ADC data is copied to, via DMA +__IO uint16_t* aADCxConvertedData_Voltage_mVoltA; // Data is converted to range from 0 to 2500 +__IO uint16_t* aADCxConvertedData_Voltage_mVoltB; // Data is converted to range from 0 to 2500 __IO uint8_t ubDmaTransferStatus = 2; // DMA transfer status -__IO uint16_t* mvoltWrite = - &aADCxConvertedData_Voltage_mVoltA[0]; // Pointer to area we write converted voltage data to -__IO uint16_t* mvoltDisplay = - &aADCxConvertedData_Voltage_mVoltB[0]; // Pointer to area of memory we display +__IO uint16_t* mvoltWrite; // Pointer to area we write converted voltage data to +__IO uint16_t* mvoltDisplay; // Pointer to area of memory we display void AdcDmaTransferComplete_Callback(); void AdcDmaTransferHalf_Callback(); @@ -153,10 +158,10 @@ static void MX_ADC1_Init(void) { DMA1, LL_DMA_CHANNEL_1, LL_ADC_DMA_GetRegAddr(ADC1, LL_ADC_DMA_REG_REGULAR_DATA), - (uint32_t)&aADCxConvertedData, + (uint32_t)aADCxConvertedData, LL_DMA_DIRECTION_PERIPH_TO_MEMORY); - LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, ADC_CONVERTED_DATA_BUFFER_SIZE); + LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, adc_buffer); LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1); LL_DMA_EnableIT_HT(DMA1, LL_DMA_CHANNEL_1); @@ -274,9 +279,7 @@ void swap(__IO uint16_t** a, __IO uint16_t** b) { void AdcDmaTransferComplete_Callback() { uint32_t tmp_index = 0; - for(tmp_index = (ADC_CONVERTED_DATA_BUFFER_SIZE / 2); - tmp_index < ADC_CONVERTED_DATA_BUFFER_SIZE; - tmp_index++) { + for(tmp_index = (adc_buffer / 2); tmp_index < adc_buffer; tmp_index++) { mvoltWrite[tmp_index] = __LL_ADC_CALC_DATA_TO_VOLTAGE( VDDA_APPLI, aADCxConvertedData[tmp_index], LL_ADC_RESOLUTION_12B); } @@ -286,7 +289,7 @@ void AdcDmaTransferComplete_Callback() { void AdcDmaTransferHalf_Callback() { uint32_t tmp_index = 0; - for(tmp_index = 0; tmp_index < (ADC_CONVERTED_DATA_BUFFER_SIZE / 2); tmp_index++) { + for(tmp_index = 0; tmp_index < (adc_buffer / 2); tmp_index++) { mvoltWrite[tmp_index] = __LL_ADC_CALC_DATA_TO_VOLTAGE( VDDA_APPLI, aADCxConvertedData[tmp_index], LL_ADC_RESOLUTION_12B); } @@ -342,14 +345,63 @@ void Activate_ADC(void) { } } +// Found from: +// https://www.algorithm-archive.org/contents/cooley_tukey/cooley_tukey.html +void bit_reverse(float complex* X, int N) { + for(int i = 0; i < N; ++i) { + int n = i; + int a = i; + int count = (int)ceil(log2((float)N)) - 1; + + n >>= 1; + while(n > 0) { + a = (a << 1) | (n & 1); + count--; + n >>= 1; + } + n = (a << count) & (int)((1 << (int)ceil(log2((float)N))) - 1); + + if(n > i) { + float complex tmp = X[i]; + X[i] = X[n]; + X[n] = tmp; + } + } +} + +// Found from: +// https://www.algorithm-archive.org/contents/cooley_tukey/cooley_tukey.html +// +// Adapted slightly to use ceil, otherwise didn't seem to calculate +// FFT correctly on the flipper zero +void iterative_cooley_tukey(float complex* X, int N) { + bit_reverse(X, N); + + for(int i = 1; i <= ceil(log2((float)N)); ++i) { + int stride = (int)pow(2, i); + float complex w = cexp(-2.0 * I * M_PI / (float)stride); + for(int j = 0; j < N; j += stride) { + float complex v = 1.0; + for(int k = 0; k < stride / 2; ++k) { + X[k + j + stride / 2] = X[k + j] - v * X[k + j + stride / 2]; + X[k + j] -= (X[k + j + stride / 2] - X[k + j]); + v *= w; + } + } + } +} + +// Found from: +// https://stackoverflow.com/questions/427477/fastest-way-to-clamp-a-real-fixed-floating-point-value +double clamp(double d, double min, double max) { + const double t = d < min ? min : d; + return t > max ? max : t; +} + // Used to draw to display static void app_draw_callback(Canvas* canvas, void* ctx) { UNUSED(ctx); - static int16_t index[ADC_CONVERTED_DATA_BUFFER_SIZE]; - static float data[ADC_CONVERTED_DATA_BUFFER_SIZE]; - static float crossings[ADC_CONVERTED_DATA_BUFFER_SIZE]; static char buf1[50]; - float max = 0.0; float min = FLT_MAX; int count = 0; @@ -364,12 +416,12 @@ static void app_draw_callback(Canvas* canvas, void* ctx) { } if(pause) - canvas_draw_icon(canvas, 115, 0, &I_pause_10x10); + canvas_draw_icon(canvas, 116, 1, &I_pause_10x10); else - canvas_draw_icon(canvas, 115, 0, &I_play_10x10); + canvas_draw_icon(canvas, 116, 1, &I_play_10x10); // Calculate voltage measurements - for(uint32_t x = 0; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) { + for(uint32_t x = 0; x < adc_buffer; x++) { if(mvoltDisplay[x] < min) min = mvoltDisplay[x]; if(mvoltDisplay[x] > max) max = mvoltDisplay[x]; } @@ -378,34 +430,38 @@ static void app_draw_callback(Canvas* canvas, void* ctx) { switch(type) { case m_time: { + // Display current scale + snprintf(buf1, 50, "%.0fx", (double)scale); + canvas_draw_str(canvas, 95, 10, buf1); // Display current time period snprintf(buf1, 50, "Time: %s", time); - canvas_draw_str(canvas, 10, 10, buf1); + canvas_draw_str(canvas, 2, 10, buf1); // Shift waveform across a virtual 0 line, so it crosses 0 - for(uint32_t x = 0; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) { - index[x] = -1; + for(uint32_t x = 0; x < adc_buffer; x++) { + index_crossings[x] = -1; crossings[x] = -1.0; data[x] = ((float)mvoltDisplay[x] / 1000) - min; data[x] = ((2 / (max - min)) * data[x]) - 1; } // Find points at which waveform crosses virtual 0 line - for(uint32_t x = 1; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) { + for(uint32_t x = 1; x < adc_buffer; x++) { if(data[x] >= 0 && data[x - 1] < 0) { - index[count++] = x - 1; + index_crossings[count++] = x - 1; } } count = 0; // Linear interpolation to find zero crossings // see https://gist.github.com/endolith/255291 for Python version - for(uint32_t x = 0; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) { - if(index[x] == -1) break; - crossings[count++] = - (float)index[x] - data[index[x]] / (data[index[x] + 1] - data[index[x]]); + for(uint32_t x = 0; x < adc_buffer; x++) { + if(index_crossings[x] == -1) break; + crossings[count++] = (float)index_crossings[x] - + data[index_crossings[x]] / + (data[index_crossings[x] + 1] - data[index_crossings[x]]); } float avg = 0.0; float countv = 0.0; - for(uint32_t x = 0; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) { - if(x + 1 >= ADC_CONVERTED_DATA_BUFFER_SIZE) break; + for(uint32_t x = 0; x < adc_buffer; x++) { + if(x + 1 >= adc_buffer) break; if(crossings[x] == -1 || crossings[x + 1] == -1) break; avg += crossings[x + 1] - crossings[x]; countv += 1; @@ -413,31 +469,91 @@ static void app_draw_callback(Canvas* canvas, void* ctx) { avg /= countv; // Display frequency of waveform snprintf(buf1, 50, "Freq: %.1f Hz", (double)((float)freq / avg)); - canvas_draw_str(canvas, 10, 20, buf1); + canvas_draw_str(canvas, 2, 20, buf1); + } break; + case m_fft: { + for(uint32_t i = 0; i < adc_buffer; i++) { + fft_data[i] = ((float)mvoltDisplay[i] / 1000); + } + + // Apply FFT + iterative_cooley_tukey(fft_data, adc_buffer); + + // Find FFT bin, with highest power + float max_val = -1; + int idx = 0; + for(uint32_t i = 1; i < adc_buffer / 2; i++) { + float f = cabsf(fft_data[i]) * cabsf(fft_data[i]); + if(f > max_val) { + max_val = f; + idx = i; + } + fft_power[i] = f; + } + + // Display frequency of waveform + snprintf(buf1, 50, "Freq: %.1fHz", (double)idx * ((double)freq / (double)adc_buffer)); + canvas_draw_str(canvas, 2, 10, buf1); } break; case m_voltage: { + // Display current scale + snprintf(buf1, 50, "%.0fx", (double)scale); + canvas_draw_str(canvas, 95, 10, buf1); // Display max, min, peak-to-peak voltages snprintf(buf1, 50, "Max: %.2fV", (double)max); - canvas_draw_str(canvas, 10, 10, buf1); + canvas_draw_str(canvas, 2, 10, buf1); snprintf(buf1, 50, "Min: %.2fV", (double)min); - canvas_draw_str(canvas, 10, 20, buf1); + canvas_draw_str(canvas, 2, 20, buf1); snprintf(buf1, 50, "Vpp: %.2fV", (double)(max - min)); - canvas_draw_str(canvas, 10, 30, buf1); + canvas_draw_str(canvas, 2, 30, buf1); } break; default: break; } - // Draw lines between each data point - for(uint32_t x = 1; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) { - uint32_t prev = 64 - (mvoltDisplay[x - 1] / (VDDA_APPLI / 64)); - uint32_t cur = 64 - (mvoltDisplay[x] / (VDDA_APPLI / 64)); - canvas_draw_line(canvas, x - 1, prev, x, cur); + if(type != m_fft) { + // Draw lines between each data point + // y should range from 0 to 63 + for(uint32_t x = 1; x < adc_buffer; x++) { + int32_t prev = + 63 - (uint32_t)(((float)mvoltDisplay[x - 1] / (float)VDDA_APPLI) * scale * 63.0f); + int32_t cur = + 63 - (uint32_t)(((float)mvoltDisplay[x] / (float)VDDA_APPLI) * scale * 63.0f); + if(!(prev < 0 && cur < 0)) + canvas_draw_line(canvas, x - 1, clamp(prev, 0, 63), x, clamp(cur, 0, 63)); + } + } else { + // Process FFT data - excluding bin 0 + float max = 0; + for(uint32_t i = 1; i < adc_buffer / 2; i += adc_buffer / 2 / 128) { + float sum = 0; + for(uint32_t i2 = i; i2 < i + (adc_buffer / 2 / 128); i2++) { + sum += fft_power[i2]; + } + if(sum > max) { + max = sum; + } + } + + uint32_t xpos = 0; + // xpos: 0 to 126 for window size 256 + // xpos: 0 to 127 for window size 512 + // xpos: 0 to 127 for window size 1024 + // y should range from 0 to 63 + for(uint32_t i = 1; i < adc_buffer / 2; i += adc_buffer / 2 / 128) { + float sum = 0; + for(uint32_t i2 = i; i2 < i + (adc_buffer / 2 / 128); i2++) { + sum += fft_power[i2]; + } + canvas_draw_line(canvas, xpos, 63, xpos, 63 - (uint32_t)(((sum / max) * 63.0f))); + xpos++; + } } + // Removing graph lines, to use extra pixel // Draw graph lines - canvas_draw_line(canvas, 0, 0, 0, 63); - canvas_draw_line(canvas, 0, 63, 128, 63); + //canvas_draw_line(canvas, 0, 0, 0, 63); + //canvas_draw_line(canvas, 0, 63, 127, 63); } static void app_input_callback(InputEvent* input_event, void* ctx) { @@ -446,6 +562,18 @@ static void app_input_callback(InputEvent* input_event, void* ctx) { furi_message_queue_put(event_queue, input_event, FuriWaitForever); } +// Free malloc'd data +void free_all() { + free(aADCxConvertedData); + free((void*)aADCxConvertedData_Voltage_mVoltA); + free((void*)aADCxConvertedData_Voltage_mVoltB); + free(index_crossings); + free(data); + free(crossings); + free(fft_data); + free(fft_power); +} + void scope_scene_run_on_enter(void* context) { ScopeApp* app = context; @@ -457,12 +585,32 @@ void scope_scene_run_on_enter(void* context) { } } + // Obtain scale value + scale = app->scale; + // Currently un-paused pause = 0; // What type of measurement are we performing type = app->measurement; + adc_buffer = ADC_CONVERTED_DATA_BUFFER_SIZE; + if(type == m_fft) adc_buffer = app->fft; + + aADCxConvertedData = malloc(adc_buffer * sizeof(uint16_t)); + aADCxConvertedData_Voltage_mVoltA = malloc(adc_buffer * sizeof(uint16_t)); + aADCxConvertedData_Voltage_mVoltB = malloc(adc_buffer * sizeof(uint16_t)); + + index_crossings = malloc(adc_buffer * sizeof(int16_t)); + data = malloc(adc_buffer * sizeof(float)); + crossings = malloc(adc_buffer * sizeof(float)); + fft_data = malloc(adc_buffer * sizeof(float complex)); + fft_power = malloc(adc_buffer * sizeof(float)); + + mvoltWrite = + &aADCxConvertedData_Voltage_mVoltA[0]; // Pointer to area we write converted voltage data to + mvoltDisplay = &aADCxConvertedData_Voltage_mVoltB[0]; // Pointer to area of memory we display + // Copy vector table, modify to use our own IRQ handlers __disable_irq(); memcpy(ramVector, (uint32_t*)(FLASH_BASE | SCB->VTOR), sizeof(uint32_t) * TABLE_SIZE); @@ -493,8 +641,7 @@ void scope_scene_run_on_enter(void* context) { MX_ADC1_Init(); // Setup initial values from ADC - for(tmp_index_adc_converted_data = 0; - tmp_index_adc_converted_data < ADC_CONVERTED_DATA_BUFFER_SIZE; + for(tmp_index_adc_converted_data = 0; tmp_index_adc_converted_data < adc_buffer; tmp_index_adc_converted_data++) { aADCxConvertedData[tmp_index_adc_converted_data] = VAR_CONVERTED_DATA_INIT_VALUE; aADCxConvertedData_Voltage_mVoltA[tmp_index_adc_converted_data] = 0; @@ -520,7 +667,7 @@ void scope_scene_run_on_enter(void* context) { bool running = true; bool save = false; while(running) { - if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) { + if(furi_message_queue_get(event_queue, &event, 150) == FuriStatusOk) { if((event.type == InputTypePress) || (event.type == InputTypeRepeat)) { switch(event.key) { case InputKeyLeft: @@ -566,6 +713,8 @@ void scope_scene_run_on_enter(void* context) { gui_remove_view_port(gui, view_port); view_port_free(view_port); + free_all(); + // Switch back to original scene furi_record_close(RECORD_GUI); scene_manager_previous_scene(app->scene_manager); @@ -575,9 +724,9 @@ void scope_scene_run_on_enter(void* context) { gui_remove_view_port(gui, view_port); view_port_free(view_port); - app->data = malloc(sizeof(uint16_t) * ADC_CONVERTED_DATA_BUFFER_SIZE); - memcpy( - app->data, (uint16_t*)mvoltDisplay, sizeof(uint16_t) * ADC_CONVERTED_DATA_BUFFER_SIZE); + app->data = malloc(sizeof(uint16_t) * adc_buffer); + memcpy(app->data, (uint16_t*)mvoltDisplay, sizeof(uint16_t) * adc_buffer); + free_all(); scene_manager_next_scene(app->scene_manager, ScopeSceneSave); } } diff --git a/oscilloscope/scenes/scope_scene_setup.c b/oscilloscope/scenes/scope_scene_setup.c index 143e23e56..bf047a4d2 100644 --- a/oscilloscope/scenes/scope_scene_setup.c +++ b/oscilloscope/scenes/scope_scene_setup.c @@ -15,6 +15,22 @@ static void timeperiod_cb(VariableItem* item) { app->time = time_list[index].time; } +static void scale_cb(VariableItem* item) { + ScopeApp* app = variable_item_get_context(item); + furi_assert(app); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, scale_list[index].str); + app->scale = scale_list[index].scale; +} + +static void fft_cb(VariableItem* item) { + ScopeApp* app = variable_item_get_context(item); + furi_assert(app); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, fft_list[index].str); + app->fft = fft_list[index].window; +} + static void measurement_cb(VariableItem* item) { ScopeApp* app = variable_item_get_context(item); furi_assert(app); @@ -38,6 +54,26 @@ void scope_scene_setup_on_enter(void* context) { } } + item = variable_item_list_add(var_item_list, "FFT window", COUNT_OF(fft_list), fft_cb, app); + + for(uint32_t i = 0; i < COUNT_OF(fft_list); i++) { + if(fft_list[i].window == app->fft) { + variable_item_set_current_value_index(item, i); + variable_item_set_current_value_text(item, fft_list[i].str); + break; + } + } + + item = variable_item_list_add(var_item_list, "Scale", COUNT_OF(scale_list), scale_cb, app); + + for(uint32_t i = 0; i < COUNT_OF(scale_list); i++) { + if(scale_list[i].scale == app->scale) { + variable_item_set_current_value_index(item, i); + variable_item_set_current_value_text(item, scale_list[i].str); + break; + } + } + item = variable_item_list_add( var_item_list, "Measurement", COUNT_OF(measurement_list), measurement_cb, app); diff --git a/oscilloscope/scope.c b/oscilloscope/scope.c index 1be7db2c3..49ca621d1 100644 --- a/oscilloscope/scope.c +++ b/oscilloscope/scope.c @@ -79,6 +79,8 @@ ScopeApp* scope_app_alloc() { app->view_dispatcher, ScopeViewSave, text_input_get_view(app->text_input)); app->time = 0.001; + app->scale = 1.0f; + app->fft = 256; app->measurement = m_time; scene_manager_next_scene(app->scene_manager, ScopeSceneStart); diff --git a/oscilloscope/scope_app_i.h b/oscilloscope/scope_app_i.h index 5e5a4c5e1..a72ff2e42 100644 --- a/oscilloscope/scope_app_i.h +++ b/oscilloscope/scope_app_i.h @@ -26,10 +26,26 @@ typedef struct { static const timeperiod time_list[] = {{1.0, "1s"}, {0.1, "0.1s"}, {1e-3, "1ms"}, {0.1e-3, "0.1ms"}, {1e-6, "1us"}}; +typedef struct { + int window; + char* str; +} fftwindow; + +static const fftwindow fft_list[] = {{256, "256"}, {512, "512"}, {1024, "1024"}}; + +typedef struct { + float scale; + char* str; +} scalesize; + +static const scalesize scale_list[] = + {{1.0f, "1x"}, {2.0f, "2x"}, {4.0f, "4x"}, {10.0f, "10x"}, {100.0f, "100x"}}; + enum measureenum { m_time, m_voltage, - m_capture + m_capture, + m_fft }; typedef struct { @@ -37,10 +53,8 @@ typedef struct { char* str; } measurement; -static const measurement measurement_list[] = { - {m_time, "Time"}, - {m_voltage, "Voltage"}, - {m_capture, "Capture"}}; +static const measurement measurement_list[] = + {{m_time, "Time"}, {m_voltage, "Voltage"}, {m_capture, "Capture"}, {m_fft, "FFT"}}; struct ScopeApp { Gui* gui; @@ -52,6 +66,8 @@ struct ScopeApp { Widget* widget; TextInput* text_input; double time; + int fft; + float scale; enum measureenum measurement; char file_name_tmp[MAX_LEN_NAME]; uint16_t* data; diff --git a/oscilloscope/screenshots/capture.png b/oscilloscope/screenshots/capture.png new file mode 100644 index 000000000..ab17eb873 Binary files /dev/null and b/oscilloscope/screenshots/capture.png differ diff --git a/oscilloscope/screenshots/fft.png b/oscilloscope/screenshots/fft.png new file mode 100644 index 000000000..5964986a1 Binary files /dev/null and b/oscilloscope/screenshots/fft.png differ diff --git a/oscilloscope/screenshots/freq.png b/oscilloscope/screenshots/freq.png index fa43900cd..543bd02f9 100644 Binary files a/oscilloscope/screenshots/freq.png and b/oscilloscope/screenshots/freq.png differ diff --git a/oscilloscope/screenshots/setup.png b/oscilloscope/screenshots/setup.png new file mode 100644 index 000000000..9900038d3 Binary files /dev/null and b/oscilloscope/screenshots/setup.png differ diff --git a/oscilloscope/screenshots/volt.png b/oscilloscope/screenshots/volt.png index e1b006ec2..0f000bad7 100644 Binary files a/oscilloscope/screenshots/volt.png and b/oscilloscope/screenshots/volt.png differ diff --git a/picopass/.gitsubtree b/picopass/.gitsubtree index 7aff1bf4e..8ffa3f69d 100644 --- a/picopass/.gitsubtree +++ b/picopass/.gitsubtree @@ -1,2 +1,2 @@ -https://github.com/xMasterX/all-the-plugins dev base_pack/picopass 4558d74c9da36abc851edd96a95d18f7d5511a75 +https://github.com/xMasterX/all-the-plugins dev base_pack/picopass ffa18100afc128abea7269fcc31d9c237523e7c6 https://gitlab.com/bettse/picopass main / diff --git a/picopass/protocol/picopass_listener.c b/picopass/protocol/picopass_listener.c index 5d639c0ab..b9cd0c6e1 100644 --- a/picopass/protocol/picopass_listener.c +++ b/picopass/protocol/picopass_listener.c @@ -752,7 +752,6 @@ void picopass_listener_start( instance->context = context; picopass_listener_reset(instance); - nfc_iso15693_force_1outof4(instance->nfc); nfc_start(instance->nfc, picopass_listener_start_callback, instance); } diff --git a/picopass/protocol/picopass_poller.c b/picopass/protocol/picopass_poller.c index 97cdf4f1a..4746437bb 100644 --- a/picopass/protocol/picopass_poller.c +++ b/picopass/protocol/picopass_poller.c @@ -539,14 +539,14 @@ NfcCommand picopass_poller_write_key_handler(PicopassPoller* instance) { const uint8_t* new_key = instance->event_data.req_write_key.key; bool is_elite_key = instance->event_data.req_write_key.is_elite_key; - const uint8_t* csn = picopass_data->card_data[PICOPASS_CSN_BLOCK_INDEX].data; - const uint8_t* config_block = picopass_data->card_data[PICOPASS_CONFIG_BLOCK_INDEX].data; - uint8_t fuses = config_block[7]; - const uint8_t* old_key = picopass_data->card_data[PICOPASS_SECURE_KD_BLOCK_INDEX].data; + const uint8_t* csn = instance->serial_num.data; + const uint8_t* old_key = instance->div_key; PicopassBlock new_block = {}; loclass_iclass_calc_div_key(csn, new_key, new_block.data, is_elite_key); + const uint8_t* config_block = picopass_data->card_data[PICOPASS_CONFIG_BLOCK_INDEX].data; + uint8_t fuses = config_block[7]; if((fuses & 0x80) == 0x80) { FURI_LOG_D(TAG, "Plain write for personalized mode key change"); } else { diff --git a/picopass/scenes/picopass_scene_saved_menu.c b/picopass/scenes/picopass_scene_saved_menu.c index e8e0771cd..c8212f976 100644 --- a/picopass/scenes/picopass_scene_saved_menu.c +++ b/picopass/scenes/picopass_scene_saved_menu.c @@ -7,6 +7,7 @@ enum SubmenuIndex { SubmenuIndexRename, SubmenuIndexDelete, SubmenuIndexSaveAsLF, + SubmenuIndexSaveLegacy, }; void picopass_scene_saved_menu_submenu_callback(void* context, uint32_t index) { @@ -25,6 +26,8 @@ void picopass_scene_saved_menu_on_enter(void* context) { bool secured = (card_data[PICOPASS_CONFIG_BLOCK_INDEX].data[7] & PICOPASS_FUSE_CRYPT10) != PICOPASS_FUSE_CRYPT0; bool no_credential = picopass_is_memset(pacs->credential, 0x00, sizeof(pacs->credential)); + bool SR = card_data[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].data[0] == 0xA3 && + card_data[10].valid && 0x30 == card_data[10].data[0]; submenu_add_item( submenu, "Info", SubmenuIndexInfo, picopass_scene_saved_menu_submenu_callback, picopass); @@ -44,6 +47,15 @@ void picopass_scene_saved_menu_on_enter(void* context) { SubmenuIndexSaveAsLF, picopass_scene_saved_menu_submenu_callback, picopass); + + if(SR) { + submenu_add_item( + submenu, + "Save as Legacy", + SubmenuIndexSaveLegacy, + picopass_scene_saved_menu_submenu_callback, + picopass); + } } submenu_add_item( @@ -91,10 +103,16 @@ bool picopass_scene_saved_menu_on_event(void* context, SceneManagerEvent event) consumed = true; } else if(event.event == SubmenuIndexSaveAsLF) { scene_manager_set_scene_state( - picopass->scene_manager, PicopassSceneCardMenu, SubmenuIndexSaveAsLF); + picopass->scene_manager, PicopassSceneSavedMenu, SubmenuIndexSaveAsLF); picopass->dev->format = PicopassDeviceSaveFormatLF; scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName); consumed = true; + } else if(event.event == SubmenuIndexSaveLegacy) { + scene_manager_set_scene_state( + picopass->scene_manager, PicopassSceneSavedMenu, SubmenuIndexSaveLegacy); + picopass->dev->format = PicopassDeviceSaveFormatLegacy; + scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName); + consumed = true; } } diff --git a/pokemon_trading/application.fam b/pokemon_trading/application.fam index f31855121..df45b6250 100644 --- a/pokemon_trading/application.fam +++ b/pokemon_trading/application.fam @@ -5,7 +5,7 @@ App( entry_point="pokemon_app", requires=["gui"], stack_size=2 * 1024, - fap_version=[2, 2], + fap_version=[2, 3], fap_category="GPIO", fap_icon="pokemon_10px.png", fap_icon_assets="assets", diff --git a/pokemon_trading/changelog.md b/pokemon_trading/changelog.md index b4be455da..9f41844d2 100644 --- a/pokemon_trading/changelog.md +++ b/pokemon_trading/changelog.md @@ -1,5 +1,8 @@ # Changelog - Patch Notes +## Version 2.3 +- Fix potential bug in copying incoming trade data to the live working struct + ## Version 2.2 **New Features** - Update to gblink v0.63 which includes saving/loading of pin configurations for the EXT link interface diff --git a/pokemon_trading/src/pokemon_data.c b/pokemon_trading/src/pokemon_data.c index 014db2a2b..0832aa652 100644 --- a/pokemon_trading/src/pokemon_data.c +++ b/pokemon_trading/src/pokemon_data.c @@ -896,12 +896,12 @@ void pokemon_stat_memcpy(PokemonData* dst, PokemonData* src, uint8_t which) { &(((TradeBlockGenI*)src->trade_block)->ot_name[which]), sizeof(struct name)); } else if(dst->gen == GEN_II) { - ((TradeBlockGenI*)dst->trade_block)->party_members[0] = - ((TradeBlockGenI*)src->trade_block)->party_members[which]; + ((TradeBlockGenII*)dst->trade_block)->party_members[0] = + ((TradeBlockGenII*)src->trade_block)->party_members[which]; memcpy( &(((TradeBlockGenII*)dst->trade_block)->party[0]), &(((TradeBlockGenII*)src->trade_block)->party[which]), - sizeof(PokemonPartyGenI)); + sizeof(PokemonPartyGenII)); memcpy( &(((TradeBlockGenII*)dst->trade_block)->nickname[0]), &(((TradeBlockGenII*)src->trade_block)->nickname[which]), diff --git a/seader/.gitsubtree b/seader/.gitsubtree index 25216eceb..a2e0571bd 100644 --- a/seader/.gitsubtree +++ b/seader/.gitsubtree @@ -1,2 +1,2 @@ -https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/seader a5a7ecf1a849e3626816e0ea645a4b706e84dfaf +https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/seader c450eaa657997d2a8776df11b837e22a69fabd77 https://github.com/bettse/seader main / diff --git a/seader/application.fam b/seader/application.fam index 3b8e72a15..c6484537d 100644 --- a/seader/application.fam +++ b/seader/application.fam @@ -20,7 +20,7 @@ App( ], fap_icon="icons/logo.png", fap_category="NFC", - fap_version="3.2", + fap_version="3.3", fap_author="bettse", # fap_extbuild=( # ExtFile( diff --git a/seader/readme.md b/seader/readme.md index 01e6249cf..3729ca2d8 100644 --- a/seader/readme.md +++ b/seader/readme.md @@ -38,8 +38,14 @@ Buy it at [Red Team Tools](https://www.redteamtools.com/flippermeister/). ### Option 3: Smart Card 2 Click - -Put SAM ([USA](https://www.cdw.com/product/hp-sim-for-hid-iclass-for-hip2-reader-security-sim/4854794) [EU](https://www.rfideas-shop.com/en/kt-sim-se-sim-card-hid-iclass-and-seos-for-sphip-r.html) [CA](https://www.pc-canada.com/item/hp-sim-for-hid-iclass-se-and-hid-iclass-seos-for-hip2-reader/y7c07a)) into **[adapter](https://a.co/d/1E9Zk1h)** (because of chip on top) and plug into **Smart Card 2 Click** ([Mikroe](https://www.mikroe.com/smart-card-2-click) [digikey](https://www.digikey.com/en/products/detail/mikroelektronika/MIKROE-5492/20840872) with cheaper US shipping). Connect Smart Card 2 Click to Flipper Zero (See `Connections` below). +Buy HID SAM: + * [Australia](https://store.dorks.com.au/products/hp-sim-for-hid-iclass-for-hip2-reader-2583766) + * [USA](https://www.cdw.com/product/hp-sim-for-hid-iclass-for-hip2-reader-security-sim/4854794) + * [Europe](https://www.rfideas-shop.com/en/kt-sim-se-sim-card-hid-iclass-and-seos-for-sphip-r.html) + * [Canada](https://www.pc-canada.com/item/hp-sim-for-hid-iclass-se-and-hid-iclass-seos-for-hip2-reader/y7c07a) + * [eBay](https://www.ebay.com/p/4037642616) + +Put SAM into **[adapter](https://a.co/d/1E9Zk1h)** (because of chip on top) and plug into **Smart Card 2 Click** ([Mikroe](https://www.mikroe.com/smart-card-2-click) [digikey](https://www.digikey.com/en/products/detail/mikroelektronika/MIKROE-5492/20840872) with cheaper US shipping). Connect Smart Card 2 Click to Flipper Zero (See `Connections` below). Optionally 3d print a [case designed by sean](https://www.printables.com/model/543149-case-for-flipper-zero-devboard-smart2click-samsim) diff --git a/unitemp/Sensors.c b/unitemp/Sensors.c index 14de195c3..84eadc8b8 100644 --- a/unitemp/Sensors.c +++ b/unitemp/Sensors.c @@ -568,6 +568,8 @@ void unitemp_sensors_free(void) { bool unitemp_sensors_init(void) { bool result = true; + app->sensors_ready = false; + //Перебор датчиков из списка for(uint8_t i = 0; i < unitemp_sensors_getCount(); i++) { //Включение 5V если на порту 1 FZ его нет @@ -585,12 +587,15 @@ bool unitemp_sensors_init(void) { } FURI_LOG_I(APP_NAME, "Sensor %s successfully initialized", app->sensors[i]->name); } + app->sensors_ready = true; + return result; } bool unitemp_sensors_deInit(void) { bool result = true; + //Выключение 5 В если до этого оно не было включено if(app->settings.lastOTGState != true) { furi_hal_power_disable_otg(); @@ -607,11 +612,14 @@ bool unitemp_sensors_deInit(void) { result = false; } } + return result; } UnitempStatus unitemp_sensor_updateData(Sensor* sensor) { - if(sensor == NULL) return UT_SENSORSTATUS_ERROR; + if(sensor == NULL) { + return UT_SENSORSTATUS_ERROR; + } //Проверка на допустимость опроса датчика if(furi_get_tick() - sensor->lastPollingTime < sensor->type->pollingInterval) { @@ -655,6 +663,7 @@ UnitempStatus unitemp_sensor_updateData(Sensor* sensor) { unitemp_pascalToHPa(sensor); } } + return sensor->status; } diff --git a/unitemp/application.fam b/unitemp/application.fam index de318e970..c547d6bce 100644 --- a/unitemp/application.fam +++ b/unitemp/application.fam @@ -9,10 +9,11 @@ App( stack_size=2 * 1024, order=100, fap_description="Application for reading temperature, humidity and pressure sensors like a DHT11/22, DS18B20, BMP280, HTU21 and more", - fap_version="1.5", fap_author="@quen0n & (fixes by @xMasterX)", fap_weburl="https://github.com/quen0n/unitemp-flipperzero", fap_category="GPIO/Sensors", fap_icon="icon.png", fap_icon_assets="assets", + fap_libs=["assets"], + fap_version="1.6", ) diff --git a/unitemp/unitemp.c b/unitemp/unitemp.c index 0d3ee523b..9250d108a 100644 --- a/unitemp/unitemp.c +++ b/unitemp/unitemp.c @@ -16,7 +16,6 @@ along with this program. If not, see . */ #include "unitemp.h" -#include "interfaces/SingleWireSensor.h" #include "Sensors.h" #include "./views/UnitempViews.h" @@ -220,16 +219,12 @@ bool unitemp_loadSettings(void) { return true; } -static void unitemp_sensors_update_callback(void* context) { - Unitemp* app = context; - if(!app->processing) { - view_dispatcher_stop(app->view_dispatcher); - return; - } - if(app->sensors_ready) { +static void view_dispatcher_tick_event_callback(void* context) { + UNUSED(context); + + if((app->sensors_ready) && (app->sensors_update)) { unitemp_sensors_updateValues(); } - view_port_update(app->view_port); } /** @@ -241,8 +236,8 @@ static void unitemp_sensors_update_callback(void* context) { static bool unitemp_alloc(void) { //Выделение памяти под данные приложения app = malloc(sizeof(Unitemp)); - //Разрешение работы приложения - app->processing = true; + + app->sensors_ready = false; //Открытие хранилища (?) app->storage = furi_record_open(RECORD_STORAGE); @@ -258,15 +253,10 @@ static bool unitemp_alloc(void) { app->settings.heat_index = false; app->gui = furi_record_open(RECORD_GUI); + //Диспетчер окон app->view_dispatcher = view_dispatcher_alloc(); - view_dispatcher_set_event_callback_context(app->view_dispatcher, app); - view_dispatcher_set_tick_event_callback( - app->view_dispatcher, unitemp_sensors_update_callback, 100); - - app->view_port = view_port_alloc(); - app->sensors = NULL; app->buff = malloc(BUFF_SIZE); @@ -283,11 +273,11 @@ static bool unitemp_alloc(void) { //Всплывающее окно app->popup = popup_alloc(); - - gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); - view_dispatcher_add_view(app->view_dispatcher, UnitempViewPopup, popup_get_view(app->popup)); + view_dispatcher_set_tick_event_callback( + app->view_dispatcher, view_dispatcher_tick_event_callback, furi_ms_to_ticks(100)); + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); return true; @@ -313,11 +303,6 @@ static void unitemp_free(void) { free(app->buff); view_dispatcher_free(app->view_dispatcher); - - view_port_enabled_set(app->view_port, false); - gui_remove_view_port(app->gui, app->view_port); - view_port_free(app->view_port); - furi_record_close(RECORD_GUI); //Очистка датчиков //Высвыбождение данных датчиков @@ -349,14 +334,18 @@ int32_t unitemp_app() { //Загрузка настроек из SD-карты unitemp_loadSettings(); + //Применение настроек if(app->settings.infinityBacklight == true) { //Постоянное свечение подсветки notification_message(app->notifications, &sequence_display_backlight_enforce_on); } + app->settings.lastOTGState = furi_hal_power_is_otg_enabled(); + //Загрузка датчиков из SD-карты unitemp_sensors_load(); + //Инициализация датчиков unitemp_sensors_init(); @@ -366,11 +355,14 @@ int32_t unitemp_app() { //Деинициализация датчиков unitemp_sensors_deInit(); + //Автоматическое управление подсветкой if(app->settings.infinityBacklight == true) notification_message(app->notifications, &sequence_display_backlight_enforce_auto); + //Освобождение памяти unitemp_free(); + //Выход return 0; } diff --git a/unitemp/unitemp.h b/unitemp/unitemp.h index bd2e70cd1..5467a01da 100644 --- a/unitemp/unitemp.h +++ b/unitemp/unitemp.h @@ -40,7 +40,7 @@ //Имя приложения #define APP_NAME "Unitemp" //Версия приложения -#define UNITEMP_APP_VER "1.4-store" +#define UNITEMP_APP_VER "1.6" //Путь хранения файлов плагина #define APP_PATH_FOLDER EXT_PATH("apps_data/unitemp") //Имя файла с настройками @@ -93,8 +93,8 @@ typedef struct { //Основная структура плагина typedef struct { //Система - bool processing; //Флаг работы приложения. При ложном значении приложение закрывается bool sensors_ready; //Флаг готовности датчиков к опросу + bool sensors_update; // Флаг допустимости опроса датчиков //Основные настройки UnitempSettings settings; //Массив указателей на датчики @@ -107,7 +107,6 @@ typedef struct { //Экран Gui* gui; - ViewPort* view_port; ViewDispatcher* view_dispatcher; NotificationApp* notifications; Widget* widget; diff --git a/unitemp/views/General_view.c b/unitemp/views/General_view.c index 8271c53da..39033f0ae 100644 --- a/unitemp/views/General_view.c +++ b/unitemp/views/General_view.c @@ -534,11 +534,11 @@ static void _draw_view_sensorsCarousel(Canvas* canvas) { static void _draw_callback(Canvas* canvas, void* _model) { UNUSED(_model); - app->sensors_ready = true; + app->sensors_update = true; uint8_t sensors_count = unitemp_sensors_getActiveCount(); - if(generalview_sensor_index + 1 > sensors_count) generalview_sensor_index = 0; + if(generalview_sensor_index >= sensors_count) generalview_sensor_index = 0; if(sensors_count == 0) { current_view = G_NO_SENSORS_VIEW; @@ -558,14 +558,14 @@ static bool _input_callback(InputEvent* event, void* context) { if(event->key == InputKeyOk && event->type == InputTypeShort) { //Меню добавления датчика при их отсутствии if(current_view == G_NO_SENSORS_VIEW) { - app->sensors_ready = false; + app->sensors_update = false; unitemp_SensorsList_switch(); } else if(current_view == G_LIST_VIEW) { //Переход в главное меню при выключенном селекторе - app->sensors_ready = false; + app->sensors_update = false; unitemp_MainMenu_switch(); } else if(current_view == G_CAROUSEL_VIEW) { - app->sensors_ready = false; + app->sensors_update = false; unitemp_SensorActions_switch(unitemp_sensor_getActive(generalview_sensor_index)); } } @@ -649,7 +649,7 @@ static bool _input_callback(InputEvent* event, void* context) { //Выход из приложения при карусели или отсутствии датчиков if(current_view == G_NO_SENSORS_VIEW || ((current_view == G_CAROUSEL_VIEW) && (carousel_info_selector == CAROUSEL_VALUES))) { - app->processing = false; + view_dispatcher_stop(app->view_dispatcher); return true; } //Переключение селектора вида карусели @@ -681,7 +681,6 @@ void unitemp_General_alloc(void) { } void unitemp_General_switch(void) { - app->sensors_ready = true; view_dispatcher_switch_to_view(app->view_dispatcher, UnitempViewGeneral); } diff --git a/web_crawler/CHANGELOG.md b/web_crawler/CHANGELOG.md index f6f5be555..e1a4d7260 100644 --- a/web_crawler/CHANGELOG.md +++ b/web_crawler/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.0.1 +- Updated the settings so they save as intended. +- Added additional error handling. +- Fixed a bug in the text input that prevented users from typing. + +## 1.0 +- Updated the HTTP Method toggle to work as intended. +- Updated FlipperHTTP to the latest version. +- Improved memory allocation +- Fixed loading display messages. +- Added a BROWSE method, which fetches HTML data from the specified path, parses the HTML, then displays the data as a "webpage". + ## 0.8 Updates from Derek Jamison: - Improved progress display. diff --git a/web_crawler/README.md b/web_crawler/README.md index dcde3b31e..34e43435a 100644 --- a/web_crawler/README.md +++ b/web_crawler/README.md @@ -1,11 +1,8 @@ -## Overview - -**Web Crawler** is a custom application designed for the Flipper Zero device, allowing users to configure and manage HTTP requests directly from their Flipper Zero. +Browse the web, fetch API data, and more on your Flipper Zero. ## Requirements -- WiFi Dev Board or Raspberry Pi Pico W for Flipper Zero with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP -- WiFi Access Point - +- WiFi Developer Board, Raspberry Pi, or ESP32 device flashed with FlipperHTTP version 1.6 or higher: https://github.com/jblanked/FlipperHTTP +- 2.4 GHz WiFi Access Point ## Installation - Download from the Official Flipper App Store: https://lab.flipper.net/apps/web_crawler @@ -52,7 +49,7 @@ - Enter the complete URL of the website you intend to crawl (e.g., https://www.example.com/). 2. **HTTP Method** - - Choose between GET, POST, DELETE, PUT, and DOWNLOAD. + - Choose between GET, POST, DELETE, PUT, DOWNLOAD, and BROWSE. 3. **Headers** - Add your required headers to be used in your HTTP requests @@ -73,19 +70,4 @@ - Provide your desired file name. After saving, the app will rename your file with the new name. -## Saving Settings -After entering the desired configuration parameters, the app automatically saves these settings for use during the HTTP request process. You can update these settings at any time by navigating back to the **Settings** menu. - -## Logging and Debugging -The Web Crawler app uses logging to help identify issues: - -- **Info Logs**: Provide general information about the app's operations (e.g., UART initialization, sending settings). -- **Error Logs**: Indicate problems encountered during execution (e.g., failed to open settings file). - -Connect your Flipper Zero to a computer and use a serial terminal to view these logs for detailed troubleshooting. - -## Known Issues -1. **Screen Delay**: Occasionally, the Run screen may get stuck on "Receiving Data". - - If it takes longer than 10 seconds, restart your Flipper Zero. - *Happy Crawling! 🕷️* diff --git a/web_crawler/alloc/web_crawler_alloc.c b/web_crawler/alloc/web_crawler_alloc.c index 097456fbc..75f295020 100644 --- a/web_crawler/alloc/web_crawler_alloc.c +++ b/web_crawler/alloc/web_crawler_alloc.c @@ -4,429 +4,54 @@ * @brief Function to allocate resources for the WebCrawlerApp. * @return Pointer to the initialized WebCrawlerApp, or NULL on failure. */ -WebCrawlerApp* web_crawler_app_alloc() { +WebCrawlerApp *web_crawler_app_alloc() +{ // Initialize the entire structure to zero to prevent undefined behavior - WebCrawlerApp* app = (WebCrawlerApp*)malloc(sizeof(WebCrawlerApp)); + WebCrawlerApp *app = (WebCrawlerApp *)malloc(sizeof(WebCrawlerApp)); // Open GUI - Gui* gui = furi_record_open(RECORD_GUI); - - // Initialize UART with the correct callback - if(!flipper_http_init(flipper_http_rx_callback, app)) { - FURI_LOG_E(TAG, "Failed to initialize UART"); - return NULL; - } + Gui *gui = furi_record_open(RECORD_GUI); // Allocate ViewDispatcher - if(!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) { + if (!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) + { return NULL; } - view_dispatcher_set_custom_event_callback( - app->view_dispatcher, web_crawler_custom_event_callback); + view_dispatcher_set_custom_event_callback(app->view_dispatcher, web_crawler_custom_event_callback); // Allocate and initialize temp_buffer and path - app->temp_buffer_size_path = 128; - app->temp_buffer_size_ssid = 64; - app->temp_buffer_size_password = 64; - app->temp_buffer_size_file_type = 16; - app->temp_buffer_size_file_rename = 128; app->temp_buffer_size_http_method = 16; - app->temp_buffer_size_headers = 256; - app->temp_buffer_size_payload = 256; - if(!easy_flipper_set_buffer(&app->temp_buffer_path, app->temp_buffer_size_path)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->path, app->temp_buffer_size_path)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->temp_buffer_ssid, app->temp_buffer_size_ssid)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->ssid, app->temp_buffer_size_ssid)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->temp_buffer_password, app->temp_buffer_size_password)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->password, app->temp_buffer_size_password)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->temp_buffer_file_type, app->temp_buffer_size_file_type)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->file_type, app->temp_buffer_size_file_type)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->temp_buffer_file_rename, app->temp_buffer_size_file_rename)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->file_rename, app->temp_buffer_size_file_rename)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->temp_buffer_http_method, app->temp_buffer_size_http_method)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->http_method, app->temp_buffer_size_http_method)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->temp_buffer_headers, app->temp_buffer_size_headers)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->headers, app->temp_buffer_size_headers)) { - return NULL; - } - if(!easy_flipper_set_buffer(&app->temp_buffer_payload, app->temp_buffer_size_payload)) { + if (!easy_flipper_set_buffer(&app->temp_buffer_http_method, app->temp_buffer_size_http_method)) + { return NULL; } - if(!easy_flipper_set_buffer(&app->payload, app->temp_buffer_size_payload)) { + if (!easy_flipper_set_buffer(&app->http_method, app->temp_buffer_size_http_method)) + { return NULL; } - // Allocate TextInput views - if(!easy_flipper_set_uart_text_input( - &app->text_input_path, - WebCrawlerViewTextInput, - "Enter URL", - app->temp_buffer_path, - app->temp_buffer_size_path, - NULL, - web_crawler_back_to_request_callback, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_uart_text_input( - &app->text_input_ssid, - WebCrawlerViewTextInputSSID, - "Enter SSID", - app->temp_buffer_ssid, - app->temp_buffer_size_ssid, - NULL, - web_crawler_back_to_wifi_callback, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_uart_text_input( - &app->text_input_password, - WebCrawlerViewTextInputPassword, - "Enter Password", - app->temp_buffer_password, - app->temp_buffer_size_password, - NULL, - web_crawler_back_to_wifi_callback, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_uart_text_input( - &app->text_input_file_type, - WebCrawlerViewTextInputFileType, - "Enter File Type", - app->temp_buffer_file_type, - app->temp_buffer_size_file_type, - NULL, - web_crawler_back_to_file_callback, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_uart_text_input( - &app->text_input_file_rename, - WebCrawlerViewTextInputFileRename, - "Enter File Rename", - app->temp_buffer_file_rename, - app->temp_buffer_size_file_rename, - NULL, - web_crawler_back_to_file_callback, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_uart_text_input( - &app->text_input_headers, - WebCrawlerViewTextInputHeaders, - "Enter Headers", - app->temp_buffer_headers, - app->temp_buffer_size_headers, - NULL, - web_crawler_back_to_request_callback, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_uart_text_input( - &app->text_input_payload, - WebCrawlerViewTextInputPayload, - "Enter Payload", - app->temp_buffer_payload, - app->temp_buffer_size_payload, - NULL, - web_crawler_back_to_request_callback, - &app->view_dispatcher, - app)) { - return NULL; - } - - // Allocate VariableItemList views - if(!easy_flipper_set_variable_item_list( - &app->variable_item_list_wifi, - WebCrawlerViewVariableItemListWifi, - web_crawler_wifi_enter_callback, - web_crawler_back_to_configure_callback, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_variable_item_list( - &app->variable_item_list_file, - WebCrawlerViewVariableItemListFile, - web_crawler_file_enter_callback, - web_crawler_back_to_configure_callback, - &app->view_dispatcher, - app)) { - return NULL; - } - if(!easy_flipper_set_variable_item_list( - &app->variable_item_list_request, - WebCrawlerViewVariableItemListRequest, - web_crawler_request_enter_callback, - web_crawler_back_to_configure_callback, - &app->view_dispatcher, - app)) { - return NULL; - } - - // set variable items - app->path_item = - variable_item_list_add(app->variable_item_list_request, "Path", 0, NULL, NULL); - app->http_method_item = variable_item_list_add( - app->variable_item_list_request, "HTTP Method", 5, web_crawler_http_method_change, app); - app->headers_item = - variable_item_list_add(app->variable_item_list_request, "Headers", 0, NULL, NULL); - app->payload_item = - variable_item_list_add(app->variable_item_list_request, "Payload", 0, NULL, NULL); - // - app->ssid_item = - variable_item_list_add(app->variable_item_list_wifi, "SSID", 0, NULL, NULL); // index 0 - app->password_item = - variable_item_list_add(app->variable_item_list_wifi, "Password", 0, NULL, NULL); // index 1 - // - app->file_read_item = variable_item_list_add( - app->variable_item_list_file, "Read File", 0, NULL, NULL); // index 0 - app->file_type_item = variable_item_list_add( - app->variable_item_list_file, "Set File Type", 0, NULL, NULL); // index 1 - app->file_rename_item = variable_item_list_add( - app->variable_item_list_file, "Rename File", 0, NULL, NULL); // index 2 - app->file_delete_item = variable_item_list_add( - app->variable_item_list_file, "Delete File", 0, NULL, NULL); // index 3 - - if(!app->ssid_item || !app->password_item || !app->file_type_item || !app->file_rename_item || - !app->path_item || !app->file_read_item || !app->file_delete_item || - !app->http_method_item || !app->headers_item || !app->payload_item) { - free_all(app, "Failed to add items to VariableItemList"); - return NULL; - } - - variable_item_set_current_value_text(app->path_item, ""); // Initialize - variable_item_set_current_value_text(app->http_method_item, ""); // Initialize - variable_item_set_current_value_text(app->headers_item, ""); // Initialize - variable_item_set_current_value_text(app->payload_item, ""); // Initialize - variable_item_set_current_value_text(app->ssid_item, ""); // Initialize - variable_item_set_current_value_text(app->password_item, ""); // Initialize - variable_item_set_current_value_text(app->file_type_item, ""); // Initialize - variable_item_set_current_value_text(app->file_rename_item, ""); // Initialize - variable_item_set_current_value_text(app->file_read_item, ""); // Initialize - variable_item_set_current_value_text(app->file_delete_item, ""); // Initialize - // Allocate Submenu views - if(!easy_flipper_set_submenu( - &app->submenu_main, - WebCrawlerViewSubmenuMain, - "Web Crawler v0.8", - web_crawler_exit_app_callback, - &app->view_dispatcher)) { - return NULL; - } - if(!easy_flipper_set_submenu( - &app->submenu_config, - WebCrawlerViewSubmenuConfig, - "Settings", - web_crawler_back_to_main_callback, - &app->view_dispatcher)) { + if (!easy_flipper_set_submenu(&app->submenu_main, WebCrawlerViewSubmenuMain, VERSION_TAG, web_crawler_exit_app_callback, &app->view_dispatcher)) + { return NULL; } // Add Submenu items - submenu_add_item( - app->submenu_main, "Run", WebCrawlerSubmenuIndexRun, web_crawler_submenu_callback, app); - submenu_add_item( - app->submenu_main, "About", WebCrawlerSubmenuIndexAbout, web_crawler_submenu_callback, app); - submenu_add_item( - app->submenu_main, - "Settings", - WebCrawlerSubmenuIndexConfig, - web_crawler_submenu_callback, - app); - - submenu_add_item( - app->submenu_config, "WiFi", WebCrawlerSubmenuIndexWifi, web_crawler_submenu_callback, app); - submenu_add_item( - app->submenu_config, "File", WebCrawlerSubmenuIndexFile, web_crawler_submenu_callback, app); - submenu_add_item( - app->submenu_config, - "Request", - WebCrawlerSubmenuIndexRequest, - web_crawler_submenu_callback, - app); + submenu_add_item(app->submenu_main, "Run", WebCrawlerSubmenuIndexRun, web_crawler_submenu_callback, app); + submenu_add_item(app->submenu_main, "About", WebCrawlerSubmenuIndexAbout, web_crawler_submenu_callback, app); + submenu_add_item(app->submenu_main, "Settings", WebCrawlerSubmenuIndexConfig, web_crawler_submenu_callback, app); // Main view - if(!easy_flipper_set_view( - &app->view_loader, - WebCrawlerViewLoader, - web_crawler_loader_draw_callback, - NULL, - web_crawler_back_to_main_callback, - &app->view_dispatcher, - app)) { + if (!easy_flipper_set_view(&app->view_loader, WebCrawlerViewLoader, web_crawler_loader_draw_callback, NULL, web_crawler_back_to_main_callback, &app->view_dispatcher, app)) + { return NULL; } web_crawler_loader_init(app->view_loader); - - //-- WIDGET ABOUT VIEW -- - if(!easy_flipper_set_widget( - &app->widget_about, - WebCrawlerViewAbout, - "Web Crawler App\n---\nThis is a web crawler app for Flipper Zero.\n---\nVisit github.com/jblanked for more details.\n---\nPress BACK to return.", - web_crawler_back_to_main_callback, - &app->view_dispatcher)) { - return NULL; - } - if(!easy_flipper_set_widget( - &app->widget_file_read, - WebCrawlerViewFileRead, - "Data will be displayed here.", - web_crawler_back_to_file_callback, - &app->view_dispatcher)) { - return NULL; - } - if(!easy_flipper_set_widget( - &app->widget_file_delete, - WebCrawlerViewFileDelete, - "File deleted.", - web_crawler_back_to_file_callback, - &app->view_dispatcher)) { - return NULL; - } - if(!easy_flipper_set_widget( - &app->widget_result, - WebCrawlerViewWidgetResult, - "Error, try again.", - web_crawler_back_to_main_callback, - &app->view_dispatcher)) { + if (!easy_flipper_set_widget(&app->widget_result, WebCrawlerViewWidgetResult, "Error, try again.", web_crawler_back_to_main_callback, &app->view_dispatcher)) + { return NULL; } - // Load Settings and Update Views - if(!load_settings( - app->path, - app->temp_buffer_size_path, - app->ssid, - app->temp_buffer_size_ssid, - app->password, - app->temp_buffer_size_password, - app->file_rename, - app->temp_buffer_size_file_rename, - app->file_type, - app->temp_buffer_size_file_type, - app->http_method, - app->temp_buffer_size_http_method, - app->headers, - app->temp_buffer_size_headers, - app->payload, - app->temp_buffer_size_payload, - app)) { - FURI_LOG_E(TAG, "Failed to load settings"); - } else { - // Update the configuration items based on loaded settings - if(app->path_item) { - variable_item_set_current_value_text(app->path_item, app->path); - } else { - variable_item_set_current_value_text( - app->path_item, "https://httpbin.org/get"); // Initialize - } - - if(app->ssid_item) { - variable_item_set_current_value_text(app->ssid_item, app->ssid); - } else { - variable_item_set_current_value_text(app->ssid_item, ""); // Initialize - } - - if(app->file_type_item) { - variable_item_set_current_value_text(app->file_type_item, app->file_type); - } else { - variable_item_set_current_value_text(app->file_type_item, ".txt"); // Initialize - } - - if(app->file_rename_item) { - variable_item_set_current_value_text(app->file_rename_item, app->file_rename); - } else { - variable_item_set_current_value_text( - app->file_rename_item, "received_data"); // Initialize - } - - if(app->http_method_item) { - variable_item_set_current_value_text(app->http_method_item, app->http_method); - } else { - variable_item_set_current_value_text(app->http_method_item, "GET"); // Initialize - } - - if(app->headers_item) { - variable_item_set_current_value_text(app->headers_item, app->headers); - } else { - variable_item_set_current_value_text( - app->headers_item, "{\n\t\"Content-Type\": \"application/json\"\n}"); // Initialize - } - - if(app->payload_item) { - variable_item_set_current_value_text(app->payload_item, app->payload); - } else { - variable_item_set_current_value_text( - app->payload_item, "{\n\t\"key\": \"value\"\n}"); // Initialize - } - - // set the file path for fhttp.file_path - char file_path[128]; - snprintf( - file_path, - sizeof(file_path), - "%s%s%s", - RECEIVED_DATA_PATH, - app->file_rename, - app->file_type); - snprintf(fhttp.file_path, sizeof(fhttp.file_path), "%s", file_path); - - // update temp buffers - strncpy(app->temp_buffer_path, app->path, app->temp_buffer_size_path - 1); - app->temp_buffer_path[app->temp_buffer_size_path - 1] = '\0'; - strncpy(app->temp_buffer_ssid, app->ssid, app->temp_buffer_size_ssid - 1); - app->temp_buffer_ssid[app->temp_buffer_size_ssid - 1] = '\0'; - strncpy(app->temp_buffer_file_type, app->file_type, app->temp_buffer_size_file_type - 1); - app->temp_buffer_file_type[app->temp_buffer_size_file_type - 1] = '\0'; - strncpy( - app->temp_buffer_file_rename, app->file_rename, app->temp_buffer_size_file_rename - 1); - app->temp_buffer_file_rename[app->temp_buffer_size_file_rename - 1] = '\0'; - strncpy( - app->temp_buffer_http_method, app->http_method, app->temp_buffer_size_http_method - 1); - app->temp_buffer_http_method[app->temp_buffer_size_http_method - 1] = '\0'; - strncpy(app->temp_buffer_headers, app->headers, app->temp_buffer_size_headers - 1); - app->temp_buffer_headers[app->temp_buffer_size_headers - 1] = '\0'; - strncpy(app->temp_buffer_payload, app->payload, app->temp_buffer_size_payload - 1); - app->temp_buffer_payload[app->temp_buffer_size_payload - 1] = '\0'; - - // Password handling can be omitted for security or handled securely - } - // Start with the Submenu view view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewSubmenuMain); diff --git a/web_crawler/alloc/web_crawler_alloc.h b/web_crawler/alloc/web_crawler_alloc.h index 54b41d450..1dbf58911 100644 --- a/web_crawler/alloc/web_crawler_alloc.h +++ b/web_crawler/alloc/web_crawler_alloc.h @@ -9,6 +9,6 @@ * @brief Function to allocate resources for the WebCrawlerApp. * @return Pointer to the initialized WebCrawlerApp, or NULL on failure. */ -WebCrawlerApp* web_crawler_app_alloc(); +WebCrawlerApp *web_crawler_app_alloc(); #endif // WEB_CRAWLER_I_H diff --git a/web_crawler/app.c b/web_crawler/app.c index 82aa80b3f..9d627831b 100644 --- a/web_crawler/app.c +++ b/web_crawler/app.c @@ -5,52 +5,51 @@ * @param p Input parameter - unused * @return 0 to indicate success, -1 on failure */ -int32_t web_crawler_app(void* p) { +int32_t web_crawler_app(void *p) +{ UNUSED(p); - app_instance = web_crawler_app_alloc(); - if(!app_instance) { + WebCrawlerApp *app = web_crawler_app_alloc(); + if (!app) + { FURI_LOG_E(TAG, "Failed to allocate WebCrawlerApp"); return -1; } - if(!flipper_http_ping()) { + // check if board is connected (Derek Jamison) + FlipperHTTP *fhttp = flipper_http_alloc(); + if (!fhttp) + { + easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero."); + return -1; + } + + if (!flipper_http_ping(fhttp)) + { FURI_LOG_E(TAG, "Failed to ping the device"); + flipper_http_free(fhttp); return -1; } - // Edit from Derek Jamison - if(app_instance->text_input_ssid != NULL && app_instance->text_input_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); - } + // Try to wait for pong response. + uint32_t counter = 10; + while (fhttp->state == INACTIVE && --counter > 0) + { + FURI_LOG_D(TAG, "Waiting for PONG"); + furi_delay_ms(100); // this causes a BusFault + } + + flipper_http_free(fhttp); + if (counter == 0) + { + easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed."); } // Run the application - view_dispatcher_run(app_instance->view_dispatcher); + view_dispatcher_run(app->view_dispatcher); // Free resources after the application loop ends - web_crawler_app_free(app_instance); + web_crawler_app_free(app); return 0; -} +} \ No newline at end of file diff --git a/web_crawler/application.fam b/web_crawler/application.fam index 06cd9cbeb..dbab5509e 100644 --- a/web_crawler/application.fam +++ b/web_crawler/application.fam @@ -7,8 +7,8 @@ App( fap_icon="app.png", fap_category="GPIO/FlipperHTTP", fap_icon_assets="assets", - fap_description="Use Wi-Fi to access the internet and scrape data from the web.", + fap_description="Browse the web, fetch API data, and more.", fap_author="JBlanked", fap_weburl="https://github.com/jblanked/WebCrawler-FlipperZero", - fap_version="0.8", + fap_version="1.0.1", ) diff --git a/web_crawler/assets/02-main.png b/web_crawler/assets/02-main.png index 0c332b1ed..60f6df900 100644 Binary files a/web_crawler/assets/02-main.png and b/web_crawler/assets/02-main.png differ diff --git a/web_crawler/callback/web_crawler_callback.c b/web_crawler/callback/web_crawler_callback.c index b113a70cf..d10de38fa 100644 --- a/web_crawler/callback/web_crawler_callback.c +++ b/web_crawler/callback/web_crawler_callback.c @@ -1,29 +1,542 @@ #include +#include +#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 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 uint32_t web_crawler_back_to_file_callback(void *context); +static bool alloc_widget(WebCrawlerApp *app, uint32_t view) +{ + furi_check(app, "alloc_widget: app is NULL"); + if (!app->widget) + { + switch (view) + { + case WebCrawlerViewAbout: + return easy_flipper_set_widget(&app->widget, WebCrawlerViewWidget, "Web Crawler App\n---\nBrowse the web, fetch API data, and more..\n---\nVisit github.com/jblanked for more details.\n---\nPress BACK to return.", web_crawler_back_to_main_callback, &app->view_dispatcher); + case WebCrawlerViewFileRead: + return easy_flipper_set_widget(&app->widget, WebCrawlerViewWidget, "Data will be displayed here.", web_crawler_back_to_file_callback, &app->view_dispatcher); + case WebCrawlerViewFileDelete: + return easy_flipper_set_widget(&app->widget, WebCrawlerViewWidget, "File deleted.", web_crawler_back_to_file_callback, &app->view_dispatcher); + } + } + return false; +} +static void free_widget(WebCrawlerApp *app) +{ + if (app->widget) + { + view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewWidget); + free(app->widget); + app->widget = NULL; + } +} +static bool alloc_submenu_config(WebCrawlerApp *app) +{ + furi_check(app, "alloc_submenu_config: WebCrawlerApp is NULL"); + if (app->submenu_config) + { + FURI_LOG_E(TAG, "alloc_submenu_config: Submenu already allocated"); + return false; + } + if (easy_flipper_set_submenu(&app->submenu_config, WebCrawlerViewSubmenuConfig, "Settings", web_crawler_back_to_main_callback, &app->view_dispatcher)) + { + submenu_add_item(app->submenu_config, "WiFi", WebCrawlerSubmenuIndexWifi, web_crawler_submenu_callback, app); + submenu_add_item(app->submenu_config, "File", WebCrawlerSubmenuIndexFile, web_crawler_submenu_callback, app); + submenu_add_item(app->submenu_config, "Request", WebCrawlerSubmenuIndexRequest, web_crawler_submenu_callback, app); + return true; + } + return false; +} +static void free_submenu_config(WebCrawlerApp *app) +{ + furi_check(app, "free_submenu_config: WebCrawlerApp is NULL"); + if (app->submenu_config) + { + view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewSubmenuConfig); + submenu_free(app->submenu_config); + app->submenu_config = NULL; + } +} -bool sent_http_request = false; -bool get_success = false; -bool already_success = false; +static bool alloc_variable_item_list(WebCrawlerApp *app, uint32_t view) +{ + furi_check(app, "alloc_variable_item_list: WebCrawlerApp is NULL"); + if (app->variable_item_list) + { + FURI_LOG_E(TAG, "Variable Item List already allocated"); + return false; + } + bool settings_loaded = true; + // load settings + char path[128]; + char ssid[64]; + char password[64]; + char file_rename[128]; + char file_type[16]; + char http_method[16]; + char headers[256]; + char payload[256]; + if (!load_settings(path, 128, ssid, 64, password, 64, file_rename, 128, file_type, 16, http_method, 16, headers, 256, payload, 256, app)) + { + FURI_LOG_E(TAG, "Failed to load settings"); + settings_loaded = false; + } + switch (view) + { + case WebCrawlerViewVariableItemListWifi: + if (!easy_flipper_set_variable_item_list(&app->variable_item_list, WebCrawlerViewVariableItemList, web_crawler_wifi_enter_callback, web_crawler_back_to_configure_callback, &app->view_dispatcher, app)) + { + return false; + } + if (!app->ssid_item) + { + app->ssid_item = variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL); // index 0 + variable_item_set_current_value_text(app->ssid_item, ""); // Initialize + } + if (!app->password_item) + { + app->password_item = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL); // index 1 + variable_item_set_current_value_text(app->password_item, ""); // Initialize + } + if (settings_loaded) + { + variable_item_set_current_value_text(app->ssid_item, ssid); + // variable_item_set_current_value_text(app->password_item, password); + } + else + { + variable_item_set_current_value_text(app->ssid_item, ""); // Initialize + // variable_item_set_current_value_text(app->password_item, "wifi-Password"); // Initialize -static void web_crawler_draw_error(Canvas* canvas) { - if(!canvas) { - FURI_LOG_E(TAG, "Canvas is NULL"); - return; + snprintf(ssid, 64, "%s", "wifi-SSID"); + snprintf(password, 64, "%s", "wifi-Password"); + } + // save for updating temp buffers later + save_char("wifi-ssid", ssid); + save_char("wifi-password", password); + // strncpy(app->temp_buffer_ssid, app->ssid, app->temp_buffer_size_ssid - 1); + // app->temp_buffer_ssid[app->temp_buffer_size_ssid - 1] = '\0'; + // strncpy(app->temp_buffer_password, app->password, app->temp_buffer_size_password - 1); + // app->temp_buffer_password[app->temp_buffer_size_password - 1] = '\0'; + break; + case WebCrawlerViewVariableItemListFile: + if (!easy_flipper_set_variable_item_list(&app->variable_item_list, WebCrawlerViewVariableItemList, web_crawler_file_enter_callback, web_crawler_back_to_configure_callback, &app->view_dispatcher, app)) + { + return false; + } + if (!app->file_read_item) + { + app->file_read_item = variable_item_list_add(app->variable_item_list, "Read File", 0, NULL, NULL); // index 0 + variable_item_set_current_value_text(app->file_read_item, ""); // Initialize + } + if (!app->file_type_item) + { + app->file_type_item = variable_item_list_add(app->variable_item_list, "Set File Type", 0, NULL, NULL); // index 1 + variable_item_set_current_value_text(app->file_type_item, ""); // Initialize + } + if (!app->file_rename_item) + { + app->file_rename_item = variable_item_list_add(app->variable_item_list, "Rename File", 0, NULL, NULL); // index 2 + variable_item_set_current_value_text(app->file_rename_item, ""); // Initialize + } + if (!app->file_delete_item) + { + app->file_delete_item = variable_item_list_add(app->variable_item_list, "Delete File", 0, NULL, NULL); // index 3 + variable_item_set_current_value_text(app->file_delete_item, ""); // Initialize + } + if (settings_loaded) + { + variable_item_set_current_value_text(app->file_type_item, file_type); + variable_item_set_current_value_text(app->file_rename_item, file_rename); + } + else + { + variable_item_set_current_value_text(app->file_type_item, ".txt"); // Initialize + variable_item_set_current_value_text(app->file_rename_item, "received_data"); // Initialize + + snprintf(file_type, 16, "%s", ".txt"); + snprintf(file_rename, 128, "%s", "received_data"); + } + // save for updating temp buffers later + save_char("file_type", file_type); + save_char("file_rename", file_rename); + // strncpy(app->temp_buffer_file_type, app->file_type, app->temp_buffer_size_file_type - 1); + // app->temp_buffer_file_type[app->temp_buffer_size_file_type - 1] = '\0'; + // strncpy(app->temp_buffer_file_rename, app->file_rename, app->temp_buffer_size_file_rename - 1); + // app->temp_buffer_file_rename[app->temp_buffer_size_file_rename - 1] = '\0'; + break; + case WebCrawlerViewVariableItemListRequest: + if (!easy_flipper_set_variable_item_list(&app->variable_item_list, WebCrawlerViewVariableItemList, web_crawler_request_enter_callback, web_crawler_back_to_configure_callback, &app->view_dispatcher, app)) + { + return false; + } + if (!app->path_item) + { + app->path_item = variable_item_list_add(app->variable_item_list, "Path", 0, NULL, NULL); + variable_item_set_current_value_text(app->path_item, ""); // Initialize + } + if (!app->http_method_item) + { + app->http_method_item = variable_item_list_add(app->variable_item_list, "HTTP Method", 6, web_crawler_http_method_change, app); + variable_item_set_current_value_text(app->http_method_item, ""); // Initialize + variable_item_set_current_value_index(app->http_method_item, 0); // Initialize + } + if (!app->headers_item) + { + app->headers_item = variable_item_list_add(app->variable_item_list, "Headers", 0, NULL, NULL); + variable_item_set_current_value_text(app->headers_item, ""); // Initialize + } + if (!app->payload_item) + { + app->payload_item = variable_item_list_add(app->variable_item_list, "Payload", 0, NULL, NULL); + variable_item_set_current_value_text(app->payload_item, ""); // Initialize + } + // + // + if (settings_loaded) + { + variable_item_set_current_value_text(app->path_item, path); + variable_item_set_current_value_text(app->http_method_item, http_method); + variable_item_set_current_value_text(app->headers_item, headers); + variable_item_set_current_value_text(app->payload_item, payload); + // + variable_item_set_current_value_index( + app->http_method_item, + strstr(http_method, "GET") != NULL ? 0 : strstr(http_method, "POST") != NULL ? 1 + : strstr(http_method, "PUT") != NULL ? 2 + : strstr(http_method, "DELETE") != NULL ? 3 + : strstr(http_method, "DOWNLOAD") != NULL ? 4 + : strstr(http_method, "BROWSE") != NULL ? 5 + : 0); + } + else + { + variable_item_set_current_value_text(app->path_item, "https://httpbin.org/get"); // Initialize + variable_item_set_current_value_text(app->http_method_item, "GET"); // Initialize + variable_item_set_current_value_text(app->headers_item, "{\"Content-Type\": \"application/json\"}"); // Initialize + variable_item_set_current_value_text(app->payload_item, "{\"key\": \"value\"}"); // Initialize + + snprintf(path, 128, "%s", "https://httpbin.org/get"); + snprintf(http_method, 16, "%s", "GET"); + snprintf(headers, 256, "%s", "{\"Content-Type\": \"application/json\"}"); + snprintf(payload, 256, "%s", "{\"key\": \"value\"}"); + } + // save for updating temp buffers later + save_char("path", path); + save_char("http_method", http_method); + save_char("headers", headers); + save_char("payload", payload); + // strncpy(app->temp_buffer_path, app->path, app->temp_buffer_size_path - 1); + // app->temp_buffer_path[app->temp_buffer_size_path - 1] = '\0'; + // strncpy(app->temp_buffer_http_method, app->http_method, app->temp_buffer_size_http_method - 1); + // app->temp_buffer_http_method[app->temp_buffer_size_http_method - 1] = '\0'; + // strncpy(app->temp_buffer_headers, app->headers, app->temp_buffer_size_headers - 1); + // app->temp_buffer_headers[app->temp_buffer_size_headers - 1] = '\0'; + // strncpy(app->temp_buffer_payload, app->payload, app->temp_buffer_size_payload - 1); + // app->temp_buffer_payload[app->temp_buffer_size_payload - 1] = '\0'; + break; + default: + FURI_LOG_E(TAG, "Invalid view"); + return false; + } + return true; +} +static void free_variable_item_list(WebCrawlerApp *app) +{ + furi_check(app, "free_variable_item_list: WebCrawlerApp is NULL"); + if (app->variable_item_list) + { + view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewVariableItemList); + variable_item_list_free(app->variable_item_list); + app->variable_item_list = NULL; + } + // check and free variable items + if (app->ssid_item) + { + free(app->ssid_item); + app->ssid_item = NULL; + } + if (app->password_item) + { + free(app->password_item); + app->password_item = NULL; + } + if (app->file_type_item) + { + free(app->file_type_item); + app->file_type_item = NULL; + } + if (app->file_rename_item) + { + free(app->file_rename_item); + app->file_rename_item = NULL; + } + if (app->file_read_item) + { + free(app->file_read_item); + app->file_read_item = NULL; + } + if (app->file_delete_item) + { + free(app->file_delete_item); + app->file_delete_item = NULL; + } + if (app->path_item) + { + free(app->path_item); + app->path_item = NULL; + } + if (app->http_method_item) + { + free(app->http_method_item); + app->http_method_item = NULL; + } + if (app->headers_item) + { + free(app->headers_item); + app->headers_item = NULL; + } + if (app->payload_item) + { + free(app->payload_item); + app->payload_item = NULL; } +} +static bool alloc_text_input(WebCrawlerApp *app, uint32_t view) +{ + furi_check(app, "alloc_text_input: WebCrawlerApp is NULL"); + if (app->uart_text_input) + { + FURI_LOG_E(TAG, "Text Input already allocated"); + return false; + } + switch (view) + { + case WebCrawlerViewTextInput: + app->temp_buffer_size_path = 128; + if (!easy_flipper_set_buffer(&app->temp_buffer_path, app->temp_buffer_size_path) || !easy_flipper_set_buffer(&app->path, app->temp_buffer_size_path)) + { + return false; + } + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, WebCrawlerViewInput, "Enter URL", app->temp_buffer_path, app->temp_buffer_size_path, web_crawler_set_path_updated, web_crawler_back_to_request_callback, &app->view_dispatcher, app)) + { + return false; + } + if (load_char("path", app->path, app->temp_buffer_size_path)) + { + snprintf(app->temp_buffer_path, app->temp_buffer_size_path, "%s", app->path); + } + break; + case WebCrawlerViewTextInputSSID: + app->temp_buffer_size_ssid = 64; + if (!easy_flipper_set_buffer(&app->temp_buffer_ssid, app->temp_buffer_size_ssid) || !easy_flipper_set_buffer(&app->ssid, app->temp_buffer_size_ssid)) + { + return false; + } + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, WebCrawlerViewInput, "Enter SSID", app->temp_buffer_ssid, app->temp_buffer_size_ssid, web_crawler_set_ssid_updated, web_crawler_back_to_wifi_callback, &app->view_dispatcher, app)) + { + return false; + } + if (load_char("wifi-ssid", app->ssid, app->temp_buffer_size_ssid)) + { + snprintf(app->temp_buffer_ssid, app->temp_buffer_size_ssid, "%s", app->ssid); + } + break; + case WebCrawlerViewTextInputPassword: + app->temp_buffer_size_password = 64; + if (!easy_flipper_set_buffer(&app->temp_buffer_password, app->temp_buffer_size_password) || !easy_flipper_set_buffer(&app->password, app->temp_buffer_size_password)) + { + return false; + } + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, WebCrawlerViewInput, "Enter Password", app->temp_buffer_password, app->temp_buffer_size_password, web_crawler_set_password_update, web_crawler_back_to_wifi_callback, &app->view_dispatcher, app)) + { + return false; + } + if (load_char("wifi-password", app->password, app->temp_buffer_size_password)) + { + snprintf(app->temp_buffer_password, app->temp_buffer_size_password, "%s", app->password); + } + break; + case WebCrawlerViewTextInputFileType: + app->temp_buffer_size_file_type = 16; + if (!easy_flipper_set_buffer(&app->temp_buffer_file_type, app->temp_buffer_size_file_type) || !easy_flipper_set_buffer(&app->file_type, app->temp_buffer_size_file_type)) + { + return false; + } + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, WebCrawlerViewInput, "Enter File Type", app->temp_buffer_file_type, app->temp_buffer_size_file_type, web_crawler_set_file_type_update, web_crawler_back_to_file_callback, &app->view_dispatcher, app)) + { + return false; + } + if (load_char("file_type", app->file_type, app->temp_buffer_size_file_type)) + { + snprintf(app->temp_buffer_file_type, app->temp_buffer_size_file_type, "%s", app->file_type); + } + break; + case WebCrawlerViewTextInputFileRename: + app->temp_buffer_size_file_rename = 128; + if (!easy_flipper_set_buffer(&app->temp_buffer_file_rename, app->temp_buffer_size_file_rename) || !easy_flipper_set_buffer(&app->file_rename, app->temp_buffer_size_file_rename)) + { + return false; + } + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, WebCrawlerViewInput, "Enter File Rename", app->temp_buffer_file_rename, app->temp_buffer_size_file_rename, web_crawler_set_file_rename_update, web_crawler_back_to_file_callback, &app->view_dispatcher, app)) + { + return false; + } + if (load_char("file_rename", app->file_rename, app->temp_buffer_size_file_rename)) + { + snprintf(app->temp_buffer_file_rename, app->temp_buffer_size_file_rename, "%s", app->file_rename); + } + break; + case WebCrawlerViewTextInputHeaders: + app->temp_buffer_size_headers = 256; + if (!easy_flipper_set_buffer(&app->temp_buffer_headers, app->temp_buffer_size_headers) || !easy_flipper_set_buffer(&app->headers, app->temp_buffer_size_headers)) + { + return false; + } + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, WebCrawlerViewInput, "Enter Headers", app->temp_buffer_headers, app->temp_buffer_size_headers, web_crawler_set_headers_updated, web_crawler_back_to_request_callback, &app->view_dispatcher, app)) + { + return false; + } + if (load_char("headers", app->headers, app->temp_buffer_size_headers)) + { + snprintf(app->temp_buffer_headers, app->temp_buffer_size_headers, "%s", app->headers); + } + break; + case WebCrawlerViewTextInputPayload: + app->temp_buffer_size_payload = 256; + if (!easy_flipper_set_buffer(&app->temp_buffer_payload, app->temp_buffer_size_payload) || !easy_flipper_set_buffer(&app->payload, app->temp_buffer_size_payload)) + { + return false; + } + if (!easy_flipper_set_uart_text_input(&app->uart_text_input, WebCrawlerViewInput, "Enter Payload", app->temp_buffer_payload, app->temp_buffer_size_payload, web_crawler_set_payload_updated, web_crawler_back_to_request_callback, &app->view_dispatcher, app)) + { + return false; + } + if (load_char("payload", app->payload, app->temp_buffer_size_payload)) + { + snprintf(app->temp_buffer_payload, app->temp_buffer_size_payload, "%s", app->payload); + } + break; + default: + FURI_LOG_E(TAG, "Invalid view"); + return false; + } + return true; +} +static void free_text_input(WebCrawlerApp *app) +{ + furi_check(app, "free_text_input: WebCrawlerApp is NULL"); + if (app->uart_text_input) + { + view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewInput); + text_input_free(app->uart_text_input); + app->uart_text_input = NULL; + } + // check and free path + if (app->temp_buffer_path) + { + free(app->temp_buffer_path); + app->temp_buffer_path = NULL; + } + if (app->path) + { + free(app->path); + app->path = NULL; + } + // check and free ssid + if (app->temp_buffer_ssid) + { + free(app->temp_buffer_ssid); + app->temp_buffer_ssid = NULL; + } + if (app->ssid) + { + free(app->ssid); + app->ssid = NULL; + } + // check and free password + if (app->temp_buffer_password) + { + free(app->temp_buffer_password); + app->temp_buffer_password = NULL; + } + if (app->password) + { + free(app->password); + app->password = NULL; + } + // check and free file type + if (app->temp_buffer_file_type) + { + free(app->temp_buffer_file_type); + app->temp_buffer_file_type = NULL; + } + if (app->file_type) + { + free(app->file_type); + app->file_type = NULL; + } + // check and free file rename + if (app->temp_buffer_file_rename) + { + free(app->temp_buffer_file_rename); + app->temp_buffer_file_rename = NULL; + } + if (app->file_rename) + { + free(app->file_rename); + app->file_rename = NULL; + } + // check and free headers + if (app->temp_buffer_headers) + { + free(app->temp_buffer_headers); + app->temp_buffer_headers = NULL; + } + if (app->headers) + { + free(app->headers); + app->headers = NULL; + } + // check and free payload + if (app->temp_buffer_payload) + { + free(app->temp_buffer_payload); + app->temp_buffer_payload = NULL; + } + if (app->payload) + { + free(app->payload); + app->payload = NULL; + } +} + +void free_all(WebCrawlerApp *app) +{ + furi_check(app, "free_all: app is NULL"); + free_widget(app); + free_submenu_config(app); + free_variable_item_list(app); + free_text_input(app); +} +static void web_crawler_draw_error(Canvas *canvas, DataLoaderModel *model) +{ + furi_check(model, "web_crawler_draw_error: DataLoaderModel is NULL"); + furi_check(model->fhttp, "web_crawler_draw_error: FlipperHTTP is NULL"); + furi_check(canvas, "Canvas is NULL"); canvas_clear(canvas); canvas_set_font(canvas, FontSecondary); - if(fhttp.state == INACTIVE) { + if (model->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,"); @@ -33,33 +546,45 @@ static void web_crawler_draw_error(Canvas* canvas) { return; } - if(fhttp.last_response) { - if(strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != - NULL) { + if (model->fhttp->last_response) + { + if (strstr(model->fhttp->last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL) + { 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."); return; } - if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) { + if (strstr(model->fhttp->last_response, "[ERROR] Failed to connect to Wifi.") != NULL) + { 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."); return; } - if(strstr( - fhttp.last_response, "[ERROR] GET request failed with error: connection refused") != - NULL) { + if (strstr(model->fhttp->last_response, "request failed with error: connection refused") != NULL) + { canvas_draw_str(canvas, 0, 10, "[ERROR] Connection refused."); canvas_draw_str(canvas, 0, 50, "Choose another URL."); canvas_draw_str(canvas, 0, 60, "Press BACK to return."); return; } - if(strstr(fhttp.last_response, "[PONG]") != NULL) { + if (strstr(model->fhttp->last_response, "[PONG]") != NULL) + { canvas_clear(canvas); canvas_draw_str(canvas, 0, 10, "[STATUS]Connecting to AP..."); return; } + // handle failed requests + if (strstr(model->fhttp->last_response, "request failed or returned empty data.") != NULL) + { + canvas_draw_str(canvas, 0, 10, "[ERROR] Request failed."); + canvas_draw_str(canvas, 0, 50, "If this is your third attempt,"); + canvas_draw_str(canvas, 0, 60, "it's likely your URL is not"); + canvas_draw_str(canvas, 0, 70, "compabilbe or correct."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + return; + } canvas_draw_str(canvas, 0, 10, "[ERROR] Failed to sync data."); canvas_draw_str(canvas, 0, 30, "If this is your third attempt,"); @@ -73,116 +598,315 @@ static void web_crawler_draw_error(Canvas* canvas) { canvas_draw_str(canvas, 0, 20, "Press BACK to return."); } -void web_crawler_http_method_change(VariableItem* item) { +static void save_simply() +{ + char path[128]; + char ssid[64]; + char password[64]; + char file_rename[128]; + char file_type[16]; + char http_method[16]; + char headers[256]; + char payload[256]; + + if (!load_char("path", path, 128)) + { + snprintf(path, 128, "%s", "https://httpbin.org/get"); + } + if (!load_char("wifi-ssid", ssid, 64)) + { + snprintf(ssid, 64, "%s", "WIFI-SSID"); + } + if (!load_char("wifi-password", password, 64)) + { + snprintf(password, 64, "%s", "wifi-Password"); + } + if (!load_char("file_rename", file_rename, 128)) + { + snprintf(file_rename, 128, "%s", "received_data"); + } + if (!load_char("file_type", file_type, 16)) + { + snprintf(file_type, 16, "%s", ".txt"); + } + if (!load_char("http_method", http_method, 16)) + { + snprintf(http_method, 16, "%s", "GET"); + } + if (!load_char("headers", headers, 256)) + { + snprintf(headers, 256, "%s", "{\"Content-Type\": \"application/json\"}"); + } + if (!load_char("payload", payload, 256)) + { + snprintf(payload, 256, "%s", "{\"key\": \"value\"}"); + } + + save_settings(path, ssid, password, file_rename, file_type, http_method, headers, payload); +} + +void web_crawler_http_method_change(VariableItem *item) +{ + WebCrawlerApp *app = (WebCrawlerApp *)variable_item_get_context(item); + furi_check(app, "web_crawler_http_method_change: WebCrawlerApp is NULL"); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, http_method_names[index]); + variable_item_set_current_value_index(item, index); // save the http method - if(app_instance) { - strncpy( - app_instance->http_method, - http_method_names[index], - strlen(http_method_names[index]) + 1); - - // save the settings - save_settings( - app_instance->path, - app_instance->ssid, - app_instance->password, - app_instance->file_rename, - app_instance->file_type, - app_instance->http_method, - app_instance->headers, - app_instance->payload); - } + save_char("http_method", http_method_names[index]); + save_simply(); } -static bool web_crawler_fetch(DataLoaderModel* model) { - UNUSED(model); - if(app_instance->file_type && app_instance->file_rename) { +static bool web_crawler_fetch(DataLoaderModel *model) +{ + WebCrawlerApp *app = (WebCrawlerApp *)model->parser_context; + furi_check(app, "web_crawler_fetch: WebCrawlerApp is NULL"); + furi_check(model->fhttp, "web_crawler_fetch: FlipperHTTP is NULL"); + char url[128]; + if (!load_char("path", url, 128)) + { + easy_flipper_dialog("Error", "Failed to load URL.\nGo into Settings -> Request\n and enter a Path."); + return false; + } + char file_type[16]; + if (!load_char("file_type", file_type, 16)) + { + easy_flipper_dialog("Error", "Failed to load file type.\nGo into settings and\nre-save the file type."); + return false; + } + char file_rename[128]; + if (!load_char("file_rename", file_rename, 128)) + { + easy_flipper_dialog("Error", "Failed to load file rename.\nGo into Settings -> File\n and Rename the file."); + return false; + } + char http_method[16]; + if (!load_char("http_method", http_method, 16)) + { + easy_flipper_dialog("Error", "Failed to load http method.\nGo into Settings -> Request\n and select an HTTP Method."); + return false; + } + char headers[256]; + if (!load_char("headers", headers, 256)) + { + easy_flipper_dialog("Error", "Failed to load headers.\nGo into Settings -> Request\n and add Headers."); + return false; + } + char payload[256]; + if (!load_char("payload", payload, 256)) + { + easy_flipper_dialog("Error", "Failed to load payload.\nGo into Settings -> Request\n and add a Payload."); + return false; + } + + if (strlen(file_rename) > 0 && strlen(file_type) > 0) + { snprintf( - fhttp.file_path, - sizeof(fhttp.file_path), + model->fhttp->file_path, + sizeof(model->fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler/%s%s", - app_instance->file_rename, - app_instance->file_type); - } else { + file_rename, + file_type); + } + else + { snprintf( - fhttp.file_path, - sizeof(fhttp.file_path), + model->fhttp->file_path, + sizeof(model->fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler/received_data.txt"); } - if(strstr(app_instance->http_method, "GET") != NULL) { - fhttp.save_received_data = true; - fhttp.is_bytes_request = false; + if (strstr(http_method, "GET") != NULL) + { + model->fhttp->save_received_data = true; + model->fhttp->is_bytes_request = false; // Perform GET request and handle the response - if(app_instance->headers == NULL || app_instance->headers[0] == '\0' || - strstr(app_instance->headers, " ") == NULL) { - get_success = flipper_http_get_request(app_instance->path); - } else { - get_success = - flipper_http_get_request_with_headers(app_instance->path, app_instance->headers); + if (strlen(headers) == 0) + { + return flipper_http_get_request(model->fhttp, url); } - } else if(strstr(app_instance->http_method, "POST") != NULL) { - fhttp.save_received_data = true; - fhttp.is_bytes_request = false; + else + { + return flipper_http_get_request_with_headers(model->fhttp, url, headers); + } + } + else if (strstr(http_method, "POST") != NULL) + { + model->fhttp->save_received_data = true; + model->fhttp->is_bytes_request = false; // Perform POST request and handle the response - get_success = flipper_http_post_request_with_headers( - app_instance->path, app_instance->headers, app_instance->payload); - } else if(strstr(app_instance->http_method, "PUT") != NULL) { - fhttp.save_received_data = true; - fhttp.is_bytes_request = false; + return flipper_http_post_request_with_headers(model->fhttp, url, headers, payload); + } + else if (strstr(http_method, "PUT") != NULL) + { + model->fhttp->save_received_data = true; + model->fhttp->is_bytes_request = false; // Perform PUT request and handle the response - get_success = flipper_http_put_request_with_headers( - app_instance->path, app_instance->headers, app_instance->payload); - } else if(strstr(app_instance->http_method, "DELETE") != NULL) { - fhttp.save_received_data = true; - fhttp.is_bytes_request = false; + return flipper_http_put_request_with_headers(model->fhttp, url, headers, payload); + } + else if (strstr(http_method, "DELETE") != NULL) + { + model->fhttp->save_received_data = true; + model->fhttp->is_bytes_request = false; // Perform DELETE request and handle the response - get_success = flipper_http_delete_request_with_headers( - app_instance->path, app_instance->headers, app_instance->payload); - } else { - fhttp.save_received_data = false; - fhttp.is_bytes_request = true; + return flipper_http_delete_request_with_headers(model->fhttp, url, headers, payload); + } + else if (strstr(http_method, "DOWNLOAD") != NULL) + { + model->fhttp->save_received_data = false; + model->fhttp->is_bytes_request = true; // Perform GET request and handle the response - get_success = flipper_http_get_request_bytes(app_instance->path, app_instance->headers); + return flipper_http_get_request_bytes(model->fhttp, url, "{\"Content-Type\": \"application/octet-stream\"}"); + } + else // BROWSE + { + model->fhttp->save_received_data = false; + model->fhttp->is_bytes_request = true; + + // download HTML response since the html could be large + return flipper_http_get_request_bytes(model->fhttp, url, "{\"Content-Type\": \"application/octet-stream\"}"); } - return get_success; + return false; } -static char* web_crawler_parse(DataLoaderModel* model) { +static char *web_crawler_parse(DataLoaderModel *model) +{ UNUSED(model); - // there is no parsing since everything is saved to file - return "Data saved to file.\nPress BACK to return."; + // parse HTML response if BROWSE request + char http_method[16]; + if (!load_char("http_method", http_method, 16)) + { + FURI_LOG_E(TAG, "Failed to load http method"); + } + else + { + if (strstr(http_method, "BROWSE") != NULL) + { + // parse HTML then return response + FuriString *returned_data = flipper_http_load_from_file(model->fhttp->file_path); + if (returned_data == NULL || furi_string_size(returned_data) == 0) + { + return "Failed to load HTML response.\n\n\n\n\nPress BACK to return."; + } + + // head is mandatory, + bool head_exists = html_furi_tag_exists("", returned_data, 0); + if (!head_exists) + { + FURI_LOG_E(TAG, "Invalid HTML response"); + return "Invalid HTML response.\n\n\n\n\nPress BACK to return."; + } + + // optional tags but we'll append them the response in order (title -> h1 -> h2 -> h3 -> p) + FuriString *response = furi_string_alloc(); + if (html_furi_tag_exists("", returned_data, 0)) + { + FuriString *title = html_furi_find_tag("<title>", returned_data, 0); + furi_string_trim(title); + furi_string_cat_str(response, "Title: "); + furi_string_cat(response, title); + furi_string_cat_str(response, "\n\n"); + furi_string_free(title); + } + if (html_furi_tag_exists("<h1>", returned_data, 0)) + { + FuriString *h1 = html_furi_find_tag("<h1>", returned_data, 0); + furi_string_trim(h1); + furi_string_cat(response, h1); + furi_string_cat_str(response, "\n\n"); + furi_string_free(h1); + } + if (html_furi_tag_exists("<h2>", returned_data, 0)) + { + FuriString *h2 = html_furi_find_tag("<h2>", returned_data, 0); + furi_string_trim(h2); + furi_string_cat(response, h2); + furi_string_cat_str(response, "\n"); + furi_string_free(h2); + } + if (html_furi_tag_exists("<h3>", returned_data, 0)) + { + FuriString *h3 = html_furi_find_tag("<h3>", returned_data, 0); + furi_string_trim(h3); + furi_string_cat(response, h3); + furi_string_cat_str(response, "\n"); + furi_string_free(h3); + } + if (html_furi_tag_exists("<p>", returned_data, 0)) + { + FuriString *p = html_furi_find_tags("<p>", returned_data); + furi_string_trim(p); + furi_string_cat(response, p); + furi_string_free(p); + } + furi_string_free(returned_data); + if (response && furi_string_size(response) > 0) + { + return (char *)furi_string_get_cstr(response); + } + return "No HTML tags found.\nTry another URL...\n\n\n\nPress BACK to return."; + } + } + return "Data saved to file.\n\n\n\n\nPress BACK to return."; } -static void web_crawler_data_switch_to_view(WebCrawlerApp* app) { - char* title = "GET Request"; - if(strstr(app_instance->http_method, "GET") != NULL) { - title = "GET Request"; - } else if(strstr(app_instance->http_method, "POST") != NULL) { - title = "POST Request"; - } else if(strstr(app_instance->http_method, "PUT") != NULL) { - title = "PUT Request"; - } else if(strstr(app_instance->http_method, "DELETE") != NULL) { - title = "DELETE Request"; - } else { - title = "File Download"; - } - web_crawler_generic_switch_to_view( - app, - title, - web_crawler_fetch, - web_crawler_parse, - 1, - web_crawler_back_to_main_callback, - WebCrawlerViewLoader); +static void web_crawler_data_switch_to_view(WebCrawlerApp *app) +{ + furi_check(app, "web_crawler_data_switch_to_view: WebCrawlerApp is NULL"); + + // Allocate title on the heap. + char *title = malloc(32); + if (title == NULL) + { + FURI_LOG_E(TAG, "Failed to allocate memory for title"); + return; // or handle the error as needed + } + + char http_method[16]; + if (!load_char("http_method", http_method, sizeof(http_method))) + { + FURI_LOG_E(TAG, "Failed to load http method"); + snprintf(title, 32, "Request"); + } + else + { + if (strstr(http_method, "GET") != NULL) + { + snprintf(title, 32, "GET Request"); + } + else if (strstr(http_method, "POST") != NULL) + { + snprintf(title, 32, "POST Request"); + } + else if (strstr(http_method, "PUT") != NULL) + { + snprintf(title, 32, "PUT Request"); + } + else if (strstr(http_method, "DELETE") != NULL) + { + snprintf(title, 32, "DELETE Request"); + } + else if (strstr(http_method, "DOWNLOAD") != NULL) + { + snprintf(title, 32, "File Download"); + } + else if (strstr(http_method, "BROWSE") != NULL) + { + snprintf(title, 32, "Browse URL"); + } + else + { + // Provide a default title if no known http method is found. + snprintf(title, 32, "Request"); + } + } + web_crawler_generic_switch_to_view(app, title, web_crawler_fetch, web_crawler_parse, 1, web_crawler_back_to_main_callback, WebCrawlerViewLoader); } /** @@ -190,12 +914,10 @@ static void web_crawler_data_switch_to_view(WebCrawlerApp* app) { * @param context The context - WebCrawlerApp object. * @return WebCrawlerViewSubmenu */ -uint32_t web_crawler_back_to_configure_callback(void* context) { - UNUSED(context); - // free file read widget if it exists - if(app_instance->widget_file_read) { - widget_reset(app_instance->widget_file_read); - } +uint32_t web_crawler_back_to_configure_callback(void *context) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "web_crawler_back_to_configure_callback: WebCrawlerApp is NULL"); return WebCrawlerViewSubmenuConfig; // Return to the configure screen } @@ -204,32 +926,29 @@ uint32_t web_crawler_back_to_configure_callback(void* context) { * @param context The context - WebCrawlerApp object. * @return WebCrawlerViewSubmenu */ -uint32_t web_crawler_back_to_main_callback(void* context) { - UNUSED(context); - // reset GET request flags - sent_http_request = false; - get_success = false; - already_success = false; - // free file read widget if it exists - if(app_instance->widget_file_read) { - widget_reset(app_instance->widget_file_read); - } +uint32_t web_crawler_back_to_main_callback(void *context) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "web_crawler_back_to_main_callback: WebCrawlerApp is NULL"); return WebCrawlerViewSubmenuMain; // Return to the main submenu } -uint32_t web_crawler_back_to_file_callback(void* context) { +static uint32_t web_crawler_back_to_file_callback(void *context) +{ UNUSED(context); - return WebCrawlerViewVariableItemListFile; // Return to the file submenu + return WebCrawlerViewVariableItemList; // Return to the file submenu } -uint32_t web_crawler_back_to_wifi_callback(void* context) { +uint32_t web_crawler_back_to_wifi_callback(void *context) +{ UNUSED(context); - return WebCrawlerViewVariableItemListWifi; // Return to the wifi submenu + return WebCrawlerViewVariableItemList; // Return to the wifi submenu } -uint32_t web_crawler_back_to_request_callback(void* context) { +uint32_t web_crawler_back_to_request_callback(void *context) +{ UNUSED(context); - return WebCrawlerViewVariableItemListRequest; // Return to the request submenu + return WebCrawlerViewVariableItemList; // Return to the request submenu } /** @@ -237,7 +956,8 @@ uint32_t web_crawler_back_to_request_callback(void* context) { * @param context The context - unused * @return VIEW_NONE to exit the app */ -uint32_t web_crawler_exit_app_callback(void* context) { +uint32_t web_crawler_exit_app_callback(void *context) +{ UNUSED(context); return VIEW_NONE; } @@ -247,32 +967,61 @@ uint32_t web_crawler_exit_app_callback(void* context) { * @param context The context - WebCrawlerApp object. * @param index The WebCrawlerSubmenuIndex item that was clicked. */ -void web_crawler_submenu_callback(void* context, uint32_t index) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - - if(app->view_dispatcher) { - switch(index) { +void web_crawler_submenu_callback(void *context, uint32_t index) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "WebCrawlerApp is NULL"); + if (app->view_dispatcher) + { + switch (index) + { case WebCrawlerSubmenuIndexRun: - sent_http_request = false; // Reset the flag web_crawler_data_switch_to_view(app); break; case WebCrawlerSubmenuIndexAbout: - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewAbout); + free_all(app); + if (!alloc_widget(app, WebCrawlerViewAbout)) + { + FURI_LOG_E(TAG, "Failed to allocate widget"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewWidget); break; case WebCrawlerSubmenuIndexConfig: + free_all(app); + if (!alloc_submenu_config(app)) + { + FURI_LOG_E(TAG, "Failed to allocate submenu"); + return; + } view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewSubmenuConfig); break; case WebCrawlerSubmenuIndexWifi: - view_dispatcher_switch_to_view( - app->view_dispatcher, WebCrawlerViewVariableItemListWifi); + free_variable_item_list(app); + if (!alloc_variable_item_list(app, WebCrawlerViewVariableItemListWifi)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemList); break; case WebCrawlerSubmenuIndexRequest: - view_dispatcher_switch_to_view( - app->view_dispatcher, WebCrawlerViewVariableItemListRequest); + free_variable_item_list(app); + if (!alloc_variable_item_list(app, WebCrawlerViewVariableItemListRequest)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemList); break; case WebCrawlerSubmenuIndexFile: - view_dispatcher_switch_to_view( - app->view_dispatcher, WebCrawlerViewVariableItemListFile); + free_variable_item_list(app); + if (!alloc_variable_item_list(app, WebCrawlerViewVariableItemListFile)) + { + FURI_LOG_E(TAG, "Failed to allocate variable item list"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemList); break; default: FURI_LOG_E(TAG, "Unknown submenu index"); @@ -286,13 +1035,29 @@ void web_crawler_submenu_callback(void* context, uint32_t index) { * @param context The context - WebCrawlerApp object. * @param index The index of the item that was clicked. */ -void web_crawler_wifi_enter_callback(void* context, uint32_t index) { - switch(index) { +void web_crawler_wifi_enter_callback(void *context, uint32_t index) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "web_crawler_wifi_enter_callback: WebCrawlerApp is NULL"); + switch (index) + { case 0: // SSID - web_crawler_setting_item_ssid_clicked(context, index); + free_text_input(app); + if (!alloc_text_input(app, WebCrawlerViewTextInputSSID)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewInput); break; case 1: // Password - web_crawler_setting_item_password_clicked(context, index); + free_text_input(app); + if (!alloc_text_input(app, WebCrawlerViewTextInputPassword)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewInput); break; default: FURI_LOG_E(TAG, "Unknown configuration item index"); @@ -305,16 +1070,32 @@ void web_crawler_wifi_enter_callback(void* context, uint32_t index) { * @param context The context - WebCrawlerApp object. * @param index The index of the item that was clicked. */ -void web_crawler_file_enter_callback(void* context, uint32_t index) { - switch(index) { +void web_crawler_file_enter_callback(void *context, uint32_t index) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "web_crawler_file_enter_callback: WebCrawlerApp is NULL"); + switch (index) + { case 0: // File Read web_crawler_setting_item_file_read_clicked(context, index); break; case 1: // FIle Type - web_crawler_setting_item_file_type_clicked(context, index); + free_text_input(app); + if (!alloc_text_input(app, WebCrawlerViewTextInputFileType)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewInput); break; case 2: // File Rename - web_crawler_setting_item_file_rename_clicked(context, index); + free_text_input(app); + if (!alloc_text_input(app, WebCrawlerViewTextInputFileRename)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewInput); break; case 3: // File Delete web_crawler_setting_item_file_delete_clicked(context, index); @@ -330,21 +1111,43 @@ void web_crawler_file_enter_callback(void* context, uint32_t index) { * @param context The context - WebCrawlerApp object. * @param index The index of the item that was clicked. */ -void web_crawler_request_enter_callback(void* context, uint32_t index) { - switch(index) { +void web_crawler_request_enter_callback(void *context, uint32_t index) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "web_crawler_request_enter_callback: WebCrawlerApp is NULL"); + switch (index) + { case 0: // URL - web_crawler_setting_item_path_clicked(context, index); + free_text_input(app); + if (!alloc_text_input(app, WebCrawlerViewTextInput)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewInput); break; case 1: // HTTP Method break; case 2: // Headers - web_crawler_setting_item_headers_clicked(context, index); + free_text_input(app); + if (!alloc_text_input(app, WebCrawlerViewTextInputHeaders)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewInput); break; case 3: // Payload - web_crawler_setting_item_payload_clicked(context, index); + free_text_input(app); + if (!alloc_text_input(app, WebCrawlerViewTextInputPayload)) + { + FURI_LOG_E(TAG, "Failed to allocate text input"); + return; + } + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewInput); break; default: FURI_LOG_E(TAG, "Unknown configuration item index"); @@ -356,723 +1159,271 @@ void web_crawler_request_enter_callback(void* context, uint32_t index) { * @brief Callback for when the user finishes entering the URL. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_path_updated(void* context) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - if(!app->path || !app->temp_buffer_path || !app->temp_buffer_size_path || !app->path_item) { - FURI_LOG_E(TAG, "Invalid path buffer"); - return; - } - // Store the entered URL from temp_buffer_path to path - strncpy(app->path, app->temp_buffer_path, app->temp_buffer_size_path - 1); - - if(app->path_item) { +void web_crawler_set_path_updated(void *context) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "WebCrawlerApp is NULL"); + snprintf(app->path, app->temp_buffer_size_path, "%s", app->temp_buffer_path); + if (app->path_item) + { variable_item_set_current_value_text(app->path_item, app->path); - - // Save the URL to the settings - save_settings( - app->path, - app->ssid, - app->password, - app->file_rename, - app->file_type, - app->http_method, - app->headers, - app->payload); } - - // Return to the Configure view - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListRequest); + save_char("path", app->temp_buffer_path); + save_simply(); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemList); } /** * @brief Callback for when the user finishes entering the headers * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_headers_updated(void* context) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - if(!app->temp_buffer_headers || !app->temp_buffer_size_headers || !app->headers_item) { - FURI_LOG_E(TAG, "Invalid headers buffer"); - return; - } - // Store the entered headers from temp_buffer_headers to headers - strncpy(app->headers, app->temp_buffer_headers, app->temp_buffer_size_headers - 1); - - if(app->headers_item) { +void web_crawler_set_headers_updated(void *context) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "WebCrawlerApp is NULL"); + snprintf(app->headers, app->temp_buffer_size_headers, "%s", app->temp_buffer_headers); + if (app->headers_item) + { variable_item_set_current_value_text(app->headers_item, app->headers); - - // Save the headers to the settings - save_settings( - app->path, - app->ssid, - app->password, - app->file_rename, - app->file_type, - app->http_method, - app->headers, - app->payload); } - - // Return to the Configure view - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListRequest); + save_char("headers", app->temp_buffer_headers); + save_simply(); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemList); } /** * @brief Callback for when the user finishes entering the payload. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_payload_updated(void* context) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - if(!app->temp_buffer_payload || !app->temp_buffer_size_payload || !app->payload_item) { - FURI_LOG_E(TAG, "Invalid payload buffer"); - return; - } - // Store the entered payload from temp_buffer_payload to payload - strncpy(app->payload, app->temp_buffer_payload, app->temp_buffer_size_payload - 1); - - if(app->payload_item) { +void web_crawler_set_payload_updated(void *context) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "WebCrawlerApp is NULL"); + snprintf(app->payload, app->temp_buffer_size_payload, "%s", app->temp_buffer_payload); + if (app->payload_item) + { variable_item_set_current_value_text(app->payload_item, app->payload); - - // Save the payload to the settings - save_settings( - app->path, - app->ssid, - app->password, - app->file_rename, - app->file_type, - app->http_method, - app->headers, - app->payload); } - - // Return to the Configure view - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListRequest); + save_char("payload", app->temp_buffer_payload); + save_simply(); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemList); } /** * @brief Callback for when the user finishes entering the SSID. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_ssid_updated(void* context) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - if(!app->temp_buffer_ssid || !app->temp_buffer_size_ssid || !app->ssid || !app->ssid_item) { - FURI_LOG_E(TAG, "Invalid SSID buffer"); +void web_crawler_set_ssid_updated(void *context) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "WebCrawlerApp is NULL"); + FlipperHTTP *fhttp = flipper_http_alloc(); + if (!fhttp) + { + FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP"); + easy_flipper_dialog("[ERROR]", "Failed to allocate FlipperHTTP\nand save wifi settings\nRestart your Flipper"); return; } - // Store the entered SSID from temp_buffer_ssid to ssid - strncpy(app->ssid, app->temp_buffer_ssid, app->temp_buffer_size_ssid - 1); - - if(app->ssid_item) { + char password[64]; + snprintf(app->ssid, app->temp_buffer_size_ssid, "%s", app->temp_buffer_ssid); + if (app->ssid_item) + { variable_item_set_current_value_text(app->ssid_item, app->ssid); - // Save the SSID to the settings - save_settings( - app->path, - app->ssid, - app->password, - app->file_rename, - app->file_type, - app->http_method, - app->headers, - app->payload); - - // send to UART - if(!flipper_http_save_wifi(app->ssid, app->password)) { - FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); - FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + if (load_char("wifi-password", password, 64)) + { + // send to UART + if (!flipper_http_save_wifi(fhttp, app->ssid, password)) + { + FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); + FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + } + // wait until save is done + while (fhttp->state != IDLE) + { + furi_delay_ms(100); + } } } - - // Return to the Configure view - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListWifi); + save_char("wifi-ssid", app->temp_buffer_ssid); + save_simply(); + flipper_http_free(fhttp); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemList); } /** * @brief Callback for when the user finishes entering the Password. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_password_update(void* context) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - if(!app->temp_buffer_password || !app->temp_buffer_size_password || !app->password || - !app->password_item) { - FURI_LOG_E(TAG, "Invalid password buffer"); +void web_crawler_set_password_update(void *context) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "WebCrawlerApp is NULL"); + FlipperHTTP *fhttp = flipper_http_alloc(); + if (!fhttp) + { + FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP"); + easy_flipper_dialog("[ERROR]", "Failed to allocate FlipperHTTP\nand save wifi settings"); return; } - // Store the entered Password from temp_buffer_password to password - strncpy(app->password, app->temp_buffer_password, app->temp_buffer_size_password - 1); - - if(app->password_item) { + char ssid[64]; + snprintf(app->password, app->temp_buffer_size_password, "%s", app->temp_buffer_password); + if (app->password_item) + { variable_item_set_current_value_text(app->password_item, app->password); - // Save the Password to the settings - save_settings( - app->path, - app->ssid, - app->password, - app->file_rename, - app->file_type, - app->http_method, - app->headers, - app->payload); - // send to UART - if(!flipper_http_save_wifi(app->ssid, app->password)) { - FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); - FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + if (load_char("wifi-ssid", ssid, 64)) + { + if (!flipper_http_save_wifi(fhttp, ssid, app->password)) + { + FURI_LOG_E(TAG, "Failed to save wifi settings via UART"); + FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board"); + } + // wait until save is done + while (fhttp->state != IDLE) + { + furi_delay_ms(100); + } } } - - // Return to the Configure view - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListWifi); + save_char("wifi-password", app->temp_buffer_password); + save_simply(); + flipper_http_free(fhttp); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemList); } /** * @brief Callback for when the user finishes entering the File Type. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_file_type_update(void* context) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - if(!app->temp_buffer_file_type || !app->temp_buffer_size_file_type || !app->file_type || - !app->file_type_item) { - FURI_LOG_E(TAG, "Invalid file type buffer"); - return; - } - // Temporary buffer to store the old name - char old_file_type[256]; - - strncpy(old_file_type, app->file_type, sizeof(old_file_type) - 1); - old_file_type[sizeof(old_file_type) - 1] = '\0'; // Null-terminate - strncpy(app->file_type, app->temp_buffer_file_type, app->temp_buffer_size_file_type - 1); - - if(app->file_type_item) { +void web_crawler_set_file_type_update(void *context) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "WebCrawlerApp is NULL"); + char old_file_type[16]; + snprintf(old_file_type, sizeof(old_file_type), "%s", app->file_type); + snprintf(app->file_type, app->temp_buffer_size_file_type, "%s", app->temp_buffer_file_type); + if (app->file_type_item) + { variable_item_set_current_value_text(app->file_type_item, app->file_type); - - // Save the File Type to the settings - save_settings( - app->path, - app->ssid, - app->password, - app->file_rename, - app->file_type, - app->http_method, - app->headers, - app->payload); } - - rename_received_data(app->file_rename, app->file_rename, app->file_type, old_file_type); - - // set the file path for fhttp.file_path - if(app->file_rename && app->file_type) { - char file_path[256]; - snprintf( - file_path, - sizeof(file_path), - "%s%s%s", - RECEIVED_DATA_PATH, - app->file_rename, - app->file_type); - file_path[sizeof(file_path) - 1] = '\0'; // Null-terminate - strncpy(fhttp.file_path, file_path, sizeof(fhttp.file_path) - 1); - fhttp.file_path[sizeof(fhttp.file_path) - 1] = '\0'; // Null-terminate - } - - // Return to the Configure view - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListFile); + char file_rename[128]; + if (load_char("file_rename", file_rename, 128)) + { + rename_received_data(file_rename, file_rename, app->file_type, old_file_type); + } + save_char("file_type", app->file_type); + save_simply(); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemList); } /** * @brief Callback for when the user finishes entering the File Rename. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_file_rename_update(void* context) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - if(!app->temp_buffer_file_rename || !app->temp_buffer_size_file_rename || !app->file_rename || - !app->file_rename_item) { - FURI_LOG_E(TAG, "Invalid file rename buffer"); - return; - } - - // Temporary buffer to store the old name +void web_crawler_set_file_rename_update(void *context) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "WebCrawlerApp is NULL"); char old_name[256]; - - // Ensure that app->file_rename is null-terminated - strncpy(old_name, app->file_rename, sizeof(old_name) - 1); - old_name[sizeof(old_name) - 1] = '\0'; // Null-terminate - - // Store the entered File Rename from temp_buffer_file_rename to file_rename - strncpy(app->file_rename, app->temp_buffer_file_rename, app->temp_buffer_size_file_rename - 1); - - if(app->file_rename_item) { + snprintf(old_name, sizeof(old_name), "%s", app->file_rename); + snprintf(app->file_rename, app->temp_buffer_size_file_rename, "%s", app->temp_buffer_file_rename); + if (app->file_rename_item) + { variable_item_set_current_value_text(app->file_rename_item, app->file_rename); - - // Save the File Rename to the settings - save_settings( - app->path, - app->ssid, - app->password, - app->file_rename, - app->file_type, - app->http_method, - app->headers, - app->payload); } - rename_received_data(old_name, app->file_rename, app->file_type, app->file_type); - - // set the file path for fhttp.file_path - if(app->file_rename && app->file_type) { - char file_path[256]; - snprintf( - file_path, - sizeof(file_path), - "%s%s%s", - RECEIVED_DATA_PATH, - app->file_rename, - app->file_type); - file_path[sizeof(file_path) - 1] = '\0'; // Null-terminate - strncpy(fhttp.file_path, file_path, sizeof(fhttp.file_path) - 1); - fhttp.file_path[sizeof(fhttp.file_path) - 1] = '\0'; // Null-terminate - } - - // Return to the Configure view - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListFile); + save_char("file_rename", app->file_rename); + save_simply(); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemList); } /** - * @brief Handler for Path configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_path_clicked(void* context, uint32_t index) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - if(!app->text_input_path) { - FURI_LOG_E(TAG, "Text input is NULL"); - return; - } - - UNUSED(index); - - // Initialize temp_buffer with existing path - if(app->path && strlen(app->path) > 0) { - strncpy(app->temp_buffer_path, app->path, app->temp_buffer_size_path - 1); - } else { - strncpy(app->temp_buffer_path, "https://httpbin.org/get", app->temp_buffer_size_path - 1); - } - - app->temp_buffer_path[app->temp_buffer_size_path - 1] = '\0'; - - // Configure the text input - bool clear_previous_text = false; - text_input_set_result_callback( - app->text_input_path, - web_crawler_set_path_updated, - app, - app->temp_buffer_path, - app->temp_buffer_size_path, - clear_previous_text); - - // Set the previous callback to return to Configure screen - view_set_previous_callback( - text_input_get_view(app->text_input_path), web_crawler_back_to_request_callback); - - // Show text input dialog - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInput); -} - -/** - * @brief Handler for headers configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_headers_clicked(void* context, uint32_t index) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - UNUSED(index); - if(!app->text_input_headers) { - FURI_LOG_E(TAG, "Text input is NULL"); - return; - } - if(!app->headers) { - FURI_LOG_E(TAG, "Headers is NULL"); - return; - } - if(!app->temp_buffer_headers) { - FURI_LOG_E(TAG, "Temp buffer headers is NULL"); - return; - } - - // Initialize temp_buffer with existing headers - if(app->headers && strlen(app->headers) > 0) { - strncpy(app->temp_buffer_headers, app->headers, app->temp_buffer_size_headers - 1); - } else { - strncpy( - app->temp_buffer_headers, - "{\"Content-Type\":\"application/json\",\"key\":\"value\"}", - app->temp_buffer_size_headers - 1); - } - - app->temp_buffer_headers[app->temp_buffer_size_headers - 1] = '\0'; - - // Configure the text input - bool clear_previous_text = false; - text_input_set_result_callback( - app->text_input_headers, - web_crawler_set_headers_updated, - app, - app->temp_buffer_headers, - app->temp_buffer_size_headers, - clear_previous_text); - - // Set the previous callback to return to Configure screen - view_set_previous_callback( - text_input_get_view(app->text_input_headers), web_crawler_back_to_request_callback); - - // Show text input dialog - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputHeaders); -} - -/** - * @brief Handler for payload configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_payload_clicked(void* context, uint32_t index) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - UNUSED(index); - if(!app->text_input_payload) { - FURI_LOG_E(TAG, "Text input is NULL"); - return; - } - - // Initialize temp_buffer with existing payload - if(app->payload && strlen(app->payload) > 0) { - strncpy(app->temp_buffer_payload, app->payload, app->temp_buffer_size_payload - 1); - } else { - strncpy( - app->temp_buffer_payload, "{\"key\":\"value\"}", app->temp_buffer_size_payload - 1); - } - - app->temp_buffer_payload[app->temp_buffer_size_payload - 1] = '\0'; - - // Configure the text input - bool clear_previous_text = false; - text_input_set_result_callback( - app->text_input_payload, - web_crawler_set_payload_updated, - app, - app->temp_buffer_payload, - app->temp_buffer_size_payload, - clear_previous_text); - - // Set the previous callback to return to Configure screen - view_set_previous_callback( - text_input_get_view(app->text_input_payload), web_crawler_back_to_request_callback); - - // Show text input dialog - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputPayload); -} - -/** - * @brief Handler for SSID configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_ssid_clicked(void* context, uint32_t index) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - UNUSED(index); - if(!app->text_input_ssid) { - FURI_LOG_E(TAG, "Text input is NULL"); - return; - } - - // Initialize temp_buffer with existing SSID - if(app->ssid && strlen(app->ssid) > 0) { - strncpy(app->temp_buffer_ssid, app->ssid, app->temp_buffer_size_ssid - 1); - } else { - strncpy(app->temp_buffer_ssid, "", app->temp_buffer_size_ssid - 1); - } - - app->temp_buffer_ssid[app->temp_buffer_size_ssid - 1] = '\0'; - - // Configure the text input - bool clear_previous_text = false; - text_input_set_result_callback( - app->text_input_ssid, - web_crawler_set_ssid_updated, - app, - app->temp_buffer_ssid, - app->temp_buffer_size_ssid, - clear_previous_text); - - // Set the previous callback to return to Configure screen - view_set_previous_callback( - text_input_get_view(app->text_input_ssid), web_crawler_back_to_wifi_callback); - - // Show text input dialog - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputSSID); -} - -/** - * @brief Handler for Password configuration item click. + * @brief Handler for File Delete configuration item click. * @param context The context - WebCrawlerApp object. * @param index The index of the item that was clicked. */ -void web_crawler_setting_item_password_clicked(void* context, uint32_t index) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } +void web_crawler_setting_item_file_delete_clicked(void *context, uint32_t index) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "WebCrawlerApp is NULL"); UNUSED(index); - if(!app->text_input_password) { - FURI_LOG_E(TAG, "Text input is NULL"); - return; - } - // Initialize temp_buffer with existing password - strncpy(app->temp_buffer_password, app->password, app->temp_buffer_size_password - 1); - app->temp_buffer_password[app->temp_buffer_size_password - 1] = '\0'; - - // Configure the text input - bool clear_previous_text = false; - text_input_set_result_callback( - app->text_input_password, - web_crawler_set_password_update, - app, - app->temp_buffer_password, - app->temp_buffer_size_password, - clear_previous_text); - - // Set the previous callback to return to Configure screen - view_set_previous_callback( - text_input_get_view(app->text_input_password), web_crawler_back_to_wifi_callback); - - // Show text input dialog - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputPassword); -} - -/** - * @brief Handler for File Type configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_file_type_clicked(void* context, uint32_t index) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - UNUSED(index); - if(!app->text_input_file_type) { - FURI_LOG_E(TAG, "Text input is NULL"); + free_widget(app); + if (!alloc_widget(app, WebCrawlerViewFileDelete)) + { + FURI_LOG_E(TAG, "web_crawler_setting_item_file_delete_clicked: Failed to allocate widget"); return; } - // Initialize temp_buffer with existing file_type - if(app->file_type && strlen(app->file_type) > 0) { - strncpy(app->temp_buffer_file_type, app->file_type, app->temp_buffer_size_file_type - 1); - } else { - strncpy(app->temp_buffer_file_type, ".txt", app->temp_buffer_size_file_type - 1); + if (!delete_received_data()) + { + FURI_LOG_E(TAG, "Failed to delete file"); } - app->temp_buffer_file_type[app->temp_buffer_size_file_type - 1] = '\0'; - - // Configure the text input - bool clear_previous_text = false; - text_input_set_result_callback( - app->text_input_file_type, - web_crawler_set_file_type_update, - app, - app->temp_buffer_file_type, - app->temp_buffer_size_file_type, - clear_previous_text); - - // Set the previous callback to return to Configure screen - view_set_previous_callback( - text_input_get_view(app->text_input_file_type), web_crawler_back_to_file_callback); - // Show text input dialog - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputFileType); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewWidget); } -/** - * @brief Handler for File Rename configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_file_rename_clicked(void* context, uint32_t index) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } +void web_crawler_setting_item_file_read_clicked(void *context, uint32_t index) +{ + WebCrawlerApp *app = (WebCrawlerApp *)context; + furi_check(app, "WebCrawlerApp is NULL"); UNUSED(index); - if(!app->text_input_file_rename) { - FURI_LOG_E(TAG, "Text input is NULL"); + free_widget(app); + if (!alloc_widget(app, WebCrawlerViewFileRead)) + { + FURI_LOG_E(TAG, "web_crawler_setting_item_file_read_clicked: Failed to allocate widget"); return; } - - // Initialize temp_buffer with existing file_rename - if(app->file_rename && strlen(app->file_rename) > 0) { - strncpy( - app->temp_buffer_file_rename, app->file_rename, app->temp_buffer_size_file_rename - 1); - } else { - strncpy( - app->temp_buffer_file_rename, "received_data", app->temp_buffer_size_file_rename - 1); + widget_reset(app->widget); + char file_path[256]; + char file_rename[128]; + char file_type[16]; + if (load_char("file_rename", file_rename, 128) && load_char("file_type", file_type, 16)) + { + snprintf(file_path, sizeof(file_path), "%s%s%s", RECEIVED_DATA_PATH, file_rename, file_type); } - - app->temp_buffer_file_rename[app->temp_buffer_size_file_rename - 1] = '\0'; - - // Configure the text input - bool clear_previous_text = false; - text_input_set_result_callback( - app->text_input_file_rename, - web_crawler_set_file_rename_update, - app, - app->temp_buffer_file_rename, - app->temp_buffer_size_file_rename, - clear_previous_text); - - // Set the previous callback to return to Configure screen - view_set_previous_callback( - text_input_get_view(app->text_input_file_rename), web_crawler_back_to_file_callback); - - // Show text input dialog - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputFileRename); -} - -/** - * @brief Handler for File Delete configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_file_delete_clicked(void* context, uint32_t index) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - UNUSED(index); - - if(!delete_received_data(app)) { - FURI_LOG_E(TAG, "Failed to delete file"); - } - - // Set the previous callback to return to Configure screen - view_set_previous_callback( - widget_get_view(app->widget_file_delete), web_crawler_back_to_file_callback); - - // Show text input dialog - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewFileDelete); -} - -void web_crawler_setting_item_file_read_clicked(void* context, uint32_t index) { - WebCrawlerApp* app = (WebCrawlerApp*)context; - if(!app) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return; - } - UNUSED(index); - widget_reset(app->widget_file_read); - - if(app->file_rename && app->file_type) { - snprintf( - fhttp.file_path, - sizeof(fhttp.file_path), - "%s%s%s", - RECEIVED_DATA_PATH, - app->file_rename, - app->file_type); - } else { - snprintf( - fhttp.file_path, - sizeof(fhttp.file_path), - "%s%s%s", - RECEIVED_DATA_PATH, - "received_data", - ".txt"); + else + { + snprintf(file_path, sizeof(file_path), "%s%s%s", RECEIVED_DATA_PATH, "received_data", ".txt"); } // load the received data from the saved file - FuriString* received_data = flipper_http_load_from_file(fhttp.file_path); - if(received_data == NULL) { + FuriString *received_data = flipper_http_load_from_file(file_path); + if (received_data == NULL) + { FURI_LOG_E(TAG, "Failed to load received data from file."); - if(app->widget_file_read) { - widget_add_text_scroll_element(app->widget_file_read, 0, 0, 128, 64, "File is empty."); - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewFileRead); - } - return; - } - const char* data_cstr = furi_string_get_cstr(received_data); - if(data_cstr == NULL) { - FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); - furi_string_free(received_data); - if(app->widget_file_read) { - widget_add_text_scroll_element(app->widget_file_read, 0, 0, 128, 64, "File is empty."); - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewFileRead); + if (app->widget) + { + widget_add_text_scroll_element( + app->widget, + 0, + 0, + 128, + 64, "File is empty."); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewWidget); } return; } - widget_add_text_scroll_element(app_instance->widget_file_read, 0, 0, 128, 64, data_cstr); + widget_add_text_scroll_element(app->widget, 0, 0, 128, 64, furi_string_get_cstr(received_data)); furi_string_free(received_data); - // Set the previous callback to return to Configure screen - view_set_previous_callback( - widget_get_view(app->widget_file_read), web_crawler_back_to_file_callback); - // Show text input dialog - view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewFileRead); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewWidget); } -static void web_crawler_widget_set_text(char* message, Widget** widget) { - if(widget == NULL) { +static void web_crawler_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) { + if (message == NULL) + { FURI_LOG_E(TAG, "flip_weather_set_widget_text - message is NULL"); DEV_CRASH(); return; @@ -1080,32 +1431,36 @@ static void web_crawler_widget_set_text(char* message, Widget** widget) { 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 + 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)) { + 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 + 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; + 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') { + 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 (found_newline) + { // If newline found, set line_length up to the newline line_length = newline_pos - i; } @@ -1116,15 +1471,19 @@ static void web_crawler_widget_set_text(char* message, Widget** widget) { line[line_length] = '\0'; // If newline was found, skip it for the next iteration - if(found_newline) { + if (found_newline) + { i += line_length + 1; // +1 to skip the '\n' character - } else { + } + 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] != ' ') { + 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) { + 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 @@ -1135,13 +1494,15 @@ static void web_crawler_widget_set_text(char* message, Widget** widget) { i += line_length; // Skip any spaces at the beginning of the next line - while(i < message_length && message[i] == ' ') { + 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++) { + for (uint32_t j = 0; j < line_length; j++) + { formatted_message[formatted_index++] = line[j]; } @@ -1156,20 +1517,23 @@ static void web_crawler_widget_set_text(char* message, Widget** widget) { widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message); } -void web_crawler_loader_draw_callback(Canvas* canvas, void* model) { - if(!canvas || !model) { +void web_crawler_loader_draw_callback(Canvas *canvas, void *model) +{ + if (!canvas || !model) + { FURI_LOG_E(TAG, "web_crawler_loader_draw_callback - canvas or model is NULL"); return; } - SerialState http_state = fhttp.state; - DataLoaderModel* data_loader_model = (DataLoaderModel*)model; + DataLoaderModel *data_loader_model = (DataLoaderModel *)model; + SerialState http_state = data_loader_model->fhttp->state; DataState data_state = data_loader_model->data_state; - char* title = data_loader_model->title; + char *title = data_loader_model->title; canvas_set_font(canvas, FontSecondary); - if(http_state == INACTIVE) { + 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,"); @@ -1179,214 +1543,253 @@ void web_crawler_loader_draw_callback(Canvas* canvas, void* model) { return; } - if(data_state == DataStateError || data_state == DataStateParseError) { - web_crawler_draw_error(canvas); + if (data_state == DataStateError || data_state == DataStateParseError) + { + web_crawler_draw_error(canvas, data_loader_model); return; } canvas_draw_str(canvas, 0, 7, title); canvas_draw_str(canvas, 0, 17, "Loading..."); - if(data_state == DataStateInitial) { + if (data_state == DataStateInitial) + { return; } - if(http_state == SENDING) { - canvas_draw_str(canvas, 0, 27, "Sending..."); + if (http_state == SENDING) + { + canvas_draw_str(canvas, 0, 27, "Fetching..."); return; } - if(http_state == RECEIVING || data_state == DataStateRequested) { + if (http_state == RECEIVING || data_state == DataStateRequested) + { canvas_draw_str(canvas, 0, 27, "Receiving..."); return; } - if(http_state == IDLE && data_state == DataStateReceived) { + if (http_state == IDLE && data_state == DataStateReceived) + { canvas_draw_str(canvas, 0, 27, "Processing..."); return; } - if(http_state == IDLE && data_state == DataStateParsed) { + if (http_state == IDLE && data_state == DataStateParsed) + { canvas_draw_str(canvas, 0, 27, "Processed..."); return; } } -static void web_crawler_loader_process_callback(void* context) { - if(context == NULL) { +static void web_crawler_loader_process_callback(void *context) +{ + if (context == NULL) + { FURI_LOG_E(TAG, "web_crawler_loader_process_callback - context is NULL"); DEV_CRASH(); return; } - WebCrawlerApp* app = (WebCrawlerApp*)context; - View* view = app->view_loader; + WebCrawlerApp *app = (WebCrawlerApp *)context; + View *view = app->view_loader; DataState current_data_state; - with_view_model( - view, DataLoaderModel * model, { current_data_state = model->data_state; }, false); + DataLoaderModel *loader_model = NULL; + with_view_model(view, DataLoaderModel * model, { current_data_state = model->data_state; loader_model = model; }, false); + if (!loader_model || !loader_model->fhttp) + { + FURI_LOG_E(TAG, "Model or fhttp is NULL"); + DEV_CRASH(); + return; + } - if(current_data_state == DataStateInitial) { + if (current_data_state == DataStateInitial) + { with_view_model( view, DataLoaderModel * model, { model->data_state = DataStateRequested; DataLoaderFetch fetch = model->fetcher; - if(fetch == NULL) { + 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); + strncpy(model->fhttp->last_response, "", 1); bool request_status = fetch(model); - if(!request_status) { + 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) { + } + else if (current_data_state == DataStateRequested || current_data_state == DataStateError) + { + if (loader_model->fhttp->state == IDLE && loader_model->fhttp->last_response != NULL) + { + if (strstr(loader_model->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) { + } + else if (strncmp(loader_model->fhttp->last_response, "[SUCCESS]", 9) == 0) + { + FURI_LOG_DEV(TAG, "SUCCESS received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL"); + } + else if (strncmp(loader_model->fhttp->last_response, "[ERROR]", 9) == 0) + { + FURI_LOG_DEV(TAG, "ERROR received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL"); + } + else if (strlen(loader_model->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) { + else + { + with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateReceived; }, true); + } + } + else if (loader_model->fhttp->state == SENDING || loader_model->fhttp->state == RECEIVING) + { // continue waiting - } else if(fhttp.state == INACTIVE) { + } + else if (loader_model->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"); + } + else if (loader_model->fhttp->state == ISSUE) + { + with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateError; }, true); + } + else + { + FURI_LOG_DEV(TAG, "Unexpected state: %d lastresp: %s", loader_model->fhttp->state, loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL"); DEV_CRASH(); } - } else if(current_data_state == DataStateReceived) { + } + else if (current_data_state == DataStateReceived) + { with_view_model( view, DataLoaderModel * model, { - char* data_text; - if(model->parser == NULL) { + char *data_text; + if (model->parser == NULL) + { data_text = NULL; FURI_LOG_DEV(TAG, "Parser is NULL"); DEV_CRASH(); - } else { + } + 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"); + FURI_LOG_DEV(TAG, "Parsed data: %s\r\ntext: %s", model->fhttp->last_response ? model->fhttp->last_response : "NULL", data_text ? data_text : "NULL"); model->data_text = data_text; - if(data_text == NULL) { + if (data_text == NULL) + { model->data_state = DataStateParseError; - } else { + } + else + { model->data_state = DataStateParsed; } }, true); - } else if(current_data_state == DataStateParsed) { + } + else if (current_data_state == DataStateParsed) + { with_view_model( view, DataLoaderModel * model, { - if(++model->request_index < model->request_count) { + if (++model->request_index < model->request_count) + { model->data_state = DataStateInitial; - } else { - web_crawler_widget_set_text( - model->data_text != NULL ? model->data_text : "Empty result", - &app_instance->widget_result); - if(model->data_text != NULL) { + } + else + { + web_crawler_widget_set_text(model->data_text != NULL ? model->data_text : "", &app->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, WebCrawlerViewWidgetResult); + view_set_previous_callback(widget_get_view(app->widget_result), model->back_callback); + view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewWidgetResult); } }, true); } } -static void web_crawler_loader_timer_callback(void* context) { - if(context == NULL) { +static void web_crawler_loader_timer_callback(void *context) +{ + if (context == NULL) + { FURI_LOG_E(TAG, "web_crawler_loader_timer_callback - context is NULL"); DEV_CRASH(); return; } - WebCrawlerApp* app = (WebCrawlerApp*)context; + WebCrawlerApp *app = (WebCrawlerApp *)context; view_dispatcher_send_custom_event(app->view_dispatcher, WebCrawlerCustomEventProcess); } -static void web_crawler_loader_on_enter(void* context) { - if(context == NULL) { +static void web_crawler_loader_on_enter(void *context) +{ + if (context == NULL) + { FURI_LOG_E(TAG, "web_crawler_loader_on_enter - context is NULL"); DEV_CRASH(); return; } - WebCrawlerApp* app = (WebCrawlerApp*)context; - View* view = app->view_loader; + WebCrawlerApp *app = (WebCrawlerApp *)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( - web_crawler_loader_timer_callback, FuriTimerTypePeriodic, app); + if (model->timer == NULL) + { + model->timer = furi_timer_alloc(web_crawler_loader_timer_callback, FuriTimerTypePeriodic, app); } furi_timer_start(model->timer, 250); }, true); } -static void web_crawler_loader_on_exit(void* context) { - if(context == NULL) { +static void web_crawler_loader_on_exit(void *context) +{ + if (context == NULL) + { FURI_LOG_E(TAG, "web_crawler_loader_on_exit - context is NULL"); DEV_CRASH(); return; } - WebCrawlerApp* app = (WebCrawlerApp*)context; - View* view = app->view_loader; + WebCrawlerApp *app = (WebCrawlerApp *)context; + View *view = app->view_loader; with_view_model( view, DataLoaderModel * model, { - if(model->timer) { + if (model->timer) + { furi_timer_stop(model->timer); } }, false); } -void web_crawler_loader_init(View* view) { - if(view == NULL) { +void web_crawler_loader_init(View *view) +{ + if (view == NULL) + { FURI_LOG_E(TAG, "web_crawler_loader_init - view is NULL"); DEV_CRASH(); return; @@ -1396,8 +1799,10 @@ void web_crawler_loader_init(View* view) { view_set_exit_callback(view, web_crawler_loader_on_exit); } -void web_crawler_loader_free_model(View* view) { - if(view == NULL) { +void web_crawler_loader_free_model(View *view) +{ + if (view == NULL) + { FURI_LOG_E(TAG, "web_crawler_loader_free_model - view is NULL"); DEV_CRASH(); return; @@ -1406,26 +1811,37 @@ void web_crawler_loader_free_model(View* view) { view, DataLoaderModel * model, { - if(model->timer) { + if (model->timer) + { furi_timer_free(model->timer); model->timer = NULL; } - if(model->parser_context) { - free(model->parser_context); - model->parser_context = NULL; + if (model->parser_context) + { + // do not free the context here, it is the app context + // free(model->parser_context); + // model->parser_context = NULL; + } + if (model->fhttp) + { + flipper_http_free(model->fhttp); + model->fhttp = NULL; } }, false); } -bool web_crawler_custom_event_callback(void* context, uint32_t index) { - if(context == NULL) { +bool web_crawler_custom_event_callback(void *context, uint32_t index) +{ + if (context == NULL) + { FURI_LOG_E(TAG, "web_crawler_custom_event_callback - context is NULL"); DEV_CRASH(); return false; } - switch(index) { + switch (index) + { case WebCrawlerCustomEventProcess: web_crawler_loader_process_callback(context); return true; @@ -1435,22 +1851,18 @@ bool web_crawler_custom_event_callback(void* context, uint32_t index) { } } -void web_crawler_generic_switch_to_view( - WebCrawlerApp* app, - char* title, - DataLoaderFetch fetcher, - DataLoaderParser parser, - size_t request_count, - ViewNavigationCallback back, - uint32_t view_id) { - if(app == NULL) { +void web_crawler_generic_switch_to_view(WebCrawlerApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id) +{ + if (app == NULL) + { FURI_LOG_E(TAG, "web_crawler_generic_switch_to_view - app is NULL"); DEV_CRASH(); return; } - View* view = app->view_loader; - if(view == NULL) { + View *view = app->view_loader; + if (view == NULL) + { FURI_LOG_E(TAG, "web_crawler_generic_switch_to_view - view is NULL"); DEV_CRASH(); return; @@ -1468,6 +1880,12 @@ void web_crawler_generic_switch_to_view( model->back_callback = back; model->data_state = DataStateInitial; model->data_text = NULL; + // + model->parser_context = app; + if (!model->fhttp) + { + model->fhttp = flipper_http_alloc(); + } }, true); diff --git a/web_crawler/callback/web_crawler_callback.h b/web_crawler/callback/web_crawler_callback.h index 81c4d8127..1f4feefdf 100644 --- a/web_crawler/callback/web_crawler_callback.h +++ b/web_crawler/callback/web_crawler_callback.h @@ -3,169 +3,111 @@ #include "web_crawler.h" #include <flip_storage/web_crawler_storage.h> -extern bool sent_http_request; -extern bool get_success; -extern bool already_success; - -void web_crawler_http_method_change(VariableItem* item); +void web_crawler_http_method_change(VariableItem *item); +uint32_t web_crawler_back_to_main_callback(void *context); +void free_all(WebCrawlerApp *app); /** * @brief Navigation callback to handle exiting from other views to the submenu. * @param context The context - WebCrawlerApp object. * @return WebCrawlerViewSubmenu */ -uint32_t web_crawler_back_to_configure_callback(void* context); - -/** - * @brief Navigation callback to handle returning to the Wifi Settings screen. - * @param context The context - WebCrawlerApp object. - * @return WebCrawlerViewSubmenu - */ -uint32_t web_crawler_back_to_main_callback(void* context); -uint32_t web_crawler_back_to_file_callback(void* context); +uint32_t web_crawler_back_to_configure_callback(void *context); -uint32_t web_crawler_back_to_wifi_callback(void* context); +uint32_t web_crawler_back_to_wifi_callback(void *context); -uint32_t web_crawler_back_to_request_callback(void* context); +uint32_t web_crawler_back_to_request_callback(void *context); /** * @brief Navigation callback to handle exiting the app from the main submenu. * @param context The context - unused * @return VIEW_NONE to exit the app */ -uint32_t web_crawler_exit_app_callback(void* context); +uint32_t web_crawler_exit_app_callback(void *context); /** * @brief Handle submenu item selection. * @param context The context - WebCrawlerApp object. * @param index The WebCrawlerSubmenuIndex item that was clicked. */ -void web_crawler_submenu_callback(void* context, uint32_t index); +void web_crawler_submenu_callback(void *context, uint32_t index); /** * @brief Configuration enter callback to handle different items. * @param context The context - WebCrawlerApp object. * @param index The index of the item that was clicked. */ -void web_crawler_wifi_enter_callback(void* context, uint32_t index); +void web_crawler_wifi_enter_callback(void *context, uint32_t index); /** * @brief Configuration enter callback to handle different items. * @param context The context - WebCrawlerApp object. * @param index The index of the item that was clicked. */ -void web_crawler_file_enter_callback(void* context, uint32_t index); +void web_crawler_file_enter_callback(void *context, uint32_t index); /** * @brief Configuration enter callback to handle different items. * @param context The context - WebCrawlerApp object. * @param index The index of the item that was clicked. */ -void web_crawler_request_enter_callback(void* context, uint32_t index); +void web_crawler_request_enter_callback(void *context, uint32_t index); /** * @brief Callback for when the user finishes entering the URL. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_path_updated(void* context); +void web_crawler_set_path_updated(void *context); /** * @brief Callback for when the user finishes entering the headers * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_headers_updated(void* context); +void web_crawler_set_headers_updated(void *context); /** * @brief Callback for when the user finishes entering the payload. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_payload_updated(void* context); +void web_crawler_set_payload_updated(void *context); /** * @brief Callback for when the user finishes entering the SSID. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_ssid_updated(void* context); +void web_crawler_set_ssid_updated(void *context); /** * @brief Callback for when the user finishes entering the Password. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_password_update(void* context); +void web_crawler_set_password_update(void *context); /** * @brief Callback for when the user finishes entering the File Type. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_file_type_update(void* context); +void web_crawler_set_file_type_update(void *context); /** * @brief Callback for when the user finishes entering the File Rename. * @param context The context - WebCrawlerApp object. */ -void web_crawler_set_file_rename_update(void* context); - -/** - * @brief Handler for Path configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_path_clicked(void* context, uint32_t index); - -/** - * @brief Handler for headers configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_headers_clicked(void* context, uint32_t index); - -/** - * @brief Handler for payload configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_payload_clicked(void* context, uint32_t index); - -/** - * @brief Handler for SSID configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_ssid_clicked(void* context, uint32_t index); - -/** - * @brief Handler for Password configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_password_clicked(void* context, uint32_t index); - -/** - * @brief Handler for File Type configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_file_type_clicked(void* context, uint32_t index); - -/** - * @brief Handler for File Rename configuration item click. - * @param context The context - WebCrawlerApp object. - * @param index The index of the item that was clicked. - */ -void web_crawler_setting_item_file_rename_clicked(void* context, uint32_t index); +void web_crawler_set_file_rename_update(void *context); /** * @brief Handler for File Delete configuration item click. * @param context The context - WebCrawlerApp object. * @param index The index of the item that was clicked. */ -void web_crawler_setting_item_file_delete_clicked(void* context, uint32_t index); +void web_crawler_setting_item_file_delete_clicked(void *context, uint32_t index); -void web_crawler_setting_item_file_read_clicked(void* context, uint32_t index); +void web_crawler_setting_item_file_read_clicked(void *context, uint32_t index); // Add edits by Derek Jamison typedef enum DataState DataState; -enum DataState { +enum DataState +{ DataStateInitial, DataStateRequested, DataStateReceived, @@ -175,40 +117,36 @@ enum DataState { }; typedef enum WebCrawlerCustomEvent WebCrawlerCustomEvent; -enum WebCrawlerCustomEvent { +enum WebCrawlerCustomEvent +{ WebCrawlerCustomEventProcess, }; typedef struct DataLoaderModel DataLoaderModel; -typedef bool (*DataLoaderFetch)(DataLoaderModel* model); -typedef char* (*DataLoaderParser)(DataLoaderModel* model); -struct DataLoaderModel { - char* title; - char* data_text; +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; + void *parser_context; size_t request_index; size_t request_count; ViewNavigationCallback back_callback; - FuriTimer* timer; + FuriTimer *timer; + FlipperHTTP *fhttp; }; -void web_crawler_generic_switch_to_view( - WebCrawlerApp* app, - char* title, - DataLoaderFetch fetcher, - DataLoaderParser parser, - size_t request_count, - ViewNavigationCallback back, - uint32_t view_id); +void web_crawler_generic_switch_to_view(WebCrawlerApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id); -void web_crawler_loader_draw_callback(Canvas* canvas, void* model); +void web_crawler_loader_draw_callback(Canvas *canvas, void *model); -void web_crawler_loader_init(View* view); +void web_crawler_loader_init(View *view); -void web_crawler_loader_free_model(View* view); +void web_crawler_loader_free_model(View *view); -bool web_crawler_custom_event_callback(void* context, uint32_t index); -#endif +bool web_crawler_custom_event_callback(void *context, uint32_t index); +#endif \ No newline at end of file diff --git a/web_crawler/easy_flipper/easy_flipper.c b/web_crawler/easy_flipper/easy_flipper.c index 8b98e1a1b..fe23e9717 100644 --- a/web_crawler/easy_flipper/easy_flipper.c +++ b/web_crawler/easy_flipper/easy_flipper.c @@ -1,13 +1,35 @@ #include <easy_flipper/easy_flipper.h> +void easy_flipper_dialog( + char *header, + char *text) +{ + DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage *message = dialog_message_alloc(); + dialog_message_set_header( + message, header, 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + message, + text, + 0, + 63, + AlignLeft, + AlignBottom); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); +} + /** * @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) { +uint32_t easy_flipper_callback_exit_app(void *context) +{ // Exit the application - if(!context) { + if (!context) + { FURI_LOG_E(EASY_TAG, "Context is NULL"); return VIEW_NONE; } @@ -21,13 +43,16 @@ uint32_t easy_flipper_callback_exit_app(void* context) { * @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) { +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) { + *buffer = (char *)malloc(buffer_size); + if (!*buffer) + { FURI_LOG_E(EASY_TAG, "Failed to allocate buffer"); return false; } @@ -46,32 +71,39 @@ bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size) { * @return true if successful, false otherwise */ bool easy_flipper_set_view( - View** 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) { + 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) { + if (!*view) + { FURI_LOG_E(EASY_TAG, "Failed to allocate View"); return false; } - if(draw_callback) { + if (draw_callback) + { view_set_draw_callback(*view, draw_callback); } - if(input_callback) { + if (input_callback) + { view_set_input_callback(*view, input_callback); } - if(context) { + if (context) + { view_set_context(*view, context); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(*view, previous_callback); } view_dispatcher_add_view(*view_dispatcher, view_id, *view); @@ -85,18 +117,22 @@ bool easy_flipper_set_view( * @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) { +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) { + 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) { + if (context) + { view_dispatcher_set_event_callback_context(*view_dispatcher, context); } return true; @@ -113,24 +149,29 @@ bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui * @return true if successful, false otherwise */ bool easy_flipper_set_submenu( - Submenu** submenu, + Submenu **submenu, int32_t view_id, - char* title, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!submenu) { + 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) { + if (!*submenu) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Submenu"); return false; } - if(title) { + if (title) + { submenu_set_header(*submenu, title); } - if(previous_callback) { + 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)); @@ -147,20 +188,24 @@ bool easy_flipper_set_submenu( * @return true if successful, false otherwise */ bool easy_flipper_set_menu( - Menu** menu, + Menu **menu, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!menu) { + 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) { + if (!*menu) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Menu"); return false; } - if(previous_callback) { + 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)); @@ -177,24 +222,29 @@ bool easy_flipper_set_menu( * @return true if successful, false otherwise */ bool easy_flipper_set_widget( - Widget** widget, + Widget **widget, int32_t view_id, - char* text, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!widget) { + 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) { + if (!*widget) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Widget"); return false; } - if(text) { + if (text) + { widget_add_text_scroll_element(*widget, 0, 0, 128, 64, text); } - if(previous_callback) { + 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)); @@ -213,30 +263,33 @@ bool easy_flipper_set_widget( * @return true if successful, false otherwise */ bool easy_flipper_set_variable_item_list( - VariableItemList** 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) { + 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) { + if (!*variable_item_list) + { FURI_LOG_E(EASY_TAG, "Failed to allocate VariableItemList"); return false; } - if(enter_callback) { + 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); + 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)); + view_dispatcher_add_view(*view_dispatcher, view_id, variable_item_list_get_view(*variable_item_list)); return true; } @@ -249,38 +302,38 @@ bool easy_flipper_set_variable_item_list( * @return true if successful, false otherwise */ bool easy_flipper_set_text_input( - TextInput** text_input, + TextInput **text_input, int32_t view_id, - char* header_text, - char* text_input_temp_buffer, + 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) { + 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) { + if (!*text_input) + { FURI_LOG_E(EASY_TAG, "Failed to allocate TextInput"); return false; } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(text_input_get_view(*text_input), previous_callback); } - if(header_text) { + 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); + 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; @@ -295,38 +348,38 @@ bool easy_flipper_set_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_uart_text_input( - TextInput** uart_text_input, + TextInput **uart_text_input, int32_t view_id, - char* header_text, - char* uart_text_input_temp_buffer, + 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) { + 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) { + if (!*uart_text_input) + { FURI_LOG_E(EASY_TAG, "Failed to allocate UART_TextInput"); return false; } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(text_input_get_view(*uart_text_input), previous_callback); } - if(header_text) { + 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); + 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)); @@ -353,52 +406,63 @@ bool easy_flipper_set_uart_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_dialog_ex( - DialogEx** dialog_ex, + DialogEx **dialog_ex, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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) { + 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) { + if (!*dialog_ex) + { FURI_LOG_E(EASY_TAG, "Failed to allocate DialogEx"); return false; } - if(header) { + if (header) + { dialog_ex_set_header(*dialog_ex, header, header_x, header_y, AlignLeft, AlignTop); } - if(text) { + if (text) + { dialog_ex_set_text(*dialog_ex, text, text_x, text_y, AlignLeft, AlignTop); } - if(left_button_text) { + if (left_button_text) + { dialog_ex_set_left_button_text(*dialog_ex, left_button_text); } - if(right_button_text) { + if (right_button_text) + { dialog_ex_set_right_button_text(*dialog_ex, right_button_text); } - if(center_button_text) { + if (center_button_text) + { dialog_ex_set_center_button_text(*dialog_ex, center_button_text); } - if(result_callback) { + if (result_callback) + { dialog_ex_set_result_callback(*dialog_ex, result_callback); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(dialog_ex_get_view(*dialog_ex), previous_callback); } - if(context) { + if (context) + { dialog_ex_set_context(*dialog_ex, context); } view_dispatcher_add_view(*view_dispatcher, view_id, dialog_ex_get_view(*dialog_ex)); @@ -422,40 +486,48 @@ bool easy_flipper_set_dialog_ex( * @return true if successful, false otherwise */ bool easy_flipper_set_popup( - Popup** popup, + Popup **popup, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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) { + 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) { + if (!*popup) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Popup"); return false; } - if(header) { + if (header) + { popup_set_header(*popup, header, header_x, header_y, AlignLeft, AlignTop); } - if(text) { + if (text) + { popup_set_text(*popup, text, text_x, text_y, AlignLeft, AlignTop); } - if(result_callback) { + if (result_callback) + { popup_set_callback(*popup, result_callback); } - if(previous_callback) { + if (previous_callback) + { view_set_previous_callback(popup_get_view(*popup), previous_callback); } - if(context) { + if (context) + { popup_set_context(*popup, context); } view_dispatcher_add_view(*view_dispatcher, view_id, popup_get_view(*popup)); @@ -471,20 +543,24 @@ bool easy_flipper_set_popup( * @return true if successful, false otherwise */ bool easy_flipper_set_loading( - Loading** loading, + Loading **loading, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher) { - if(!loading) { + 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) { + if (!*loading) + { FURI_LOG_E(EASY_TAG, "Failed to allocate Loading"); return false; } - if(previous_callback) { + 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)); @@ -497,16 +573,55 @@ bool easy_flipper_set_loading( * @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) { +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) { + if (!furi_string) + { FURI_LOG_E(EASY_TAG, "Failed to allocate FuriString"); return false; } furi_string_set_str(*furi_string, buffer); return true; } + +bool easy_flipper_set_text_box( + TextBox **text_box, + int32_t view_id, + char *text, + bool start_at_end, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher) +{ + if (!text_box) + { + FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_text_box"); + return false; + } + *text_box = text_box_alloc(); + if (!*text_box) + { + FURI_LOG_E(EASY_TAG, "Failed to allocate TextBox"); + return false; + } + if (text) + { + text_box_set_text(*text_box, text); + } + if (previous_callback) + { + view_set_previous_callback(text_box_get_view(*text_box), previous_callback); + } + text_box_set_font(*text_box, TextBoxFontText); + if (start_at_end) + { + text_box_set_focus(*text_box, TextBoxFocusEnd); + } + view_dispatcher_add_view(*view_dispatcher, view_id, text_box_get_view(*text_box)); + return true; +} \ No newline at end of file diff --git a/web_crawler/easy_flipper/easy_flipper.h b/web_crawler/easy_flipper/easy_flipper.h index 219e42c74..1da7471de 100644 --- a/web_crawler/easy_flipper/easy_flipper.h +++ b/web_crawler/easy_flipper/easy_flipper.h @@ -8,6 +8,7 @@ #include <gui/view.h> #include <gui/modules/submenu.h> #include <gui/view_dispatcher.h> +#include <gui/elements.h> #include <gui/modules/menu.h> #include <gui/modules/submenu.h> #include <gui/modules/widget.h> @@ -21,23 +22,28 @@ #include <gui/modules/loading.h> #include <stdio.h> #include <string.h> +#include <jsmn/jsmn_furi.h> #include <jsmn/jsmn.h> #define EASY_TAG "EasyFlipper" +void easy_flipper_dialog( + char *header, + char *text); + /** * @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); +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); +bool easy_flipper_set_buffer(char **buffer, uint32_t buffer_size); /** * @brief Initialize a View object * @param view The View object to initialize @@ -49,13 +55,13 @@ bool easy_flipper_set_buffer(char** buffer, uint32_t buffer_size); * @return true if successful, false otherwise */ bool easy_flipper_set_view( - View** 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); + 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 @@ -64,7 +70,7 @@ bool easy_flipper_set_view( * @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); +bool easy_flipper_set_view_dispatcher(ViewDispatcher **view_dispatcher, Gui *gui, void *context); /** * @brief Initialize a Submenu object @@ -77,11 +83,11 @@ bool easy_flipper_set_view_dispatcher(ViewDispatcher** view_dispatcher, Gui* gui * @return true if successful, false otherwise */ bool easy_flipper_set_submenu( - Submenu** submenu, + Submenu **submenu, int32_t view_id, - char* title, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + char *title, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a Menu object @@ -94,10 +100,10 @@ bool easy_flipper_set_submenu( * @return true if successful, false otherwise */ bool easy_flipper_set_menu( - Menu** menu, + Menu **menu, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a Widget object @@ -109,11 +115,11 @@ bool easy_flipper_set_menu( * @return true if successful, false otherwise */ bool easy_flipper_set_widget( - Widget** widget, + Widget **widget, int32_t view_id, - char* text, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + char *text, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Initialize a VariableItemList object @@ -127,12 +133,12 @@ bool easy_flipper_set_widget( * @return true if successful, false otherwise */ bool easy_flipper_set_variable_item_list( - VariableItemList** 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); + void (*enter_callback)(void *, uint32_t), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a TextInput object @@ -143,15 +149,15 @@ bool easy_flipper_set_variable_item_list( * @return true if successful, false otherwise */ bool easy_flipper_set_text_input( - TextInput** text_input, + TextInput **text_input, int32_t view_id, - char* header_text, - char* text_input_temp_buffer, + 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); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a TextInput object with extra symbols @@ -162,15 +168,15 @@ bool easy_flipper_set_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_uart_text_input( - TextInput** uart_text_input, + TextInput **uart_text_input, int32_t view_id, - char* header_text, - char* uart_text_input_temp_buffer, + 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); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a DialogEx object @@ -192,21 +198,21 @@ bool easy_flipper_set_uart_text_input( * @return true if successful, false otherwise */ bool easy_flipper_set_dialog_ex( - DialogEx** dialog_ex, + DialogEx **dialog_ex, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + 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); + 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 @@ -225,18 +231,18 @@ bool easy_flipper_set_dialog_ex( * @return true if successful, false otherwise */ bool easy_flipper_set_popup( - Popup** popup, + Popup **popup, int32_t view_id, - char* header, + char *header, uint16_t header_x, uint16_t header_y, - char* text, + char *text, uint16_t text_x, uint16_t text_y, - void (*result_callback)(void*), - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher, - void* context); + void (*result_callback)(void *), + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher, + void *context); /** * @brief Initialize a Loading object @@ -247,10 +253,10 @@ bool easy_flipper_set_popup( * @return true if successful, false otherwise */ bool easy_flipper_set_loading( - Loading** loading, + Loading **loading, int32_t view_id, - uint32_t(previous_callback)(void*), - ViewDispatcher** view_dispatcher); + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); /** * @brief Set a char butter to a FuriString @@ -258,6 +264,24 @@ bool easy_flipper_set_loading( * @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); +bool easy_flipper_set_char_to_furi_string(FuriString **furi_string, char *buffer); + +/** + * @brief Initialize a TextBox object + * @param text_box The TextBox object to initialize + * @param view_id The ID/Index of the view + * @param text The text to display in the text box + * @param start_at_end Start the text box at the end + * @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_box( + TextBox **text_box, + int32_t view_id, + char *text, + bool start_at_end, + uint32_t(previous_callback)(void *), + ViewDispatcher **view_dispatcher); -#endif +#endif \ No newline at end of file diff --git a/web_crawler/flip_storage/web_crawler_storage.c b/web_crawler/flip_storage/web_crawler_storage.c index 14df53854..5136bfee2 100644 --- a/web_crawler/flip_storage/web_crawler_storage.c +++ b/web_crawler/flip_storage/web_crawler_storage.c @@ -2,88 +2,98 @@ // Function to save settings: path, SSID, and password void save_settings( - const char* path, - const char* ssid, - const char* password, - const char* file_rename, - const char* file_type, - const char* http_method, - const char* headers, - const char* payload) { + const char *path, + const char *ssid, + const char *password, + const char *file_rename, + const char *file_type, + const char *http_method, + const char *headers, + const char *payload) +{ // Create the directory for saving settings char directory_path[256]; - snprintf( - directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler"); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler"); // Create the directory - Storage* storage = furi_record_open(RECORD_STORAGE); + Storage *storage = furi_record_open(RECORD_STORAGE); storage_common_mkdir(storage, directory_path); // Open the settings file - File* file = storage_file_alloc(storage); - if(!storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + File *file = storage_file_alloc(storage); + if (!storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) + { FURI_LOG_E(TAG, "Failed to open settings file for writing: %s", SETTINGS_PATH); storage_file_free(file); furi_record_close(RECORD_STORAGE); return; } - if(file_type == NULL || strlen(file_type) == 0) { + if (file_type == NULL || strlen(file_type) == 0) + { file_type = ".txt"; } // Save the SSID length and data size_t ssid_length = strlen(ssid) + 1; // Include null terminator - if(storage_file_write(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, ssid, ssid_length) != ssid_length) { + if (storage_file_write(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, ssid, ssid_length) != ssid_length) + { FURI_LOG_E(TAG, "Failed to write SSID"); } // Save the password length and data size_t password_length = strlen(password) + 1; // Include null terminator - if(storage_file_write(file, &password_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, password, password_length) != password_length) { + if (storage_file_write(file, &password_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, password, password_length) != password_length) + { FURI_LOG_E(TAG, "Failed to write password"); } // Save the path length and data size_t path_length = strlen(path) + 1; // Include null terminator - if(storage_file_write(file, &path_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, path, path_length) != path_length) { + if (storage_file_write(file, &path_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, path, path_length) != path_length) + { FURI_LOG_E(TAG, "Failed to write path"); } // Save the file rename length and data size_t file_rename_length = strlen(file_rename) + 1; // Include null terminator - if(storage_file_write(file, &file_rename_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, file_rename, file_rename_length) != file_rename_length) { + if (storage_file_write(file, &file_rename_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, file_rename, file_rename_length) != file_rename_length) + { FURI_LOG_E(TAG, "Failed to write file rename"); } // Save the file type length and data size_t file_type_length = strlen(file_type) + 1; // Include null terminator - if(storage_file_write(file, &file_type_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, file_type, file_type_length) != file_type_length) { + if (storage_file_write(file, &file_type_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, file_type, file_type_length) != file_type_length) + { FURI_LOG_E(TAG, "Failed to write file type"); } // Save the http method length and data size_t http_method_length = strlen(http_method) + 1; // Include null terminator - if(storage_file_write(file, &http_method_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, http_method, http_method_length) != http_method_length) { + if (storage_file_write(file, &http_method_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, http_method, http_method_length) != http_method_length) + { FURI_LOG_E(TAG, "Failed to write http method"); } // Save the headers length and data size_t headers_length = strlen(headers) + 1; // Include null terminator - if(storage_file_write(file, &headers_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, headers, headers_length) != headers_length) { + if (storage_file_write(file, &headers_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, headers, headers_length) != headers_length) + { FURI_LOG_E(TAG, "Failed to write headers"); } // Save the payload length and data size_t payload_length = strlen(payload) + 1; // Include null terminator - if(storage_file_write(file, &payload_length, sizeof(size_t)) != sizeof(size_t) || - storage_file_write(file, payload, payload_length) != payload_length) { + if (storage_file_write(file, &payload_length, sizeof(size_t)) != sizeof(size_t) || + storage_file_write(file, payload, payload_length) != payload_length) + { FURI_LOG_E(TAG, "Failed to write payload"); } @@ -94,31 +104,34 @@ void save_settings( // Function to load settings (the variables must be opened in the order they were saved) bool load_settings( - char* path, + char *path, size_t path_size, - char* ssid, + char *ssid, size_t ssid_size, - char* password, + char *password, size_t password_size, - char* file_rename, + char *file_rename, size_t file_rename_size, - char* file_type, + char *file_type, size_t file_type_size, - char* http_method, + char *http_method, size_t http_method_size, - char* headers, + char *headers, size_t headers_size, - char* payload, + char *payload, size_t payload_size, - WebCrawlerApp* app) { - if(!app) { + WebCrawlerApp *app) +{ + if (!app) + { FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); return false; } - Storage* storage = furi_record_open(RECORD_STORAGE); - File* file = storage_file_alloc(storage); + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); - if(!storage_file_open(file, SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) { + if (!storage_file_open(file, SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) + { FURI_LOG_E(TAG, "Failed to open settings file for reading: %s", SETTINGS_PATH); storage_file_free(file); furi_record_close(RECORD_STORAGE); @@ -127,8 +140,9 @@ bool load_settings( // Load the SSID size_t ssid_length; - if(storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || - ssid_length > ssid_size || storage_file_read(file, ssid, ssid_length) != ssid_length) { + if (storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || ssid_length > ssid_size || + storage_file_read(file, ssid, ssid_length) != ssid_length) + { FURI_LOG_E(TAG, "Failed to read SSID"); storage_file_close(file); storage_file_free(file); @@ -139,9 +153,9 @@ bool load_settings( // Load the password size_t password_length; - if(storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || - password_length > password_size || - storage_file_read(file, password, password_length) != password_length) { + if (storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || password_length > password_size || + storage_file_read(file, password, password_length) != password_length) + { FURI_LOG_E(TAG, "Failed to read password"); storage_file_close(file); storage_file_free(file); @@ -152,8 +166,9 @@ bool load_settings( // Load the path size_t path_length; - if(storage_file_read(file, &path_length, sizeof(size_t)) != sizeof(size_t) || - path_length > path_size || storage_file_read(file, path, path_length) != path_length) { + if (storage_file_read(file, &path_length, sizeof(size_t)) != sizeof(size_t) || path_length > path_size || + storage_file_read(file, path, path_length) != path_length) + { FURI_LOG_E(TAG, "Failed to read path"); storage_file_close(file); storage_file_free(file); @@ -164,9 +179,9 @@ bool load_settings( // Load the file rename size_t file_rename_length; - if(storage_file_read(file, &file_rename_length, sizeof(size_t)) != sizeof(size_t) || - file_rename_length > file_rename_size || - storage_file_read(file, file_rename, file_rename_length) != file_rename_length) { + if (storage_file_read(file, &file_rename_length, sizeof(size_t)) != sizeof(size_t) || file_rename_length > file_rename_size || + storage_file_read(file, file_rename, file_rename_length) != file_rename_length) + { FURI_LOG_E(TAG, "Failed to read file rename"); storage_file_close(file); storage_file_free(file); @@ -177,9 +192,9 @@ bool load_settings( // Load the file type size_t file_type_length; - if(storage_file_read(file, &file_type_length, sizeof(size_t)) != sizeof(size_t) || - file_type_length > file_type_size || - storage_file_read(file, file_type, file_type_length) != file_type_length) { + if (storage_file_read(file, &file_type_length, sizeof(size_t)) != sizeof(size_t) || file_type_length > file_type_size || + storage_file_read(file, file_type, file_type_length) != file_type_length) + { FURI_LOG_E(TAG, "Failed to read file type"); storage_file_close(file); storage_file_free(file); @@ -190,9 +205,9 @@ bool load_settings( // Load the http method size_t http_method_length; - if(storage_file_read(file, &http_method_length, sizeof(size_t)) != sizeof(size_t) || - http_method_length > http_method_size || - storage_file_read(file, http_method, http_method_length) != http_method_length) { + if (storage_file_read(file, &http_method_length, sizeof(size_t)) != sizeof(size_t) || http_method_length > http_method_size || + storage_file_read(file, http_method, http_method_length) != http_method_length) + { FURI_LOG_E(TAG, "Failed to read http method"); storage_file_close(file); storage_file_free(file); @@ -202,9 +217,9 @@ bool load_settings( // Load the headers size_t headers_length; - if(storage_file_read(file, &headers_length, sizeof(size_t)) != sizeof(size_t) || - headers_length > headers_size || - storage_file_read(file, headers, headers_length) != headers_length) { + if (storage_file_read(file, &headers_length, sizeof(size_t)) != sizeof(size_t) || headers_length > headers_size || + storage_file_read(file, headers, headers_length) != headers_length) + { FURI_LOG_E(TAG, "Failed to read headers"); storage_file_close(file); storage_file_free(file); @@ -214,9 +229,9 @@ bool load_settings( // Load the payload size_t payload_length; - if(storage_file_read(file, &payload_length, sizeof(size_t)) != sizeof(size_t) || - payload_length > payload_size || - storage_file_read(file, payload, payload_length) != payload_length) { + if (storage_file_read(file, &payload_length, sizeof(size_t)) != sizeof(size_t) || payload_length > payload_size || + storage_file_read(file, payload, payload_length) != payload_length) + { FURI_LOG_E(TAG, "Failed to read payload"); storage_file_close(file); storage_file_free(file); @@ -224,62 +239,56 @@ bool load_settings( return false; } - // set the path, ssid, and password - strncpy(app->path, path, path_size); - strncpy(app->ssid, ssid, ssid_size); - strncpy(app->password, password, password_size); - strncpy(app->file_rename, file_rename, file_rename_size); - strncpy(app->file_type, file_type, file_type_size); - strncpy(app->http_method, http_method, http_method_size); - strncpy(app->headers, headers, headers_size); - strncpy(app->payload, payload, payload_size); - storage_file_close(file); storage_file_free(file); furi_record_close(RECORD_STORAGE); return true; } -bool delete_received_data(WebCrawlerApp* app) { - if(app == NULL) { - FURI_LOG_E(TAG, "WebCrawlerApp is NULL"); - return false; - } +bool delete_received_data() +{ // Open the storage record - Storage* storage = furi_record_open(RECORD_STORAGE); - if(!storage) { - FURI_LOG_E(TAG, "Failed to open storage record"); - return false; - } + Storage *storage = furi_record_open(RECORD_STORAGE); + furi_check(storage, "Failed to open storage record"); - if(!storage_simply_remove_recursive(storage, RECEIVED_DATA_PATH "received_data.txt")) { + if (!storage_simply_remove_recursive(storage, RECEIVED_DATA_PATH "received_data.txt")) + { FURI_LOG_E(TAG, "Failed to delete main file"); furi_record_close(RECORD_STORAGE); return false; } // Allocate memory for new_path - char* new_path = malloc(256); - if(new_path == NULL) { + char *new_path = malloc(256); + if (new_path == NULL) + { FURI_LOG_E(TAG, "Memory allocation failed for paths"); free(new_path); return false; } - if(app->file_type == NULL || strlen(app->file_type) == 0) { - app->file_type = ".txt"; + char file_type[16]; + if (!load_char("file_type", file_type, sizeof(file_type))) + { + snprintf(file_type, sizeof(file_type), ".txt"); + } + char file_rename[128]; + if (!load_char("file_rename", file_rename, sizeof(file_rename))) + { + snprintf(file_rename, sizeof(file_rename), "received_data"); } // Format the new_path - int ret_new = - snprintf(new_path, 256, "%s%s%s", RECEIVED_DATA_PATH, app->file_rename, app->file_type); - if(ret_new < 0 || (size_t)ret_new >= 256) { + int ret_new = snprintf(new_path, 256, "%s%s%s", RECEIVED_DATA_PATH, file_rename, file_type); + if (ret_new < 0 || (size_t)ret_new >= 256) + { FURI_LOG_E(TAG, "Failed to create new_path"); free(new_path); return false; } - if(!storage_simply_remove_recursive(storage, new_path)) { + if (!storage_simply_remove_recursive(storage, new_path)) + { FURI_LOG_E(TAG, "Failed to delete duplicate file"); furi_record_close(RECORD_STORAGE); return false; @@ -290,37 +299,39 @@ bool delete_received_data(WebCrawlerApp* app) { return true; } -bool rename_received_data( - const char* old_name, - const char* new_name, - const char* file_type, - const char* old_file_type) { +bool rename_received_data(const char *old_name, const char *new_name, const char *file_type, const char *old_file_type) +{ // Open the storage record - Storage* storage = furi_record_open(RECORD_STORAGE); - if(!storage) { + Storage *storage = furi_record_open(RECORD_STORAGE); + if (!storage) + { FURI_LOG_E(TAG, "Failed to open storage record"); return false; } // Allocate memory for old_path and new_path - char* new_path = malloc(256); - char* old_path = malloc(256); - if(new_path == NULL || old_path == NULL) { + char *new_path = malloc(256); + char *old_path = malloc(256); + if (new_path == NULL || old_path == NULL) + { FURI_LOG_E(TAG, "Memory allocation failed for paths"); free(old_path); free(new_path); return false; } - if(file_type == NULL || strlen(file_type) == 0) { + if (file_type == NULL || strlen(file_type) == 0) + { file_type = ".txt"; } - if(old_file_type == NULL || strlen(old_file_type) == 0) { + if (old_file_type == NULL || strlen(old_file_type) == 0) + { old_file_type = ".txt"; } // Format the old_path int ret_old = snprintf(old_path, 256, "%s%s%s", RECEIVED_DATA_PATH, old_name, old_file_type); - if(ret_old < 0 || (size_t)ret_old >= 256) { + if (ret_old < 0 || (size_t)ret_old >= 256) + { FURI_LOG_E(TAG, "Failed to create old_path"); free(old_path); free(new_path); @@ -329,7 +340,8 @@ bool rename_received_data( // Format the new_path int ret_new = snprintf(new_path, 256, "%s%s%s", RECEIVED_DATA_PATH, new_name, file_type); - if(ret_new < 0 || (size_t)ret_new >= 256) { + if (ret_new < 0 || (size_t)ret_new >= 256) + { FURI_LOG_E(TAG, "Failed to create new_path"); free(old_path); free(new_path); @@ -337,25 +349,122 @@ bool rename_received_data( } // Check if the file exists - if(!storage_file_exists(storage, old_path)) { - if(!storage_file_exists(storage, RECEIVED_DATA_PATH "received_data.txt")) { + if (!storage_file_exists(storage, old_path)) + { + if (!storage_file_exists(storage, RECEIVED_DATA_PATH "received_data.txt")) + { FURI_LOG_E(TAG, "No saved file exists"); free(old_path); free(new_path); furi_record_close(RECORD_STORAGE); return false; - } else { - bool renamed = - storage_common_copy(storage, RECEIVED_DATA_PATH "received_data.txt", new_path) == - FSE_OK; + } + else + { + bool renamed = storage_common_copy(storage, RECEIVED_DATA_PATH "received_data.txt", new_path) == FSE_OK; furi_record_close(RECORD_STORAGE); return renamed; } - } else { + } + else + { bool renamed = storage_common_rename(storage, old_path, new_path) == FSE_OK; storage_simply_remove_recursive(storage, old_path); furi_record_close(RECORD_STORAGE); return renamed; } } + +bool save_char( + const char *path_name, const char *value) +{ + if (!value) + { + return false; + } + // Create the directory for saving settings + char directory_path[256]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler"); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler/data"); + storage_common_mkdir(storage, directory_path); + + // Open the settings file + File *file = storage_file_alloc(storage); + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler/data/%s.txt", path_name); + + // 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; + } + + // Write the data to the file + size_t data_size = strlen(value) + 1; // Include null terminator + if (storage_file_write(file, value, 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; +} + +bool load_char( + const char *path_name, + char *value, + size_t value_size) +{ + if (!value) + { + return false; + } + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); + + char file_path[256]; + snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler/data/%s.txt", path_name); + + // 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 false; // Return false if the file does not exist + } + + // Read data into the buffer + size_t read_count = storage_file_read(file, value, value_size); + if (storage_file_get_error(file) != FSE_OK) + { + FURI_LOG_E(HTTP_TAG, "Error reading from file."); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + + // Ensure null-termination + value[read_count - 1] = '\0'; + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + return strlen(value) > 0; +} diff --git a/web_crawler/flip_storage/web_crawler_storage.h b/web_crawler/flip_storage/web_crawler_storage.h index 5e4e634fa..e6b1548c1 100644 --- a/web_crawler/flip_storage/web_crawler_storage.h +++ b/web_crawler/flip_storage/web_crawler_storage.h @@ -1,49 +1,49 @@ -#ifndef WEB_CRAWLER_STORAGE_H -#define WEB_CRAWLER_STORAGE_H +#pragma once #include <web_crawler.h> #include <furi.h> #include <storage/storage.h> #define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/settings.bin" -#define RECEIVED_DATA_PATH \ - STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag \ - "/" // add the file name to the end (e.g. "received_data.txt") +#define RECEIVED_DATA_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/" // add the file name to the end (e.g. "received_data.txt") // Function to save settings: path, SSID, and password void save_settings( - const char* path, - const char* ssid, - const char* password, - const char* file_rename, - const char* file_type, - const char* http_method, - const char* headers, - const char* payload); + const char *path, + const char *ssid, + const char *password, + const char *file_rename, + const char *file_type, + const char *http_method, + const char *headers, + const char *payload); // Function to load settings (the variables must be opened in the order they were saved) bool load_settings( - char* path, + char *path, size_t path_size, - char* ssid, + char *ssid, size_t ssid_size, - char* password, + char *password, size_t password_size, - char* file_rename, + char *file_rename, size_t file_rename_size, - char* file_type, + char *file_type, size_t file_type_size, - char* http_method, + char *http_method, size_t http_method_size, - char* headers, + char *headers, size_t headers_size, - char* payload, + char *payload, size_t payload_size, - WebCrawlerApp* app); + WebCrawlerApp *app); -bool delete_received_data(WebCrawlerApp* app); -bool rename_received_data( - const char* old_name, - const char* new_name, - const char* file_type, - const char* old_file_type); -#endif // WEB_CRAWLER_STORAGE_H +bool delete_received_data(); +bool rename_received_data(const char *old_name, const char *new_name, const char *file_type, const char *old_file_type); + +bool save_char( + const char *path_name, const char *value); + +bool load_char( + const char *path_name, + char *value, + size_t value_size); diff --git a/web_crawler/flipper_http/flipper_http.c b/web_crawler/flipper_http/flipper_http.c index 1c689aaa2..bbcbbe55b 100644 --- a/web_crawler/flipper_http/flipper_http.c +++ b/web_crawler/flipper_http/flipper_http.c @@ -1,22 +1,27 @@ -#include <flipper_http/flipper_http.h> -FlipperHTTP fhttp; -char rx_line_buffer[RX_LINE_BUFFER_SIZE]; -uint8_t file_buffer[FILE_BUFFER_SIZE]; -size_t file_buffer_len = 0; +// Description: Flipper HTTP API (For use with Flipper Zero and the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP) +// License: MIT +// Author: JBlanked +// File: flipper_http.c +#include <flipper_http/flipper_http.h> // change this to where flipper_http.h is located + // 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, + 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); + char *file_path) +{ + Storage *storage = furi_record_open(RECORD_STORAGE); + File *file = storage_file_alloc(storage); - if(start_new_file) { + 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)) { + 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); @@ -24,15 +29,19 @@ bool flipper_http_append_to_file( } } // Open the file in write mode - if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + 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 { + } + else + { // Open the file in append mode - if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_OPEN_APPEND)) { + 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); @@ -41,7 +50,8 @@ bool flipper_http_append_to_file( } // Write the data to the file - if(storage_file_write(file, data, data_size) != data_size) { + 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); @@ -55,32 +65,38 @@ bool flipper_http_append_to_file( return true; } -FuriString* flipper_http_load_from_file(char* file_path) { +FuriString *flipper_http_load_from_file(char *file_path) +{ // Open the storage record - Storage* storage = furi_record_open(RECORD_STORAGE); - if(!storage) { + 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) { + 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)) { + 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 + FURI_LOG_E(HTTP_TAG, "Failed to open file for reading: %s", file_path); + return NULL; } // Allocate a FuriString to hold the received data - FuriString* str_result = furi_string_alloc(); - if(!str_result) { + 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); @@ -92,8 +108,9 @@ FuriString* flipper_http_load_from_file(char* file_path) { furi_string_reset(str_result); // Define a buffer to hold the read data - uint8_t* buffer = (uint8_t*)malloc(MAX_FILE_SHOW); - if(!buffer) { + 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); @@ -104,7 +121,8 @@ FuriString* flipper_http_load_from_file(char* file_path) { // 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) { + 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); @@ -114,13 +132,105 @@ FuriString* flipper_http_load_from_file(char* file_path) { } // Append each byte to the FuriString - for(size_t i = 0; i < read_count; i++) { + 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; +} + +FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit) +{ + // 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); + FURI_LOG_E(HTTP_TAG, "Failed to open file for reading: %s", file_path); + return NULL; + } + + if (memmgr_get_free_heap() < limit) + { + FURI_LOG_E(HTTP_TAG, "Not enough heap to read file."); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Allocate a buffer to hold the read data + uint8_t *buffer = (uint8_t *)malloc(limit); + if (!buffer) + { + FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer"); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Allocate a FuriString with preallocated capacity + FuriString *str_result = furi_string_alloc(); + if (!str_result) + { + FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString"); + free(buffer); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + furi_string_reserve(str_result, limit); + + // Read data into the buffer + size_t read_count = storage_file_read(file, buffer, limit); + if (storage_file_get_error(file) != FSE_OK) + { + FURI_LOG_E(HTTP_TAG, "Error reading from file."); + furi_string_free(str_result); + free(buffer); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + if (read_count == 0) + { + FURI_LOG_E(HTTP_TAG, "No data read from file."); + furi_string_free(str_result); + free(buffer); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return NULL; + } + + // Append the entire buffer to FuriString in one operation + furi_string_cat_str(str_result, (char *)buffer); // Clean up storage_file_close(file); @@ -134,63 +244,86 @@ FuriString* flipper_http_load_from_file(char* file_path) { /** * @brief Worker thread to handle UART data asynchronously. * @return 0 - * @param context The context to pass to the callback. + * @param context The FlipperHTTP context. * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -int32_t flipper_http_worker(void* context) { - UNUSED(context); +int32_t flipper_http_worker(void *context) +{ + if (!context) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return -1; + } + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return -1; + } size_t rx_line_pos = 0; - while(1) { + while (1) + { uint32_t events = furi_thread_flags_wait( WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever); - if(events & WorkerEvtStop) { + if (events & WorkerEvtStop) + { break; } - if(events & WorkerEvtRxDone) { + if (events & WorkerEvtRxDone) + { // Continuously read from the stream buffer until it's empty - while(!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) { + 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); + size_t received = furi_stream_buffer_receive(fhttp->flipper_http_stream, &c, 1, 0); - if(received == 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) { + if (fhttp->save_bytes) + { // Add byte to the buffer - file_buffer[file_buffer_len++] = c; + fhttp->file_buffer[fhttp->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)) { + if (fhttp->file_buffer_len >= FILE_BUFFER_SIZE) + { + if (!flipper_http_append_to_file( + fhttp->file_buffer, + fhttp->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; + fhttp->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) { + 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 + if (c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1) + { + fhttp->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); + fhttp->handle_rx_line_cb(fhttp->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 + } + else + { + fhttp->rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer } } } @@ -203,21 +336,27 @@ int32_t flipper_http_worker(void* context) { /** * @brief Callback function for the GET timeout timer. * @return 0 - * @param context The context to pass to the callback. + * @param context The FlipperHTTP context. * @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."); +void get_timeout_timer_callback(void *context) +{ + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + FURI_LOG_E(HTTP_TAG, "Timeout reached 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; + 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; + fhttp->state = ISSUE; } // UART RX Handler Callback (Interrupt Context) @@ -226,164 +365,216 @@ void get_timeout_timer_callback(void* context) { * @return void * @param handle The UART handle. * @param event The event type. - * @param context The context to pass to the callback. + * @param context The FlipperHTTP context. * @note This function will handle received data asynchronously via the callback. */ void _flipper_http_rx_callback( - FuriHalSerialHandle* handle, + FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, - void* context) { - UNUSED(context); - if(event == FuriHalSerialRxEventData) { + void *context) +{ + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + 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); + 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. + * @return FlipperHTTP context if the UART was initialized successfully, NULL otherwise. * @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; +FlipperHTTP *flipper_http_alloc() +{ + FlipperHTTP *fhttp = (FlipperHTTP *)malloc(sizeof(FlipperHTTP)); + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to allocate FlipperHTTP."); + return NULL; } - fhttp.flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1); - if(!fhttp.flipper_http_stream) { + memset(fhttp, 0, sizeof(FlipperHTTP)); // Initialize allocated memory to zero + + 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; + free(fhttp); + return NULL; } - fhttp.rx_thread = furi_thread_alloc(); - if(!fhttp.rx_thread) { + 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_stream_buffer_free(fhttp->flipper_http_stream); + free(fhttp); + return NULL; } - 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); + 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); // Corrected context + furi_thread_set_callback(fhttp->rx_thread, flipper_http_worker); - fhttp.handle_rx_line_cb = callback; - fhttp.callback_context = context; + fhttp->handle_rx_line_cb = flipper_http_rx_callback; + fhttp->callback_context = fhttp; - furi_thread_start(fhttp.rx_thread); - fhttp.rx_thread_id = furi_thread_get_id(fhttp.rx_thread); + 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)) { + // 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; + // Cleanup resources + 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); + free(fhttp); + return NULL; } - fhttp.serial_handle = furi_hal_serial_control_acquire(UART_CH); - if(!fhttp.serial_handle) { + 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; + 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); + free(fhttp); + return NULL; } // Initialize UART with acquired handle - furi_hal_serial_init(fhttp.serial_handle, BAUDRATE); + furi_hal_serial_init(fhttp->serial_handle, BAUDRATE); // Enable RX direction - furi_hal_serial_enable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx); + 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); + // Start asynchronous RX with the corrected callback and context + furi_hal_serial_async_rx_start(fhttp->serial_handle, _flipper_http_rx_callback, fhttp, false); // Corrected context // Wait for the TX to complete to ensure UART is ready - furi_hal_serial_tx_wait_complete(fhttp.serial_handle); + furi_hal_serial_tx_wait_complete(fhttp->serial_handle); // Allocate the timer for handling timeouts - fhttp.get_timeout_timer = furi_timer_alloc( + fhttp->get_timeout_timer = furi_timer_alloc( get_timeout_timer_callback, // Callback function - FuriTimerTypeOnce, // One-shot timer - &fhttp // Context passed to callback + FuriTimerTypeOnce, // One-shot timer + fhttp // Corrected context ); - if(!fhttp.get_timeout_timer) { + 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; + 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); + free(fhttp); + return NULL; } // 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) { + 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; + // Cleanup resources + furi_timer_free(fhttp->get_timeout_timer); + 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); + free(fhttp); + return NULL; } + memset(fhttp->last_response, 0, RX_BUF_SIZE); // Initialize last_response + + fhttp->state = IDLE; // FURI_LOG_I(HTTP_TAG, "UART initialized successfully."); - return true; + return fhttp; } // Deinitialize UART /** * @brief Deinitialize UART. * @return void + * @param fhttp The FlipperHTTP context * @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) { +void flipper_http_free(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + 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); + 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); + 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); + furi_thread_flags_set(fhttp->rx_thread_id, WorkerEvtStop); // Wait for the thread to finish - furi_thread_join(fhttp.rx_thread); + furi_thread_join(fhttp->rx_thread); // Free the thread resources - furi_thread_free(fhttp.rx_thread); + furi_thread_free(fhttp->rx_thread); // Free the stream buffer - furi_stream_buffer_free(fhttp.flipper_http_stream); + 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; + 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; + if (fhttp->last_response) + { + free(fhttp->last_response); + fhttp->last_response = NULL; } + // Free the FlipperHTTP context + free(fhttp); + fhttp = NULL; + // FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully."); } @@ -391,41 +582,51 @@ void flipper_http_deinit() { /** * @brief Send data over UART with newline termination. * @return true if the data was sent successfully, false otherwise. + * @param fhttp The FlipperHTTP context * @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) { +bool flipper_http_send_data(FlipperHTTP *fhttp, const char *data) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } size_t data_length = strlen(data); - if(data_length == 0) { + 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."); + 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); - send_buffer[data_length] = '\n'; // Append newline + 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))) { + 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."; + 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); + 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; + fhttp->state = IDLE; return true; } @@ -433,18 +634,26 @@ bool flipper_http_send_data(const char* data) { /** * @brief Send a PING request to check if the Wifi Dev Board is connected. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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)) { +bool flipper_http_ping(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[PING]"; + if (!flipper_http_send_data(fhttp, 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; + fhttp->state = INACTIVE; // The response will be handled asynchronously via the callback return true; } @@ -453,11 +662,19 @@ bool flipper_http_ping() { /** * @brief Send a command to list available commands. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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)) { +bool flipper_http_list_commands(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[LIST]"; + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send LIST command."); return false; } @@ -470,11 +687,19 @@ bool flipper_http_list_commands() { /** * @brief Allow the LED to display while processing. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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)) { +bool flipper_http_led_on(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[LED/ON]"; + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send LED ON command."); return false; } @@ -487,11 +712,19 @@ bool flipper_http_led_on() { /** * @brief Disable the LED from displaying while processing. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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)) { +bool flipper_http_led_off(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[LED/OFF]"; + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send LED OFF command."); return false; } @@ -504,12 +737,20 @@ bool flipper_http_led_off() { /** * @brief Parse JSON data. * @return true if the JSON data was parsed successfully, false otherwise. + * @param fhttp The FlipperHTTP context * @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) { +bool flipper_http_parse_json(FlipperHTTP *fhttp, const char *key, const char *json_data) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + if (!key || !json_data) + { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json."); return false; } @@ -517,12 +758,14 @@ bool flipper_http_parse_json(const char* key, const char* json_data) { char buffer[256]; int ret = snprintf(buffer, sizeof(buffer), "[PARSE]{\"key\":\"%s\",\"json\":%s}", key, json_data); - if(ret < 0 || ret >= (int)sizeof(buffer)) { + 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)) { + if (!flipper_http_send_data(fhttp, buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse command."); return false; } @@ -535,13 +778,21 @@ bool flipper_http_parse_json(const char* key, const char* json_data) { /** * @brief Parse JSON array data. * @return true if the JSON array data was parsed successfully, false otherwise. + * @param fhttp The FlipperHTTP context * @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) { +bool flipper_http_parse_json_array(FlipperHTTP *fhttp, const char *key, int index, const char *json_data) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + if (!key || !json_data) + { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json_array."); return false; } @@ -554,12 +805,14 @@ bool flipper_http_parse_json_array(const char* key, int index, const char* json_ key, index, json_data); - if(ret < 0 || ret >= (int)sizeof(buffer)) { + 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)) { + if (!flipper_http_send_data(fhttp, buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse array command."); return false; } @@ -572,11 +825,19 @@ bool flipper_http_parse_json_array(const char* key, int index, const char* json_ /** * @brief Send a command to scan for WiFi networks. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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)) { +bool flipper_http_scan_wifi(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[WIFI/SCAN]"; + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi scan command."); return false; } @@ -589,22 +850,32 @@ bool flipper_http_scan_wifi() { /** * @brief Send a command to save WiFi settings. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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) { +bool flipper_http_save_wifi(FlipperHTTP *fhttp, const char *ssid, const char *password) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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)) { + 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)) { + if (!flipper_http_send_data(fhttp, buffer)) + { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command."); return false; } @@ -617,11 +888,19 @@ bool flipper_http_save_wifi(const char* ssid, const char* password) { /** * @brief Send a command to get the IP address of the WiFi Devboard * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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)) { +bool flipper_http_ip_address(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[IP/ADDRESS]"; + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send IP address command."); return false; } @@ -634,11 +913,19 @@ bool flipper_http_ip_address() { /** * @brief Send a command to get the IP address of the connected WiFi network. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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)) { +bool flipper_http_ip_wifi(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[WIFI/IP]"; + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi IP command."); return false; } @@ -651,11 +938,19 @@ bool flipper_http_ip_wifi() { /** * @brief Send a command to disconnect from WiFi. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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)) { +bool flipper_http_disconnect_wifi(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[WIFI/DISCONNECT]"; + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command."); return false; } @@ -668,11 +963,19 @@ bool flipper_http_disconnect_wifi() { /** * @brief Send a command to connect to WiFi. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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)) { +bool flipper_http_connect_wifi(FlipperHTTP *fhttp) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + const char *command = "[WIFI/CONNECT]"; + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command."); return false; } @@ -685,25 +988,35 @@ bool flipper_http_connect_wifi() { /** * @brief Send a GET request to the specified URL. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @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) { +bool flipper_http_get_request(FlipperHTTP *fhttp, const char *url) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + if (!url) + { FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request."); return false; } // 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)) { + 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)) { + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command."); return false; } @@ -711,32 +1024,43 @@ bool flipper_http_get_request(const char* url) { // 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 fhttp The FlipperHTTP context * @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) { +bool flipper_http_get_request_with_headers(FlipperHTTP *fhttp, const char *url, const char *headers) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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]; + char command[512]; int ret = snprintf( command, sizeof(command), "[GET/HTTP]{\"url\":\"%s\",\"headers\":%s}", url, headers); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); return false; } @@ -744,31 +1068,42 @@ bool flipper_http_get_request_with_headers(const char* url, const char* headers) // 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 fhttp The FlipperHTTP context * @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) { +bool flipper_http_get_request_bytes(FlipperHTTP *fhttp, const char *url, const char *headers) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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]; + char command[512]; int ret = snprintf( command, sizeof(command), "[GET/BYTES]{\"url\":\"%s\",\"headers\":%s}", url, headers); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers."); return false; } @@ -776,20 +1111,30 @@ bool flipper_http_get_request_bytes(const char* url, const char* headers) { // 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 fhttp The FlipperHTTP context * @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) { + FlipperHTTP *fhttp, + const char *url, + const char *headers, + const char *payload) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + if (!url || !headers || !payload) + { FURI_LOG_E( "FlipperHTTP", "Invalid arguments provided to flipper_http_post_request_with_headers."); @@ -797,7 +1142,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), @@ -805,13 +1150,15 @@ bool flipper_http_post_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); return false; } @@ -819,24 +1166,33 @@ bool flipper_http_post_request_with_headers( // 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 fhttp The FlipperHTTP context * @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) { +bool flipper_http_post_request_bytes(FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + 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]; + char command[512]; int ret = snprintf( command, sizeof(command), @@ -844,13 +1200,15 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data."); return false; } @@ -858,20 +1216,30 @@ bool flipper_http_post_request_bytes(const char* url, const char* headers, const // 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 fhttp The FlipperHTTP context * @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) { + FlipperHTTP *fhttp, + const char *url, + const char *headers, + const char *payload) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + if (!url || !headers || !payload) + { FURI_LOG_E( "FlipperHTTP", "Invalid arguments provided to flipper_http_put_request_with_headers."); return false; @@ -886,13 +1254,15 @@ bool flipper_http_put_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data."); return false; } @@ -900,20 +1270,30 @@ bool flipper_http_put_request_with_headers( // 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 fhttp The FlipperHTTP context * @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) { + FlipperHTTP *fhttp, + const char *url, + const char *headers, + const char *payload) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + if (!url || !headers || !payload) + { FURI_LOG_E( "FlipperHTTP", "Invalid arguments provided to flipper_http_delete_request_with_headers."); @@ -929,14 +1309,16 @@ bool flipper_http_delete_request_with_headers( url, headers, payload); - if(ret < 0 || ret >= (int)sizeof(command)) { + 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)) { + if (!flipper_http_send_data(fhttp, command)) + { FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data."); return false; } @@ -944,358 +1326,491 @@ bool flipper_http_delete_request_with_headers( // The response will be handled asynchronously via the callback return true; } +// Function to trim leading and trailing spaces and newlines from a constant string +static 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; +} + // 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. + * @param context The FlipperHTTP context. * @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) { +void flipper_http_rx_callback(const char *line, void *context) +{ + FlipperHTTP *fhttp = (FlipperHTTP *)context; + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + if (!line) + { 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') { + 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); + 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; + 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) { + 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); + furi_timer_restart(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS); - if(strstr(line, "[GET/END]") != NULL) { + 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) { + 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++) { + for (size_t i = 0; i <= fhttp->file_buffer_len - marker_len; i++) + { // Check if the marker is found - if(memcmp(&file_buffer[i], marker, marker_len) == 0) { + if (memcmp(&fhttp->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; + size_t remaining_len = fhttp->file_buffer_len - (i + marker_len); + memmove(&fhttp->file_buffer[i], &fhttp->file_buffer[i + marker_len], remaining_len); + fhttp->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)) { + if (fhttp->file_buffer_len > 0) + { + if (!flipper_http_append_to_file(fhttp->file_buffer, fhttp->file_buffer_len, false, fhttp->file_path)) + { FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); } - file_buffer_len = 0; + fhttp->file_buffer_len = 0; } } - fhttp.is_bytes_request = false; + 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)) { + 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; + fhttp->started_receiving_get = false; + fhttp->just_started_get = false; + fhttp->state = IDLE; return; } - if(!fhttp.just_started_get) { - fhttp.just_started_get = true; + 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) { + 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); + furi_timer_restart(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS); - if(strstr(line, "[POST/END]") != NULL) { + 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) { + 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++) { + for (size_t i = 0; i <= fhttp->file_buffer_len - marker_len; i++) + { // Check if the marker is found - if(memcmp(&file_buffer[i], marker, marker_len) == 0) { + if (memcmp(&fhttp->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; + size_t remaining_len = fhttp->file_buffer_len - (i + marker_len); + memmove(&fhttp->file_buffer[i], &fhttp->file_buffer[i + marker_len], remaining_len); + fhttp->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)) { + if (fhttp->file_buffer_len > 0) + { + if (!flipper_http_append_to_file(fhttp->file_buffer, fhttp->file_buffer_len, false, fhttp->file_path)) + { FURI_LOG_E(HTTP_TAG, "Failed to append data to file."); } - file_buffer_len = 0; + fhttp->file_buffer_len = 0; } } - fhttp.is_bytes_request = false; + 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)) { + 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; + fhttp->started_receiving_post = false; + fhttp->just_started_post = false; + fhttp->state = IDLE; return; } - if(!fhttp.just_started_post) { - fhttp.just_started_post = true; + 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) { + 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); + furi_timer_restart(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS); - if(strstr(line, "[PUT/END]") != NULL) { + 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; + 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)) { + 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; + fhttp->started_receiving_put = false; + fhttp->just_started_put = false; + fhttp->state = IDLE; return; } - if(!fhttp.just_started_put) { - fhttp.just_started_put = true; + 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) { + 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); + furi_timer_restart(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS); - if(strstr(line, "[DELETE/END]") != NULL) { + 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; + 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)) { + 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; + fhttp->started_receiving_delete = false; + fhttp->just_started_delete = false; + fhttp->state = IDLE; return; } - if(!fhttp.just_started_delete) { - fhttp.just_started_delete = true; + 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) { + if (strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL) + { FURI_LOG_I(HTTP_TAG, "Operation succeeded."); - } else if(strstr(line, "[INFO]") != NULL) { + } + 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; + if (fhttp->state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL) + { + fhttp->state = IDLE; } - } else if(strstr(line, "[GET/SUCCESS]") != NULL) { + } + 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->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; + fhttp->save_bytes = fhttp->is_bytes_request; + fhttp->just_started_bytes = true; + fhttp->file_buffer_len = 0; return; - } else if(strstr(line, "[POST/SUCCESS]") != NULL) { + } + 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->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; + fhttp->save_bytes = fhttp->is_bytes_request; + fhttp->just_started_bytes = true; + fhttp->file_buffer_len = 0; return; - } else if(strstr(line, "[PUT/SUCCESS]") != NULL) { + } + 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->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) { + } + 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->started_receiving_delete = true; + furi_timer_start(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp->state = RECEIVING; return; - } else if(strstr(line, "[DISCONNECTED]") != NULL) { + } + else if (strstr(line, "[DISCONNECTED]") != NULL) + { FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully."); - } else if(strstr(line, "[ERROR]") != NULL) { + } + else if (strstr(line, "[ERROR]") != NULL) + { FURI_LOG_E(HTTP_TAG, "Received error: %s", line); - fhttp.state = ISSUE; + fhttp->state = ISSUE; return; - } else if(strstr(line, "[PONG]") != NULL) { + } + 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; + 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; + if (fhttp->state == INACTIVE && strstr(line, "[PONG]") != NULL) + { + 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 + else if (fhttp->state == INACTIVE && strstr(line, "[PONG]") == NULL) + { + fhttp->state = INACTIVE; + } + else + { + fhttp->state = IDLE; } - - // 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 fhttp The FlipperHTTP context * @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 +bool flipper_http_process_response_async(FlipperHTTP *fhttp, bool (*http_request)(void), bool (*parse_json)(void)) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return false; + } + if (http_request()) // start the async request + { + furi_timer_start(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp->state = RECEIVING; + } + else { - 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) { + 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_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 fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, + bool (*http_request)(void), + bool (*parse_response)(void), + uint32_t success_view_id, + uint32_t failure_view_id, + ViewDispatcher **view_dispatcher) +{ + if (!fhttp) + { + FURI_LOG_E(HTTP_TAG, "Failed to get context."); + return; + } + if (fhttp->state == INACTIVE) + { + view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); + return; + } + 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(fhttp, 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); // comment this out if you experience a freeze +} \ No newline at end of file diff --git a/web_crawler/flipper_http/flipper_http.h b/web_crawler/flipper_http/flipper_http.h index 5fac400bb..408afbf05 100644 --- a/web_crawler/flipper_http/flipper_http.h +++ b/web_crawler/flipper_http/flipper_http.h @@ -1,7 +1,13 @@ -// flipper_http.h -#ifndef FLIPPER_HTTP_H -#define FLIPPER_HTTP_H - +// Description: Flipper HTTP API (For use with Flipper Zero and the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP) +// License: MIT +// Author: JBlanked +// File: flipper_http.h +#pragma once + +#include <gui/gui.h> +#include <gui/view.h> +#include <gui/view_dispatcher.h> +#include <gui/modules/loading.h> #include <furi.h> #include <furi_hal.h> #include <furi_hal_gpio.h> @@ -11,90 +17,92 @@ // STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext -#define HTTP_TAG "Web Crawler" // change this to your app name -#define http_tag "web_crawler" // change this to your app id -#define UART_CH (momentum_settings.uart_esp_channel) // UART channel +#define HTTP_TAG "Web Crawler" // change this to your app name +#define http_tag "web_crawler" // 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 2048 // 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 +#define BAUDRATE (115200) // UART baudrate +#define RX_BUF_SIZE 2048 // UART RX buffer size +#define RX_LINE_BUFFER_SIZE 4096 // UART RX line buffer size (increase for large responses) +#define MAX_FILE_SHOW 8192 // 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); +typedef void (*FlipperHTTP_Callback)(const char *line, void *context); // State variable to track the UART state -typedef enum { - INACTIVE, // Inactive state - IDLE, // Default state +typedef enum +{ + INACTIVE, // Inactive state + IDLE, // Default state RECEIVING, // Receiving data - SENDING, // Sending data - ISSUE, // Issue with connection + SENDING, // Sending data + ISSUE, // Issue with connection } SerialState; // Event Flags for UART Worker Thread -typedef enum { +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 +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 + 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 *last_response; char file_path[256]; // Path to save the received data // Timer-related members - FuriTimer* get_timeout_timer; // Timer for HTTP request timeout + 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 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 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 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 + 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; + 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 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; + char rx_line_buffer[RX_LINE_BUFFER_SIZE]; + uint8_t file_buffer[FILE_BUFFER_SIZE]; + size_t file_buffer_len; +} FlipperHTTP; // 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, + const void *data, size_t data_size, bool start_new_file, - char* file_path); + char *file_path); -FuriString* flipper_http_load_from_file(char* file_path); +FuriString *flipper_http_load_from_file(char *file_path); +FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit); // UART worker thread /** @@ -104,7 +112,7 @@ FuriString* flipper_http_load_from_file(char* file_path); * @note This function will handle received data asynchronously via the callback. */ // UART worker thread -int32_t flipper_http_worker(void* context); +int32_t flipper_http_worker(void *context); // Timer callback function /** @@ -113,7 +121,7 @@ int32_t flipper_http_worker(void* context); * @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); +void get_timeout_timer_callback(void *context); // UART RX Handler Callback (Interrupt Context) /** @@ -125,240 +133,276 @@ void get_timeout_timer_callback(void* context); * @note This function will handle received data asynchronously via the callback. */ void _flipper_http_rx_callback( - FuriHalSerialHandle* handle, + FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, - void* context); + 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. + * @return FlipperHTTP context if the UART was initialized successfully, NULL otherwise. * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_init(FlipperHTTP_Callback callback, void* context); +FlipperHTTP *flipper_http_alloc(); // Deinitialize UART /** * @brief Deinitialize UART. * @return void + * @param fhttp The FlipperHTTP context * @note This function will stop the asynchronous RX, release the serial handle, and free the resources. */ -void flipper_http_deinit(); +void flipper_http_free(FlipperHTTP *fhttp); // 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_send_data(FlipperHTTP *fhttp, 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. + * @param fhttp The FlipperHTTP context * @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(); +bool flipper_http_ping(FlipperHTTP *fhttp); // Function to list available commands /** * @brief Send a command to list available commands. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_list_commands(); +bool flipper_http_list_commands(FlipperHTTP *fhttp); // Function to turn on the LED /** * @brief Allow the LED to display while processing. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_led_on(); +bool flipper_http_led_on(FlipperHTTP *fhttp); // Function to turn off the LED /** * @brief Disable the LED from displaying while processing. * @return true if the request was successful, false otherwise. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_led_off(); +bool flipper_http_led_off(FlipperHTTP *fhttp); // Function to parse JSON data /** * @brief Parse JSON data. * @return true if the JSON data was parsed successfully, false otherwise. + * @param fhttp The FlipperHTTP context * @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); +bool flipper_http_parse_json(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_parse_json_array(FlipperHTTP *fhttp, 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_scan_wifi(); +bool flipper_http_scan_wifi(FlipperHTTP *fhttp); // 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_save_wifi(const char* ssid, const char* password); +bool flipper_http_save_wifi(FlipperHTTP *fhttp, 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_ip_address(); +bool flipper_http_ip_address(FlipperHTTP *fhttp); // 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_ip_wifi(); +bool flipper_http_ip_wifi(FlipperHTTP *fhttp); // 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_disconnect_wifi(); +bool flipper_http_disconnect_wifi(FlipperHTTP *fhttp); // 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. + * @param fhttp The FlipperHTTP context * @note The received data will be handled asynchronously via the callback. */ -bool flipper_http_connect_wifi(); +bool flipper_http_connect_wifi(FlipperHTTP *fhttp); // 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_get_request(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_get_request_with_headers(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_get_request_bytes(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context * @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); + FlipperHTTP *fhttp, + 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 fhttp The FlipperHTTP context * @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); +bool flipper_http_post_request_bytes(FlipperHTTP *fhttp, 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 fhttp The FlipperHTTP context * @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); + FlipperHTTP *fhttp, + 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 fhttp The FlipperHTTP context * @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); + FlipperHTTP *fhttp, + 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. + * @param context The FlipperHTTP context. * @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); +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 fhttp The FlipperHTTP context * @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)); +bool flipper_http_process_response_async(FlipperHTTP *fhttp, bool (*http_request)(void), bool (*parse_json)(void)); -#endif // FLIPPER_HTTP_H +/** + * @brief Perform a task while displaying a loading screen + * @param fhttp The FlipperHTTP context + * @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(FlipperHTTP *fhttp, + bool (*http_request)(void), + bool (*parse_response)(void), + uint32_t success_view_id, + uint32_t failure_view_id, + ViewDispatcher **view_dispatcher); diff --git a/web_crawler/html/html_furi.c b/web_crawler/html/html_furi.c new file mode 100644 index 000000000..083cf07e7 --- /dev/null +++ b/web_crawler/html/html_furi.c @@ -0,0 +1,288 @@ +#include <stdio.h> +#include <string.h> +#include <stdbool.h> +#include <html/html_furi.h> + +/* + * Checks if the substring of the FuriString starting at index `pos` + * matches the given C-string `needle`. + */ +static bool furi_string_sub_equals(FuriString *str, int pos, const char *needle) +{ + size_t needle_len = strlen(needle); + if ((size_t)pos + needle_len > furi_string_size(str)) + { + return false; + } + for (size_t i = 0; i < needle_len; i++) + { + if (furi_string_get_char(str, pos + i) != needle[i]) + { + return false; + } + } + return true; +} + +/* + * Parse the content for a given HTML tag <tag> in `html`, handling nested tags. + * Returns a newly allocated FuriString or NULL on error. + * + * @param tag e.g. "<p>" + * @param html The HTML string to parse. + * @param index The position in `html` from where to start searching. + */ +FuriString *html_furi_find_tag(const char *tag, FuriString *html, size_t index) +{ + int tag_len = strlen(tag); + if (tag_len < 3) + { + FURI_LOG_E("html_furi_parse", "Invalid tag length"); + return NULL; + } + + // Extract the tag name from <p> => "p" + int inner_len = tag_len - 2; // exclude '<' and '>' + char inner_tag[inner_len + 1]; + for (int i = 0; i < inner_len; i++) + { + inner_tag[i] = tag[i + 1]; + } + inner_tag[inner_len] = '\0'; + + // Build closing tag => "</p>" + char closing_tag[inner_len + 4]; + snprintf(closing_tag, sizeof(closing_tag), "</%s>", inner_tag); + + int html_len = furi_string_size(html); + + // Find the first occurrence of the opening tag + int open_tag_index = -1; + for (int i = index; i <= html_len - tag_len; i++) + { + if (furi_string_sub_equals(html, i, tag)) + { + open_tag_index = i; + break; + } + } + if (open_tag_index == -1) + { + // Tag not found + return NULL; + } + + // Content starts after the opening tag + int content_start = open_tag_index + tag_len; + + // Skip leading whitespace + while (content_start < html_len && furi_string_get_char(html, content_start) == ' ') + { + content_start++; + } + + // Find matching closing tag, accounting for nested tags + int depth = 1; + int i = content_start; + int matching_close_index = -1; + while (i <= html_len - 1) + { + if (furi_string_sub_equals(html, i, tag)) + { + depth++; + i += tag_len; + continue; + } + if (furi_string_sub_equals(html, i, closing_tag)) + { + depth--; + if (depth == 0) + { + matching_close_index = i; + break; + } + i += strlen(closing_tag); + continue; + } + i++; + } + + if (matching_close_index == -1) + { + // No matching close => return NULL or partial content as you choose + return NULL; + } + + // Copy the content between <tag>...</tag> + size_t content_length = matching_close_index - content_start; + + if (memmgr_get_free_heap() < (content_length + 1 + 1024)) + { + FURI_LOG_E("html_furi_parse", "Not enough heap to allocate result"); + return NULL; + } + + // Allocate and copy + FuriString *result = furi_string_alloc(); + furi_string_reserve(result, content_length + 1); + furi_string_set_n(result, html, content_start, content_length); + furi_string_trim(result); + return result; +} + +static FuriString *_html_furi_find_tag(const char *tag, FuriString *html, size_t index, int *out_next_index) +{ + // Clear next index in case of early return + *out_next_index = -1; + + int tag_len = strlen(tag); + if (tag_len < 3) + { + FURI_LOG_E("html_furi_parse", "Invalid tag length"); + return NULL; + } + + // Extract "p" from "<p>" + int inner_len = tag_len - 2; + char inner_tag[inner_len + 1]; + for (int i = 0; i < inner_len; i++) + { + inner_tag[i] = tag[i + 1]; + } + inner_tag[inner_len] = '\0'; + + // Create closing tag => "</p>" + char closing_tag[inner_len + 4]; + snprintf(closing_tag, sizeof(closing_tag), "</%s>", inner_tag); + + int html_len = furi_string_size(html); + + // 1) Find opening tag from `index`. + int open_tag_index = -1; + for (int i = index; i <= html_len - tag_len; i++) + { + if (furi_string_sub_equals(html, i, tag)) + { + open_tag_index = i; + break; + } + } + if (open_tag_index == -1) + { + return NULL; // no more occurrences + } + + // The content begins after the opening tag. + int content_start = open_tag_index + tag_len; + + // skip leading spaces + while (content_start < html_len && furi_string_get_char(html, content_start) == ' ') + { + content_start++; + } + + int depth = 1; + int i = content_start; + int matching_close_index = -1; + + while (i < html_len) + { + if (furi_string_sub_equals(html, i, tag)) + { + depth++; + i += tag_len; + } + else if (furi_string_sub_equals(html, i, closing_tag)) + { + depth--; + i += strlen(closing_tag); + if (depth == 0) + { + matching_close_index = i - strlen(closing_tag); + // i now points just after "</p>" + break; + } + } + else + { + i++; + } + } + + if (matching_close_index == -1) + { + // No matching close tag found + return NULL; + } + + size_t content_length = matching_close_index - content_start; + + // Allocate the result + FuriString *result = furi_string_alloc(); + furi_string_reserve(result, content_length + 1); // +1 for safety + furi_string_set_n(result, html, content_start, content_length); + furi_string_trim(result); + *out_next_index = i; + + return result; +} + +/* + * Parse *all* occurrences of <tag> in `html`, handling nested tags. + * Returns a FuriString concatenating all parsed contents. + */ +FuriString *html_furi_find_tags(const char *tag, FuriString *html) +{ + FuriString *result = furi_string_alloc(); + size_t index = 0; + + while (true) + { + int next_index; + FuriString *parsed = _html_furi_find_tag(tag, html, index, &next_index); + if (parsed == NULL) + { + // No more tags from 'index' onward + break; + } + + // Append the found content + furi_string_cat(result, parsed); + furi_string_cat_str(result, "\n"); + furi_string_free(parsed); + + // Resume searching at `next_index` (just after `</tag>`). + index = next_index; + } + + return result; +} + +/* + * @brief Check if an HTML tag exists in the provided HTML string. + * @param tag The HTML tag to search for (including the angle brackets). + * @param html The HTML string to search (as a FuriString). + * @param index The starting index to search from. + * @return True if the tag exists in the HTML string, false otherwise. + */ +bool html_furi_tag_exists(const char *tag, FuriString *html, size_t index) +{ + int tag_len = strlen(tag); + if (tag_len < 3) + { + FURI_LOG_E("html_furi_parse", "Invalid tag length"); + return false; + } + + int html_len = furi_string_size(html); + + for (int i = index; i <= html_len - tag_len; i++) + { + if (furi_string_sub_equals(html, i, tag)) + { + return true; + } + } + + return false; +} \ No newline at end of file diff --git a/web_crawler/html/html_furi.h b/web_crawler/html/html_furi.h new file mode 100644 index 000000000..a95adbecf --- /dev/null +++ b/web_crawler/html/html_furi.h @@ -0,0 +1,40 @@ +#pragma once +#include <furi.h> +#include <furi_hal.h> + +/* + * @brief Parse a Furigana string from an HTML tag, handling nested child tags. + * + * This version accepts an HTML tag as a C-string (e.g., "<p>") and searches + * for the content inside the corresponding opening and closing tags within + * the provided HTML string, taking into account nested occurrences of the tag. + * + * For example, given the HTML string: + * "<p><h1><p><h1>Test</h1></p></h1></p>" + * and searching with tag "<p>" the function will return: + * "<h1><p><h1>Test</h1></p></h1>" + * + * @param tag The HTML tag to parse (including the angle brackets). + * @param html The HTML string to parse (as a FuriString). + * @return A newly allocated FuriString containing the parsed content, + * or an empty FuriString if the tag is not found. + */ +FuriString *html_furi_find_tag(const char *tag, FuriString *html, size_t index); + +/* + * @brief Parse all Furigana strings from an HTML tag, handling nested child tags. + * @param tag The HTML tag to parse (including the angle brackets). + * @param html The HTML string to parse (as a FuriString). + * @return A newly allocated FuriString containing the parsed content, + * or an empty FuriString if the tag is not found. + */ +FuriString *html_furi_find_tags(const char *tag, FuriString *html); + +/* + * @brief Check if an HTML tag exists in the provided HTML string. + * @param tag The HTML tag to search for (including the angle brackets). + * @param html The HTML string to search (as a FuriString). + * @param index The starting index to search from. + * @return True if the tag exists in the HTML string, false otherwise. + */ +bool html_furi_tag_exists(const char *tag, FuriString *html, size_t index); \ No newline at end of file diff --git a/web_crawler/jsmn/jsmn.c b/web_crawler/jsmn/jsmn.c index eb33b3cc7..d101530c6 100644 --- a/web_crawler/jsmn/jsmn.c +++ b/web_crawler/jsmn/jsmn.c @@ -7,17 +7,17 @@ */ #include <jsmn/jsmn.h> -#include <stdlib.h> -#include <string.h> /** * 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; +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) { + if (parser->toknext >= num_tokens) + { return NULL; } tok = &tokens[parser->toknext++]; @@ -32,8 +32,9 @@ static jsmntok_t* /** * Fills token type and boundaries. */ -static void - jsmn_fill_token(jsmntok_t* token, const jsmntype_t type, const int start, const int end) { +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; @@ -43,19 +44,19 @@ static void /** * 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; +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]) { + 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 ':': @@ -72,7 +73,8 @@ static int jsmn_parse_primitive( /* to quiet a warning from gcc*/ break; } - if(js[parser->pos] < 32 || js[parser->pos] >= 127) { + if (js[parser->pos] < 32 || js[parser->pos] >= 127) + { parser->pos = start; return JSMN_ERROR_INVAL; } @@ -84,12 +86,14 @@ static int jsmn_parse_primitive( #endif found: - if(tokens == NULL) { + if (tokens == NULL) + { parser->pos--; return 0; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { parser->pos = start; return JSMN_ERROR_NOMEM; } @@ -104,29 +108,31 @@ static int jsmn_parse_primitive( /** * 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; +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++) { + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { char c = js[parser->pos]; /* Quote: end of string */ - if(c == '\"') { - if(tokens == NULL) { + if (c == '\"') + { + if (tokens == NULL) + { return 0; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { parser->pos = start; return JSMN_ERROR_NOMEM; } @@ -138,10 +144,12 @@ static int jsmn_parse_string( } /* Backslash: Quoted symbol expected */ - if(c == '\\' && parser->pos + 1 < len) { + if (c == '\\' && parser->pos + 1 < len) + { int i; parser->pos++; - switch(js[parser->pos]) { + switch (js[parser->pos]) + { /* Allowed escaped symbols */ case '\"': case '/': @@ -155,11 +163,13 @@ static int jsmn_parse_string( /* Allows escaped symbol \uXXXX */ case 'u': parser->pos++; - for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + 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 */ + 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; } @@ -181,7 +191,8 @@ static int jsmn_parse_string( /** * Create JSON parser over an array of tokens */ -void jsmn_init(jsmn_parser* parser) { +void jsmn_init(jsmn_parser *parser) +{ parser->pos = 0; parser->toknext = 0; parser->toksuper = -1; @@ -190,38 +201,41 @@ void jsmn_init(jsmn_parser* parser) { /** * 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 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; + jsmntok_t *token; int count = parser->toknext; - for(; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { char c; jsmntype_t type; c = js[parser->pos]; - switch(c) { + switch (c) + { case '{': case '[': count++; - if(tokens == NULL) { + if (tokens == NULL) + { break; } token = jsmn_alloc_token(parser, tokens, num_tokens); - if(token == NULL) { + if (token == NULL) + { return JSMN_ERROR_NOMEM; } - if(parser->toksuper != -1) { - jsmntok_t* t = &tokens[parser->toksuper]; + 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) { + if (t->type == JSMN_OBJECT) + { return JSMN_ERROR_INVAL; } #endif @@ -236,26 +250,33 @@ int jsmn_parse( break; case '}': case ']': - if(tokens == NULL) { + if (tokens == NULL) + { break; } type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); #ifdef JSMN_PARENT_LINKS - if(parser->toknext < 1) { + 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) { + 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) { + if (token->parent == -1) + { + if (token->type != type || parser->toksuper == -1) + { return JSMN_ERROR_INVAL; } break; @@ -263,10 +284,13 @@ int jsmn_parse( token = &tokens[token->parent]; } #else - for(i = parser->toknext - 1; i >= 0; i--) { + for (i = parser->toknext - 1; i >= 0; i--) + { token = &tokens[i]; - if(token->start != -1 && token->end == -1) { - if(token->type != type) { + if (token->start != -1 && token->end == -1) + { + if (token->type != type) + { return JSMN_ERROR_INVAL; } parser->toksuper = -1; @@ -275,12 +299,15 @@ int jsmn_parse( } } /* Error if unmatched closing bracket */ - if(i == -1) { + if (i == -1) + { return JSMN_ERROR_INVAL; } - for(; i >= 0; i--) { + for (; i >= 0; i--) + { token = &tokens[i]; - if(token->start != -1 && token->end == -1) { + if (token->start != -1 && token->end == -1) + { parser->toksuper = i; break; } @@ -289,11 +316,13 @@ int jsmn_parse( break; case '\"': r = jsmn_parse_string(parser, js, len, tokens, num_tokens); - if(r < 0) { + if (r < 0) + { return r; } count++; - if(parser->toksuper != -1 && tokens != NULL) { + if (parser->toksuper != -1 && tokens != NULL) + { tokens[parser->toksuper].size++; } break; @@ -306,15 +335,19 @@ int jsmn_parse( 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) { + 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) { + 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; } @@ -340,9 +373,12 @@ int jsmn_parse( 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)) { + 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; } } @@ -351,11 +387,13 @@ int jsmn_parse( default: #endif r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); - if(r < 0) { + if (r < 0) + { return r; } count++; - if(parser->toksuper != -1 && tokens != NULL) { + if (parser->toksuper != -1 && tokens != NULL) + { tokens[parser->toksuper].size++; } break; @@ -368,10 +406,13 @@ int jsmn_parse( } } - if(tokens != NULL) { - for(i = parser->toknext - 1; i >= 0; i--) { + 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) { + if (tokens[i].start != -1 && tokens[i].end == -1) + { return JSMN_ERROR_PART; } } @@ -381,10 +422,12 @@ int jsmn_parse( } // 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) { +char *get_json(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); @@ -392,30 +435,41 @@ 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) { - if(tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && - strncmp(json + tok->start, s, tok->end - tok->start) == 0) { +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) { +char *get_json_value(char *key, const char *json_data) +{ // Parse the JSON feed - if(json_data != NULL) { + if (json_data != NULL) + { jsmn_parser parser; jsmn_init(&parser); - + uint32_t max_tokens = json_token_count(json_data); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + return NULL; + } // Allocate tokens array on the heap - jsmntok_t* tokens = malloc(sizeof(jsmntok_t) * max_tokens); - if(tokens == NULL) { + 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) { + if (ret < 0) + { // Handle parsing errors FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); free(tokens); @@ -423,19 +477,23 @@ char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { } // Ensure that the root element is an object - if(ret < 1 || tokens[0].type != JSMN_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) { + 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) { + char *value = malloc(length + 1); + if (value == NULL) + { FURI_LOG_E("JSMM.H", "Failed to allocate memory for value."); free(tokens); return NULL; @@ -450,102 +508,146 @@ char* get_json_value(char* key, char* json_data, uint32_t max_tokens) { // Free the token array if key was not found free(tokens); - } else { + } + else + { FURI_LOG_E("JSMM.H", "JSON data is NULL"); } - FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON."); + char warning[128]; + snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key); + FURI_LOG_E("JSMM.H", warning); 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) { +// Helper function to skip a token and all its descendants. +// Returns the index of the next token after skipping this one. +// On error or out of bounds, returns -1. +static int skip_token(const jsmntok_t *tokens, int start, int total) +{ + if (start < 0 || start >= total) + return -1; + + int i = start; + if (tokens[i].type == JSMN_OBJECT) + { + // For an object: size is number of key-value pairs + int pairs = tokens[i].size; + i++; // move to first key-value pair + for (int p = 0; p < pairs; p++) + { + // skip key (primitive/string) + i++; + if (i >= total) + return -1; + // skip value (which could be object/array and must be skipped recursively) + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; // i is now just past the object + } + else if (tokens[i].type == JSMN_ARRAY) + { + // For an array: size is number of elements + int elems = tokens[i].size; + i++; // move to first element + for (int e = 0; e < elems; e++) + { + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; // i is now just past the array + } + else + { + // Primitive or string token, just skip it + return i + 1; + } +} + +// Revised get_json_array_value +char *get_json_array_value(char *key, uint32_t index, const char *json_data) +{ + // Always extract the full array each time from the original json_data + char *array_str = get_json_value(key, json_data); + if (array_str == NULL) + { FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); return NULL; } + uint32_t max_tokens = json_token_count(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + free(array_str); + 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) { + 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) { + 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) { + 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); + if (index >= (uint32_t)tokens[0].size) + { + // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", 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."); + // Find the index-th element: start from token[1], which is the first element + int elem_token = 1; + for (uint32_t i = 0; i < index; i++) + { + elem_token = skip_token(tokens, elem_token, ret); + if (elem_token == -1 || elem_token >= ret) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i); free(tokens); free(array_str); return NULL; } } - // Extract the array element - jsmntok_t element = tokens[current_token]; + // Now elem_token should point to the token of the requested element + jsmntok_t element = tokens[elem_token]; int length = element.end - element.start; - char* value = malloc(length + 1); - if(value == NULL) { + char *value = malloc(length + 1); + if (!value) + { 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 + value[length] = '\0'; - // Clean up free(tokens); free(array_str); @@ -553,21 +655,30 @@ char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t } // 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) { +char **get_json_array_values(char *key, char *json_data, 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) { + char *array_str = get_json_value(key, json_data); + if (array_str == NULL) + { FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); return NULL; } - + uint32_t max_tokens = json_token_count(array_str); + if (!jsmn_memory_check(max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + free(array_str); + 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) { + 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; @@ -575,7 +686,8 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // Parse the JSON array int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); - if(ret < 0) { + if (ret < 0) + { FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); free(tokens); free(array_str); @@ -583,7 +695,8 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in } // Ensure the root element is an array - if(tokens[0].type != JSMN_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); @@ -592,8 +705,9 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // 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) { + 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); @@ -604,15 +718,18 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in // 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) { + 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) { + 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; @@ -622,10 +739,12 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in 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) { + 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++) { + for (int j = 0; j < actual_num_values; j++) + { free(values[j]); } free(values); @@ -647,14 +766,17 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in *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) { + 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++) { + for (int i = actual_num_values; i < array_size; i++) + { free(values[i]); } } @@ -664,3 +786,18 @@ char** get_json_array_values(char* key, char* json_data, uint32_t max_tokens, in free(array_str); return values; } + +int json_token_count(const char *json) +{ + if (json == NULL) + { + return JSMN_ERROR_INVAL; + } + + jsmn_parser parser; + jsmn_init(&parser); + + // Pass NULL for tokens and 0 for num_tokens to get the token count only + int ret = jsmn_parse(&parser, json, strlen(json), NULL, 0); + return ret; // If ret >= 0, it represents the number of tokens needed. +} diff --git a/web_crawler/jsmn/jsmn.h b/web_crawler/jsmn/jsmn.h index cd95a0e58..5f96e5596 100644 --- a/web_crawler/jsmn/jsmn.h +++ b/web_crawler/jsmn/jsmn.h @@ -17,9 +17,11 @@ #define JSMN_H #include <stddef.h> +#include <jsmn/jsmn_h.h> #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif #ifdef JSMN_STATIC @@ -27,72 +29,17 @@ extern "C" { #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); + 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); + 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 */ @@ -109,23 +56,19 @@ JSMN_API int jsmn_parse( #define JB_JSMN_EDIT /* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ -#include <string.h> -#include <stdint.h> -#include <stdlib.h> -#include <stdio.h> -#include <furi.h> - // Helper function to create a JSON object -char* jsmn(const char* key, const char* value); +char *get_json(const char *key, const char *value); // Helper function to compare JSON keys -int jsoneq(const char* json, jsmntok_t* tok, const char* s); +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); +char *get_json_value(char *key, const char *json_data); // Revised get_json_array_value function -char* get_json_array_value(char* key, uint32_t index, char* json_data, uint32_t max_tokens); +char *get_json_array_value(char *key, uint32_t index, const char *json_data); // 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); +char **get_json_array_values(char *key, char *json_data, int *num_values); + +int json_token_count(const char *json); #endif /* JB_JSMN_EDIT */ diff --git a/web_crawler/jsmn/jsmn_furi.c b/web_crawler/jsmn/jsmn_furi.c new file mode 100644 index 000000000..0eab40c99 --- /dev/null +++ b/web_crawler/jsmn/jsmn_furi.c @@ -0,0 +1,736 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * [License text continues...] + */ + +#include <jsmn/jsmn_furi.h> + +// Forward declarations of helper functions +static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s); +static int skip_token(const jsmntok_t *tokens, int start, int total); + +/** + * 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) +{ + if (parser->toknext >= num_tokens) + { + return NULL; + } + jsmntok_t *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. + * Now uses FuriString to access characters. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const size_t num_tokens) +{ + size_t len = furi_string_size(js); + int start = parser->pos; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + switch (c) + { +#ifndef JSMN_STRICT + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + break; + } + if (c < 32 || c >= 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; + } + jsmntok_t *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. + * Now uses FuriString to access characters. + */ +static int jsmn_parse_string(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const size_t num_tokens) +{ + size_t len = furi_string_size(js); + int start = parser->pos; + parser->pos++; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + if (c == '\"') + { + if (tokens == NULL) + { + return 0; + } + jsmntok_t *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; + } + + if (c == '\\' && (parser->pos + 1) < len) + { + parser->pos++; + char esc = furi_string_get_char(js, parser->pos); + switch (esc) + { + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + case 'u': + { + parser->pos++; + for (int i = 0; i < 4 && parser->pos < len; i++) + { + char hex = furi_string_get_char(js, parser->pos); + if (!((hex >= '0' && hex <= '9') || + (hex >= 'A' && hex <= 'F') || + (hex >= 'a' && hex <= 'f'))) + { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + } + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Create JSON parser + */ +void jsmn_init_furi(jsmn_parser *parser) +{ + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +/** + * Parse JSON string and fill tokens. + * Now uses FuriString for the input JSON. + */ +int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const unsigned int num_tokens) +{ + size_t len = furi_string_size(js); + int r; + int i; + int count = parser->toknext; + + for (; parser->pos < len; parser->pos++) + { + char c = furi_string_get_char(js, parser->pos); + jsmntype_t type; + + switch (c) + { + case '{': + case '[': + { + count++; + if (tokens == NULL) + { + break; + } + jsmntok_t *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 + 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; + } + { + jsmntok_t *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 + { + jsmntok_t *token; + 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; + } + } + 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, 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 ' ': + // Whitespace - ignore + 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 + 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': + 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 + default: +#endif + r = jsmn_parse_primitive(parser, js, tokens, num_tokens); + if (r < 0) + return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + { + tokens[parser->toksuper].size++; + } + break; +#ifdef JSMN_STRICT + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) + { + for (i = parser->toknext - 1; i >= 0; i--) + { + if (tokens[i].start != -1 && tokens[i].end == -1) + { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +// Helper function to create a JSON object: {"key":"value"} +FuriString *get_json_furi(const FuriString *key, const FuriString *value) +{ + FuriString *result = furi_string_alloc(); + furi_string_printf(result, "{\"%s\":\"%s\"}", + furi_string_get_cstr(key), + furi_string_get_cstr(value)); + return result; // Caller responsible for furi_string_free +} + +// Helper function to compare JSON keys +static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s) +{ + size_t s_len = furi_string_size(s); + size_t tok_len = tok->end - tok->start; + + if (tok->type != JSMN_STRING) + return -1; + if (s_len != tok_len) + return -1; + + FuriString *sub = furi_string_alloc_set(json); + furi_string_mid(sub, tok->start, tok_len); + + int res = furi_string_cmp(sub, s); + furi_string_free(sub); + + return (res == 0) ? 0 : -1; +} + +// Skip a token and its descendants +static int skip_token(const jsmntok_t *tokens, int start, int total) +{ + if (start < 0 || start >= total) + return -1; + + int i = start; + if (tokens[i].type == JSMN_OBJECT) + { + int pairs = tokens[i].size; + i++; + for (int p = 0; p < pairs; p++) + { + i++; // skip key + if (i >= total) + return -1; + i = skip_token(tokens, i, total); // skip value + if (i == -1) + return -1; + } + return i; + } + else if (tokens[i].type == JSMN_ARRAY) + { + int elems = tokens[i].size; + i++; + for (int e = 0; e < elems; e++) + { + i = skip_token(tokens, i, total); + if (i == -1) + return -1; + } + return i; + } + else + { + return i + 1; + } +} + +/** + * Parse JSON and return the value associated with a given char* key. + */ +FuriString *get_json_value_furi(const char *key, const FuriString *json_data) +{ + if (json_data == NULL) + { + FURI_LOG_E("JSMM.H", "JSON data is NULL"); + return NULL; + } + uint32_t max_tokens = json_token_count_furi(json_data); + if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + return NULL; + } + // Create a temporary FuriString from key + FuriString *key_str = furi_string_alloc(); + furi_string_cat_str(key_str, key); + + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(key_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, json_data, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); + free(tokens); + furi_string_free(key_str); + return NULL; + } + + if (ret < 1 || tokens[0].type != JSMN_OBJECT) + { + FURI_LOG_E("JSMM.H", "Root element is not an object."); + free(tokens); + furi_string_free(key_str); + return NULL; + } + + for (int i = 1; i < ret; i++) + { + if (jsoneq_furi(json_data, &tokens[i], key_str) == 0) + { + int length = tokens[i + 1].end - tokens[i + 1].start; + FuriString *value = furi_string_alloc_set(json_data); + furi_string_mid(value, tokens[i + 1].start, length); + free(tokens); + furi_string_free(key_str); + return value; + } + } + + free(tokens); + furi_string_free(key_str); + char warning[128]; + snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key); + FURI_LOG_E("JSMM.H", warning); + return NULL; +} + +/** + * Return the value at a given index in a JSON array for a given char* key. + */ +FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data) +{ + FuriString *array_str = get_json_value_furi(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key"); + return NULL; + } + uint32_t max_tokens = json_token_count_furi(array_str); + if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (ret < 1 || tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key is not an array."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (index >= (uint32_t)tokens[0].size) + { + // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int elem_token = 1; + for (uint32_t i = 0; i < index; i++) + { + elem_token = skip_token(tokens, elem_token, ret); + if (elem_token == -1 || elem_token >= ret) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i); + free(tokens); + furi_string_free(array_str); + return NULL; + } + } + + jsmntok_t element = tokens[elem_token]; + int length = element.end - element.start; + + FuriString *value = furi_string_alloc_set(array_str); + furi_string_mid(value, element.start, length); + + free(tokens); + furi_string_free(array_str); + + return value; +} + +/** + * Extract all object values from a JSON array associated with a given char* key. + */ +FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values) +{ + *num_values = 0; + // Convert key to FuriString and call get_json_value_furi + FuriString *array_str = get_json_value_furi(key, json_data); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key"); + return NULL; + } + + uint32_t max_tokens = json_token_count_furi(array_str); + if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens)) + { + FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + jsmn_parser parser; + jsmn_init_furi(&parser); + + jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + furi_string_free(array_str); + return NULL; + } + + int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + if (tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key is not an array."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int array_size = tokens[0].size; + FuriString **values = (FuriString **)malloc(array_size * sizeof(FuriString *)); + if (values == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values."); + free(tokens); + furi_string_free(array_str); + return NULL; + } + + int actual_num_values = 0; + int current_token = 1; + 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]; + + int length = element.end - element.start; + FuriString *value = furi_string_alloc_set(array_str); + furi_string_mid(value, element.start, length); + + values[actual_num_values] = value; + actual_num_values++; + + // Skip this element and its descendants + current_token = skip_token(tokens, current_token, ret); + if (current_token == -1) + { + FURI_LOG_E("JSMM.H", "Error skipping tokens after element %d.", i); + break; + } + } + + *num_values = actual_num_values; + if (actual_num_values < array_size) + { + FuriString **reduced_values = (FuriString **)realloc(values, actual_num_values * sizeof(FuriString *)); + if (reduced_values != NULL) + { + values = reduced_values; + } + } + + free(tokens); + furi_string_free(array_str); + return values; +} + +uint32_t json_token_count_furi(const FuriString *json) +{ + if (json == NULL) + { + return JSMN_ERROR_INVAL; + } + + jsmn_parser parser; + jsmn_init_furi(&parser); + + // Pass NULL for tokens and 0 for num_tokens to get the token count only + int ret = jsmn_parse_furi(&parser, json, NULL, 0); + return ret; // If ret >= 0, it represents the number of tokens needed. +} diff --git a/web_crawler/jsmn/jsmn_furi.h b/web_crawler/jsmn/jsmn_furi.h new file mode 100644 index 000000000..cb01f38ca --- /dev/null +++ b/web_crawler/jsmn/jsmn_furi.h @@ -0,0 +1,74 @@ +/* + * 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_FURI_H +#define JSMN_FURI_H + +#include <jsmn/jsmn_h.h> + +#ifdef __cplusplus +extern "C" +{ +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + + JSMN_API void jsmn_init_furi(jsmn_parser *parser); + JSMN_API int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/* Implementation in jsmn_furi.c */ +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_FURI_H */ + +#ifndef JB_JSMN_FURI_EDIT +#define JB_JSMN_FURI_EDIT + +// Helper function to create a JSON object +FuriString *get_json_furi(const FuriString *key, const FuriString *value); + +// Updated signatures to accept const char* key +FuriString *get_json_value_furi(const char *key, const FuriString *json_data); +FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data); +FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values); + +uint32_t json_token_count_furi(const FuriString *json); +/* Example usage: +char *json = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; +FuriString *json_data = char_to_furi_string(json); +if (!json_data) +{ + FURI_LOG_E(TAG, "Failed to allocate FuriString"); + return -1; +} +FuriString *value = get_json_value_furi("key1", json_data, json_token_count_furi(json_data)); +if (value) +{ + FURI_LOG_I(TAG, "Value: %s", furi_string_get_cstr(value)); + furi_string_free(value); +} +furi_string_free(json_data); +*/ +#endif /* JB_JSMN_EDIT */ diff --git a/web_crawler/jsmn/jsmn_h.c b/web_crawler/jsmn/jsmn_h.c new file mode 100644 index 000000000..59480e6e6 --- /dev/null +++ b/web_crawler/jsmn/jsmn_h.c @@ -0,0 +1,15 @@ +#include <jsmn/jsmn_h.h> +FuriString *char_to_furi_string(const char *str) +{ + FuriString *furi_str = furi_string_alloc(); + if (!furi_str) + { + return NULL; + } + for (size_t i = 0; i < strlen(str); i++) + { + furi_string_push_back(furi_str, str[i]); + } + return furi_str; +} +bool jsmn_memory_check(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); } \ No newline at end of file diff --git a/web_crawler/jsmn/jsmn_h.h b/web_crawler/jsmn/jsmn_h.h new file mode 100644 index 000000000..97d53e7ff --- /dev/null +++ b/web_crawler/jsmn/jsmn_h.h @@ -0,0 +1,56 @@ +#pragma once +#include <furi.h> +#include <stddef.h> +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +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 +{ + JSMN_ERROR_NOMEM = -1, + JSMN_ERROR_INVAL = -2, + JSMN_ERROR_PART = -3 +}; + +typedef struct +{ + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +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; + +typedef struct +{ + char *key; + char *value; +} JSON; + +typedef struct +{ + FuriString *key; + FuriString *value; +} FuriJSON; + +FuriString *char_to_furi_string(const char *str); + +// check memory +bool jsmn_memory_check(size_t heap_size); diff --git a/web_crawler/web_crawler.c b/web_crawler/web_crawler.c index eded0741d..1d423b5a8 100644 --- a/web_crawler/web_crawler.c +++ b/web_crawler/web_crawler.c @@ -1,221 +1,51 @@ #include <web_crawler.h> +#include <callback/web_crawler_callback.h> -void web_crawler_loader_free_model(View* view); - -void free_buffers(WebCrawlerApp* app) { - if(!app) { - FURI_LOG_E(TAG, "Invalid app context"); - return; - } - if(app->path) { - free(app->path); - app->path = NULL; - } - - if(app->temp_buffer_path) { - free(app->temp_buffer_path); - app->temp_buffer_path = NULL; - } - - if(app->ssid) { - free(app->ssid); - app->ssid = NULL; - } - - if(app->temp_buffer_ssid) { - free(app->temp_buffer_ssid); - app->temp_buffer_ssid = NULL; - } - - if(app->password) { - free(app->password); - app->password = NULL; - } - - if(app->temp_buffer_password) { - free(app->temp_buffer_password); - app->temp_buffer_password = NULL; - } - - if(app->file_type) { - free(app->file_type); - app->file_type = NULL; - } - - if(app->temp_buffer_file_type) { - free(app->temp_buffer_file_type); - app->temp_buffer_file_type = NULL; - } - - if(app->file_rename) { - free(app->file_rename); - app->file_rename = NULL; - } - - if(app->temp_buffer_file_rename) { - free(app->temp_buffer_file_rename); - app->temp_buffer_file_rename = NULL; - } - - if(app->temp_buffer_http_method) { - free(app->temp_buffer_http_method); - app->temp_buffer_http_method = NULL; - } - - if(app->temp_buffer_headers) { - free(app->temp_buffer_headers); - app->temp_buffer_headers = NULL; - } - - if(app->temp_buffer_payload) { - free(app->temp_buffer_payload); - app->temp_buffer_payload = NULL; - } - - if(app->http_method) { - free(app->http_method); - app->http_method = NULL; - } - - if(app->headers) { - free(app->headers); - app->headers = NULL; - } - - if(app->payload) { - free(app->payload); - app->payload = NULL; - } -} - -void free_resources(WebCrawlerApp* app) { - if(!app) { - FURI_LOG_E(TAG, "Invalid app context"); - return; - } - - free_buffers(app); -} - -void free_all(WebCrawlerApp* app, char* reason) { - if(!app) { - FURI_LOG_E(TAG, "Invalid app context"); - return; - } - if(reason) { - FURI_LOG_I(TAG, reason); - } - - if(app->view_loader) view_free(app->view_loader); - if(app->submenu_main) submenu_free(app->submenu_main); - if(app->submenu_config) submenu_free(app->submenu_config); - if(app->variable_item_list_wifi) variable_item_list_free(app->variable_item_list_wifi); - if(app->variable_item_list_file) variable_item_list_free(app->variable_item_list_file); - if(app->variable_item_list_request) variable_item_list_free(app->variable_item_list_request); - if(app->view_dispatcher) view_dispatcher_free(app->view_dispatcher); - if(app->widget_about) widget_free(app->widget_about); - if(app->widget_file_read) widget_free(app->widget_file_read); - if(app->widget_file_delete) widget_free(app->widget_file_delete); - if(app->text_input_path) text_input_free(app->text_input_path); - if(app->text_input_ssid) text_input_free(app->text_input_ssid); - if(app->text_input_password) text_input_free(app->text_input_password); - if(app->text_input_file_type) text_input_free(app->text_input_file_type); - if(app->text_input_file_rename) text_input_free(app->text_input_file_rename); - if(app->text_input_headers) text_input_free(app->text_input_headers); - if(app->text_input_payload) text_input_free(app->text_input_payload); - - furi_record_close(RECORD_GUI); - free_resources(app); -} /** * @brief Function to free the resources used by WebCrawlerApp. * @param app The WebCrawlerApp object to free. */ -void web_crawler_app_free(WebCrawlerApp* app) { - if(!app) { - FURI_LOG_E(TAG, "Invalid app context"); - return; - } - - if(!app->view_dispatcher) { - FURI_LOG_E(TAG, "Invalid view dispatcher"); - return; - } +void web_crawler_app_free(WebCrawlerApp *app) +{ + furi_check(app, "web_crawler_app_free: App is NULL"); // Free View(s) - if(app->view_loader) { + if (app->view_loader) + { view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewLoader); web_crawler_loader_free_model(app->view_loader); view_free(app->view_loader); } - // Deinitialize UART - flipper_http_deinit(); // Remove and free Submenu - if(app->submenu_main) { + if (app->submenu_main) + { view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewSubmenuMain); submenu_free(app->submenu_main); } - if(app->submenu_config) { - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewSubmenuConfig); - submenu_free(app->submenu_config); - } - // Remove and free Variable Item Lists - if(app->variable_item_list_wifi) { - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewVariableItemListWifi); - variable_item_list_free(app->variable_item_list_wifi); - } - if(app->variable_item_list_file) { - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewVariableItemListFile); - variable_item_list_free(app->variable_item_list_file); - } - if(app->variable_item_list_request) { - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewVariableItemListRequest); - variable_item_list_free(app->variable_item_list_request); - } - - // Remove and free Text Input views - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInput); - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputSSID); - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputPassword); - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputFileType); - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputFileRename); - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputHeaders); - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputPayload); - if(app->text_input_path) text_input_free(app->text_input_path); - if(app->text_input_ssid) text_input_free(app->text_input_ssid); - if(app->text_input_password) text_input_free(app->text_input_password); - if(app->text_input_file_type) text_input_free(app->text_input_file_type); - if(app->text_input_file_rename) text_input_free(app->text_input_file_rename); - if(app->text_input_headers) text_input_free(app->text_input_headers); - if(app->text_input_payload) text_input_free(app->text_input_payload); // Remove and free Widgets - if(app->widget_about) { - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewAbout); - widget_free(app->widget_about); - } - if(app->widget_file_read) { - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewFileRead); - widget_free(app->widget_file_read); - } - if(app->widget_file_delete) { - view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewFileDelete); - widget_free(app->widget_file_delete); - } - if(app->widget_result) { + if (app->widget_result) + { view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewWidgetResult); widget_free(app->widget_result); } - // Free the ViewDispatcher and close GUI - if(app->view_dispatcher) view_dispatcher_free(app->view_dispatcher); - - // Free the application structure - if(app) { - free(app); + // check and free http method + if (app->temp_buffer_http_method) + { + free(app->temp_buffer_http_method); + app->temp_buffer_http_method = NULL; + } + if (app->http_method) + { + free(app->http_method); + app->http_method = NULL; } -} -WebCrawlerApp* app_instance = NULL; -char* http_method_names[] = {"GET", "POST", "PUT", "DELETE", "DOWNLOAD"}; + free_all(app); + furi_record_close(RECORD_STORAGE); + view_dispatcher_free(app->view_dispatcher); + free(app); +} +char *http_method_names[] = {"GET", "POST", "PUT", "DELETE", "DOWNLOAD", "BROWSE"}; \ No newline at end of file diff --git a/web_crawler/web_crawler.h b/web_crawler/web_crawler.h index bf8fe5785..a7ddf1e2b 100644 --- a/web_crawler/web_crawler.h +++ b/web_crawler/web_crawler.h @@ -4,129 +4,111 @@ #include <easy_flipper/easy_flipper.h> #include <flipper_http/flipper_http.h> -#include <jsmn/jsmn.h> #include "web_crawler_icons.h" -#include <storage/storage.h> #define TAG "Web Crawler" -extern char* http_method_names[]; +#define VERSION_TAG TAG " v1.0.1" +extern char *http_method_names[]; // Define the submenu items for our WebCrawler application -typedef enum { - WebCrawlerSubmenuIndexRun, // click to go to Run the GET request - WebCrawlerSubmenuIndexAbout, // click to go to About screen - WebCrawlerSubmenuIndexConfig, // click to go to Config submenu (Wifi, File) +typedef enum +{ + WebCrawlerSubmenuIndexRun, // click to go to Run the GET request + WebCrawlerSubmenuIndexAbout, // click to go to About screen + WebCrawlerSubmenuIndexConfig, // click to go to Config submenu (Wifi, File) WebCrawlerSubmenuIndexRequest, // click to go to Request submenu (Set URL, HTTP Method, Headers) - WebCrawlerSubmenuIndexWifi, // click to go to Wifi submenu (SSID, Password) - WebCrawlerSubmenuIndexFile, // click to go to file submenu (Read, File Type, Rename, Delete) + WebCrawlerSubmenuIndexWifi, // click to go to Wifi submenu (SSID, Password) + WebCrawlerSubmenuIndexFile, // click to go to file submenu (Read, File Type, Rename, Delete) } WebCrawlerSubmenuIndex; -typedef enum { - WebCrawlerViewAbout, // About screen - WebCrawlerViewSubmenuConfig, // Submenu Config view for App (Wifi, File) +typedef enum +{ + WebCrawlerViewAbout, // About screen + WebCrawlerViewSubmenuConfig, // Submenu Config view for App (Wifi, File) WebCrawlerViewVariableItemListRequest, // Submenu for URL (Set URL, HTTP Method, Headers) - WebCrawlerViewVariableItemListWifi, // Wifi Configuration screen (Submenu for SSID, Password) - WebCrawlerViewVariableItemListFile, // Submenu for File (Read, File Type, Rename, Delete) - WebCrawlerViewSubmenuMain, // Submenu Main view for App (Run, About, Config) - WebCrawlerViewTextInput, // Text input for Path - WebCrawlerViewTextInputSSID, // Text input for SSID - WebCrawlerViewTextInputPassword, // Text input for Password - WebCrawlerViewFileRead, // File Read - WebCrawlerViewTextInputFileType, // Text input for File Type - WebCrawlerViewTextInputFileRename, // Text input for File Rename - WebCrawlerViewTextInputHeaders, // Text input for Headers - WebCrawlerViewTextInputPayload, // Text input for Payload - WebCrawlerViewFileDelete, // File Delete + WebCrawlerViewVariableItemListWifi, // Wifi Configuration screen (Submenu for SSID, Password) + WebCrawlerViewVariableItemListFile, // Submenu for File (Read, File Type, Rename, Delete) + WebCrawlerViewSubmenuMain, // Submenu Main view for App (Run, About, Config) + WebCrawlerViewTextInput, // Text input for Path + WebCrawlerViewTextInputSSID, // Text input for SSID + WebCrawlerViewTextInputPassword, // Text input for Password + WebCrawlerViewFileRead, // File Read + WebCrawlerViewTextInputFileType, // Text input for File Type + WebCrawlerViewTextInputFileRename, // Text input for File Rename + WebCrawlerViewTextInputHeaders, // Text input for Headers + WebCrawlerViewTextInputPayload, // Text input for Payload + WebCrawlerViewFileDelete, // File Delete // WebCrawlerViewWidgetResult, // The text box that displays the random fact - WebCrawlerViewLoader, // The loader screen retrieves data from the internet + WebCrawlerViewLoader, // The loader screen retrieves data from the internet + // + WebCrawlerViewWidget, // Generic widget view + WebCrawlerViewVariableItemList, // Generic variable item list view + WebCrawlerViewInput, // Generic text input view } WebCrawlerViewIndex; // Define the application structure -typedef struct { - ViewDispatcher* view_dispatcher; - View* view_main; - View* view_run; - View* view_loader; - Submenu* submenu_main; - Submenu* submenu_config; - Widget* widget_about; - Widget* widget_result; // The widget that displays the result - - TextInput* text_input_path; - TextInput* text_input_ssid; - TextInput* text_input_password; - TextInput* text_input_file_type; - TextInput* text_input_file_rename; - // - TextInput* text_input_headers; - TextInput* text_input_payload; - - Widget* widget_file_read; - Widget* widget_file_delete; - - VariableItemList* variable_item_list_wifi; - VariableItemList* variable_item_list_file; - VariableItemList* variable_item_list_request; - - VariableItem* path_item; - VariableItem* ssid_item; - VariableItem* password_item; - VariableItem* file_type_item; - VariableItem* file_rename_item; - VariableItem* file_read_item; - VariableItem* file_delete_item; +typedef struct +{ + ViewDispatcher *view_dispatcher; + View *view_loader; + Widget *widget_result; // The widget that displays the result + Submenu *submenu_main; + Submenu *submenu_config; + Widget *widget; + VariableItemList *variable_item_list; + TextInput *uart_text_input; + + VariableItem *path_item; + VariableItem *ssid_item; + VariableItem *password_item; + VariableItem *file_type_item; + VariableItem *file_rename_item; + VariableItem *file_read_item; + VariableItem *file_delete_item; // - VariableItem* http_method_item; - VariableItem* headers_item; - VariableItem* payload_item; - - char* path; - char* ssid; - char* password; - char* file_type; - char* file_rename; - char* http_method; - char* headers; - char* payload; - - char* temp_buffer_path; + VariableItem *http_method_item; + VariableItem *headers_item; + VariableItem *payload_item; + + char *path; + char *ssid; + char *password; + char *file_type; + char *file_rename; + char *http_method; + char *headers; + char *payload; + + char *temp_buffer_path; uint32_t temp_buffer_size_path; - char* temp_buffer_ssid; + char *temp_buffer_ssid; uint32_t temp_buffer_size_ssid; - char* temp_buffer_password; + char *temp_buffer_password; uint32_t temp_buffer_size_password; - char* temp_buffer_file_type; + char *temp_buffer_file_type; uint32_t temp_buffer_size_file_type; - char* temp_buffer_file_rename; + char *temp_buffer_file_rename; uint32_t temp_buffer_size_file_rename; - char* temp_buffer_http_method; + char *temp_buffer_http_method; uint32_t temp_buffer_size_http_method; - char* temp_buffer_headers; + char *temp_buffer_headers; uint32_t temp_buffer_size_headers; - char* temp_buffer_payload; + char *temp_buffer_payload; uint32_t temp_buffer_size_payload; } WebCrawlerApp; -void free_buffers(WebCrawlerApp* app); - -void free_resources(WebCrawlerApp* app); - -void free_all(WebCrawlerApp* app, char* reason); - /** * @brief Function to free the resources used by WebCrawlerApp. * @param app The WebCrawlerApp object to free. */ -void web_crawler_app_free(WebCrawlerApp* app); - -extern WebCrawlerApp* app_instance; +void web_crawler_app_free(WebCrawlerApp *app); -#endif // WEB_CRAWLER_E +#endif // WEB_CRAWLER_E \ No newline at end of file diff --git a/xinput/.github/workflows/build.yml b/xinput/.github/workflows/build.yml new file mode 100644 index 000000000..143847c4a --- /dev/null +++ b/xinput/.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/xinput/.gitignore b/xinput/.gitignore new file mode 100644 index 000000000..81a8981f7 --- /dev/null +++ b/xinput/.gitignore @@ -0,0 +1,6 @@ +dist/* +.vscode +.clang-format +.editorconfig +.env +.ufbt diff --git a/xinput/.gitsubtree b/xinput/.gitsubtree new file mode 100644 index 000000000..ba58143a1 --- /dev/null +++ b/xinput/.gitsubtree @@ -0,0 +1 @@ +https://github.com/expected-ingot/flipper-xinput master / diff --git a/xinput/LICENSE b/xinput/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/xinput/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/xinput/README.md b/xinput/README.md new file mode 100644 index 000000000..903d3b246 --- /dev/null +++ b/xinput/README.md @@ -0,0 +1,18 @@ +# USB Game Controller for Flipper Zero +This is an app for the Flipper Zero that emulates a game controller (specifically the Xbox 360 controller) and lets the user interact with the emulated joystick, A and B buttons. +It also contains a full implementation of XInput for the Flipper Zero that supports every input that a regular Xbox controller does, which you may use in other applications. + +## How to use it? +Connect your Flipper Zero to a computer, or any other device that supports XInput. **This app likely doesn't work on Xbox consoles.** + +From there, open the app and it should connect automatically. The D-Pad acts as your left joystick, the center button is A and the back button is B. Holding the back button by itself will close the app, but using other inputs with the back button won't. **Closing the app will reboot your Flipper.** + +## Special Thanks + +[Dave Madison's "Understanding the Xbox 360 Wired Controller’s USB Data"](https://www.partsnotincluded.com/understanding-the-xbox-360-wired-controllers-usb-data/) - Detailed documentation of the protocol + +[Willy-JL](https://github.com/Willy-JL) - Help with bugs + +Microsoft - Creating the Xbox itself + +[The Flipper Zero Team](https://github.com/flipperdevices) - Creating the Flipper Zero itself diff --git a/xinput/app.c b/xinput/app.c new file mode 100644 index 000000000..4197a4b50 --- /dev/null +++ b/xinput/app.c @@ -0,0 +1,666 @@ +#include <furi.h> +#include <furi_hal.h> +#include <furi_hal_usb.h> +#include <applications/services/power/power_service/power.h> +#include <usb.h> +#include <usb_hid.h> +#include <stdlib.h> +#include <gui/gui.h> +#include <input/input.h> + +/* generated by fbt from .png files in images folder */ +#include <xinput_controller_icons.h> + +#define XBOX_SURFACE_EP_IN 0x81 // IN Endpoint 1 +#define HID_EP_SZ 0x10 + +typedef struct { + uint8_t x, y; +} Position; +enum { + UP = 1 << 0, + DOWN = 1 << 1, + LEFT = 1 << 2, + RIGHT = 1 << 3, + START = 1 << 4, + BACK = 1 << 5, + L3 = 1 << 6, + R3 = 1 << 7, + LEFT_BUMPER = 1 << 8, + RIGHT_BUMPER = 1 << 9, + LOGO = 1 << 10, + THE_VOID_ONE = 1 << 11, // https://www.partsnotincluded.com/wp-content/uploads/2019/03/X360_ButtonPackets.jpg + A = 1 << 12, + B = 1 << 13, + X = 1 << 14, + Y = 1 << 15, +} xbox_buttons; + +// "shit" variables are unknown variables. I couldn't find any other way to add those. /shrug + +struct xbox_control_surface { + // For the buttons, 0 means not pressed and 1 means pressed + uint8_t message_type; // 0x00 + uint8_t length; // 20 + uint16_t buttons; // Refer to xbox_buttons enum for flags + uint8_t left_trigger; // 0-255 + uint8_t right_trigger; + int16_t left_x; + int16_t left_y; + int16_t right_x; + int16_t right_y; + uint8_t shit[6]; +} FURI_PACKED; + +struct xbox_control_surface current_surface; + +struct usb_unknown0_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t shit1, shit2, shit3, shit4; // 0x00, 0x01, 0x01, 0x25 + uint8_t bEndpointAddress; // 0x81 IN endpoint 1 + uint8_t bMaxDataSize; + uint8_t shit5, shit6, shit7, shit8, shit9; // 0x00, 0x00, 0x00, 0x00, 0x13 + uint8_t bEndpointAddress2; // 0x01 OUT endpoint 1 + uint8_t bMaxDataSize2; + uint8_t shit10, shit11; // 0x00, 0x00 +}; + +struct usb_unknown1_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t shit1, shit2, shit3, shit4; + uint8_t bEndpointAddress; + uint8_t bMaxDataSize; + uint8_t shit5; + uint8_t bEndpointAddress2; + uint8_t bMaxDataSize2; + uint8_t shit6; + uint8_t bEndpointAddress3; + uint8_t bMaxDataSize3; + uint8_t shit7, shit8, shit9, shit10, shit11, shit12; + uint8_t bEndpointAddress4; + uint8_t bMaxDataSize4; + uint8_t shit13, shit14, shit15, shit16, shit17; +}; + +struct usb_unknown2_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t shit1, shit2, shit3, shit4; + uint8_t bEndpointAddress; + uint8_t bMaxDataSize; + uint8_t shit5; +}; + +struct usb_unknown3_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t shit1, shit2, shit3, shit4; +}; + +struct XboxIntf0Descriptor { + struct usb_interface_descriptor hid; + struct usb_unknown0_descriptor unknown; + struct usb_endpoint_descriptor hid_ep_in; + struct usb_endpoint_descriptor hid_ep_out; +}; + +struct XboxIntf1Descriptor { + struct usb_interface_descriptor hid; + struct usb_unknown1_descriptor unknown; + struct usb_endpoint_descriptor hid_ep_in_2; + struct usb_endpoint_descriptor hid_ep_out_2; + struct usb_endpoint_descriptor hid_ep_in_3; + struct usb_endpoint_descriptor hid_ep_out_3; +}; + +struct XboxIntf2Descriptor { + struct usb_interface_descriptor hid; + struct usb_unknown2_descriptor unknown; + struct usb_endpoint_descriptor hid_ep_in_4; +}; + +struct XboxIntf3Descriptor { + struct usb_interface_descriptor hid; + struct usb_unknown3_descriptor unknown; +}; + +struct XboxConfigDescriptor { + struct usb_config_descriptor config; + struct XboxIntf0Descriptor intf_0; + struct XboxIntf1Descriptor intf_1; + struct XboxIntf2Descriptor intf_2; + struct XboxIntf3Descriptor intf_3; +} FURI_PACKED; + +static struct usb_device_descriptor xbox_device_desc = { + .bLength = 0x12, + .bDescriptorType = 0x01, + .bcdUSB = 0x0200, + .bDeviceClass = 0xFF, // Vendor specific + .bDeviceSubClass = 0xFF, + .bDeviceProtocol = 0xFF, + .bMaxPacketSize0 = 0x08, + .idVendor = 0x045E, + .idProduct = 0x028E, + .bcdDevice = 0x0114, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01, +}; + +static const struct XboxConfigDescriptor xbox_cfg_desc = { + .config = { + .bLength = 9, + .bDescriptorType = 0x02, // CONFIGURATION + .wTotalLength = 153, + .bNumInterfaces = 4, + .bConfigurationValue = 1, + .iConfiguration = 0, + .bmAttributes = 0b10100000, // Not self-powered, remote wake-up + .bMaxPower = USB_CFG_POWER_MA(500), + }, + .intf_0 = { + .hid = { + .bLength = 9, + .bDescriptorType = 0x04, // INTERFACE + .bInterfaceNumber = 0, + .bAlternateSetting = 0, + .bNumEndpoints = 2, + .bInterfaceClass = 0xFF, // Vendor Specific + .bInterfaceSubClass = 0x5D, + .bInterfaceProtocol = 0x01, + .iInterface = 0, + }, + .unknown = { // Unknown descriptor + .bLength = 17, + .bDescriptorType = 0x21, // UNKNOWN + .shit1 = 0x00, .shit2 = 0x01, .shit3 = 0x01, + .shit4 = 0x25, + .bEndpointAddress = 0x81, // IN Endpoint 1 + .bMaxDataSize = 20, + .shit5 = 0x00, .shit6 = 0x00, .shit7 = 0x00, .shit8 = 0x00, .shit9 = 0x13, + .bEndpointAddress2 = 0x01, // OUT Endpoint 1 + .bMaxDataSize2 = 8, + .shit10 = 0x00, .shit11 = 0x00, + }, + .hid_ep_in = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x81, // IN Endpoint 1 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 4, + }, + .hid_ep_out = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x01, // OUT Endpoint 1 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 8, + } + }, + .intf_1 = { + .hid = { + .bLength = 9, + .bDescriptorType = 0x04, // INTERFACE + .bInterfaceNumber = 1, + .bAlternateSetting = 0, + .bNumEndpoints = 4, + .bInterfaceClass = 0xFF, // Vendor Specific + .bInterfaceSubClass = 0x5D, + .bInterfaceProtocol = 0x03, + .iInterface = 0, + }, + .unknown = { + .bLength = 27, + .bDescriptorType = 0x21, // UNKNOWN + .shit1 = 0x00, + .shit2 = 0x01, + .shit3 = 0x01, + .shit4 = 0x01, + .bEndpointAddress = 0x82, // IN Endpoint 2 + .bMaxDataSize = 64, + .shit5 = 0x01, + .bEndpointAddress2 = 0x02, // OUT Endpoint 2 + .bMaxDataSize2 = 32, + .shit6 = 0x16, + .bEndpointAddress3 = 0x83, // IN Endpoint 3 + .bMaxDataSize3 = 0, + .shit7 = 0x00, + .shit8 = 0x00, + .shit9 = 0x00, + .shit10 = 0x00, + .shit11 = 0x00, + .shit12 = 0x16, + .bEndpointAddress4 = 0x03, // OUT Endpoint 3 + .bMaxDataSize4 = 0, + .shit13 = 0x00, + .shit14 = 0x00, + .shit15 = 0x00, + .shit16 = 0x00, + .shit17 = 0x00, + }, + .hid_ep_in_2 = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x82, // IN Endpoint 2 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 2, + }, + .hid_ep_out_2 = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x02, // OUT Endpoint 2 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 4, + }, + .hid_ep_in_3 = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x83, // IN Endpoint 3 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 64, + }, + .hid_ep_out_3 = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x03, // OUT Endpoint 3 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 16, + }, + }, + .intf_2 = { + .hid = { + .bLength = 9, + .bDescriptorType = 0x04, // INTERFACE + .bInterfaceNumber = 2, + .bAlternateSetting = 0, + .bNumEndpoints = 1, + .bInterfaceClass = 0xFF, // Vendor Specific + .bInterfaceSubClass = 0x5D, + .bInterfaceProtocol = 0x02, + .iInterface = 0, + }, + .unknown = { + .bLength = 9, + .bDescriptorType = 0x21, // UNKNOWN + .shit1 = 0x00, + .shit2 = 0x01, + .shit3 = 0x01, + .shit4 = 0x22, + .bEndpointAddress = 0x84, // IN Endpoint 4 + .bMaxDataSize = 7, + .shit5 = 0x00, + }, + .hid_ep_in_4 = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x84, // IN Endpoint 4 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 16, + } + }, + .intf_3 = { + .hid = { + .bLength = 9, + .bDescriptorType = 0x04, // INTERFACE + .bInterfaceNumber = 3, + .bAlternateSetting = 0, + .bNumEndpoints = 0, + .bInterfaceClass = 0xFF, // Vendor Specific + .bInterfaceSubClass = 0xFD, + .bInterfaceProtocol = 0x13, + .iInterface = 4, + }, + .unknown = { + .bLength = 6, + .bDescriptorType = 0x41, // UNKNOWN + .shit1 = 0x00, + .shit2 = 0x01, + .shit3 = 0x01, + .shit4 = 0x03, + }, + }, + +}; + +static bool boot_protocol = false; +static usbd_device* usb_dev; +static FuriSemaphore* hid_semaphore = NULL; +static bool hid_connected = false; + +static void* hid_set_string_descr(char* str) { + furi_assert(str); + + size_t len = strlen(str); + struct usb_string_descriptor* dev_str_desc = malloc(len * 2 + 2); + dev_str_desc->bLength = len * 2 + 2; + dev_str_desc->bDescriptorType = USB_DTYPE_STRING; + for(size_t i = 0; i < len; i++) + dev_str_desc->wString[i] = str[i]; + + return dev_str_desc; +} + +static void hid_txrx_ep_callback(usbd_device* dev, uint8_t event, uint8_t ep) { + UNUSED(dev); + UNUSED(ep); + if(event == usbd_evt_eptx) { + furi_semaphore_release(hid_semaphore); + } else if(boot_protocol == true) { + //uint8_t message_type; + //usbd_ep_read(usb_dev, ep, &led_state, sizeof(led_state)); + } else { + //struct HidReportLED leds; + //usbd_ep_read(usb_dev, ep, &leds, sizeof(leds)); + //led_state = leds.led_state; + } +} +static usbd_respond hid_ep_config(usbd_device* dev, uint8_t cfg) { + switch(cfg) { + case 0: + /* deconfiguring device */ + usbd_ep_deconfig(dev, XBOX_SURFACE_EP_IN); + usbd_reg_endpoint(dev, XBOX_SURFACE_EP_IN, 0); + return usbd_ack; + case 1: + /* configuring device */ + usbd_ep_config(dev, XBOX_SURFACE_EP_IN, USB_EPTYPE_INTERRUPT, 32); + usbd_reg_endpoint(dev, XBOX_SURFACE_EP_IN, hid_txrx_ep_callback); + //usbd_ep_write(dev, HID_EP_IN, 0, 0); + boot_protocol = false; /* BIOS will SET_PROTOCOL if it wants this */ + return usbd_ack; + default: + return usbd_fail; + } +} +static usbd_respond hid_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback) { + UNUSED(callback); + /* HID control requests */ + if(((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) == + (USB_REQ_INTERFACE | USB_REQ_CLASS) && + req->wIndex == 0) { + switch(req->bRequest) { + case USB_HID_SETIDLE: + return usbd_ack; + case USB_HID_GETREPORT: + //if(boot_protocol == true) { + // dev->status.data_ptr = &hid_report.keyboard.boot; + // dev->status.data_count = sizeof(hid_report.keyboard.boot); + //} else { + // dev->status.data_ptr = &hid_report; + // dev->status.data_count = sizeof(hid_report); + //} + return usbd_ack; + case USB_HID_SETPROTOCOL: + if(req->wValue == 0) + boot_protocol = true; + else if(req->wValue == 1) + boot_protocol = false; + else + return usbd_fail; + return usbd_ack; + default: + return usbd_fail; + } + } + if(((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) == + (USB_REQ_INTERFACE | USB_REQ_STANDARD) && + req->wIndex == 0 && req->bRequest == USB_STD_GET_DESCRIPTOR) { + switch(req->wValue >> 8) { + case USB_DTYPE_HID: + dev->status.data_ptr = (uint8_t*)&(xbox_cfg_desc.intf_0.hid); + dev->status.data_count = sizeof(xbox_cfg_desc.intf_0.hid); + return usbd_ack; + default: + return usbd_fail; + } + } + return usbd_fail; +} + +static void hid_deinit(usbd_device* dev) { + usbd_reg_config(dev, NULL); + usbd_reg_control(dev, NULL); +} +static void hid_on_wakeup(usbd_device* dev) { + UNUSED(dev); + if(!hid_connected) { + hid_connected = true; + } +} +static void hid_on_suspend(usbd_device* dev) { + UNUSED(dev); + if(hid_connected) { + hid_connected = false; + furi_semaphore_release(hid_semaphore); + } +} + +static bool hid_send_report() { + if((hid_semaphore == NULL) || (hid_connected == false)) return false; + FuriStatus status = furi_semaphore_acquire(hid_semaphore, 8 * 2); + if(status == FuriStatusErrorTimeout) { + return false; + } + furi_check(status == FuriStatusOk); + if(hid_connected == false) { + return false; + } + if (boot_protocol != true) { + usbd_ep_write(usb_dev, XBOX_SURFACE_EP_IN, ¤t_surface, sizeof(current_surface)); + } + return true; +} + +static void hid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); + +FuriHalUsbInterface usb_xbox = { + .init = hid_init, + .deinit = hid_deinit, + .wakeup = hid_on_wakeup, + .suspend = hid_on_suspend, + + .dev_descr = (struct usb_device_descriptor*)&xbox_device_desc, + + .str_manuf_descr = NULL, //USB_STRING_DESC("Microsoft Corporation"), + .str_prod_descr = NULL, //USB_STRING_DESC("Controller"), + .str_serial_descr = NULL, //USB_STRING_DESC("08FEC93"), + + .cfg_descr = (void*)&xbox_cfg_desc +}; + +static void hid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { + UNUSED(intf); + UNUSED(ctx); + // FuriHalUsbHidConfig* cfg = (FuriHalUsbHidConfig*)ctx; + // UNUSED(cfg); + if(hid_semaphore == NULL) hid_semaphore = furi_semaphore_alloc(1, 1); + usb_dev = dev; + + usb_xbox.dev_descr->iManufacturer = 1; + usb_xbox.dev_descr->iProduct = 2; + usb_xbox.dev_descr->iSerialNumber = 3; + usb_xbox.dev_descr->idVendor = 0x045E; // Microsoft + usb_xbox.dev_descr->idProduct = 0x028E; // Xbox 360 Controller + + usb_xbox.str_manuf_descr = hid_set_string_descr("Microsoft Corporation"); + usb_xbox.str_prod_descr = hid_set_string_descr("Controller"); + usb_xbox.str_serial_descr = hid_set_string_descr("08FEC93"); + + usbd_reg_config(dev, hid_ep_config); + usbd_reg_control(dev, hid_control); + usbd_connect(dev, true); +} + +char* message; + +static void app_draw_callback(Canvas* canvas, void* ctx) { + UNUSED(ctx); + + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontSecondary); + if (!hid_connected) { + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 60, 35, "Connecting..."); + canvas_draw_icon(canvas, 5, 20, &I_usb); + } else { + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 74, 12, "Controller"); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 64, 24, "Hold"); + canvas_draw_icon(canvas, 84, 16, &I_back); + canvas_draw_str(canvas, 97, 24, "to exit"); + + // Joystick up + if (current_surface.left_y > 200) { + canvas_draw_icon(canvas, 26, 13, &I_up_held); + } else { + canvas_draw_icon(canvas, 26, 13, &I_up); + } + + // Joystick down + if (current_surface.left_y < -200) { + canvas_draw_icon(canvas, 26, 36, &I_down_held); + } else { + canvas_draw_icon(canvas, 26, 36, &I_down); + } + + // Joystick left + if (current_surface.left_x < -200) { + canvas_draw_icon(canvas, 3, 33, &I_left_held); + } else { + canvas_draw_icon(canvas, 3, 33, &I_left); + } + + // Joystick right + if (current_surface.left_x > 200) { + canvas_draw_icon(canvas, 52, 33, &I_right_held); + } else { + canvas_draw_icon(canvas, 52, 33, &I_right); + } + + // A + if (current_surface.buttons & A) { + canvas_draw_icon(canvas, 79, 40, &I_a_held); + } else { + canvas_draw_icon(canvas, 79, 40, &I_a); + } + + // B + if (current_surface.buttons & B) { + canvas_draw_icon(canvas, 100, 40, &I_b_held); + } else { + canvas_draw_icon(canvas, 100, 40, &I_b); + } + } +} + +static void app_input_callback(InputEvent* input_event, void* ctx) { + furi_assert(ctx); + + FuriMessageQueue* event_queue = ctx; + furi_message_queue_put(event_queue, input_event, FuriWaitForever); +} + +int32_t xinput_app(void* p) { + UNUSED(p); + + FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + ViewPort* view_port = view_port_alloc(); + view_port_draw_callback_set(view_port, app_draw_callback, view_port); + view_port_input_callback_set(view_port, app_input_callback, event_queue); + + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + InputEvent input_event; + + bool running = true; + + FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); + furi_hal_usb_unlock(); + furi_check(furi_hal_usb_set_config(&usb_xbox, NULL)); + while (true) { + while (!hid_connected) { + view_port_update(view_port); + } + furi_check(furi_message_queue_get(event_queue, &input_event, FuriWaitForever) == FuriStatusOk); + if (!running) { + break; + } + message = strdup(input_get_type_name(input_event.type)); + if (input_event.type == InputTypeLong && input_event.key == InputKeyBack && current_surface.buttons == B) { + running = false; + } + + if (input_event.type == InputTypeRepeat) { + // No + } else if (input_event.type == InputTypePress) { + switch (input_event.key) { + case InputKeyLeft: + current_surface.left_x = -32767; + break; + case InputKeyRight: + current_surface.left_x = 32767; + break; + case InputKeyOk: + current_surface.buttons = current_surface.buttons | A; + break; + case InputKeyUp: + current_surface.left_y = 32767; + break; + case InputKeyDown: + current_surface.left_y = -32767; + break; + case InputKeyBack: + current_surface.buttons = current_surface.buttons | B; + default: + break; + } + } + if (input_event.type == InputTypeRelease) { + switch (input_event.key) { + case InputKeyLeft: + case InputKeyRight: + current_surface.left_x = 0; + break; + case InputKeyOk: + // current_surface.buttons = current_surface.buttons | THE_VOID_ONE; + current_surface.buttons = current_surface.buttons & ~ A; + break; + case InputKeyBack: + current_surface.buttons = current_surface.buttons & ~ B; + break; + case InputKeyUp: + case InputKeyDown: + current_surface.left_y = 0; + break; + default: + break; + } + } + view_port_update(view_port); + hid_send_report(); + } + furi_check(furi_hal_usb_set_config(usb_mode_prev, NULL)); + + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + view_port_free(view_port); + furi_message_queue_free(event_queue); + + furi_record_close(RECORD_GUI); + + return 0; +} \ No newline at end of file diff --git a/xinput/application.fam b/xinput/application.fam new file mode 100644 index 000000000..0b1999b0f --- /dev/null +++ b/xinput/application.fam @@ -0,0 +1,18 @@ +# For details & more options, see documentation/AppManifests.md in firmware repo + +App( + appid="xinput_controller", # Must be unique + name="USB Game Controller", # Displayed in menus + apptype=FlipperAppType.EXTERNAL, + entry_point="xinput_app", + stack_size=2 * 1024, + fap_category="USB", + requires=["gui"], + # Optional values + fap_version="0.1", + fap_icon="icon.png", # 10x10 1-bit PNG + fap_description="An app that emulates XInput controllers", + fap_author="crapbass", + fap_weburl="https://github.com/expected-ingot", + fap_icon_assets="images", # Image assets to compile for this application +) diff --git a/xinput/docs/changelog.md b/xinput/docs/changelog.md new file mode 100644 index 000000000..d24b11050 --- /dev/null +++ b/xinput/docs/changelog.md @@ -0,0 +1,2 @@ +v0.1: +Initial commit, created the app diff --git a/xinput/icon.png b/xinput/icon.png new file mode 100644 index 000000000..5a884ba56 Binary files /dev/null and b/xinput/icon.png differ diff --git a/xinput/images/.gitkeep b/xinput/images/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/xinput/images/a.png b/xinput/images/a.png new file mode 100644 index 000000000..609a621e1 Binary files /dev/null and b/xinput/images/a.png differ diff --git a/xinput/images/a_held.png b/xinput/images/a_held.png new file mode 100644 index 000000000..51d997ae9 Binary files /dev/null and b/xinput/images/a_held.png differ diff --git a/xinput/images/b.png b/xinput/images/b.png new file mode 100644 index 000000000..e360d5c6e Binary files /dev/null and b/xinput/images/b.png differ diff --git a/xinput/images/b_held.png b/xinput/images/b_held.png new file mode 100644 index 000000000..a4b692fca Binary files /dev/null and b/xinput/images/b_held.png differ diff --git a/xinput/images/back.png b/xinput/images/back.png new file mode 100644 index 000000000..fc66d0e56 Binary files /dev/null and b/xinput/images/back.png differ diff --git a/xinput/images/down.png b/xinput/images/down.png new file mode 100644 index 000000000..69568d399 Binary files /dev/null and b/xinput/images/down.png differ diff --git a/xinput/images/down_held.png b/xinput/images/down_held.png new file mode 100644 index 000000000..c063b4364 Binary files /dev/null and b/xinput/images/down_held.png differ diff --git a/xinput/images/left.png b/xinput/images/left.png new file mode 100644 index 000000000..13f8b99ee Binary files /dev/null and b/xinput/images/left.png differ diff --git a/xinput/images/left_held.png b/xinput/images/left_held.png new file mode 100644 index 000000000..4828caa25 Binary files /dev/null and b/xinput/images/left_held.png differ diff --git a/xinput/images/right.png b/xinput/images/right.png new file mode 100644 index 000000000..b4293c7de Binary files /dev/null and b/xinput/images/right.png differ diff --git a/xinput/images/right_held.png b/xinput/images/right_held.png new file mode 100644 index 000000000..f780f9da2 Binary files /dev/null and b/xinput/images/right_held.png differ diff --git a/xinput/images/up.png b/xinput/images/up.png new file mode 100644 index 000000000..d9cf5a543 Binary files /dev/null and b/xinput/images/up.png differ diff --git a/xinput/images/up_held.png b/xinput/images/up_held.png new file mode 100644 index 000000000..acad777c6 Binary files /dev/null and b/xinput/images/up_held.png differ diff --git a/xinput/images/usb.png b/xinput/images/usb.png new file mode 100644 index 000000000..49428e4ca Binary files /dev/null and b/xinput/images/usb.png differ diff --git a/xinput/screenshots/ss0.png b/xinput/screenshots/ss0.png new file mode 100644 index 000000000..5ad7ef2c4 Binary files /dev/null and b/xinput/screenshots/ss0.png differ diff --git a/xinput/screenshots/ss1.png b/xinput/screenshots/ss1.png new file mode 100644 index 000000000..196bbb41c Binary files /dev/null and b/xinput/screenshots/ss1.png differ diff --git a/yappy_invaders/.github/metrics.json b/yappy_invaders/.github/metrics.json new file mode 100644 index 000000000..a51aeeaa7 --- /dev/null +++ b/yappy_invaders/.github/metrics.json @@ -0,0 +1 @@ +{"stars": 3, "forks": 0, "followers": 2, "open_issues": 0, "closed_issues": 0} \ No newline at end of file diff --git a/yappy_invaders/.github/workflows/github-metrics-notify.yml b/yappy_invaders/.github/workflows/github-metrics-notify.yml new file mode 100644 index 000000000..580a869c1 --- /dev/null +++ b/yappy_invaders/.github/workflows/github-metrics-notify.yml @@ -0,0 +1,376 @@ +# .github/workflows/github-metrics-notify.yml + +name: GitHub Metrics Notification + +# Grants specific permissions to the GITHUB_TOKEN +permissions: + contents: write # Allows pushing changes to the repository + issues: read # Optional: if you're interacting with issues + pull-requests: write # Optional: if you're interacting with pull requests + +# Triggers the workflow on specific GitHub events and schedules it as a backup +on: + # Trigger on repository star events + watch: + types: [started, deleted] # 'started' for star, 'deleted' for unstar + + # Trigger on repository forks + fork: + + # Trigger on issue events + issues: + types: [opened, closed, reopened, edited] + + # Trigger on pull request events + pull_request: + types: [opened, closed, reopened, edited] + + # Trigger on release events + release: + types: [published, edited, prereleased, released] + + # Trigger on push events to the main branch + push: + branches: + - main + + # Scheduled backup trigger every hour for followers/subscribers + schedule: + - cron: '0 */1 * * *' # Every hour at minute 0 + + # Allows manual triggering + workflow_dispatch: + +jobs: + notify_metrics: + runs-on: ubuntu-latest + + steps: + # Step 1: Checkout the repository + - name: Checkout Repository + uses: actions/checkout@v3 + with: + persist-credentials: true # Enables Git commands to use GITHUB_TOKEN + fetch-depth: 0 # Fetch all history for accurate metric tracking + + # Step 2: Set Up Python Environment + - name: Set Up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' # Specify the Python version + + # Step 3: Install Python Dependencies + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install requests + + # Step 4: Fetch and Compare Metrics + - name: Fetch and Compare Metrics + id: fetch_metrics + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Built-in secret provided by GitHub Actions + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} # Your Discord webhook URL + GITHUB_EVENT_NAME: ${{ github.event_name }} # To determine if run is manual or triggered by an event + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} # Dynamic repository owner + run: | + python3 - <<'EOF' > fetch_metrics.out + import os + import requests + import json + from datetime import datetime + + # Configuration + REPO_OWNER = os.getenv('GITHUB_REPOSITORY_OWNER') + REPO_NAME = os.getenv('GITHUB_REPOSITORY').split('/')[-1] + METRICS_FILE = ".github/metrics.json" + + # Ensure .github directory exists + os.makedirs(os.path.dirname(METRICS_FILE), exist_ok=True) + + # GitHub API Headers + headers = { + "Authorization": f"token {os.getenv('GITHUB_TOKEN')}", + "Accept": "application/vnd.github.v3+json" + } + + # Function to fetch closed issues count using GitHub Search API + def fetch_closed_issues(owner, repo, headers): + search_api = f"https://api.github.com/search/issues?q=repo:{owner}/{repo}+type:issue+state:closed" + try: + response = requests.get(search_api, headers=headers) + response.raise_for_status() + data = response.json() + return data.get('total_count', 0) + except requests.exceptions.RequestException as e: + print(f"Error fetching closed issues count: {e}") + return 0 + + # Fetch current metrics from GitHub API + repo_api = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}" + + try: + response = requests.get(repo_api, headers=headers) + response.raise_for_status() + repo_data = response.json() + stars = repo_data.get('stargazers_count', 0) + forks = repo_data.get('forks_count', 0) + followers = repo_data.get('subscribers_count', 0) + open_issues = repo_data.get('open_issues_count', 0) + closed_issues = fetch_closed_issues(REPO_OWNER, REPO_NAME, headers) + except requests.exceptions.RequestException as e: + print(f"Error fetching repository data: {e}") + exit(1) + + # Function to load previous metrics with error handling + def load_previous_metrics(file_path): + try: + with open(file_path, 'r') as file: + return json.load(file) + except json.decoder.JSONDecodeError: + print("metrics.json is corrupted or empty. Reinitializing.") + return { + "stars": 0, + "forks": 0, + "followers": 0, + "open_issues": 0, + "closed_issues": 0 + } + except FileNotFoundError: + return { + "stars": 0, + "forks": 0, + "followers": 0, + "open_issues": 0, + "closed_issues": 0 + } + + # Load previous metrics + prev_metrics = load_previous_metrics(METRICS_FILE) + is_initial_run = not os.path.exists(METRICS_FILE) + + # Determine changes (both increases and decreases) + changes = {} + metrics = ["stars", "forks", "followers", "open_issues", "closed_issues"] + current_metrics = { + "stars": stars, + "forks": forks, + "followers": followers, + "open_issues": open_issues, + "closed_issues": closed_issues + } + + for metric in metrics: + current = current_metrics.get(metric, 0) + previous = prev_metrics.get(metric, 0) + if current != previous: + changes[metric] = current - previous + + # Update metrics file + with open(METRICS_FILE, 'w') as file: + json.dump(current_metrics, file) + + # Determine if a notification should be sent + event_name = os.getenv('GITHUB_EVENT_NAME') + send_notification = False + no_changes = False + initial_setup = False + + if is_initial_run: + if event_name == 'workflow_dispatch': + # Manual run: Send notification for initial setup + send_notification = True + initial_setup = True + elif event_name == 'watch' and changes.get('stars'): + # Initial run triggered by a star event: Send notification + send_notification = True + else: + # Event-triggered runs: Do not send notification on initial setup + send_notification = False + else: + if event_name == 'workflow_dispatch': + # Manual run: Always send notification + send_notification = True + if not changes: + no_changes = True + elif event_name == 'watch': + # Star event: Send notification only if stars changed + if changes.get('stars'): + send_notification = True + else: + # Scheduled run or other events: Send notification only if there are changes + if changes: + send_notification = True + + if send_notification: + triggering_actor = os.getenv('GITHUB_ACTOR', 'Unknown') + + # Prepare Discord notification payload + payload = { + "embeds": [ + { + "title": "📈 GitHub Repository Metrics Updated", + "url": f"https://github.com/{REPO_OWNER}/{REPO_NAME}", # Link back to the repository + "color": 0x7289DA, # Discord blurple color + "thumbnail": { + "url": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" # GitHub logo + }, + "fields": [ + { + "name": "📂 Repository", + "value": f"[{REPO_OWNER}/{REPO_NAME}](https://github.com/{REPO_OWNER}/{REPO_NAME})", + "inline": False + }, + { + "name": "⭐ Stars", + "value": f"{stars}", + "inline": True + }, + { + "name": "🍴 Forks", + "value": f"{forks}", + "inline": True + }, + { + "name": "👥 Followers", + "value": f"{followers}", + "inline": True + }, + { + "name": "🐛 Open Issues", + "value": f"{open_issues}", + "inline": True + }, + { + "name": "🔒 Closed Issues", + "value": f"{closed_issues}", + "inline": True + }, + { + "name": "👤 Triggered By", + "value": triggering_actor, + "inline": False + }, + ], + "footer": { + "text": "GitHub Metrics Monitor", + "icon_url": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" # GitHub logo + }, + "timestamp": datetime.utcnow().isoformat() # Adds a timestamp to the embed + } + ] + } + + if initial_setup: + # Add a field indicating initial setup + payload["embeds"][0]["fields"].append({ + "name": "⚙️ Initial Setup", + "value": "Metrics tracking has been initialized.", + "inline": False + }) + elif changes: + # Add fields for each updated metric + for metric, count in changes.items(): + emoji = { + "stars": "⭐", + "forks": "🍴", + "followers": "👥", + "open_issues": "🐛", + "closed_issues": "🔒" + }.get(metric, "") + change_symbol = "+" if count > 0 else "" + payload["embeds"][0]["fields"].append({ + "name": f"{emoji} {metric.replace('_', ' ').capitalize()} (Change)", + "value": f"{change_symbol}{count}", + "inline": True + }) + elif no_changes: + # Indicate that there were no changes during a manual run + payload["embeds"][0]["fields"].append({ + "name": "✅ No Changes", + "value": "No updates to metrics since the last check.", + "inline": False + }) + + # Save payload to a temporary file for the next step + with open('payload.json', 'w') as f: + json.dump(payload, f) + + # Output whether to send notification + if initial_setup or changes or no_changes: + print("SEND_NOTIFICATION=true") + else: + print("SEND_NOTIFICATION=false") + else: + print("SEND_NOTIFICATION=false") + EOF + + # Step 5: Ensure .gitignore Ignores Temporary Files + - name: Ensure .gitignore Ignores Temporary Files + run: | + # Check if .gitignore exists; if not, create it + if [ ! -f .gitignore ]; then + touch .gitignore + fi + + # Add 'fetch_metrics.out' if not present + if ! grep -Fxq "fetch_metrics.out" .gitignore; then + echo "fetch_metrics.out" >> .gitignore + echo "Added 'fetch_metrics.out' to .gitignore" + else + echo "'fetch_metrics.out' already present in .gitignore" + fi + + # Add 'payload.json' if not present + if ! grep -Fxq "payload.json" .gitignore; then + echo "payload.json" >> .gitignore + echo "Added 'payload.json' to .gitignore" + else + echo "'payload.json' already present in .gitignore" + fi + + # Step 6: Decide Whether to Send Notification + - name: Check if Notification Should Be Sent + id: decide_notification + run: | + if grep -q "SEND_NOTIFICATION=true" fetch_metrics.out; then + echo "send=true" >> $GITHUB_OUTPUT + else + echo "send=false" >> $GITHUB_OUTPUT + fi + shell: bash + + # Step 7: Send Discord Notification using curl + - name: Send Discord Notification + if: steps.decide_notification.outputs.send == 'true' + run: | + curl -H "Content-Type: application/json" -d @payload.json ${{ secrets.DISCORD_WEBHOOK_URL }} + + # Step 8: Commit and Push Updated metrics.json and .gitignore + - name: Commit and Push Changes + if: steps.decide_notification.outputs.send == 'true' + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + + # Stage metrics.json + git add .github/metrics.json + + # Stage .gitignore only if it was modified + if git diff --name-only | grep -q "^\.gitignore$"; then + git add .gitignore + else + echo ".gitignore not modified" + fi + + # Commit changes if there are any + git commit -m "Update metrics.json and ensure temporary files are ignored [skip ci]" || echo "No changes to commit" + + # Push changes to the main branch + git push origin main # Replace 'main' with your default branch if different + + # Step 9: Clean Up Temporary Files + - name: Clean Up Temporary Files + if: always() + run: | + rm -f fetch_metrics.out payload.json