diff --git a/.DS_Store b/.DS_Store index 41c1aaa..3437c98 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index a41a209..200bf16 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,15 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long ## Roadmap **v0.2** - Stability Patch -- LED options +- App Categories **v0.3** - Caching -- Delete Apps +- Stability Patch 2 +- App Catalog Patch (add in required functionalility) **v0.4** -- App Categories (currently the apps download to a folder called FlipStore) +- Delete Apps **v0.5** - App short description @@ -46,7 +47,7 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long This is a big task, and I welcome all contributors, especially developers interested in animations and graphics. Fork the repository, create a pull request, and I will review your edits. ## Known Bugs -1. When clicking the Catalog, I get an "out of memory" error. - - This has been addressed but may still occur. If it does, just restart the app. +1. Clicking the catalog results in an "Out of Memory" error. + - This issue has been addressed, but it may still occur. If it does, restart the app. 2. The app file is corrupted. - - It's likely there was an error parsing the data. Restart the app and wait until the green LED light turns off after downloading the app before exiting the view. + - 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. diff --git a/application.fam b/application.fam index 501d7cf..6aef29b 100644 --- a/application.fam +++ b/application.fam @@ -8,7 +8,7 @@ App( fap_category="GPIO", fap_icon_assets="assets", fap_description="Download apps via WiFi directly to your Flipper Zero", - fap_author="jblanked", + fap_author="JBlanked", fap_weburl="https://github.com/jblanked/FlipStore", - fap_version="0.1", + fap_version="0.2", ) diff --git a/assets/CHANGELOG.md b/assets/CHANGELOG.md index 0177f60..2b2c700 100644 --- a/assets/CHANGELOG.md +++ b/assets/CHANGELOG.md @@ -1,2 +1,8 @@ +## v0.2 +- Refactored using the Easy Flipper library. +- Added categories: Users can now navigate through categories, and when FlipStore installs a selected app, it will download directly to the corresponding category's folder in the apps directory. +- Improved memory allocation to prevent "Out of Memory" warnings +- Updated installation messages + ## v0.1 -- Initial Release \ No newline at end of file +- Initial release \ No newline at end of file diff --git a/assets/README.md b/assets/README.md index a41a209..7c79602 100644 --- a/assets/README.md +++ b/assets/README.md @@ -17,14 +17,14 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long ## Roadmap **v0.2** - Stability Patch -- LED options +- App Categories **v0.3** - Caching -- Delete Apps +- App Catalog Patch (add in required functionalility) **v0.4** -- App Categories (currently the apps download to a folder called FlipStore) +- Delete Apps **v0.5** - App short description @@ -50,3 +50,5 @@ This is a big task, and I welcome all contributors, especially developers intere - This has been addressed but may still occur. If it does, just restart the app. 2. The app file is corrupted. - It's likely there was an error parsing the data. Restart the app and wait until the green LED light turns off after downloading the app before exiting the view. +3. The app is stuck on "receiving". + - Restart your Flipper Zero with your WiFi Devboard plugged in. diff --git a/flip_store_apps.h b/flip_store_apps.h new file mode 100644 index 0000000..a2d6d52 --- /dev/null +++ b/flip_store_apps.h @@ -0,0 +1,529 @@ +#ifndef FLIP_STORE_APPS_H +#define FLIP_STORE_APPS_H + +// Define maximum limits +#define MAX_APP_NAME_LENGTH 32 +#define MAX_ID_LENGTH 32 +#define MAX_APP_COUNT 100 + +typedef struct +{ + char app_name[MAX_APP_NAME_LENGTH]; + char app_id[MAX_APP_NAME_LENGTH]; + char app_build_id[MAX_ID_LENGTH]; +} FlipStoreAppInfo; + +static FlipStoreAppInfo *flip_catalog = NULL; + +static uint32_t app_selected_index = 0; +static bool flip_store_sent_request = false; +static bool flip_store_success = false; +static bool flip_store_saved_data = false; +static bool flip_store_saved_success = false; +static uint32_t flip_store_category_index = 0; + +enum ObjectState +{ + OBJECT_EXPECT_KEY, + OBJECT_EXPECT_COLON, + OBJECT_EXPECT_VALUE, + OBJECT_EXPECT_COMMA_OR_END +}; + +void flip_catalog_free() +{ + if (flip_catalog) + { + free(flip_catalog); + flip_catalog = NULL; + } +} + +// Utility function to parse JSON incrementally from a file +bool flip_store_process_app_list(const char *file_path) +{ + if (file_path == NULL) + { + FURI_LOG_E(TAG, "JSON file path is NULL."); + return false; + } + + // initialize the flip_catalog + flip_catalog = (FlipStoreAppInfo *)malloc(MAX_APP_COUNT * sizeof(FlipStoreAppInfo)); + if (!flip_catalog) + { + FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog."); + return false; + } + + Storage *_storage = NULL; + File *_file = NULL; + char buffer[BUFFER_SIZE]; + size_t bytes_read; + bool in_string = false; + bool is_escaped = false; + bool reading_key = false; + bool reading_value = false; + bool inside_app_object = false; + bool found_name = false; + bool found_id = false; + bool found_build_id = false; + char current_key[MAX_KEY_LENGTH] = {0}; + size_t key_index = 0; + char current_value[MAX_VALUE_LENGTH] = {0}; + size_t value_index = 0; + int app_count = 0; + enum ObjectState object_state = OBJECT_EXPECT_KEY; // Initialize object_state + + // Initialize parser state + enum + { + STATE_SEARCH_APPS_KEY, + STATE_SEARCH_ARRAY_START, + STATE_READ_ARRAY_ELEMENTS, + STATE_DONE + } state = STATE_SEARCH_APPS_KEY; + + // Open storage and file + _storage = furi_record_open(RECORD_STORAGE); + if (!_storage) + { + FURI_LOG_E(TAG, "Failed to open storage."); + return false; + } + + _file = storage_file_alloc(_storage); + if (!_file) + { + FURI_LOG_E(TAG, "Failed to allocate file."); + furi_record_close(RECORD_STORAGE); + return false; + } + + if (!storage_file_open(_file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) + { + FURI_LOG_E(TAG, "Failed to open JSON file for reading."); + storage_file_free(_file); + furi_record_close(RECORD_STORAGE); + return false; + } + + while ((bytes_read = storage_file_read(_file, buffer, BUFFER_SIZE)) > 0 && state != STATE_DONE) + { + for (size_t i = 0; i < bytes_read; ++i) + { + char c = buffer[i]; + + if (is_escaped) + { + is_escaped = false; + if (reading_key) + { + if (key_index < MAX_KEY_LENGTH - 1) + { + current_key[key_index++] = c; + } + } + else if (reading_value) + { + if (value_index < MAX_VALUE_LENGTH - 1) + { + current_value[value_index++] = c; + } + } + continue; + } + + if (c == '\\') + { + is_escaped = true; + continue; + } + + if (c == '\"') + { + in_string = !in_string; + + if (in_string) + { + // Start of a string + if (!reading_key && !reading_value) + { + if (state == STATE_SEARCH_APPS_KEY) + { + reading_key = true; + key_index = 0; + current_key[0] = '\0'; + } + else if (inside_app_object) + { + if (object_state == OBJECT_EXPECT_KEY) + { + reading_key = true; + key_index = 0; + current_key[0] = '\0'; + } + else if (object_state == OBJECT_EXPECT_VALUE) + { + reading_value = true; + value_index = 0; + current_value[0] = '\0'; + } + } + } + } + else + { + // End of a string + if (reading_key) + { + reading_key = false; + current_key[key_index] = '\0'; + + if (state == STATE_SEARCH_APPS_KEY && strcmp(current_key, "apps") == 0) + { + state = STATE_SEARCH_ARRAY_START; + } + else if (inside_app_object) + { + object_state = OBJECT_EXPECT_COLON; + } + } + else if (reading_value) + { + reading_value = false; + current_value[value_index] = '\0'; + + if (inside_app_object) + { + if (strcmp(current_key, "name") == 0) + { + strncpy(flip_catalog[app_count].app_name, current_value, MAX_APP_NAME_LENGTH - 1); + flip_catalog[app_count].app_name[MAX_APP_NAME_LENGTH - 1] = '\0'; + found_name = true; + } + else if (strcmp(current_key, "id") == 0) + { + strncpy(flip_catalog[app_count].app_id, current_value, MAX_APP_NAME_LENGTH - 1); + flip_catalog[app_count].app_id[MAX_APP_NAME_LENGTH - 1] = '\0'; + found_id = true; + } + else if (strcmp(current_key, "build_id") == 0) + { + strncpy(flip_catalog[app_count].app_build_id, current_value, MAX_APP_NAME_LENGTH - 1); + flip_catalog[app_count].app_build_id[MAX_ID_LENGTH - 1] = '\0'; + found_build_id = true; + } + + // After processing value, expect comma or end + object_state = OBJECT_EXPECT_COMMA_OR_END; + + // Check if both name and id are found + if (found_name && found_id && found_build_id) + { + app_count++; + if (app_count >= MAX_APP_COUNT) + { + FURI_LOG_I(TAG, "Reached maximum app count."); + state = STATE_DONE; + break; + } + + // Reset for next app + found_name = false; + found_id = false; + found_build_id = false; + } + } + } + } + continue; + } + + if (in_string) + { + if (reading_key) + { + if (key_index < MAX_KEY_LENGTH - 1) + { + current_key[key_index++] = c; + } + } + else if (reading_value) + { + if (value_index < MAX_VALUE_LENGTH - 1) + { + current_value[value_index++] = c; + } + } + continue; + } + + // Not inside a string + + if (state == STATE_SEARCH_ARRAY_START) + { + if (c == '[') + { + state = STATE_READ_ARRAY_ELEMENTS; + } + continue; + } + + if (state == STATE_READ_ARRAY_ELEMENTS) + { + if (c == '{') + { + inside_app_object = true; + found_name = false; + found_id = false; + found_build_id = false; + object_state = OBJECT_EXPECT_KEY; + } + else if (c == '}') + { + inside_app_object = false; + object_state = OBJECT_EXPECT_KEY; + } + else if (c == ']') + { + state = STATE_DONE; + break; + } + else if (c == ':') + { + if (inside_app_object && object_state == OBJECT_EXPECT_COLON) + { + object_state = OBJECT_EXPECT_VALUE; + } + } + else if (c == ',') + { + if (inside_app_object && object_state == OBJECT_EXPECT_COMMA_OR_END) + { + object_state = OBJECT_EXPECT_KEY; + } + // Else, separator between objects or values + } + // Ignore other characters like whitespace, etc. + continue; + } + } + } + + // free remaining app catalog memory + for (int i = app_count; i < MAX_APP_COUNT; i++) + { + flip_catalog[i].app_name[0] = '\0'; + flip_catalog[i].app_id[0] = '\0'; + flip_catalog[i].app_build_id[0] = '\0'; + } + + // Clean up + storage_file_close(_file); + storage_file_free(_file); + furi_record_close(RECORD_STORAGE); + + if (app_count == 0) + { + FURI_LOG_E(TAG, "No valid apps were parsed."); + return false; + } + return true; +} + +bool flip_store_get_fap_file(char *build_id, char *target, char *api) +{ + is_compile_app_request = true; + char url[164]; + snprintf(url, sizeof(url), "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=%s&api=%s", build_id, target, api); + return flipper_http_get_request_bytes(url, "{\"Content-Type\":\"application/json\"}"); +} + +void flip_store_request_error(Canvas *canvas) +{ + if (fhttp.received_data == NULL) + { + if (fhttp.last_response != NULL) + { + if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL) + { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } + else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) + { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } + else + { + FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response); + canvas_draw_str(canvas, 0, 42, "Unusual error..."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } + } + else + { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error."); + canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } + } + else + { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Failed to receive data."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } +} +// function to handle the entire installation process "asynchronously" +bool flip_store_install_app(Canvas *canvas, char *category) +{ + // create /apps/FlipStore directory if it doesn't exist + char directory_path[128]; + snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps/%s", category); + + // Create the directory + Storage *storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(storage, directory_path); + + // Adjusted to access flip_catalog as an array of structures + char *app_name = flip_catalog[app_selected_index].app_name; + char installing_text[128]; + snprintf(installing_text, sizeof(installing_text), "Installing %s", app_name); + char bin_path[256]; + snprintf(bin_path, sizeof(bin_path), STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", category, flip_catalog[app_selected_index].app_id); + strncpy(fhttp.file_path, bin_path, sizeof(fhttp.file_path) - 1); + canvas_draw_str(canvas, 0, 10, installing_text); + canvas_draw_str(canvas, 0, 20, "Sending request.."); + if (fhttp.state != INACTIVE && flip_store_get_fap_file(flip_catalog[app_selected_index].app_build_id, "f7", "73.0")) + { + canvas_draw_str(canvas, 0, 30, "Request sent."); + fhttp.state = RECEIVING; + // furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + canvas_draw_str(canvas, 0, 40, "Receiving..."); + } + else + { + FURI_LOG_E(TAG, "Failed to send the request"); + flip_store_success = false; + return false; + } + while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) + { + // Wait for the feed to be received + furi_delay_ms(10); + } + // furi_timer_stop(fhttp.get_timeout_timer); + if (fhttp.state == ISSUE || fhttp.received_data == NULL) + { + flip_store_request_error(canvas); + flip_store_success = false; + return false; + } + flip_store_success = true; + return true; +} + +// process the app list and return view +int32_t flip_store_handle_app_list(FlipStoreApp *app, int32_t success_view, char *category, Submenu **submenu) +{ + // reset the flip_catalog + if (flip_catalog) + { + flip_catalog_free(); + } + + if (!app) + { + FURI_LOG_E(TAG, "FlipStoreApp is NULL"); + return FlipStoreViewPopup; + } + char url[128]; + is_compile_app_request = false; + // append the category to the end of the url + snprintf(url, sizeof(url), "https://www.flipsocial.net/api/flipper/apps/%s/extended/", category); + // async call to the app list with timer + if (fhttp.state != INACTIVE && flipper_http_get_request_with_headers(url, "{\"Content-Type\":\"application/json\"}")) + { + furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); + fhttp.state = RECEIVING; + } + else + { + FURI_LOG_E(TAG, "Failed to send the request"); + return FlipStoreViewPopup; + } + while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) + { + // Wait for the feed to be received + furi_delay_ms(100); + } + furi_timer_stop(fhttp.get_timeout_timer); + if (fhttp.state == ISSUE || fhttp.received_data == NULL) + { + if (fhttp.received_data == NULL) + { + FURI_LOG_E(TAG, "Failed to receive data"); + if (fhttp.last_response != NULL) + { + if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL) + { + popup_set_text(app->popup, "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop); + } + else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) + { + popup_set_text(app->popup, "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop); + } + else + { + popup_set_text(app->popup, fhttp.last_response, 0, 10, AlignLeft, AlignTop); + } + } + else + { + popup_set_text(app->popup, "[ERROR] Unknown Error.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop); + } + return FlipStoreViewPopup; + } + else + { + FURI_LOG_E(TAG, "Failed to receive data"); + popup_set_text(app->popup, "Failed to received data.", 0, 10, AlignLeft, AlignTop); + return FlipStoreViewPopup; + } + } + else + { + // process the app list + const char *output_file_path = STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/received_data.txt"; + if (flip_store_process_app_list(output_file_path)) + { + submenu_reset(*submenu); + // add each app name to submenu + for (int i = 0; i < MAX_APP_COUNT; i++) + { + if (strlen(flip_catalog[i].app_name) > 0) + { + submenu_add_item(*submenu, flip_catalog[i].app_name, FlipStoreSubmenuIndexStartAppList + i, callback_submenu_choices, app); + } + } + return success_view; + } + else + { + FURI_LOG_E(TAG, "Failed to process the app list"); + popup_set_text(app->popup, "Failed to process the app list", 0, 10, AlignLeft, AlignTop); + return FlipStoreViewPopup; + } + } +} + +#endif // FLIP_STORE_APPS_H \ No newline at end of file diff --git a/flip_store_callback.h b/flip_store_callback.h index f00fde1..c73ef03 100644 --- a/flip_store_callback.h +++ b/flip_store_callback.h @@ -3,356 +3,9 @@ #include #include #include -#include "jsmn.h" #include #include - -// Define maximum limits -#define MAX_APP_NAME_LENGTH 32 -#define MAX_APP_COUNT 200 -#define MAX_TOKENS 1600 // there are currently 1505 tokens in the JSON response - -typedef struct -{ - char *app_name; - char *app_id; -} FlipStoreAppInfo; - -static FlipStoreAppInfo flip_catalog[MAX_APP_COUNT]; - -static uint32_t app_selected_index = 0; -static bool flip_store_sent_request = false; -static bool flip_store_success = false; -static bool flip_store_saved_data = false; -static bool flip_store_saved_success = false; - -// Function to free the flip_catalog -static void flip_catalog_free() -{ - for (int i = 0; i < MAX_APP_COUNT; i++) - { - if (flip_catalog[i].app_name != NULL) - { - free(flip_catalog[i].app_name); - flip_catalog[i].app_name = NULL; - } - if (flip_catalog[i].app_id != NULL) - { - free(flip_catalog[i].app_id); - flip_catalog[i].app_id = NULL; - } - } -} - -// 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; -} - -// Function to clean app name string -void clean_app_name(char *name) -{ - // Remove leading and trailing whitespace (if needed) - char *end; - while (isspace((unsigned char)*name)) - name++; // Trim leading - end = name + strlen(name) - 1; - while (end > name && isspace((unsigned char)*end)) - end--; // Trim trailing - *(end + 1) = '\0'; // Null terminate -} - -// Function to skip tokens correctly -int skip_tokens(jsmntok_t *tokens, int index, int total_tokens) -{ - int skip = 1; // Start with 1 to skip the current token - int child_count = tokens[index].size; - - for (int i = 0; i < child_count; i++) - { - if ((index + skip) >= total_tokens) - break; - - skip += skip_tokens(tokens, index + skip, total_tokens); - } - - return skip; -} - -bool flip_store_process_app_list(char *json_data) -{ - if (json_data == NULL) - { - FURI_LOG_E(TAG, "JSON data is NULL."); - return false; - } - - // Free existing catalog to prevent memory leaks - flip_catalog_free(); - - jsmn_parser parser; - jsmn_init(&parser); - - // Initial token allocation - jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * MAX_TOKENS); - if (tokens == NULL) - { - FURI_LOG_E(TAG, "Failed to allocate memory for JSON tokens."); - return false; - } - - int ret = jsmn_parse(&parser, json_data, strlen(json_data), tokens, MAX_TOKENS); - - if (ret < 0) - { - FURI_LOG_E(TAG, "Failed to parse JSON: %d", ret); - free(tokens); - return false; - } - - if (ret < 1 || tokens[0].type != JSMN_OBJECT) - { - FURI_LOG_E(TAG, "Root element is not an object."); - free(tokens); - return false; - } - - int app_count = 0; - int i = 1; - - while (i < ret) - { - if (jsoneq(json_data, &tokens[i], "apps") == 0) - { - jsmntok_t *apps_array = &tokens[i + 1]; - - if (apps_array->type != JSMN_ARRAY) - { - FURI_LOG_E(TAG, "\"apps\" is not an array."); - free(tokens); - return false; - } - - int current = i + 2; - - for (int j = 0; j < apps_array->size; j++) - { - if (current >= ret) - { - FURI_LOG_E(TAG, "Token index out of bounds while accessing apps."); - break; - } - - jsmntok_t *app_token = &tokens[current]; - - if (app_token->type != JSMN_OBJECT) - { - FURI_LOG_E(TAG, "App entry is not an object."); - current++; - continue; - } - - int app_size = app_token->size; - int app_token_index = current + 1; - - char name_value[MAX_APP_NAME_LENGTH] = {0}; - char id_value[MAX_APP_NAME_LENGTH] = {0}; - - for (int k = 0; k < app_size; k++) - { - if (app_token_index + 1 >= ret) - { - FURI_LOG_E(TAG, "Token index out of bounds while accessing app properties."); - break; - } - - jsmntok_t *key_token = &tokens[app_token_index]; - jsmntok_t *val_token = &tokens[app_token_index + 1]; - - if (jsoneq(json_data, key_token, "name") == 0) - { - int val_length = val_token->end - val_token->start; - if (val_length >= MAX_APP_NAME_LENGTH) - val_length = MAX_APP_NAME_LENGTH - 1; - strncpy(name_value, json_data + val_token->start, val_length); - name_value[val_length] = '\0'; - clean_app_name(name_value); - } - else if (jsoneq(json_data, key_token, "id") == 0) - { - int val_length = val_token->end - val_token->start; - if (val_length >= MAX_APP_NAME_LENGTH) - val_length = MAX_APP_NAME_LENGTH - 1; - strncpy(id_value, json_data + val_token->start, val_length); - id_value[val_length] = '\0'; - clean_app_name(id_value); - } - - app_token_index += 2; - } - - if (app_count >= MAX_APP_COUNT) - { - FURI_LOG_E(TAG, "Reached maximum app count limit."); - break; - } - - // Allocate memory for app_name and app_id - flip_catalog[app_count].app_name = (char *)malloc(MAX_APP_NAME_LENGTH); - flip_catalog[app_count].app_id = (char *)malloc(MAX_APP_NAME_LENGTH); - - if (flip_catalog[app_count].app_name == NULL || flip_catalog[app_count].app_id == NULL) - { - FURI_LOG_E(TAG, "Memory allocation failed for app_name or app_id."); - - // Cleanup already allocated entries - for (int cleanup = 0; cleanup < app_count; cleanup++) - { - if (flip_catalog[cleanup].app_name != NULL) - { - free(flip_catalog[cleanup].app_name); - flip_catalog[cleanup].app_name = NULL; - } - if (flip_catalog[cleanup].app_id != NULL) - { - free(flip_catalog[cleanup].app_id); - flip_catalog[cleanup].app_id = NULL; - } - } - - free(tokens); - return false; - } - - strncpy(flip_catalog[app_count].app_name, name_value, MAX_APP_NAME_LENGTH - 1); - flip_catalog[app_count].app_name[MAX_APP_NAME_LENGTH - 1] = '\0'; - - strncpy(flip_catalog[app_count].app_id, id_value, MAX_APP_NAME_LENGTH - 1); - flip_catalog[app_count].app_id[MAX_APP_NAME_LENGTH - 1] = '\0'; - - app_count++; - - // Update current to skip the current app object tokens - int tokens_to_skip = 1 + 2 * app_size; - current += tokens_to_skip; - } - - break; - } - else - { - i += 2; - } - } - - free(tokens); - return true; -} - -bool flip_store_get_fap_file(char *app_id) -{ - char payload[164]; - snprintf(payload, sizeof(payload), "{\"app_id\":\"%s\"}", app_id); - return flipper_http_post_request_bytes("https://www.flipsocial.net/api/app/compile/", "{\"Content-Type\":\"application/json\"}", payload); -} -void flip_store_request_error(Canvas *canvas) -{ - if (fhttp.received_data == NULL) - { - if (fhttp.last_response != NULL) - { - if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL) - { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) - { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - else - { - FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response); - canvas_draw_str(canvas, 0, 42, "Unusual error..."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - } - else - { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error."); - canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } - } - else - { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Failed to receive data."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } -} -// function to handle the entire installation process "asynchronously" -bool flip_store_install_app(Canvas *canvas) -{ - // create /apps/FlipStore directory if it doesn't exist - char directory_path[128]; - snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps/FlipStore"); - - // Create the directory - Storage *storage = furi_record_open(RECORD_STORAGE); - storage_common_mkdir(storage, directory_path); - - // Adjusted to access flip_catalog as an array of structures - char *app_name = flip_catalog[app_selected_index].app_name; - char installing_text[128]; - snprintf(installing_text, sizeof(installing_text), "Installing %s", app_name); - char bin_path[256]; - snprintf(bin_path, sizeof(bin_path), STORAGE_EXT_PATH_PREFIX "/apps/FlipStore/%s.fap", flip_catalog[app_selected_index].app_id); - strncpy(fhttp.file_path, bin_path, sizeof(fhttp.file_path) - 1); - canvas_draw_str(canvas, 0, 10, installing_text); - canvas_draw_str(canvas, 0, 20, "Sending request.."); - if (fhttp.state != INACTIVE && flip_store_get_fap_file(flip_catalog[app_selected_index].app_id)) - { - canvas_draw_str(canvas, 0, 30, "Request sent."); - fhttp.state = RECEIVING; - // furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - canvas_draw_str(canvas, 0, 40, "Receiving..."); - } - else - { - FURI_LOG_E(TAG, "Failed to send the request"); - flip_store_success = false; - return false; - } - while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) - { - // Wait for the feed to be received - // furi_delay_ms(100); - } - // furi_timer_stop(fhttp.get_timeout_timer); - if (fhttp.state == ISSUE || fhttp.received_data == NULL) - { - flip_store_request_error(canvas); - flip_store_success = false; - return false; - } - flip_store_success = true; - return true; -} +#include // Callback for drawing the main screen static void flip_store_view_draw_callback_main(Canvas *canvas, void *model) @@ -375,27 +28,30 @@ static void flip_store_view_draw_callback_main(Canvas *canvas, void *model) { flip_store_sent_request = true; - if (!flip_store_install_app(canvas)) + if (!flip_store_install_app(canvas, categories[flip_store_category_index])) { canvas_clear(canvas); canvas_draw_str(canvas, 0, 10, "Failed to install app."); canvas_draw_str(canvas, 0, 60, "Press BACK to return."); } - else - { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "App installed successfully."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); - } } else { if (flip_store_success) { - - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "App installed successfully."); - canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + if (fhttp.state == RECEIVING) + { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "Downloading app..."); + canvas_draw_str(canvas, 0, 60, "Please wait..."); + return; + } + else if (fhttp.state == IDLE) + { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "App installed successfully."); + canvas_draw_str(canvas, 0, 60, "Press BACK to return."); + } } else { @@ -435,7 +91,7 @@ static bool flip_store_input_callback(InputEvent *event, void *context) // if (event->key == InputKeyLeft) //{ // Left button clicked, delete the app with DialogEx confirmation - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppDelete); + // view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppDelete); // return true; //} if (event->key == InputKeyRight) @@ -541,8 +197,6 @@ static uint32_t callback_to_submenu(void *context) return VIEW_NONE; } UNUSED(context); - // free the app list - flip_catalog_free(); return FlipStoreViewSubmenu; } @@ -651,93 +305,53 @@ static void callback_submenu_choices(void *context, uint32_t index) case FlipStoreSubmenuIndexSettings: view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings); break; - // Ideally users should be sent to a draw callback view to show to request process (like in FlipSocial and WebCrawler) case FlipStoreSubmenuIndexAppList: - // initialize the flip_catalog[MAX_APP_COUNT]; - // if (!flip_catalog_init()) - // { - // FURI_LOG_E(TAG, "Failed to initialize flip catalog"); - // return; - // } - // async call to the app list with timer - if (fhttp.state != INACTIVE && flipper_http_get_request_with_headers("https://www.flipsocial.net/api/flipper/apps/", "{\"Content-Type\":\"application/json\"}")) - { - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - fhttp.state = RECEIVING; - } - else - { - FURI_LOG_E(TAG, "Failed to send the request"); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup); - return; - } - while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) - { - // Wait for the feed to be received - furi_delay_ms(100); - } - furi_timer_stop(fhttp.get_timeout_timer); - - if (fhttp.state == ISSUE || fhttp.received_data == NULL) - { - if (fhttp.received_data == NULL) - { - FURI_LOG_E(TAG, "Failed to receive data"); - if (fhttp.last_response != NULL) - { - if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL) - { - popup_set_text(app->popup, "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop); - } - else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) - { - popup_set_text(app->popup, "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop); - } - else - { - popup_set_text(app->popup, fhttp.last_response, 0, 10, AlignLeft, AlignTop); - } - } - else - { - popup_set_text(app->popup, "[ERROR] Unknown Error.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop); - } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup); - return; - } - else - { - FURI_LOG_E(TAG, "Failed to receive data"); - popup_set_text(app->popup, "Failed to received data.", 0, 10, AlignLeft, AlignTop); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup); - return; - } - } - else - { - // process the app list - if (flip_store_process_app_list(fhttp.received_data)) - { - submenu_reset(app->submenu_app_list); // clear the submenu - // add each app name to submenu - for (int i = 0; i < MAX_APP_COUNT; i++) - { - if (flip_catalog[i].app_name != NULL && strlen(flip_catalog[i].app_name) > 0) - { - submenu_add_item(app->submenu_app_list, flip_catalog[i].app_name, FlipStoreSubmenuIndexStartAppList + i, callback_submenu_choices, app); - } - } - - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); - } - else - { - FURI_LOG_E(TAG, "Failed to process the app list"); - popup_set_text(app->popup, "Failed to process the app list", 0, 10, AlignLeft, AlignTop); - view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup); - return; - } - } + flip_store_category_index = 0; + view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList); + break; + case FlipStoreSubmenuIndexAppListBluetooth: + flip_store_category_index = 0; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListBluetooth, "Bluetooth", &app->submenu_app_list_bluetooth)); + break; + case FlipStoreSubmenuIndexAppListGames: + flip_store_category_index = 1; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListGames, "Games", &app->submenu_app_list_games)); + break; + case FlipStoreSubmenuIndexAppListGPIO: + flip_store_category_index = 2; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListGPIO, "GPIO", &app->submenu_app_list_gpio)); + break; + case FlipStoreSubmenuIndexAppListInfrared: + flip_store_category_index = 3; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListInfrared, "Infrared", &app->submenu_app_list_infrared)); + break; + case FlipStoreSubmenuIndexAppListiButton: + flip_store_category_index = 4; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListiButton, "iButton", &app->submenu_app_list_ibutton)); + break; + case FlipStoreSubmenuIndexAppListMedia: + flip_store_category_index = 5; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListMedia, "Media", &app->submenu_app_list_media)); + break; + case FlipStoreSubmenuIndexAppListNFC: + flip_store_category_index = 6; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListNFC, "NFC", &app->submenu_app_list_nfc)); + break; + case FlipStoreSubmenuIndexAppListRFID: + flip_store_category_index = 7; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListRFID, "RFID", &app->submenu_app_list_rfid)); + break; + case FlipStoreSubmenuIndexAppListSubGHz: + flip_store_category_index = 8; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListSubGHz, "Sub-GHz", &app->submenu_app_list_subghz)); + break; + case FlipStoreSubmenuIndexAppListTools: + flip_store_category_index = 9; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListTools, "Tools", &app->submenu_app_list_tools)); + break; + case FlipStoreSubmenuIndexAppListUSB: + flip_store_category_index = 10; + view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListUSB, "USB", &app->submenu_app_list_usb)); break; default: // Check if the index is within the app list range diff --git a/flip_store_e.h b/flip_store_e.h index cbb7cf9..dea05d8 100644 --- a/flip_store_e.h +++ b/flip_store_e.h @@ -11,8 +11,24 @@ #include #include #include +#include #define TAG "FlipStore" +// define the list of categories +char *categories[] = { + "Bluetooth", + "Games", + "GPIO", + "Infrared", + "iButton", + "Media", + "NFC", + "RFID", + "Sub-GHz", + "Tools", + "USB", +}; + // Define the submenu items for our FlipStore application typedef enum { @@ -21,6 +37,18 @@ typedef enum FlipStoreSubmenuIndexSettings, FlipStoreSubmenuIndexAppList, // + FlipStoreSubmenuIndexAppListBluetooth, + FlipStoreSubmenuIndexAppListGames, + FlipStoreSubmenuIndexAppListGPIO, + FlipStoreSubmenuIndexAppListInfrared, + FlipStoreSubmenuIndexAppListiButton, + FlipStoreSubmenuIndexAppListMedia, + FlipStoreSubmenuIndexAppListNFC, + FlipStoreSubmenuIndexAppListRFID, + FlipStoreSubmenuIndexAppListSubGHz, + FlipStoreSubmenuIndexAppListTools, + FlipStoreSubmenuIndexAppListUSB, + // FlipStoreSubmenuIndexStartAppList } FlipStoreSubmenuIndex; @@ -41,16 +69,41 @@ typedef enum 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 } FlipStoreView; // Each screen will have its own view typedef struct { - ViewDispatcher *view_dispatcher; // Switches between our views - View *view_main; // The main screen for downloading apps - View *view_app_info; // The app info screen (view) of the selected app - Submenu *submenu; // The submenu (main) - Submenu *submenu_app_list; // The submenu (app list) + ViewDispatcher *view_dispatcher; // Switches between our views + View *view_main; // The main screen for downloading apps + View *view_app_info; // The app info screen (view) of the selected app + Submenu *submenu; // The submenu (main) + // + Submenu *submenu_app_list; // The submenu (app list) for the selected category + // + 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 @@ -66,7 +119,26 @@ typedef struct 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 + uint32_t uart_text_input_buffer_size_pass; // Size of the text input buffer } FlipStoreApp; +// include strndup (otherwise NULL pointer dereference) +char *strndup(const char *s, size_t n) +{ + char *result; + size_t len = strlen(s); + + if (n < len) + len = n; + + result = (char *)malloc(len + 1); + if (!result) + return NULL; + + result[len] = '\0'; + return (char *)memcpy(result, s, len); +} + +static void callback_submenu_choices(void *context, uint32_t index); + #endif // FLIP_STORE_E_H \ No newline at end of file diff --git a/flip_store_free.h b/flip_store_free.h index 5742c60..a896def 100644 --- a/flip_store_free.h +++ b/flip_store_free.h @@ -38,6 +38,61 @@ static void flip_store_app_free(FlipStoreApp *app) view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppList); submenu_free(app->submenu_app_list); } + 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) @@ -79,7 +134,7 @@ static void flip_store_app_free(FlipStoreApp *app) dialog_ex_free(app->dialog_delete); } - // Free the view dispatcher + // Free the flip catalog flip_catalog_free(); // deinitalize flipper http diff --git a/flip_store_i.h b/flip_store_i.h index 8128422..63518e3 100644 --- a/flip_store_i.h +++ b/flip_store_i.h @@ -115,10 +115,67 @@ static FlipStoreApp *flip_store_app_alloc() { return NULL; } + if (!easy_flipper_set_submenu(&app->submenu_app_list_bluetooth, FlipStoreViewAppListBluetooth, "Bluetooth", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_app_list_games, FlipStoreViewAppListGames, "Games", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_app_list_gpio, FlipStoreViewAppListGPIO, "GPIO", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_app_list_infrared, FlipStoreViewAppListInfrared, "Infrared", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_app_list_ibutton, FlipStoreViewAppListiButton, "iButton", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_app_list_media, FlipStoreViewAppListMedia, "Media", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_app_list_nfc, FlipStoreViewAppListNFC, "NFC", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_app_list_rfid, FlipStoreViewAppListRFID, "RFID", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_app_list_subghz, FlipStoreViewAppListSubGHz, "Sub-GHz", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_app_list_tools, FlipStoreViewAppListTools, "Tools", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } + if (!easy_flipper_set_submenu(&app->submenu_app_list_usb, FlipStoreViewAppListUSB, "USB", callback_to_app_list, &app->view_dispatcher)) + { + return NULL; + } submenu_add_item(app->submenu, "Catalog", FlipStoreSubmenuIndexAppList, callback_submenu_choices, app); submenu_add_item(app->submenu, "About", FlipStoreSubmenuIndexAbout, callback_submenu_choices, app); submenu_add_item(app->submenu, "Settings", FlipStoreSubmenuIndexSettings, callback_submenu_choices, app); - // dont add any items to the app list submenu yet + // + 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); + submenu_add_item(app->submenu_app_list, "GPIO", FlipStoreSubmenuIndexAppListGPIO, callback_submenu_choices, app); + submenu_add_item(app->submenu_app_list, "Infrared", FlipStoreSubmenuIndexAppListInfrared, callback_submenu_choices, app); + submenu_add_item(app->submenu_app_list, "iButton", FlipStoreSubmenuIndexAppListiButton, callback_submenu_choices, app); + submenu_add_item(app->submenu_app_list, "Media", FlipStoreSubmenuIndexAppListMedia, callback_submenu_choices, app); + submenu_add_item(app->submenu_app_list, "NFC", FlipStoreSubmenuIndexAppListNFC, callback_submenu_choices, app); + submenu_add_item(app->submenu_app_list, "RFID", FlipStoreSubmenuIndexAppListRFID, callback_submenu_choices, app); + submenu_add_item(app->submenu_app_list, "Sub-GHz", FlipStoreSubmenuIndexAppListSubGHz, callback_submenu_choices, app); + 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)) diff --git a/flip_store_storage.h b/flip_store_storage.h index 36c475c..4d3017b 100644 --- a/flip_store_storage.h +++ b/flip_store_storage.h @@ -11,7 +11,7 @@ static void save_settings( const char *password) { // Create the directory for saving settings - char directory_path[256]; + char directory_path[128]; snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store"); // Create the directory @@ -103,7 +103,7 @@ static bool load_settings( bool delete_app(const char *app_id, const char *app_category) { // Create the directory for saving settings - char directory_path[256]; + char directory_path[128]; snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps/%s/%s", app_category, app_id); // Create the directory @@ -118,4 +118,200 @@ bool delete_app(const char *app_id, const char *app_category) furi_record_close(RECORD_STORAGE); return true; } + +#define BUFFER_SIZE 64 +#define MAX_KEY_LENGTH 32 +#define MAX_VALUE_LENGTH 64 + +// 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) +{ + Storage *_storage = NULL; + File *_file = NULL; + char buffer[BUFFER_SIZE]; + size_t bytes_read; + bool key_found = false; + bool in_string = false; + bool is_escaped = false; + bool reading_key = false; + bool reading_value = false; + char current_key[MAX_KEY_LENGTH] = {0}; + size_t key_index = 0; + size_t value_index = 0; + + // Open storage and file + _storage = furi_record_open(RECORD_STORAGE); + if (!_storage) + { + FURI_LOG_E("JSON_PARSE", "Failed to open storage."); + return false; + } + + _file = storage_file_alloc(_storage); + if (!_file) + { + FURI_LOG_E("JSON_PARSE", "Failed to allocate file."); + furi_record_close(RECORD_STORAGE); + return false; + } + + if (!storage_file_open(_file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) + { + FURI_LOG_E("JSON_PARSE", "Failed to open JSON file for reading."); + goto cleanup; + } + + while ((bytes_read = storage_file_read(_file, buffer, BUFFER_SIZE)) > 0) + { + for (size_t i = 0; i < bytes_read; ++i) + { + char c = buffer[i]; + + if (is_escaped) + { + is_escaped = false; + if (reading_key) + { + if (key_index < MAX_KEY_LENGTH - 1) + { + current_key[key_index++] = c; + } + } + else if (reading_value) + { + if (value_index < value_buffer_size - 1) + { + value_buffer[value_index++] = c; + } + } + continue; + } + + if (c == '\\') + { + is_escaped = true; + continue; + } + + if (c == '\"') + { + in_string = !in_string; + + if (in_string) + { + // Start of a string + if (!reading_key && !reading_value) + { + // Possible start of a key + reading_key = true; + key_index = 0; + current_key[0] = '\0'; + } + } + else + { + // End of a string + if (reading_key) + { + reading_key = false; + current_key[key_index] = '\0'; + + if (strcmp(current_key, target_key) == 0) + { + key_found = true; + } + } + else if (reading_value) + { + reading_value = false; + value_buffer[value_index] = '\0'; + + if (key_found) + { + // Found the target value + goto success; + } + } + } + continue; + } + + if (in_string) + { + if (reading_key) + { + if (key_index < MAX_KEY_LENGTH - 1) + { + current_key[key_index++] = c; + } + } + else if (reading_value) + { + if (value_index < value_buffer_size - 1) + { + value_buffer[value_index++] = c; + } + } + continue; + } + + if (c == ':' && key_found && !reading_value) + { + // After colon, start reading the value + // Skip whitespace and possible opening quote + while (i + 1 < bytes_read && (buffer[i + 1] == ' ' || buffer[i + 1] == '\n' || buffer[i + 1] == '\r')) + { + i++; + } + + if (i + 1 < bytes_read && buffer[i + 1] == '\"') + { + i++; // Move to the quote + in_string = true; + reading_value = true; + value_index = 0; + } + else + { + // Handle non-string values (e.g., numbers, booleans) + reading_value = true; + value_index = 0; + } + continue; + } + + if (reading_value && (c == ',' || c == '}' || c == ']')) + { + // End of the value + reading_value = false; + value_buffer[value_index] = '\0'; + + if (key_found) + { + // Found the target value + goto success; + } + key_found = false; + } + } + } + +success: + storage_file_close(_file); + storage_file_free(_file); + furi_record_close(RECORD_STORAGE); + return key_found; + +cleanup: + if (_file) + { + storage_file_free(_file); + } + if (_storage) + { + furi_record_close(RECORD_STORAGE); + } + return false; +} + #endif \ No newline at end of file diff --git a/flipper_http.h b/flipper_http.h index 4251467..7557484 100644 --- a/flipper_http.h +++ b/flipper_http.h @@ -15,10 +15,10 @@ #define HTTP_TAG "FlipStore" // change this to your app name #define http_tag "flip_store" // change this to your app id #define UART_CH (FuriHalSerialIdUsart) // UART channel -#define TIMEOUT_DURATION_TICKS (2 * 1000) // 2 seconds +#define TIMEOUT_DURATION_TICKS (3 * 1000) // 5 seconds #define BAUDRATE (115200) // UART baudrate #define RX_BUF_SIZE 1024 // UART RX buffer size -#define RX_LINE_BUFFER_SIZE 10000 // UART RX line buffer size (increase for large responses) +#define RX_LINE_BUFFER_SIZE 5000 // UART RX line buffer size (increase for large responses) // Forward declaration for callback typedef void (*FlipperHTTP_Callback)(const char *line, void *context); @@ -65,13 +65,15 @@ typedef enum WorkerEvtRxDone = (1 << 1), } WorkerEvtFlags; +static bool is_compile_app_request = false; // personal use in flip_store_apps.h + // FlipperHTTP Structure typedef struct { - FuriStreamBuffer *flipper_http_stream; // Stream buffer for UART communication - FuriHalSerialHandle *serial_handle; // Serial handle for UART communication - FuriThread *rx_thread; // Worker thread for UART - uint8_t rx_buf[RX_BUF_SIZE]; // Buffer for received data + FuriStreamBuffer *flipper_http_stream; // Stream buffer for UART communication + FuriHalSerialHandle *serial_handle; // Serial handle for UART communication + FuriThread *rx_thread; // Worker thread for UART + // uint8_t rx_buf[RX_BUF_SIZE]; // Buffer for received data FuriThreadId rx_thread_id; // Worker thread ID FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines void *callback_context; // Context for the callback @@ -107,10 +109,10 @@ typedef struct } FlipperHTTP; -FlipperHTTP fhttp; +static FlipperHTTP fhttp; // Function to append received data to file -bool append_to_file(const char *file_path, const void *data, size_t data_size) +static bool append_to_file(const char *file_path, const void *data, size_t data_size) { Storage *storage = furi_record_open(RECORD_STORAGE); File *file = storage_file_alloc(storage); @@ -140,6 +142,10 @@ bool append_to_file(const char *file_path, const void *data, size_t data_size) return true; } +// Global static array for the line buffer +static char rx_line_buffer[RX_LINE_BUFFER_SIZE]; +#define FILE_BUFFER_SIZE 512 +static uint8_t file_buffer[FILE_BUFFER_SIZE]; // UART worker thread /** @@ -148,21 +154,12 @@ bool append_to_file(const char *file_path, const void *data, size_t data_size) * @param context The context to pass to the callback. * @note This function will handle received data asynchronously via the callback. */ +// UART worker thread static int32_t flipper_http_worker(void *context) { UNUSED(context); size_t rx_line_pos = 0; - char *rx_line_buffer = (char *)malloc(RX_LINE_BUFFER_SIZE); - - if (!rx_line_buffer) - { - // Handle malloc failure - FURI_LOG_E(HTTP_TAG, "Failed to allocate memory for rx_line_buffer"); - return -1; - } - - // Create the file path if not already set - // snprintf(fhttp.file_path, sizeof(fhttp.file_path), STORAGE_EXT_PATH_PREFIX "/apps/http_received_data.fap"); + static size_t file_buffer_len = 0; while (1) { @@ -171,38 +168,101 @@ static int32_t flipper_http_worker(void *context) break; if (events & WorkerEvtRxDone) { - size_t len = furi_stream_buffer_receive(fhttp.flipper_http_stream, fhttp.rx_buf, RX_BUF_SIZE, 0); - // Append each received byte chunk to the file - if (fhttp.save_data && !append_to_file(fhttp.file_path, fhttp.rx_buf, len)) + // Continuously read from the stream buffer until it's empty + while (!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) { - FURI_LOG_E(HTTP_TAG, "Failed to append received data to file"); - } + // Read one byte at a time + char c = 0; + size_t received = furi_stream_buffer_receive(fhttp.flipper_http_stream, &c, 1, 0); - for (size_t i = 0; i < len; i++) - { - char c = fhttp.rx_buf[i]; // Raw byte received + if (received == 0) + { + // No more data to read + break; + } - if (c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1) + // Append the received byte to the file if saving is enabled + if (fhttp.save_data) { - rx_line_buffer[rx_line_pos] = '\0'; // Null-terminate the line - // Invoke the callback with the complete line - if (fhttp.handle_rx_line_cb) + // 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) { - fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context); + if (!append_to_file(fhttp.file_path, file_buffer, file_buffer_len)) + { + FURI_LOG_E(HTTP_TAG, "Failed to append data to file"); + } + file_buffer_len = 0; } - // Reset the line buffer position - rx_line_pos = 0; } - else + + // Handle line buffering only if callback is set (text data) + if (fhttp.handle_rx_line_cb) { - rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer + // Handle line buffering + if (c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1) + { + rx_line_buffer[rx_line_pos] = '\0'; // Null-terminate the line + + // Invoke the callback with the complete line + fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context); + + // Reset the line buffer position + rx_line_pos = 0; + } + else + { + rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer + } } } } } - // Free the allocated memory before exiting the thread - free(rx_line_buffer); + if (fhttp.save_data) + { + // Write the remaining data to the file + if (file_buffer_len > 0) + { + if (!append_to_file(fhttp.file_path, file_buffer, file_buffer_len)) + { + FURI_LOG_E(HTTP_TAG, "Failed to append remaining data to file"); + } + } + } + + // remove [POST/END] and/or [GET/END] from the file + if (fhttp.save_data) + { + char *end = NULL; + if ((end = strstr(fhttp.file_path, "[POST/END]")) != NULL) + { + *end = '\0'; + } + else if ((end = strstr(fhttp.file_path, "[GET/END]")) != NULL) + { + *end = '\0'; + } + } + + // remove newline from the from the end of the file + if (fhttp.save_data) + { + char *end = NULL; + if ((end = strstr(fhttp.file_path, "\n")) != NULL) + { + *end = '\0'; + } + } + + // Reset the file buffer length + file_buffer_len = 0; + + if (fhttp.save_data) + { + FURI_LOG_I(HTTP_TAG, "Data saved to file: %s", fhttp.file_path); + } return 0; } @@ -869,7 +929,7 @@ void flipper_http_rx_callback(const char *line, void *context) if (fhttp.received_data) { // uncomment if you want to save the received data to the external storage - // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data); + flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data); fhttp.started_receiving_get = false; fhttp.just_started_get = false; fhttp.state = IDLE; @@ -1124,6 +1184,10 @@ void flipper_http_rx_callback(const char *line, void *context) furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); fhttp.state = RECEIVING; fhttp.received_data = NULL; + if (is_compile_app_request) + { + fhttp.save_data = true; + } return; } else if (strstr(line, "[POST/SUCCESS]") != NULL) @@ -1133,7 +1197,10 @@ void flipper_http_rx_callback(const char *line, void *context) furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); fhttp.state = RECEIVING; fhttp.received_data = NULL; - fhttp.save_data = true; + if (is_compile_app_request) + { + fhttp.save_data = true; + } return; } else if (strstr(line, "[PUT/SUCCESS]") != NULL) diff --git a/jsmn.h b/jsmn.h index 8ac14c1..cbada18 100644 --- a/jsmn.h +++ b/jsmn.h @@ -27,7 +27,8 @@ #include #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif #ifdef JSMN_STATIC @@ -36,431 +37,487 @@ 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 -}; - -/** - * JSON token description. - * type type (object, array, string etc.) - * start start position in JSON data string - * end end position in JSON data string - */ -typedef struct jsmntok { - jsmntype_t type; - int start; - int end; - int size; + /** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ + typedef enum + { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 + } jsmntype_t; + + enum jsmnerr + { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 + }; + + /** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ + typedef struct jsmntok + { + jsmntype_t type; + int start; + int end; + int size; #ifdef JSMN_PARENT_LINKS - int parent; + 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 jsmn_parser { - unsigned int pos; /* offset in the JSON string */ - unsigned int toknext; /* next token to allocate */ - int toksuper; /* superior token node, e.g. parent object or array */ -} jsmn_parser; - -/** - * Create JSON parser over an array of tokens - */ -JSMN_API void jsmn_init(jsmn_parser *parser); + /** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ + typedef struct jsmn_parser + { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ + } jsmn_parser; -/** - * 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); + /** + * Create JSON parser over an array of tokens + */ + JSMN_API void jsmn_init(jsmn_parser *parser); + + /** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing + * a single JSON object. + */ + JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens); #ifndef JSMN_HEADER -/** - * Allocates a fresh unused token from the token pool. - */ -static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, - const size_t num_tokens) { - jsmntok_t *tok; - if (parser->toknext >= num_tokens) { - return NULL; - } - tok = &tokens[parser->toknext++]; - tok->start = tok->end = -1; - tok->size = 0; + /** + * 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; + tok->parent = -1; #endif - return tok; -} + 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 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; + /** + * 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; + 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 ':': + /* 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; + 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; + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; #endif -found: - if (tokens == NULL) { + 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; } - 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); + + /** + * 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; + 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': + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) + { + int i; 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; - } + 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--; - break; - /* Unexpected symbol */ - default: - parser->pos = start; - return JSMN_ERROR_INVAL; } } + parser->pos = start; + return JSMN_ERROR_PART; } - parser->pos = start; - return JSMN_ERROR_PART; -} -/** - * Parse JSON string and fill tokens. - */ -JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, - jsmntok_t *tokens, const unsigned int num_tokens) { - int r; - int i; - jsmntok_t *token; - int count = parser->toknext; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c; - jsmntype_t type; + /** + * Parse JSON string and fill tokens. + */ + JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens) + { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; - 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; + 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++; + t->size++; #ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; + 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) { + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; 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; + case '}': + case ']': + if (tokens == NULL) + { break; } - if (token->parent == -1) { - if (token->type != type || parser->toksuper == -1) { - return JSMN_ERROR_INVAL; + 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; } - break; + if (token->parent == -1) + { + if (token->type != type || parser->toksuper == -1) + { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; } - 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; + 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; } - 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; + /* 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) { + 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; + 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; + 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; + } + 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; + /* 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: + /* 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; + 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; + /* 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; + 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; -} + return count; + } -/** - * Creates a new parser based over a given buffer with an array of tokens - * available. - */ -JSMN_API void jsmn_init(jsmn_parser *parser) { - parser->pos = 0; - parser->toknext = 0; - parser->toksuper = -1; -} + /** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ + JSMN_API void jsmn_init(jsmn_parser *parser) + { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; + } #endif /* JSMN_HEADER */ @@ -469,3 +526,220 @@ JSMN_API void jsmn_init(jsmn_parser *parser) { #endif #endif /* JSMN_H */ + +#ifndef JB_JSMN_EDIT +#define JB_JSMN_EDIT +/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/ + +#include +#include +#include +#include +#include + +// Helper function to compare JSON keys +int jsoneq(const char *json, jsmntok_t *tok, const char *s) +{ + if (tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && + strncmp(json + tok->start, s, tok->end - tok->start) == 0) + { + return 0; + } + return -1; +} + +// return the value of the key in the JSON data +char *get_json_value(char *key, char *json_data, uint32_t max_tokens) +{ + // Parse the JSON feed + if (json_data != NULL) + { + jsmn_parser parser; + jsmn_init(&parser); + + // Allocate tokens array on the heap + jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens); + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + return NULL; + } + + int ret = jsmn_parse(&parser, json_data, strlen(json_data), tokens, max_tokens); + if (ret < 0) + { + // Handle parsing errors + FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret); + free(tokens); + return NULL; + } + + // Ensure that the root element is an object + if (ret < 1 || tokens[0].type != JSMN_OBJECT) + { + FURI_LOG_E("JSMM.H", "Root element is not an object."); + free(tokens); + return NULL; + } + + // Loop through the tokens to find the key + for (int i = 1; i < ret; i++) + { + if (jsoneq(json_data, &tokens[i], key) == 0) + { + // We found the key. Now, return the associated value. + int length = tokens[i + 1].end - tokens[i + 1].start; + char *value = malloc(length + 1); + if (value == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for value."); + free(tokens); + return NULL; + } + strncpy(value, json_data + tokens[i + 1].start, length); + value[length] = '\0'; // Null-terminate the string + + free(tokens); // Free the token array + return value; // Return the extracted value + } + } + + // Free the token array if key was not found + free(tokens); + } + else + { + FURI_LOG_E("JSMM.H", "JSON data is NULL"); + } + FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON."); + return NULL; // Return NULL if something goes wrong +} + +// Revised get_json_array_values function with correct token skipping +char **get_json_array_values(char *key, char *json_data, uint32_t max_tokens, int *num_values) +{ + // Retrieve the array string for the given key + char *array_str = get_json_value(key, json_data, max_tokens); + if (array_str == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key); + return NULL; + } + + // Initialize the JSON parser + jsmn_parser parser; + jsmn_init(&parser); + + // Allocate memory for JSON tokens + jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens); // Allocate on the heap + if (tokens == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens."); + free(array_str); + return NULL; + } + + // Parse the JSON array + int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens); + if (ret < 0) + { + FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret); + free(tokens); + free(array_str); + return NULL; + } + + // Ensure the root element is an array + if (tokens[0].type != JSMN_ARRAY) + { + FURI_LOG_E("JSMM.H", "Value for key '%s' is not an array.", key); + free(tokens); + free(array_str); + return NULL; + } + + // Allocate memory for the array of values (maximum possible) + int array_size = tokens[0].size; + char **values = malloc(array_size * sizeof(char *)); + if (values == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values."); + free(tokens); + free(array_str); + return NULL; + } + + int actual_num_values = 0; + + // Traverse the array and extract all object values + int current_token = 1; // Start after the array token + for (int i = 0; i < array_size; i++) + { + if (current_token >= ret) + { + FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array."); + break; + } + + jsmntok_t element = tokens[current_token]; + + if (element.type != JSMN_OBJECT) + { + FURI_LOG_E("JSMM.H", "Array element %d is not an object, skipping.", i); + // Skip this element + current_token += 1; + continue; + } + + int length = element.end - element.start; + + // Allocate a new string for the value and copy the data + char *value = malloc(length + 1); + if (value == NULL) + { + FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element."); + for (int j = 0; j < actual_num_values; j++) + { + free(values[j]); + } + free(values); + free(tokens); + free(array_str); + return NULL; + } + + strncpy(value, array_str + element.start, length); + value[length] = '\0'; // Null-terminate the string + + values[actual_num_values] = value; + actual_num_values++; + + // Skip all tokens related to this object to avoid misparsing + current_token += 1 + (2 * element.size); // Each key-value pair consumes two tokens + } + + *num_values = actual_num_values; + + // Reallocate the values array to actual_num_values if necessary + if (actual_num_values < array_size) + { + char **reduced_values = realloc(values, actual_num_values * sizeof(char *)); + if (reduced_values != NULL) + { + values = reduced_values; + } + + // Free the remaining values + for (int i = actual_num_values; i < array_size; i++) + { + free(values[i]); + } + } + + // Clean up + free(tokens); + free(array_str); + return values; +} + +#endif /* JB_JSMN_EDIT */ \ No newline at end of file