diff --git a/CHANGELOG.md b/CHANGELOG.md index 3691e80..4070ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v1.2 +Updates from Derek Jamison: +- Improved progress display. +- Added connectivity check on startup. + ## 1.1 - Improved memory allocation. - Updated WiFi configuration. diff --git a/alloc/flip_weather_alloc.c b/alloc/flip_weather_alloc.c index d354ae0..f187115 100644 --- a/alloc/flip_weather_alloc.c +++ b/alloc/flip_weather_alloc.c @@ -39,19 +39,20 @@ FlipWeatherApp *flip_weather_app_alloc() { return NULL; } - + view_dispatcher_set_custom_event_callback(app->view_dispatcher, flip_weather_custom_event_callback); // Main view - if (!easy_flipper_set_view(&app->view_weather, FlipWeatherViewWeather, flip_weather_view_draw_callback_weather, NULL, callback_to_submenu, &app->view_dispatcher, app)) + if (!easy_flipper_set_view(&app->view_loader, FlipWeatherViewLoader, flip_weather_loader_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app)) { return NULL; } - if (!easy_flipper_set_view(&app->view_gps, FlipWeatherViewGPS, flip_weather_view_draw_callback_gps, NULL, callback_to_submenu, &app->view_dispatcher, app)) + 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)) { return NULL; } - - // Widget - if (!easy_flipper_set_widget(&app->widget, FlipWeatherViewAbout, "FlipWeather v1.1\n-----\nUse WiFi to get GPS and \nWeather information.\n-----\nwww.github.com/jblanked", 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; } @@ -77,7 +78,7 @@ FlipWeatherApp *flip_weather_app_alloc() variable_item_set_current_value_text(app->variable_item_password, ""); // Submenu - if (!easy_flipper_set_submenu(&app->submenu, FlipWeatherViewSubmenu, "FlipWeather v1.1", 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; } @@ -107,12 +108,6 @@ FlipWeatherApp *flip_weather_app_alloc() } } - // Popup - if (!easy_flipper_set_popup(&app->popup_error, FlipWeatherViewPopupError, "[ERROR]", 0, 0, "Wifi Dev Board disconnected.\nIf your board is connected,\nmake sure you have the\nlatest FlipperHTTP flash.", 0, 12, flip_weather_popup_callback, callback_to_submenu, &app->view_dispatcher, app)) - { - return NULL; - } - // Switch to the main view view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSubmenu); diff --git a/app.c b/app.c index 392373f..195130e 100644 --- a/app.c +++ b/app.c @@ -8,8 +8,8 @@ int32_t flip_weather_app(void *p) UNUSED(p); // Initialize the FlipWeather application - FlipWeatherApp *app = flip_weather_app_alloc(); - if (!app) + app_instance = flip_weather_app_alloc(); + if (!app_instance) { FURI_LOG_E(TAG, "Failed to allocate FlipWeatherApp"); return -1; @@ -21,11 +21,42 @@ int32_t flip_weather_app(void *p) return -1; } + // Thanks to Derek Jamison for the following edits + if (app_instance->uart_text_input_buffer_ssid != NULL && + app_instance->uart_text_input_buffer_password != NULL) + { + // Try to wait for pong response. + uint8_t counter = 10; + while (fhttp.state == INACTIVE && --counter > 0) + { + FURI_LOG_D(TAG, "Waiting for PONG"); + furi_delay_ms(100); + } + + if (counter == 0) + { + DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage *message = dialog_message_alloc(); + dialog_message_set_header( + message, "[FlipperHTTP Error]", 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + message, + "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.", + 0, + 63, + AlignLeft, + AlignBottom); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); + } + } + // Run the view dispatcher - view_dispatcher_run(app->view_dispatcher); + view_dispatcher_run(app_instance->view_dispatcher); // Free the resources used by the FlipWeather application - flip_weather_app_free(app); + flip_weather_app_free(app_instance); // Return 0 to indicate success return 0; diff --git a/application.fam b/application.fam index ca7798c..d094194 100644 --- a/application.fam +++ b/application.fam @@ -10,5 +10,5 @@ App( fap_description="Use WiFi to get GPS and Weather information on your Flipper Zero.", fap_author="JBlanked", fap_weburl="https://github.com/jblanked/FlipWeather", - fap_version = "1.1", + fap_version = "1.2", ) diff --git a/assets/01-home.png b/assets/01-home.png index ebbcf53..409e3cd 100644 Binary files a/assets/01-home.png and b/assets/01-home.png differ diff --git a/callback/flip_weather_callback.c b/callback/flip_weather_callback.c index ae93ea3..019e86b 100644 --- a/callback/flip_weather_callback.c +++ b/callback/flip_weather_callback.c @@ -1,22 +1,27 @@ #include "callback/flip_weather_callback.h" +// Below added by Derek Jamison +// FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port. +#ifdef DEVELOPMENT +#define FURI_LOG_DEV(tag, format, ...) furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__) +#define DEV_CRASH() furi_crash() +#else +#define FURI_LOG_DEV(tag, format, ...) +#define DEV_CRASH() +#endif + bool weather_request_success = false; bool sent_weather_request = false; bool got_weather_data = false; -void flip_weather_popup_callback(void *context) +static void flip_weather_request_error_draw(Canvas *canvas) { - FlipWeatherApp *app = (FlipWeatherApp *)context; - if (!app) + if (canvas == NULL) { - FURI_LOG_E(TAG, "FlipWeatherApp is NULL"); + FURI_LOG_E(TAG, "flip_weather_request_error_draw - canvas is NULL"); + DEV_CRASH(); return; } - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewSubmenu); -} - -void flip_weather_request_error(Canvas *canvas) -{ if (fhttp.last_response != NULL) { if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL) @@ -40,6 +45,11 @@ void flip_weather_request_error(Canvas *canvas) canvas_draw_str(canvas, 0, 50, "Update your WiFi settings."); canvas_draw_str(canvas, 0, 60, "Press BACK to return."); } + else if (strstr(fhttp.last_response, "[PONG]") != NULL) + { + canvas_clear(canvas); + canvas_draw_str(canvas, 0, 10, "[STATUS]Connecting to AP..."); + } else { canvas_clear(canvas); @@ -56,164 +66,14 @@ void flip_weather_request_error(Canvas *canvas) canvas_draw_str(canvas, 0, 60, "Press BACK to return."); } } - -void flip_weather_handle_gps_draw(Canvas *canvas, bool show_gps_data) +static void flip_weather_gps_switch_to_view(FlipWeatherApp *app) { - if (sent_get_request) - { - if (fhttp.state == RECEIVING) - { - if (show_gps_data) - { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Loading GPS..."); - canvas_draw_str(canvas, 0, 22, "Receiving..."); - } - } - // check status - else if (fhttp.state == ISSUE || !get_request_success) - { - flip_weather_request_error(canvas); - } - else if (fhttp.state == IDLE) - { - // success, draw GPS - process_geo_location(); - - if (show_gps_data) - { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, city_data); - canvas_draw_str(canvas, 0, 20, region_data); - canvas_draw_str(canvas, 0, 30, country_data); - canvas_draw_str(canvas, 0, 40, lat_data); - canvas_draw_str(canvas, 0, 50, lon_data); - canvas_draw_str(canvas, 0, 60, ip_data); - } - } - } -} - -// Callback for drawing the weather screen -void flip_weather_view_draw_callback_weather(Canvas *canvas, void *model) -{ - if (!canvas) - { - return; - } - UNUSED(model); - - canvas_set_font(canvas, FontSecondary); - - if (fhttp.state == INACTIVE) - { - canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); - canvas_draw_str(canvas, 0, 17, "Please connect to the board."); - canvas_draw_str(canvas, 0, 32, "If your board is connected,"); - canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); - canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); - canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); - return; - } - - canvas_draw_str(canvas, 0, 10, "Loading location data..."); - // handle geo location until it's processed and then handle weather - - // start the process - if (!send_geo_location_request() || fhttp.state == ISSUE) - { - flip_weather_request_error(canvas); - } - // wait until geo location is processed - if (!sent_get_request || !get_request_success || fhttp.state == RECEIVING) - { - canvas_draw_str(canvas, 0, 22, "Receiving data..."); - return; - } - // get/set geo lcoation once - if (!geo_information_processed) - { - flip_weather_handle_gps_draw(canvas, false); - canvas_draw_str(canvas, 0, 34, "Parsed location data."); - } - // start the weather process - if (!sent_weather_request && fhttp.state == IDLE) - { - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, "Getting Weather..."); - sent_weather_request = true; - char url[512]; - char *lattitude = lat_data + 10; - char *longitude = lon_data + 11; - char *headers = jsmn("Content-Type", "application/json"); - snprintf(url, 512, "https://api.open-meteo.com/v1/forecast?latitude=%s&longitude=%s¤t=temperature_2m,precipitation,rain,showers,snowfall&temperature_unit=celsius&wind_speed_unit=mph&precipitation_unit=inch&forecast_days=1", lattitude, longitude); - weather_request_success = flipper_http_get_request_with_headers(url, headers); - free(headers); - if (!weather_request_success) - { - FURI_LOG_E(TAG, "Failed to send GET request"); - fhttp.state = ISSUE; - flip_weather_request_error(canvas); - } - fhttp.state = RECEIVING; - } - else - { - if (fhttp.state == RECEIVING) - { - canvas_draw_str(canvas, 0, 10, "Loading Weather..."); - canvas_draw_str(canvas, 0, 22, "Receiving..."); - return; - } - - // check status - if (fhttp.state == ISSUE || !weather_request_success) - { - flip_weather_request_error(canvas); - fhttp.state = ISSUE; - } - else - { - // success, draw weather - process_weather(); - canvas_clear(canvas); - canvas_draw_str(canvas, 0, 10, temperature_data); - canvas_draw_str(canvas, 0, 20, precipitation_data); - canvas_draw_str(canvas, 0, 30, rain_data); - canvas_draw_str(canvas, 0, 40, showers_data); - canvas_draw_str(canvas, 0, 50, snowfall_data); - canvas_draw_str(canvas, 0, 60, time_data); - } - } + flip_weather_generic_switch_to_view(app, "Fetching GPS data..", send_geo_location_request, process_geo_location, 1, callback_to_submenu, FlipWeatherViewLoader); } -// Callback for drawing the GPS screen -void flip_weather_view_draw_callback_gps(Canvas *canvas, void *model) +static void flip_weather_weather_switch_to_view(FlipWeatherApp *app) { - if (!canvas) - { - return; - } - UNUSED(model); - - if (fhttp.state == INACTIVE) - { - canvas_set_font(canvas, FontSecondary); - canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); - canvas_draw_str(canvas, 0, 17, "Please connect to the board."); - canvas_draw_str(canvas, 0, 32, "If your board is connected,"); - canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); - canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); - canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); - return; - } - - if (!send_geo_location_request() || fhttp.state == ISSUE) - { - flip_weather_request_error(canvas); - } - - flip_weather_handle_gps_draw(canvas, true); + 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) @@ -227,24 +87,11 @@ void callback_submenu_choices(void *context, uint32_t index) switch (index) { case FlipWeatherSubmenuIndexWeather: - if (!flip_weather_handle_ip_address()) - { - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewPopupError); - } - else - { - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewWeather); - } + 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: - if (!flip_weather_handle_ip_address()) - { - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewPopupError); - } - else - { - view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewGPS); - } + flip_weather_gps_switch_to_view(app); break; case FlipWeatherSubmenuIndexAbout: view_dispatcher_switch_to_view(app->view_dispatcher, FlipWeatherViewAbout); @@ -347,6 +194,16 @@ uint32_t callback_to_submenu(void *context) weather_information_processed = false; sent_weather_request = false; weather_request_success = false; + if (weather_data != NULL) + { + free(weather_data); + weather_data = NULL; + } + if (total_data != NULL) + { + free(total_data); + total_data = NULL; + } return FlipWeatherViewSubmenu; } @@ -398,4 +255,463 @@ uint32_t callback_to_wifi_settings(void *context) } UNUSED(context); return FlipWeatherViewSettings; -} \ No newline at end of file +} + +static void flip_weather_widget_set_text(char *message, Widget **widget) +{ + if (widget == NULL) + { + FURI_LOG_E(TAG, "flip_weather_set_widget_text - widget is NULL"); + DEV_CRASH(); + return; + } + if (message == NULL) + { + FURI_LOG_E(TAG, "flip_weather_set_widget_text - message is NULL"); + DEV_CRASH(); + return; + } + widget_reset(*widget); + + uint32_t message_length = strlen(message); // Length of the message + uint32_t i = 0; // Index tracker + uint32_t formatted_index = 0; // Tracker for where we are in the formatted message + char *formatted_message; // Buffer to hold the final formatted message + + // Allocate buffer with double the message length plus one for safety + if (!easy_flipper_set_buffer(&formatted_message, message_length * 2 + 1)) + { + return; + } + + while (i < message_length) + { + uint32_t max_line_length = 31; // Maximum characters per line + uint32_t remaining_length = message_length - i; // Remaining characters + uint32_t line_length = (remaining_length < max_line_length) ? remaining_length : max_line_length; + + // Check for newline character within the current segment + uint32_t newline_pos = i; + bool found_newline = false; + for (; newline_pos < i + line_length && newline_pos < message_length; newline_pos++) + { + if (message[newline_pos] == '\n') + { + found_newline = true; + break; + } + } + + if (found_newline) + { + // If newline found, set line_length up to the newline + line_length = newline_pos - i; + } + + // Temporary buffer to hold the current line + char line[32]; + strncpy(line, message + i, line_length); + line[line_length] = '\0'; + + // If newline was found, skip it for the next iteration + if (found_newline) + { + i += line_length + 1; // +1 to skip the '\n' character + } + else + { + // Check if the line ends in the middle of a word and adjust accordingly + if (line_length == max_line_length && message[i + line_length] != '\0' && message[i + line_length] != ' ') + { + // Find the last space within the current line to avoid breaking a word + char *last_space = strrchr(line, ' '); + if (last_space != NULL) + { + // Adjust the line_length to avoid cutting the word + line_length = last_space - line; + line[line_length] = '\0'; // Null-terminate at the space + } + } + + // Move the index forward by the determined line_length + i += line_length; + + // Skip any spaces at the beginning of the next line + while (i < message_length && message[i] == ' ') + { + i++; + } + } + + // Manually copy the fixed line into the formatted_message buffer + for (uint32_t j = 0; j < line_length; j++) + { + formatted_message[formatted_index++] = line[j]; + } + + // Add a newline character for line spacing + formatted_message[formatted_index++] = '\n'; + } + + // Null-terminate the formatted_message + formatted_message[formatted_index] = '\0'; + + // Add the formatted message to the widget + widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message); +} + +void flip_weather_loader_draw_callback(Canvas *canvas, void *model) +{ + if (!canvas || !model) + { + FURI_LOG_E(TAG, "flip_weather_loader_draw_callback - canvas or model is NULL"); + return; + } + + SerialState http_state = fhttp.state; + DataLoaderModel *data_loader_model = (DataLoaderModel *)model; + DataState data_state = data_loader_model->data_state; + char *title = data_loader_model->title; + + canvas_set_font(canvas, FontSecondary); + + if (http_state == INACTIVE) + { + canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected."); + canvas_draw_str(canvas, 0, 17, "Please connect to the board."); + canvas_draw_str(canvas, 0, 32, "If your board is connected,"); + canvas_draw_str(canvas, 0, 42, "make sure you have flashed"); + canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the"); + canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash."); + return; + } + + if (data_state == DataStateError || data_state == DataStateParseError) + { + flip_weather_request_error_draw(canvas); + return; + } + + canvas_draw_str(canvas, 0, 7, title); + canvas_draw_str(canvas, 0, 17, "Loading..."); + + if (data_state == DataStateInitial) + { + return; + } + + if (http_state == SENDING) + { + canvas_draw_str(canvas, 0, 27, "Sending..."); + return; + } + + if (http_state == RECEIVING || data_state == DataStateRequested) + { + canvas_draw_str(canvas, 0, 27, "Receiving..."); + return; + } + + if (http_state == IDLE && data_state == DataStateReceived) + { + canvas_draw_str(canvas, 0, 27, "Processing..."); + return; + } + + if (http_state == IDLE && data_state == DataStateParsed) + { + canvas_draw_str(canvas, 0, 27, "Processed..."); + return; + } +} + +static void flip_weather_loader_process_callback(void *context) +{ + if (context == NULL) + { + FURI_LOG_E(TAG, "flip_weather_loader_process_callback - context is NULL"); + DEV_CRASH(); + return; + } + + FlipWeatherApp *app = (FlipWeatherApp *)context; + View *view = app->view_loader; + + DataState current_data_state; + with_view_model(view, DataLoaderModel * model, { current_data_state = model->data_state; }, false); + + if (current_data_state == DataStateInitial) + { + with_view_model( + view, + DataLoaderModel * model, + { + model->data_state = DataStateRequested; + DataLoaderFetch fetch = model->fetcher; + if (fetch == NULL) + { + FURI_LOG_E(TAG, "Model doesn't have Fetch function assigned."); + model->data_state = DataStateError; + return; + } + + // Clear any previous responses + strncpy(fhttp.last_response, "", 1); + bool request_status = fetch(model); + if (!request_status) + { + model->data_state = DataStateError; + } + }, + true); + } + else if (current_data_state == DataStateRequested || current_data_state == DataStateError) + { + if (fhttp.state == IDLE && fhttp.last_response != NULL) + { + if (strstr(fhttp.last_response, "[PONG]") != NULL) + { + FURI_LOG_DEV(TAG, "PONG received."); + } + else if (strncmp(fhttp.last_response, "[SUCCESS]", 9) == 0) + { + FURI_LOG_DEV(TAG, "SUCCESS received. %s", fhttp.last_response ? fhttp.last_response : "NULL"); + } + else if (strncmp(fhttp.last_response, "[ERROR]", 9) == 0) + { + FURI_LOG_DEV(TAG, "ERROR received. %s", fhttp.last_response ? fhttp.last_response : "NULL"); + } + else if (strlen(fhttp.last_response) == 0) + { + // Still waiting on response + } + else + { + with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateReceived; }, true); + } + } + else if (fhttp.state == SENDING || fhttp.state == RECEIVING) + { + // continue waiting + } + else if (fhttp.state == INACTIVE) + { + // inactive. try again + } + else if (fhttp.state == ISSUE) + { + with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateError; }, true); + } + else + { + FURI_LOG_DEV(TAG, "Unexpected state: %d lastresp: %s", fhttp.state, fhttp.last_response ? fhttp.last_response : "NULL"); + DEV_CRASH(); + } + } + else if (current_data_state == DataStateReceived) + { + with_view_model( + view, + DataLoaderModel * model, + { + char *data_text; + if (model->parser == NULL) + { + data_text = NULL; + FURI_LOG_DEV(TAG, "Parser is NULL"); + DEV_CRASH(); + } + else + { + data_text = model->parser(model); + } + FURI_LOG_DEV(TAG, "Parsed data: %s\r\ntext: %s", fhttp.last_response ? fhttp.last_response : "NULL", data_text ? data_text : "NULL"); + model->data_text = data_text; + if (data_text == NULL) + { + model->data_state = DataStateParseError; + } + else + { + model->data_state = DataStateParsed; + } + }, + true); + } + else if (current_data_state == DataStateParsed) + { + with_view_model( + view, + DataLoaderModel * model, + { + if (++model->request_index < model->request_count) + { + model->data_state = DataStateInitial; + } + else + { + flip_weather_widget_set_text(model->data_text != NULL ? model->data_text : "Empty result", &app_instance->widget_result); + if (model->data_text != NULL) + { + free(model->data_text); + model->data_text = NULL; + } + view_set_previous_callback(widget_get_view(app_instance->widget_result), model->back_callback); + view_dispatcher_switch_to_view(app_instance->view_dispatcher, FlipWeatherViewWidgetResult); + } + }, + true); + } +} + +static void flip_weather_loader_timer_callback(void *context) +{ + if (context == NULL) + { + FURI_LOG_E(TAG, "flip_weather_loader_timer_callback - context is NULL"); + DEV_CRASH(); + return; + } + FlipWeatherApp *app = (FlipWeatherApp *)context; + view_dispatcher_send_custom_event(app->view_dispatcher, FlipWeatherCustomEventProcess); +} + +static void flip_weather_loader_on_enter(void *context) +{ + if (context == NULL) + { + FURI_LOG_E(TAG, "flip_weather_loader_on_enter - context is NULL"); + DEV_CRASH(); + return; + } + FlipWeatherApp *app = (FlipWeatherApp *)context; + View *view = app->view_loader; + with_view_model( + view, + DataLoaderModel * model, + { + view_set_previous_callback(view, model->back_callback); + if (model->timer == NULL) + { + model->timer = furi_timer_alloc(flip_weather_loader_timer_callback, FuriTimerTypePeriodic, app); + } + furi_timer_start(model->timer, 250); + }, + true); +} + +static void flip_weather_loader_on_exit(void *context) +{ + if (context == NULL) + { + FURI_LOG_E(TAG, "flip_weather_loader_on_exit - context is NULL"); + DEV_CRASH(); + return; + } + FlipWeatherApp *app = (FlipWeatherApp *)context; + View *view = app->view_loader; + with_view_model( + view, + DataLoaderModel * model, + { + if (model->timer) + { + furi_timer_stop(model->timer); + } + }, + false); +} + +void flip_weather_loader_init(View *view) +{ + if (view == NULL) + { + FURI_LOG_E(TAG, "flip_weather_loader_init - view is NULL"); + DEV_CRASH(); + return; + } + view_allocate_model(view, ViewModelTypeLocking, sizeof(DataLoaderModel)); + view_set_enter_callback(view, flip_weather_loader_on_enter); + view_set_exit_callback(view, flip_weather_loader_on_exit); +} + +void flip_weather_loader_free_model(View *view) +{ + if (view == NULL) + { + FURI_LOG_E(TAG, "flip_weather_loader_free_model - view is NULL"); + DEV_CRASH(); + return; + } + with_view_model( + view, + DataLoaderModel * model, + { + if (model->timer) + { + furi_timer_free(model->timer); + model->timer = NULL; + } + if (model->parser_context) + { + free(model->parser_context); + model->parser_context = NULL; + } + }, + false); +} + +bool flip_weather_custom_event_callback(void *context, uint32_t index) +{ + if (context == NULL) + { + FURI_LOG_E(TAG, "flip_weather_custom_event_callback - context is NULL"); + DEV_CRASH(); + return false; + } + + switch (index) + { + case FlipWeatherCustomEventProcess: + flip_weather_loader_process_callback(context); + return true; + default: + FURI_LOG_DEV(TAG, "flip_weather_custom_event_callback. Unknown index: %ld", index); + return false; + } +} + +void flip_weather_generic_switch_to_view(FlipWeatherApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id) +{ + if (app == NULL) + { + FURI_LOG_E(TAG, "flip_weather_generic_switch_to_view - app is NULL"); + DEV_CRASH(); + return; + } + + View *view = app->view_loader; + if (view == NULL) + { + FURI_LOG_E(TAG, "flip_weather_generic_switch_to_view - view is NULL"); + DEV_CRASH(); + return; + } + + with_view_model( + view, + DataLoaderModel * model, + { + model->title = title; + model->fetcher = fetcher; + model->parser = parser; + model->request_index = 0; + model->request_count = request_count; + model->back_callback = back; + model->data_state = DataStateInitial; + model->data_text = NULL; + }, + true); + + view_dispatcher_switch_to_view(app->view_dispatcher, view_id); +} diff --git a/callback/flip_weather_callback.h b/callback/flip_weather_callback.h index 27921bb..9b12bdd 100644 --- a/callback/flip_weather_callback.h +++ b/callback/flip_weather_callback.h @@ -9,9 +9,6 @@ extern bool weather_request_success; extern bool sent_weather_request; extern bool got_weather_data; -void flip_weather_popup_callback(void *context); -void flip_weather_request_error(Canvas *canvas); -void flip_weather_handle_gps_draw(Canvas *canvas, bool show_gps_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); @@ -27,4 +24,15 @@ void settings_item_selected(void *context, uint32_t index); */ uint32_t callback_exit_app(void *context); uint32_t callback_to_wifi_settings(void *context); + +// Add edits by Derek Jamison +void flip_weather_generic_switch_to_view(FlipWeatherApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id); + +void flip_weather_loader_draw_callback(Canvas *canvas, void *model); + +void flip_weather_loader_init(View *view); + +void flip_weather_loader_free_model(View *view); + +bool flip_weather_custom_event_callback(void *context, uint32_t index); #endif \ No newline at end of file diff --git a/easy_flipper/easy_flipper.h b/easy_flipper/easy_flipper.h index f87d9ae..da3d102 100644 --- a/easy_flipper/easy_flipper.h +++ b/easy_flipper/easy_flipper.h @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/flip_weather.c b/flip_weather.c index 6f5ed99..54b451d 100644 --- a/flip_weather.c +++ b/flip_weather.c @@ -1,18 +1,13 @@ #include "flip_weather.h" -char city_data[48]; -char region_data[48]; -char country_data[48]; char lat_data[32]; char lon_data[32]; -char ip_data[32]; -char temperature_data[32]; -char precipitation_data[32]; -char rain_data[32]; -char showers_data[32]; -char snowfall_data[32]; -char time_data[32]; -char ip_address[16]; + +char *total_data = NULL; +char *weather_data = NULL; + +FlipWeatherApp *app_instance = NULL; +void flip_weather_loader_free_model(View *view); // Function to free the resources used by FlipWeatherApp void flip_weather_app_free(FlipWeatherApp *app) @@ -24,15 +19,11 @@ void flip_weather_app_free(FlipWeatherApp *app) } // Free View(s) - if (app->view_weather) + if (app->view_loader) { - view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewWeather); - view_free(app->view_weather); - } - if (app->view_gps) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewGPS); - view_free(app->view_gps); + view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewLoader); + flip_weather_loader_free_model(app->view_loader); + view_free(app->view_loader); } // Free Submenu(s) @@ -48,6 +39,11 @@ void flip_weather_app_free(FlipWeatherApp *app) view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewAbout); widget_free(app->widget); } + if (app->widget_result) + { + view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewWidgetResult); + widget_free(app->widget_result); + } // Free Variable Item List(s) if (app->variable_item_list) @@ -68,13 +64,6 @@ void flip_weather_app_free(FlipWeatherApp *app) uart_text_input_free(app->uart_text_input_password); } - // Free Popup(s) - if (app->popup_error) - { - view_dispatcher_remove_view(app->view_dispatcher, FlipWeatherViewPopupError); - popup_free(app->popup_error); - } - // Free the text input buffer if (app->uart_text_input_buffer_ssid) free(app->uart_text_input_buffer_ssid); @@ -95,6 +84,9 @@ void flip_weather_app_free(FlipWeatherApp *app) // close the gui furi_record_close(RECORD_GUI); + if (total_data) + free(total_data); + // free the app if (app) free(app); diff --git a/flip_weather.h b/flip_weather.h index 09a839b..c24df06 100644 --- a/flip_weather.h +++ b/flip_weather.h @@ -20,25 +20,25 @@ typedef enum // Define a single view for our FlipWeather application typedef enum { - FlipWeatherViewWeather, // The weather screen - FlipWeatherViewGPS, // The GPS screen FlipWeatherViewSubmenu, // The main submenu FlipWeatherViewAbout, // The about screen FlipWeatherViewSettings, // The wifi settings screen FlipWeatherViewTextInputSSID, // The text input screen for SSID FlipWeatherViewTextInputPassword, // The text input screen for password // - FlipWeatherViewPopupError, // The error popup screen + FlipWeatherViewPopupError, // The error popup screen + FlipWeatherViewWidgetResult, // The text box that displays the random fact + FlipWeatherViewLoader, // The loader screen retrieves data from the internet } FlipWeatherView; // Each screen will have its own view typedef struct { ViewDispatcher *view_dispatcher; // Switches between our views - View *view_weather; // The weather view - View *view_gps; // The GPS view + View *view_loader; // The screen that loads data from internet Submenu *submenu; // The main submenu Widget *widget; // The widget (about) + Widget *widget_result; // The widget that displays the result Popup *popup_error; // The error popup VariableItemList *variable_item_list; // The variable item list (settngs) VariableItem *variable_item_ssid; // The variable item @@ -55,21 +55,14 @@ typedef struct uint32_t uart_text_input_buffer_size_password; // Size of the text input buffer } FlipWeatherApp; -extern char city_data[48]; -extern char region_data[48]; -extern char country_data[48]; extern char lat_data[32]; extern char lon_data[32]; -extern char ip_data[32]; -extern char temperature_data[32]; -extern char precipitation_data[32]; -extern char rain_data[32]; -extern char showers_data[32]; -extern char snowfall_data[32]; -extern char time_data[32]; -extern char ip_address[16]; + +extern char *total_data; +extern char *weather_data; // Function to free the resources used by FlipWeatherApp void flip_weather_app_free(FlipWeatherApp *app); +extern FlipWeatherApp *app_instance; #endif \ No newline at end of file diff --git a/flipper_http/flipper_http.c b/flipper_http/flipper_http.c index 8c95b60..177ee3b 100644 --- a/flipper_http/flipper_http.c +++ b/flipper_http/flipper_http.c @@ -1467,4 +1467,53 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars return false; } return true; -} \ No newline at end of file +} + +/** + * @brief Perform a task while displaying a loading screen + * @param http_request The function to send the request + * @param parse_response The function to parse the response + * @param success_view_id The view ID to switch to on success + * @param failure_view_id The view ID to switch to on failure + * @param view_dispatcher The view dispatcher to use + * @return + */ +void flipper_http_loading_task(bool (*http_request)(void), + bool (*parse_response)(void), + uint32_t success_view_id, + uint32_t failure_view_id, + ViewDispatcher **view_dispatcher) +{ + Loading *loading; + int32_t loading_view_id = 987654321; // Random ID + + loading = loading_alloc(); + if (!loading) + { + FURI_LOG_E(HTTP_TAG, "Failed to allocate loading"); + view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); + + return; + } + + view_dispatcher_add_view(*view_dispatcher, loading_view_id, loading_get_view(loading)); + + // Switch to the loading view + view_dispatcher_switch_to_view(*view_dispatcher, loading_view_id); + + // Make the request + if (!flipper_http_process_response_async(http_request, parse_response)) + { + FURI_LOG_E(HTTP_TAG, "Failed to make request"); + view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id); + view_dispatcher_remove_view(*view_dispatcher, loading_view_id); + loading_free(loading); + + return; + } + + // Switch to the success view + view_dispatcher_switch_to_view(*view_dispatcher, success_view_id); + view_dispatcher_remove_view(*view_dispatcher, loading_view_id); + loading_free(loading); +} diff --git a/flipper_http/flipper_http.h b/flipper_http/flipper_http.h index a38bd8e..e5c90a7 100644 --- a/flipper_http/flipper_http.h +++ b/flipper_http/flipper_http.h @@ -2,6 +2,10 @@ #ifndef FLIPPER_HTTP_H #define FLIPPER_HTTP_H +#include +#include +#include +#include #include #include #include @@ -363,4 +367,19 @@ char *trim(const char *str); */ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void)); +/** + * @brief Perform a task while displaying a loading screen + * @param http_request The function to send the request + * @param parse_response The function to parse the response + * @param success_view_id The view ID to switch to on success + * @param failure_view_id The view ID to switch to on failure + * @param view_dispatcher The view dispatcher to use + * @return + */ +void flipper_http_loading_task(bool (*http_request)(void), + bool (*parse_response)(void), + uint32_t success_view_id, + uint32_t failure_view_id, + ViewDispatcher **view_dispatcher); + #endif // FLIPPER_HTTP_H diff --git a/parse/flip_weather_parse.c b/parse/flip_weather_parse.c index ca2602b..a503299 100644 --- a/parse/flip_weather_parse.c +++ b/parse/flip_weather_parse.c @@ -6,117 +6,88 @@ bool got_ip_address = false; bool geo_information_processed = false; bool weather_information_processed = false; -bool flip_weather_parse_ip_address() +bool send_geo_location_request() { - // load the received data from the saved file - FuriString *returned_data = flipper_http_load_from_file(fhttp.file_path); - if (returned_data == NULL) - { - FURI_LOG_E(TAG, "Failed to load received data from file."); - return false; - } - char *data_cstr = (char *)furi_string_get_cstr(returned_data); - if (data_cstr == NULL) + if (fhttp.state == INACTIVE) { - FURI_LOG_E(TAG, "Failed to get C-string from FuriString."); - furi_string_free(returned_data); + FURI_LOG_E(TAG, "Board is INACTIVE"); + flipper_http_ping(); // ping the device + fhttp.state = ISSUE; return false; } - char *ip = get_json_value("origin", (char *)data_cstr, MAX_TOKENS); - if (ip == NULL) + if (!flipper_http_get_request_with_headers("https://ipwhois.app/json/", "{\"Content-Type\": \"application/json\"}")) { - FURI_LOG_E(TAG, "Failed to get IP address"); - sent_get_request = true; - get_request_success = false; + FURI_LOG_E(TAG, "Failed to send GET request"); fhttp.state = ISSUE; - furi_string_free(returned_data); - free(data_cstr); return false; } - snprintf(ip_address, 16, "%s", ip); - free(ip); - furi_string_free(returned_data); - free(data_cstr); + fhttp.state = RECEIVING; return true; } -// handle the async-to-sync process to get and set the IP address -bool flip_weather_handle_ip_address() +bool send_geo_weather_request(DataLoaderModel *model) { - if (fhttp.state == INACTIVE) + UNUSED(model); + char url[512]; + char *lattitude = lat_data + 10; + char *longitude = lon_data + 11; + snprintf(url, 512, "https://api.open-meteo.com/v1/forecast?latitude=%s&longitude=%s¤t=temperature_2m,precipitation,rain,showers,snowfall&temperature_unit=celsius&wind_speed_unit=mph&precipitation_unit=inch&forecast_days=1", lattitude, longitude); + if (!flipper_http_get_request_with_headers(url, "{\"Content-Type\": \"application/json\"}")) { - FURI_LOG_E(TAG, "Board is INACTIVE"); - flipper_http_ping(); // ping the device + FURI_LOG_E(TAG, "Failed to send GET request"); + fhttp.state = ISSUE; return false; } - if (!got_ip_address) - { - got_ip_address = true; - snprintf( - fhttp.file_path, - sizeof(fhttp.file_path), - STORAGE_EXT_PATH_PREFIX "/apps_data/flip_weather/ip.txt"); - - fhttp.save_received_data = true; - if (!flipper_http_get_request("https://httpbin.org/get")) - { - FURI_LOG_E(TAG, "Failed to get IP address"); - fhttp.state = ISSUE; - return false; - } - else - { - fhttp.state = RECEIVING; - furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS); - } - while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) - { - // Wait for the feed to be received - furi_delay_ms(10); - } - furi_timer_stop(fhttp.get_timeout_timer); - if (!flip_weather_parse_ip_address()) - { - FURI_LOG_E(TAG, "Failed to get IP address"); - sent_get_request = true; - get_request_success = false; - fhttp.state = ISSUE; - return false; - } - } + fhttp.state = RECEIVING; return true; } - -bool send_geo_location_request() +char *process_geo_location(DataLoaderModel *model) { - if (fhttp.state == INACTIVE) + UNUSED(model); + if (fhttp.last_response != NULL) { - FURI_LOG_E(TAG, "Board is INACTIVE"); - flipper_http_ping(); // ping the device - return false; - } - if (!sent_get_request && fhttp.state == IDLE) - { - sent_get_request = true; - char *headers = jsmn("Content-Type", "application/json"); - get_request_success = flipper_http_get_request_with_headers("https://ipwhois.app/json/", headers); - free(headers); - if (!get_request_success) + 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 send GET request"); + FURI_LOG_E(TAG, "Failed to get geo location data"); fhttp.state = ISSUE; - return false; + return NULL; + } + + snprintf(lat_data, sizeof(lat_data), "Latitude: %s", latitude); + snprintf(lon_data, sizeof(lon_data), "Longitude: %s", longitude); + + if (!total_data) + { + total_data = (char *)malloc(512); + if (!total_data) + { + FURI_LOG_E(TAG, "Failed to allocate memory for total_data"); + fhttp.state = ISSUE; + return NULL; + } } - fhttp.state = RECEIVING; + snprintf(total_data, 512, "You are in %s, %s, %s. \nLatitude: %s, Longitude: %s", city, region, country, latitude, longitude); + + fhttp.state = IDLE; + free(city); + free(region); + free(country); + free(latitude); + free(longitude); } - return true; + return total_data; } -void process_geo_location() +bool process_geo_location_2() { - if (!geo_information_processed && fhttp.last_response != NULL) + if (fhttp.last_response != NULL) { - geo_information_processed = true; char *city = get_json_value("city", fhttp.last_response, MAX_TOKENS); char *region = get_json_value("region", fhttp.last_response, MAX_TOKENS); char *country = get_json_value("country", fhttp.last_response, MAX_TOKENS); @@ -127,15 +98,11 @@ void process_geo_location() { FURI_LOG_E(TAG, "Failed to get geo location data"); fhttp.state = ISSUE; - return; + return false; } - snprintf(city_data, 64, "City: %s", city); - snprintf(region_data, 64, "Region: %s", region); - snprintf(country_data, 64, "Country: %s", country); - snprintf(lat_data, 64, "Latitude: %s", latitude); - snprintf(lon_data, 64, "Longitude: %s", longitude); - snprintf(ip_data, 64, "IP Address: %s", ip_address); + snprintf(lat_data, sizeof(lat_data), "Latitude: %s", latitude); + snprintf(lon_data, sizeof(lon_data), "Longitude: %s", longitude); fhttp.state = IDLE; free(city); @@ -143,14 +110,16 @@ void process_geo_location() free(country); free(latitude); free(longitude); + return true; } + return false; } -void process_weather() +char *process_weather(DataLoaderModel *model) { - if (!weather_information_processed && fhttp.last_response != NULL) + UNUSED(model); + if (fhttp.last_response != NULL) { - weather_information_processed = true; char *current_data = get_json_value("current", fhttp.last_response, MAX_TOKENS); char *temperature = get_json_value("temperature_2m", current_data, MAX_TOKENS); char *precipitation = get_json_value("precipitation", current_data, MAX_TOKENS); @@ -163,7 +132,7 @@ void process_weather() { FURI_LOG_E(TAG, "Failed to get weather data"); fhttp.state = ISSUE; - return; + return NULL; } // replace the "T" in time with a space @@ -173,12 +142,17 @@ void process_weather() *ptr = ' '; } - snprintf(temperature_data, 64, "Temperature (C): %s", temperature); - snprintf(precipitation_data, 64, "Precipitation: %s", precipitation); - snprintf(rain_data, 64, "Rain: %s", rain); - snprintf(showers_data, 64, "Showers: %s", showers); - snprintf(snowfall_data, 64, "Snowfall: %s", snowfall); - snprintf(time_data, 64, "Time: %s", time); + if (!weather_data) + { + weather_data = (char *)malloc(512); + if (!weather_data) + { + FURI_LOG_E(TAG, "Failed to allocate memory for weather_data"); + fhttp.state = ISSUE; + return NULL; + } + } + snprintf(weather_data, 512, "Temperature: %s C\nPrecipitation: %s\nRain: %s\nShowers: %s\nSnowfall: %s\nTime: %s", temperature, precipitation, rain, showers, snowfall, time); fhttp.state = IDLE; free(current_data); @@ -189,11 +163,5 @@ void process_weather() free(snowfall); free(time); } - else if (!weather_information_processed && fhttp.last_response == NULL) - { - FURI_LOG_E(TAG, "Failed to process weather data"); - // store error message - snprintf(temperature_data, 64, "Failed. Update WiFi settings."); - fhttp.state = ISSUE; - } + return weather_data; } \ No newline at end of file diff --git a/parse/flip_weather_parse.h b/parse/flip_weather_parse.h index 8075081..4175561 100644 --- a/parse/flip_weather_parse.h +++ b/parse/flip_weather_parse.h @@ -7,10 +7,45 @@ extern bool got_ip_address; extern bool geo_information_processed; extern bool weather_information_processed; -bool flip_weather_parse_ip_address(); -bool flip_weather_handle_ip_address(); +// Add edits by Derek Jamison +typedef enum DataState DataState; +enum DataState +{ + DataStateInitial, + DataStateRequested, + DataStateReceived, + DataStateParsed, + DataStateParseError, + DataStateError, +}; + +typedef enum FlipWeatherCustomEvent FlipWeatherCustomEvent; +enum FlipWeatherCustomEvent +{ + FlipWeatherCustomEventProcess, +}; + +typedef struct DataLoaderModel DataLoaderModel; +typedef bool (*DataLoaderFetch)(DataLoaderModel *model); +typedef char *(*DataLoaderParser)(DataLoaderModel *model); +struct DataLoaderModel +{ + char *title; + char *data_text; + DataState data_state; + DataLoaderFetch fetcher; + DataLoaderParser parser; + void *parser_context; + size_t request_index; + size_t request_count; + ViewNavigationCallback back_callback; + FuriTimer *timer; +}; + bool send_geo_location_request(); -void process_geo_location(); -void process_weather(); +char *process_geo_location(DataLoaderModel *model); +bool process_geo_location_2(); +char *process_weather(DataLoaderModel *model); +bool send_geo_weather_request(DataLoaderModel *model); #endif \ No newline at end of file