From 55163b9aa73ddf6c7ae985df2e65bc0e389b56c8 Mon Sep 17 00:00:00 2001 From: Matthias Meulien Date: Wed, 4 Oct 2023 01:08:19 +0200 Subject: [PATCH] Add log file Refs: #37 --- .gitignore | 1 + CONTRIBUTING.md | 18 +- NEWS.md | 269 ++++++++++++++++++++++++++++ docs/desktop_integration.md | 4 + meson.build | 13 +- src/app.cc | 6 +- src/app.h | 74 ++++++-- src/config.h | 13 +- src/currentconditionbox.cc | 3 +- src/currentconditionbox.h | 6 +- src/events.cc | 340 ++++++++++++++++++++++++++++++++++++ src/events.h | 9 + src/history.h | 14 ++ src/http.cc | 91 +++++++++- src/http.h | 76 +------- src/icons.h | 4 + src/l10n.h | 14 +- src/logging.cc | 42 +++++ src/logging.h | 10 ++ src/main.cc | 16 +- src/meson.build | 4 + src/service.cc | 266 ++++++++++++++++++++++++++++ src/service.h | 256 ++------------------------- src/state.cc | 94 ++++++++++ src/state.h | 68 +------- src/ui.cc | 6 + 26 files changed, 1300 insertions(+), 417 deletions(-) create mode 100644 src/events.cc create mode 100644 src/logging.cc create mode 100644 src/logging.h create mode 100644 src/service.cc create mode 100644 src/state.cc diff --git a/.gitignore b/.gitignore index b23cb6c..7a0f2f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /builddir /crossfile_arm.ini /3rd-parties +/data*.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b367924..41ef987 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,7 +101,20 @@ convert /media/matthias/Vivlio/screens/scr0002.bmp \ docs/screenshot-hourly-forecast.jpg ``` -## Remote debugging +## Debugging + +### Log file + +The application can be started with the `--verbose` command line +argument to generate a detailed log file. The file path is of the form +`/mnt/ext1/system/state/taranis.log` but may vary depending on the use +of profiles. + +One can specify command line arguments when starting the application +from [pbterm](https://github.com/Alastor27/pbterm) or through [Desktop +integration](./docs/desktop_integration.md) + +### Remote debugging One must first start `gdbserver` on the e-reader: @@ -114,7 +127,8 @@ One must first start `gdbserver` on the e-reader: 5. Run `gdbserver --attach :10002 ${TARANIS_PID}` The e-reader must be connected to Internet and its IP address must be -known (eg start `ipconfig` in `pbterm`). +known (eg start `ipconfig` in `pbterm`). It is recommended to disable +automatic poweroff. On the host computer, start a shell with current working directory the root directory of a Git clone of taranis repository. Then start GDB: diff --git a/NEWS.md b/NEWS.md index c9598d9..a05e0a4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added +- Add log file + [#37](https://github.com/orontee/taranis/issues/37) + - Document desktop integration [#36](https://github.com/orontee/taranis/issues/36) @@ -309,3 +312,269 @@ Fix some (minor) visual glitches [#3](https://github.com/orontee/taranis/issues/3) - Weather icons [#1](https://github.com/orontee/taranis/issues/1) +Don't open menu during data refresh + [#51](https://github.com/orontee/taranis/issues/51) + +- Fix some (minor) visual glitches + [#47](https://github.com/orontee/taranis/issues/47) + +- Display full date in status bar + [#46](https://github.com/orontee/taranis/issues/46) + +- Fix background color of icons + [#45](https://github.com/orontee/taranis/issues/45) + +### Removed + +## [1.1.0] - 2023-09-16 + +### Added + +- Display daily forecast + [#8](https://github.com/orontee/taranis/issues/8) + +- Display credits in about dialog + [#34](https://github.com/orontee/taranis/issues/34) + +- History of locations in menu + [#18](https://github.com/orontee/taranis/issues/18) + +- Display alerts [#24](https://github.com/orontee/taranis/issues/24) + +- Polish translations + [#22](https://github.com/orontee/taranis/issues/22) + +### Changed + +- Switching forecast view should not refresh data nor update whole + screen [#38](https://github.com/orontee/taranis/issues/38) + +- Don't refresh data after cancel of location edit + [#39](https://github.com/orontee/taranis/issues/39) + +- Fix invisible alerts button handing touch events + [#35](https://github.com/orontee/taranis/issues/35) + +## [1.0.1] - 2023-09-07 + +### Added + +- Check software version at startup + [#30](https://github.com/orontee/taranis/issues/30) + +## [1.0.0] - 2023-09-06 + +### Added + +- Draw precipitation histogram + [#20](https://github.com/orontee/taranis/issues/20) + +- Sub-menu to select unit system + [#4](https://github.com/orontee/taranis/issues/4) + +- Location selection through text input + [#12](https://github.com/orontee/taranis/issues/12) + +- Specify language when calling OpenWeather API + [#15](https://github.com/orontee/taranis/issues/15) + +- Take profile into account when reading configuration + [#13](https://github.com/orontee/taranis/issues/13) + +- Display current weather description + [#14](https://github.com/orontee/taranis/issues/14) + +- Elide location text when too long + [#12](https://github.com/orontee/taranis/issues/12) + +- About dialog [#7](https://github.com/orontee/taranis/issues/7) + +- Application menu [#6](https://github.com/orontee/taranis/issues/6) + +- Support for translations + [#3](https://github.com/orontee/taranis/issues/3) + +- Weather icons [#1](https://github.com/orontee/taranis/issues/1) +Automatic refresh should not popup connection dialog + [#50](https://github.com/orontee/taranis/issues/50) + +- API key can be specified through configuration file + [#44](https://github.com/orontee/taranis/issues/44) + +- Automatic refresh every hour + [#26](https://github.com/orontee/taranis/issues/26) + +- Show month day and name under day name + [#48](https://github.com/orontee/taranis/issues/48) + +- Evenly distribute space between columns of daily forecast view + [#49](https://github.com/orontee/taranis/issues/49) + +- Touch control allow to switch pages + [#27](https://github.com/orontee/taranis/issues/27) + +- Min and max temperatures are displayed in daily forecast + [#43](https://github.com/orontee/taranis/issues/43) + +### Changed + +- Fix some (minor) visual glitches + [#47](https://github.com/orontee/taranis/issues/47) + +- Display full date in status bar + [#46](https://github.com/orontee/taranis/issues/46) + +- Fix background color of icons + [#45](https://github.com/orontee/taranis/issues/45) + +### Removed + +## [1.1.0] - 2023-09-16 + +### Added + +- Display daily forecast + [#8](https://github.com/orontee/taranis/issues/8) + +- Display credits in about dialog + [#34](https://github.com/orontee/taranis/issues/34) + +- History of locations in menu + [#18](https://github.com/orontee/taranis/issues/18) + +- Display alerts [#24](https://github.com/orontee/taranis/issues/24) + +- Polish translations + [#22](https://github.com/orontee/taranis/issues/22) + +### Changed + +- Switching forecast view should not refresh data nor update whole + screen [#38](https://github.com/orontee/taranis/issues/38) + +- Don't refresh data after cancel of location edit + [#39](https://github.com/orontee/taranis/issues/39) + +- Fix invisible alerts button handing touch events + [#35](https://github.com/orontee/taranis/issues/35) + +## [1.0.1] - 2023-09-07 + +### Added + +- Check software version at startup + [#30](https://github.com/orontee/taranis/issues/30) + +## [1.0.0] - 2023-09-06 + +### Added + +- Draw precipitation histogram + [#20](https://github.com/orontee/taranis/issues/20) + +- Sub-menu to select unit system + [#4](https://github.com/orontee/taranis/issues/4) + +- Location selection through text input + [#12](https://github.com/orontee/taranis/issues/12) + +- Specify language when calling OpenWeather API + [#15](https://github.com/orontee/taranis/issues/15) + +- Take profile into account when reading configuration + [#13](https://github.com/orontee/taranis/issues/13) + +- Display current weather description + [#14](https://github.com/orontee/taranis/issues/14) + +- Elide location text when too long + [#12](https://github.com/orontee/taranis/issues/12) + +- About dialog [#7](https://github.com/orontee/taranis/issues/7) + +- Application menu [#6](https://github.com/orontee/taranis/issues/6) + +- Support for translations + [#3](https://github.com/orontee/taranis/issues/3) + +- Weather icons [#1](https://github.com/orontee/taranis/issues/1) +Fix some (minor) visual glitches + [#47](https://github.com/orontee/taranis/issues/47) + +- Display full date in status bar + [#46](https://github.com/orontee/taranis/issues/46) + +- Fix background color of icons + [#45](https://github.com/orontee/taranis/issues/45) + +### Removed + +## [1.1.0] - 2023-09-16 + +### Added + +- Display daily forecast + [#8](https://github.com/orontee/taranis/issues/8) + +- Display credits in about dialog + [#34](https://github.com/orontee/taranis/issues/34) + +- History of locations in menu + [#18](https://github.com/orontee/taranis/issues/18) + +- Display alerts [#24](https://github.com/orontee/taranis/issues/24) + +- Polish translations + [#22](https://github.com/orontee/taranis/issues/22) + +### Changed + +- Switching forecast view should not refresh data nor update whole + screen [#38](https://github.com/orontee/taranis/issues/38) + +- Don't refresh data after cancel of location edit + [#39](https://github.com/orontee/taranis/issues/39) + +- Fix invisible alerts button handing touch events + [#35](https://github.com/orontee/taranis/issues/35) + +## [1.0.1] - 2023-09-07 + +### Added + +- Check software version at startup + [#30](https://github.com/orontee/taranis/issues/30) + +## [1.0.0] - 2023-09-06 + +### Added + +- Draw precipitation histogram + [#20](https://github.com/orontee/taranis/issues/20) + +- Sub-menu to select unit system + [#4](https://github.com/orontee/taranis/issues/4) + +- Location selection through text input + [#12](https://github.com/orontee/taranis/issues/12) + +- Specify language when calling OpenWeather API + [#15](https://github.com/orontee/taranis/issues/15) + +- Take profile into account when reading configuration + [#13](https://github.com/orontee/taranis/issues/13) + +- Display current weather description + [#14](https://github.com/orontee/taranis/issues/14) + +- Elide location text when too long + [#12](https://github.com/orontee/taranis/issues/12) + +- About dialog [#7](https://github.com/orontee/taranis/issues/7) + +- Application menu [#6](https://github.com/orontee/taranis/issues/6) + +- Support for translations + [#3](https://github.com/orontee/taranis/issues/3) + +- Weather icons [#1](https://github.com/orontee/taranis/issues/1) diff --git a/docs/desktop_integration.md b/docs/desktop_integration.md index 0fa70f4..1ee66bb 100644 --- a/docs/desktop_integration.md +++ b/docs/desktop_integration.md @@ -43,3 +43,7 @@ To customize the application icon and group: }, { ``` + +⚠️ Note that the comments describing the format and possible attributes +contains a typo: One must use the `params` (plural!) attribute to pass +parameters to an application. diff --git a/meson.build b/meson.build index 023551d..57dc14e 100644 --- a/meson.build +++ b/meson.build @@ -8,12 +8,6 @@ project( ] ) -configure_file( - input: 'crossfile_arm.ini.in', - output: 'crossfile_arm-v2.ini', - configuration: {'pwd': meson.global_build_root()} -) - pbres_command = find_program( './SDK_6.3.0/SDK-B288/usr/bin/pbres', required: true @@ -41,11 +35,18 @@ compiler = meson.get_compiler('cpp') taranis_deps = [ compiler.find_library('inkview', required: true), + compiler.find_library('boost_system', required: true, static: true), + compiler.find_library('boost_filesystem', required: true, static: true), + compiler.find_library('boost_date_time', required: true, static: true), + compiler.find_library('boost_log', required: true, static: true), + compiler.find_library('boost_log_setup', required: true, static: true), + compiler.find_library('boost_thread', required: true, static: true), compiler.find_library('curl', required: true), compiler.find_library('jsoncpp', required: true), compiler.find_library('gsl', required: true, static: true), compiler.find_library('gslcblas', required: true, static: true), compiler.find_library('m', required: true, static: true), + compiler.find_library('pthread', required: true), ] subdir('src') diff --git a/src/app.cc b/src/app.cc index c4c47ae..f8ef0d3 100644 --- a/src/app.cc +++ b/src/app.cc @@ -2,15 +2,19 @@ #include "events.h" #include "inkview.h" -void taranis::handle_about_dialog_button_clicked(int button_index) { +void taranis::App::handle_about_dialog_button_clicked(int button_index) { CloseDialog(); } void taranis::App::refresh_data_maybe() { + BOOST_LOG_TRIVIAL(debug) << "Refreshing data maybe"; + iv_netinfo *netinfo = NetInfo(); if (netinfo == nullptr or not netinfo->connected) { SetHardTimer(App::refresh_timer_name, &taranis::App::refresh_data_maybe, App::refresh_period); + + BOOST_LOG_TRIVIAL(debug) << "Restarting refresh timer since not connected"; return; } const auto event_handler = GetEventHandler(); diff --git a/src/app.h b/src/app.h index aad9af7..f08a72f 100644 --- a/src/app.h +++ b/src/app.h @@ -12,11 +12,13 @@ #include #include +#include "boost/log/trivial.hpp" #include "config.h" #include "errors.h" #include "events.h" #include "history.h" #include "l10n.h" +#include "logging.h" #include "model.h" #include "service.h" #include "state.h" @@ -33,8 +35,6 @@ template using optional = std::experimental::optional; namespace taranis { -void handle_about_dialog_button_clicked(int button_index); - std::string get_about_content(); // defined in generated file about.cc @@ -46,6 +46,9 @@ class App { history{std::make_unique(this->model)} {} int process_event(int event_type, int param_one, int param_two) { + BOOST_LOG_TRIVIAL(debug) + << "Processing event of type " << format_event_type(event_type); + if (event_type == EVT_INIT) { this->setup(); return 1; @@ -80,7 +83,6 @@ class App { } } } - return 0; } @@ -98,10 +100,15 @@ class App { std::string language; void setup() { + BOOST_LOG_TRIVIAL(info) << "Application setup"; + + this->initialize_language(); + const auto version = GetSoftwareVersion(); try { check_software_version(version); } catch (const UnsupportedSoftwareVersion &error) { + BOOST_LOG_TRIVIAL(warning) << "Unsupported software version " << version; Message(ICON_WARNING, GetLangText("Unsupported software version"), GetLangText("The application isn't compatible with the software " "version of this reader."), @@ -110,7 +117,6 @@ class App { return; } this->ui = std::make_unique(this->model); - this->initialize_language(); this->load_config(); } @@ -122,6 +128,8 @@ class App { } void exit() { + BOOST_LOG_TRIVIAL(info) << "Exiting application"; + this->ui.reset(); this->history.reset(); this->application_state.reset(); @@ -137,6 +145,8 @@ class App { } void load_config() { + BOOST_LOG_TRIVIAL(debug) << "Loading config"; + Config config; const auto api_key_from_config = config.read_string("api_key", ""); @@ -204,18 +214,22 @@ class App { } int handle_custom_event(int param_one, int param_two) { + BOOST_LOG_TRIVIAL(debug) + << "Handling custom event " << format_custom_event_param(param_one) + << " " << param_two; + // Commands if (param_one == CustomEvent::change_daily_forecast_display) { const bool enable = not this->model->display_daily_forecast; - this->display_daily_forecast(enable); + this->update_configured_display_daily_forecast(enable); } else if (param_one == CustomEvent::change_location) { auto *const raw_location = reinterpret_cast *>(GetCurrentEventExData()); const std::string location{raw_location->data()}; - this->update_config_location(location); + this->update_configured_location(location); return 1; } else if (param_one == CustomEvent::change_unit_system) { - this->change_unit_system(static_cast(param_two)); + this->update_configured_unit_system(static_cast(param_two)); } else if (param_one == CustomEvent::display_alert) { if (this->ui) { this->ui->display_alert(); @@ -228,7 +242,7 @@ class App { const size_t history_index = param_two; auto location = this->history->get_location(history_index); if (location) { - this->update_config_location(*location); + this->write_config_location(*location); } else { DialogSynchro(ICON_WARNING, "Title", "Location not found!", GetLangText("Ok"), nullptr, nullptr); @@ -267,6 +281,8 @@ class App { } void clear_model_weather_conditions() { + BOOST_LOG_TRIVIAL(debug) << "Clearing weather conditions stored in model"; + this->model->current_condition = std::experimental::nullopt; this->model->hourly_forecast.clear(); this->model->daily_forecast.clear(); @@ -274,6 +290,8 @@ class App { } void refresh_data() { + BOOST_LOG_TRIVIAL(debug) << "Refreshing data"; + ShowHourglassForce(); ClearTimerByName(App::refresh_timer_name); @@ -300,18 +318,27 @@ class App { this->model->alerts = this->service->get_alerts(); } catch (const ConnectionError &error) { + BOOST_LOG_TRIVIAL(debug) + << "Connection error while refreshing weather conditions " + << error.what(); Message( ICON_WARNING, GetLangText("Network error"), GetLangText("Failure while fetching weather data. Check your network " "connection."), App::error_dialog_delay); } catch (const RequestError &error) { + BOOST_LOG_TRIVIAL(debug) + << "Request error while refreshing weather conditions " + << error.what(); Message( ICON_WARNING, GetLangText("Network error"), GetLangText("Failure while fetching weather data. Check your network " "connection."), App::error_dialog_delay); } catch (const ServiceError &error) { + BOOST_LOG_TRIVIAL(debug) + << "Service error while refreshing weather conditions " + << error.what(); Message(ICON_WARNING, GetLangText("Service unavailable"), error.what(), App::error_dialog_delay); } @@ -325,12 +352,16 @@ class App { } void open_about_dialog() { + BOOST_LOG_TRIVIAL(debug) << "Opening about dialog"; + const auto about_content = get_about_content(); Dialog(ICON_INFORMATION, GetLangText("About"), about_content.c_str(), GetLangText("Ok"), nullptr, &handle_about_dialog_button_clicked); } static Location parse_location(const std::string &location) { + BOOST_LOG_TRIVIAL(debug) << "Parsing location"; + std::stringstream to_parse{location}; std::vector tokens; std::string token; @@ -354,17 +385,22 @@ class App { return {town, country}; } + BOOST_LOG_TRIVIAL(error) << "Failed to parse location " << location; throw InvalidLocation{}; } - void update_config_location(const std::string &text) const { + void update_configured_location(const std::string &text) const { + BOOST_LOG_TRIVIAL(debug) << "Updating configured location"; + if (text == format_location(this->model->location)) { + BOOST_LOG_TRIVIAL(debug) << "No need to updated configured location"; + return; } try { auto location = App::parse_location(text); - this->update_config_location(location); + this->write_config_location(location); } catch (const InvalidLocation &error) { const auto event_handler = GetEventHandler(); SendEvent(event_handler, EVT_CUSTOM, CustomEvent::warning_emitted, @@ -372,7 +408,9 @@ class App { } } - void update_config_location(const Location &location) const { + void write_config_location(const Location &location) const { + BOOST_LOG_TRIVIAL(debug) << "Writing config location"; + Config config; config.write_string("location_town"s, location.town); config.write_string("location_country"s, location.country); @@ -380,8 +418,12 @@ class App { Config::config_changed(); } - void change_unit_system(UnitSystem unit_system) { + void update_configured_unit_system(UnitSystem unit_system) { + BOOST_LOG_TRIVIAL(debug) << "Updating configured unit system"; + if (unit_system == this->model->unit_system) { + BOOST_LOG_TRIVIAL(debug) << "No need to update configured unit system"; + return; } @@ -391,8 +433,12 @@ class App { Config::config_changed(); } - void display_daily_forecast(bool enable) const { + void update_configured_display_daily_forecast(bool enable) const { + BOOST_LOG_TRIVIAL(debug) << "Updating configured display daily forecast"; + if (enable == this->model->display_daily_forecast) { + BOOST_LOG_TRIVIAL(debug) + << "No need to update configured display daily forecast"; return; } Config config; @@ -402,5 +448,7 @@ class App { } static void refresh_data_maybe(); + + static void handle_about_dialog_button_clicked(int button_index); }; } // namespace taranis diff --git a/src/config.h b/src/config.h index 9b96e08..8bf457d 100644 --- a/src/config.h +++ b/src/config.h @@ -4,6 +4,8 @@ #include #include +#include "logging.h" + namespace taranis { struct Config { @@ -30,10 +32,13 @@ struct Config { static std::string get_config_path() { iprofile profile = CreateProfileStruct(); const auto failed = GetCurrentProfileEx(&profile); - if (not failed) { - return std::string{profile.path} + "/config/taranis.cfg"; - } - return std::string{CONFIGPATH} + "/taranis.cfg"; + const auto stem = not failed ? std::string{profile.path} + "/config" + : std::string{CONFIGPATH}; + const auto config_path = stem + "/taranis.cfg"; + + BOOST_LOG_TRIVIAL(info) << "Config path " << config_path; + + return config_path; } static void config_changed(); diff --git a/src/currentconditionbox.cc b/src/currentconditionbox.cc index 0156da3..d6f0188 100644 --- a/src/currentconditionbox.cc +++ b/src/currentconditionbox.cc @@ -1,5 +1,6 @@ #include "currentconditionbox.h" -void taranis::handle_current_condition_dialog_button_clicked(int button_index) { +void taranis::CurrentConditionBox::handle_dialog_button_clicked( + int button_index) { CloseDialog(); } diff --git a/src/currentconditionbox.h b/src/currentconditionbox.h index 25eb108..ce4ca18 100644 --- a/src/currentconditionbox.h +++ b/src/currentconditionbox.h @@ -13,8 +13,6 @@ namespace taranis { -void handle_current_condition_dialog_button_clicked(int button_index); - class CurrentConditionBox : public Widget { public: static constexpr int bottom_padding{25}; @@ -131,8 +129,10 @@ class CurrentConditionBox : public Widget { Dialog(ICON_INFORMATION, GetLangText("Current Weather Conditions"), content.str().c_str(), GetLangText("Ok"), nullptr, - &handle_current_condition_dialog_button_clicked); + &handle_dialog_button_clicked); } + + static void handle_dialog_button_clicked(int button_index); }; } // namespace taranis diff --git a/src/events.cc b/src/events.cc new file mode 100644 index 0000000..bab6918 --- /dev/null +++ b/src/events.cc @@ -0,0 +1,340 @@ +#include "events.h" + +std::string taranis::format_custom_event_param(int param) { + if (param == CustomEvent::change_daily_forecast_display) { + return "change_daily_forecast_display"; + } + if (param == CustomEvent::change_location) { + return "change_location"; + } + if (param == CustomEvent::change_unit_system) { + return "change_unit_system"; + } + if (param == CustomEvent::display_alert) { + return "display_alert"; + } + if (param == CustomEvent::refresh_data) { + return "refresh_data"; + } + if (param == CustomEvent::select_location_from_history) { + return "select_location_from_history"; + } + if (param == CustomEvent::show_about_dialog) { + return "show_about_dialog"; + } + if (param == CustomEvent::toggle_current_location_favorite) { + return "toggle_current_location_favorite"; + } + + if (param == CustomEvent::model_daily_forecast_display_changed) { + return "model_daily_forecast_display_changed"; + } + if (param == CustomEvent::model_updated) { + return "model_updated"; + } + if (param == CustomEvent::warning_emitted) { + return "warning_emitted"; + } + return "Unknow custom event"; +} + +std::string taranis::format_event_type(int event_type) { + if (event_type == EVT_INIT) { + return "EVT_INIT"; + } + if (event_type == EVT_EXIT) { + return "EVT_EXIT"; + } + if (event_type == EVT_SHOW) { + return "EVT_SHOW"; + } + if (event_type == EVT_HIDE) { + return "EVT_HIDE"; + } + if (event_type == EVT_KEYDOWN) { + return "EVT_KEYDOWN"; + } + if (event_type == EVT_KEYPRESS) { + return "EVT_KEYPRESS"; + } + if (event_type == EVT_KEYUP) { + return "EVT_KEYUP"; + } + if (event_type == EVT_KEYRELEASE) { + return "EVT_KEYRELEASE"; + } + if (event_type == EVT_KEYREPEAT) { + return "EVT_KEYREPEAT"; + } + if (event_type == EVT_KEYPRESS_EXT) { + return "EVT_KEYPRESS_EXT"; + } + if (event_type == EVT_KEYRELEASE_EXT) { + return "EVT_KEYRELEASE_EXT"; + } + if (event_type == EVT_KEYREPEAT_EXT) { + return "EVT_KEYREPEAT_EXT"; + } + if (event_type == EVT_POINTERUP) { + return "EVT_POINTERUP"; + } + if (event_type == EVT_POINTERDOWN) { + return "EVT_POINTERDOWN"; + } + if (event_type == EVT_POINTERMOVE) { + return "EVT_POINTERMOVE"; + } + if (event_type == EVT_SCROLL) { + return "EVT_SCROLL"; + } + if (event_type == EVT_POINTERLONG) { + return "EVT_POINTERLONG"; + } + if (event_type == EVT_POINTERHOLD) { + return "EVT_POINTERHOLD"; + } + if (event_type == EVT_POINTERDRAG) { + return "EVT_POINTERDRAG"; + } + if (event_type == EVT_POINTERCANCEL) { + return "EVT_POINTERCANCEL"; + } + if (event_type == EVT_POINTERCHANGED) { + return "EVT_POINTERCHANGED"; + } + if (event_type == EVT_ORIENTATION) { + return "EVT_ORIENTATION"; + } + if (event_type == EVT_FOCUS) { + return "EVT_FOCUS"; + } + if (event_type == EVT_UNFOCUS) { + return "EVT_UNFOCUS"; + } + if (event_type == EVT_ACTIVATE) { + return "EVT_ACTIVATE"; + } + if (event_type == EVT_MTSYNC) { + return "EVT_MTSYNC"; + } + if (event_type == EVT_TOUCHUP) { + return "EVT_TOUCHUP"; + } + if (event_type == EVT_TOUCHDOWN) { + return "EVT_TOUCHDOWN"; + } + if (event_type == EVT_TOUCHMOVE) { + return "EVT_TOUCHMOVE"; + } + if (event_type == EVT_REPAINT) { + return "EVT_REPAINT"; + } + if (event_type == EVT_QN_MOVE) { + return "EVT_QN_MOVE"; + } + if (event_type == EVT_QN_RELEASE) { + return "EVT_QN_RELEASE"; + } + if (event_type == EVT_QN_BORDER) { + return "EVT_QN_BORDER"; + } + if (event_type == EVT_SNAPSHOT) { + return "EVT_SNAPSHOT"; + } + if (event_type == EVT_FSINCOMING) { + return "EVT_FSINCOMING"; + } + if (event_type == EVT_FSCHANGED) { + return "EVT_FSCHANGED"; + } + if (event_type == EVT_MP_STATECHANGED) { + return "EVT_MP_STATECHANGED"; + } + if (event_type == EVT_MP_TRACKCHANGED) { + return "EVT_MP_TRACKCHANGED"; + } + if (event_type == EVT_PREVPAGE) { + return "EVT_PREVPAGE"; + } + if (event_type == EVT_NEXTPAGE) { + return "EVT_NEXTPAGE"; + } + if (event_type == EVT_OPENDIC) { + return "EVT_OPENDIC"; + } + if (event_type == EVT_CONTROL_PANEL_ABOUT_TO_OPEN) { + return "EVT_CONTROL_PANEL_ABOUT_TO_OPEN"; + } + if (event_type == EVT_UPDATE) { + return "EVT_UPDATE"; + } + if (event_type == EVT_PANEL_BLUETOOTH_A2DP) { + return "EVT_PANEL_BLUETOOTH_A2DP"; + } + if (event_type == EVT_TAB) { + return "EVT_TAB"; + } + if (event_type == EVT_PANEL) { + return "EVT_PANEL"; + } + if (event_type == EVT_PANEL_ICON) { + return "EVT_PANEL_ICON"; + } + if (event_type == EVT_PANEL_TEXT) { + return "EVT_PANEL_TEXT"; + } + if (event_type == EVT_PANEL_PROGRESS) { + return "EVT_PANEL_PROGRESS"; + } + if (event_type == EVT_PANEL_MPLAYER) { + return "EVT_PANEL_MPLAYER"; + } + if (event_type == EVT_PANEL_USBDRIVE) { + return "EVT_PANEL_USBDRIVE"; + } + if (event_type == EVT_PANEL_NETWORK) { + return "EVT_PANEL_NETWORK"; + } + if (event_type == EVT_PANEL_CLOCK) { + return "EVT_PANEL_CLOCK"; + } + if (event_type == EVT_PANEL_BLUETOOTH) { + return "EVT_PANEL_BLUETOOTH"; + } + if (event_type == EVT_PANEL_TASKLIST) { + return "EVT_PANEL_TASKLIST"; + } + if (event_type == EVT_PANEL_OBREEY_SYNC) { + return "EVT_PANEL_OBREEY_SYNC"; + } + if (event_type == EVT_PANEL_SETREADINGMODE) { + return "EVT_PANEL_SETREADINGMODE"; + } + if (event_type == EVT_PANEL_SETREADINGMODE_INVERT) { + return "EVT_PANEL_SETREADINGMODE_INVERT"; + } + if (event_type == EVT_PANEL_FRONT_LIGHT) { + return "EVT_PANEL_FRONT_LIGHT"; + } + if (event_type == EVT_GLOBALREQUEST) { + return "EVT_GLOBALREQUEST"; + } + if (event_type == EVT_GLOBALACTION) { + return "EVT_GLOBALACTION"; + } + if (event_type == EVT_FOREGROUND) { + return "EVT_FOREGROUND"; + } + if (event_type == EVT_BACKGROUND) { + return "EVT_BACKGROUND"; + } + if (event_type == EVT_SUBTASKCLOSE) { + return "EVT_SUBTASKCLOSE"; + } + if (event_type == EVT_CONFIGCHANGED) { + return "EVT_CONFIGCHANGED"; + } + if (event_type == EVT_SAVESTATE) { + return "EVT_SAVESTATE"; + } + if (event_type == EVT_OBREEY_CONFIG_CHANGED) { + return "EVT_OBREEY_CONFIG_CHANGED"; + } + if (event_type == EVT_SDIN) { + return "EVT_SDIN"; + } + if (event_type == EVT_SDOUT) { + return "EVT_SDOUT"; + } + if (event_type == EVT_USBSTORE_IN) { + return "EVT_USBSTORE_IN "; + } + if (event_type == EVT_USBSTORE_OUT) { + return "EVT_USBSTORE_OUT"; + } + if (event_type == EVT_BT_RXCOMPLETE) { + return "EVT_BT_RXCOMPLETE"; + } + if (event_type == EVT_BT_TXCOMPLETE) { + return "EVT_BT_TXCOMPLETE"; + } + if (event_type == EVT_SYNTH_ENDED) { + return "EVT_SYNTH_ENDED"; + } + if (event_type == EVT_DIC_CLOSED) { + return "EVT_DIC_CLOSED"; + } + if (event_type == EVT_SHOW_KEYBOARD) { + return "EVT_SHOW_KEYBOARD"; + } + if (event_type == EVT_TEXTCLEAR) { + return "EVT_TEXTCLEAR"; + } + if (event_type == EVT_EXT_KB) { + return "EVT_EXT_KB "; + } + if (event_type == EVT_LETTER) { + return "EVT_LETTER"; + } + if (event_type == EVT_CALLBACK) { + return "EVT_CALLBACK"; + } + if (event_type == EVT_SCANPROGRESS) { + return "EVT_SCANPROGRESS"; + } + if (event_type == EVT_STOPSCAN) { + return "EVT_STOPSCAN"; + } + if (event_type == EVT_STARTSCAN) { + return "EVT_STARTSCAN"; + } + if (event_type == EVT_SCANSTOPPED) { + return "EVT_SCANSTOPPED"; + } + if (event_type == EVT_POSTPONE_TIMED_POWEROFF) { + return "EVT_POSTPONE_TIMED_POWEROFF"; + } + if (event_type == EVT_FRAME_ACTIVATED) { + return "EVT_FRAME_ACTIVATED"; + } + if (event_type == EVT_FRAME_DEACTIVATED) { + return "EVT_FRAME_DEACTIVATED"; + } + if (event_type == EVT_READ_PROGRESS_CHANGED) { + return "EVT_READ_PROGRESS_CHANGED"; + } + if (event_type == EVT_DUMP_BITMAPS_DEBUG_INFO) { + return "EVT_DUMP_BITMAPS_DEBUG_INFO"; + } + if (event_type == EVT_NET_CONNECTED) { + return "EVT_NET_CONNECTED"; + } + if (event_type == EVT_NET_DISCONNECTED) { + return "EVT_NET_DISCONNECTED"; + } + if (event_type == EVT_NET_FOUND_NEW_FW) { + return "EVT_NET_FOUND_NEW_FW"; + } + if (event_type == EVT_SYNTH_POSITION) { + return "EVT_SYNTH_POSITION"; + } + if (event_type == EVT_ASYNC_TASK_FINISHED) { + return "EVT_ASYNC_TASK_FINISHED"; + } + if (event_type == EVT_STOP_PLAYING) { + return "EVT_STOP_PLAYING"; + } + if (event_type == EVT_AVRCP_COMMAND) { + return "EVT_AVRCP_COMMAND"; + } + if (event_type == EVT_AUDIO_CHANGED) { + return "EVT_AUDIO_CHANGED"; + } + if (event_type == EVT_PACKAGE_JOB_CHANGED) { + return "EVT_PACKAGE_JOB_CHANGED"; + } + if (event_type == EVT_CUSTOM) { + return "EVT_CUSTOM"; + } + return "Unknow event type"; +} diff --git a/src/events.h b/src/events.h index 3a96e3d..4ad2c5d 100644 --- a/src/events.h +++ b/src/events.h @@ -1,5 +1,9 @@ #pragma once +#include +#include + +namespace taranis { enum CustomEvent { // Commands change_daily_forecast_display, @@ -21,6 +25,11 @@ enum CustomEventParam { invalid_location, }; +std::string format_custom_event_param(int param); + +std::string format_event_type(int event_type); +} // namespace taranis + // Local variables: // mode: c++-ts; // End: diff --git a/src/history.h b/src/history.h index 967fd1d..df5d275 100644 --- a/src/history.h +++ b/src/history.h @@ -6,6 +6,7 @@ #include #include +#include "logging.h" #include "model.h" namespace std { @@ -37,6 +38,8 @@ class LocationHistoryProxy { } void update_history_maybe() { + BOOST_LOG_TRIVIAL(debug) << "Updating history"; + auto ¤t_location = this->model->location; auto &history = this->model->location_history; const auto found = std::find_if(history.begin(), history.end(), @@ -45,6 +48,12 @@ class LocationHistoryProxy { }); const bool found_favorite = (found != history.end() and found->favorite); if (found != history.end()) { + if (found == history.begin()) { + BOOST_LOG_TRIVIAL(debug) << "History already up-to-date"; + return; + } + BOOST_LOG_TRIVIAL(debug) << "Removing history item to avoid duplicates"; + history.erase(found); } if (history.size() == LocationHistoryProxy::max_size) { @@ -52,10 +61,15 @@ class LocationHistoryProxy { std::find_if(history.rbegin(), history.rend(), [](const auto &item) { return not item.favorite; }); if (last_not_favorite != history.rend()) { + BOOST_LOG_TRIVIAL(debug) + << "Removing last not favorite history item since history is full"; + history.erase(std::prev(last_not_favorite.base())); } } if (history.size() < LocationHistoryProxy::max_size) { + BOOST_LOG_TRIVIAL(debug) << "Adding new item in history"; + history.push_front(HistoryItem{this->model->location, found_favorite}); } } diff --git a/src/http.cc b/src/http.cc index 1764438..00fd9e0 100644 --- a/src/http.cc +++ b/src/http.cc @@ -1,7 +1,94 @@ #include "http.h" -size_t taranis::write_callback(void *contents, size_t size, size_t nmemb, - void *userp) { +#include "errors.h" +#include "logging.h" + +Json::Value taranis::HttpClient::get(const std::string &url) { + BOOST_LOG_TRIVIAL(debug) << "Sending GET request " << url; + + this->ensure_network(); + + auto curl = this->preprare_curl(); + curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); + + std::string response_data; + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response_data); + + const CURLcode code = curl_easy_perform(curl.get()); + if (code != CURLE_OK) { + BOOST_LOG_TRIVIAL(error) << "Error in GET request " << code << " " << url; + throw RequestError{code}; + } + long response_code; + curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &response_code); + + if (response_code != 200) { + BOOST_LOG_TRIVIAL(error) + << "Unexpected response " << response_code << " " << url; + throw HttpError{response_code}; + } + + Json::Value root; + Json::Reader reader; + if (not reader.parse(response_data, root)) { + BOOST_LOG_TRIVIAL(error) + << "JSON parser error " << response_data << " " << url; + throw JsonParseError{}; + } + return root; +} + +std::unique_ptr taranis::HttpClient::preprare_curl() { + std::unique_ptr curl{curl_easy_init(), + &curl_easy_cleanup}; + + curl_slist *headers = nullptr; + headers = curl_slist_append(headers, + "Content-Type: application/json; charset=UTF-8"); + headers = curl_slist_append(headers, "X-Accept: application/json"); + + curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl.get(), CURLOPT_BUFFERSIZE, 102400L); + curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, "taranis/0.0.1"); + curl_easy_setopt(curl.get(), CURLOPT_MAXREDIRS, 50L); + curl_easy_setopt(curl.get(), CURLOPT_HTTP_VERSION, + static_cast(CURL_HTTP_VERSION_2TLS)); + curl_easy_setopt(curl.get(), CURLOPT_FTP_SKIP_PASV_IP, 1L); + curl_easy_setopt(curl.get(), CURLOPT_TCP_KEEPALIVE, 1L); + + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, write_callback); + return curl; +} + +void taranis::HttpClient::ensure_network() { + iv_netinfo *netinfo = NetInfo(); + if (netinfo == nullptr or not netinfo->connected) { + BOOST_LOG_TRIVIAL(debug) << "Will try to establish connection"; + + const char *network_name = nullptr; + const auto result = NetConnect2(network_name, true); + // This shows hourglass pointer and may popup a connection + // dialog + + if (result != 0) { + BOOST_LOG_TRIVIAL(warning) << "Failed to establish connection"; + + throw ConnectionError{}; + } + + netinfo = NetInfo(); + if (netinfo == nullptr or not netinfo->connected) { + BOOST_LOG_TRIVIAL(warning) << "Still not connected!"; + + throw ConnectionError{}; + } + } +} + +size_t taranis::HttpClient::write_callback(void *contents, size_t size, + size_t nmemb, void *userp) { static_cast(userp)->append(static_cast(contents), size * nmemb); return size * nmemb; diff --git a/src/http.h b/src/http.h index 6429476..9947d44 100644 --- a/src/http.h +++ b/src/http.h @@ -8,84 +8,18 @@ #include -#include "errors.h" - namespace taranis { -size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp); - class HttpClient { public: - Json::Value get(const std::string &url) { - this->ensure_network(); - - auto curl = this->preprare_curl(); - curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); - - std::string response_data; - curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response_data); - - const CURLcode code = curl_easy_perform(curl.get()); - if (code != CURLE_OK) { - throw RequestError{code}; - } - long response_code; - curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &response_code); - - if (response_code != 200) { - throw HttpError{response_code}; - } - - Json::Value root; - Json::Reader reader; - if (not reader.parse(response_data, root)) { - throw JsonParseError{}; - } - return root; - } + Json::Value get(const std::string &url); private: - std::unique_ptr preprare_curl() { - std::unique_ptr curl{curl_easy_init(), - &curl_easy_cleanup}; - - curl_slist *headers = nullptr; - headers = curl_slist_append( - headers, "Content-Type: application/json; charset=UTF-8"); - headers = curl_slist_append(headers, "X-Accept: application/json"); - - curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl.get(), CURLOPT_BUFFERSIZE, 102400L); - curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, "taranis/0.0.1"); - curl_easy_setopt(curl.get(), CURLOPT_MAXREDIRS, 50L); - curl_easy_setopt(curl.get(), CURLOPT_HTTP_VERSION, - static_cast(CURL_HTTP_VERSION_2TLS)); - curl_easy_setopt(curl.get(), CURLOPT_FTP_SKIP_PASV_IP, 1L); - curl_easy_setopt(curl.get(), CURLOPT_TCP_KEEPALIVE, 1L); - - curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, write_callback); - return curl; - } - - void ensure_network() { - iv_netinfo *netinfo = NetInfo(); - if (netinfo == nullptr or not netinfo->connected) { - const char *network_name = nullptr; - const auto result = NetConnect2(network_name, true); - // This shows hourglass pointer and may popup a connection - // dialog + std::unique_ptr preprare_curl(); - if (result != 0) { - throw ConnectionError{}; - } + void ensure_network(); - netinfo = NetInfo(); - if (netinfo == nullptr or not netinfo->connected) { - throw ConnectionError{}; - } - } - } + static size_t write_callback(void *contents, size_t size, size_t nmemb, + void *userp); }; } // namespace taranis diff --git a/src/icons.h b/src/icons.h index ef65b8b..5864299 100644 --- a/src/icons.h +++ b/src/icons.h @@ -3,6 +3,8 @@ #include #include +#include "logging.h" + extern const ibitmap icon_01n_2x; extern const ibitmap icon_02n_2x; extern const ibitmap icon_03n_2x; @@ -57,6 +59,8 @@ class Icons { if (name == "warning") { return const_cast(&icon_warning); } + BOOST_LOG_TRIVIAL(error) << "Unknown icon name " << name; + return nullptr; } }; diff --git a/src/l10n.h b/src/l10n.h index 4e5e34e..ef34b43 100644 --- a/src/l10n.h +++ b/src/l10n.h @@ -3,11 +3,17 @@ #include #include +#include "logging.h" + namespace taranis { inline void initialize_translations() { - LoadLanguage(currentLang()); + const auto current_language = currentLang(); + + BOOST_LOG_TRIVIAL(debug) << "Initializing translations " << current_language; + + LoadLanguage(current_language); - if (std::strcmp(currentLang(), "fr") == 0) { + if (std::strcmp(current_language, "fr") == 0) { // French translations // about.cc.in @@ -96,7 +102,7 @@ inline void initialize_translations() { AddTranslation("Wind", "Vent"); AddTranslation("Gust", "Rafale"); - } else if (std::strcmp(currentLang(), "pl") == 0) { + } else if (std::strcmp(current_language, "pl") == 0) { // Polish translations // about.cc.in @@ -188,7 +194,7 @@ inline void initialize_translations() { AddTranslation("Wind", "Wiatr"); AddTranslation("Gust", "Porywisty wiatr"); - } else if (std::strcmp(currentLang(), "de") == 0) { + } else if (std::strcmp(current_language, "de") == 0) { // German translations // currentconditionbox.h diff --git a/src/logging.cc b/src/logging.cc new file mode 100644 index 0000000..444cd2c --- /dev/null +++ b/src/logging.cc @@ -0,0 +1,42 @@ +#include "logging.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace logging = boost::log; + +void taranis::initialize_logging(bool verbose_log) { + iprofile profile = CreateProfileStruct(); + const auto failed = GetCurrentProfileEx(&profile); + const auto stem = not failed ? std::string{profile.path} + "/state" + : std::string{STATEPATH}; + const auto log_path = stem + "/taranis.log"; + + logging::add_common_attributes(); + + const auto format = + (logging::expressions::stream + << logging::expressions::format_date_time( + "TimeStamp", "%Y-%m-%d %H:%M:%S") + << ": <" << logging::trivial::severity << "> " + << logging::expressions::smessage); + + logging::add_file_log(logging::keywords::file_name = log_path, + logging::keywords::rotation_size = 1024 * 1024, + logging::keywords::format = format); + + if (verbose_log) { + logging::core::get()->set_filter(logging::trivial::severity >= + logging::trivial::debug); + } else { + logging::core::get()->set_filter(logging::trivial::severity >= + logging::trivial::info); + } +} diff --git a/src/logging.h b/src/logging.h new file mode 100644 index 0000000..b4b802a --- /dev/null +++ b/src/logging.h @@ -0,0 +1,10 @@ +#pragma once + +#include +// Unused here but exposes the macros to consumers + +namespace taranis { + +void initialize_logging(bool verbose_log); + +} diff --git a/src/main.cc b/src/main.cc index ffedd81..d95b38e 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,14 +1,26 @@ #include +#include #include "app.h" +#include "logging.h" taranis::App app; int event_handler(int event_type, int param_one, int param_two) { - return app.process_event(event_type, param_one, param_two); + try { + return app.process_event(event_type, param_one, param_two); + } catch (const std::runtime_error &error) { + BOOST_LOG_TRIVIAL(error) << "Unhandled exception " << error.what(); + throw error; + } } -int main() { +int main(int argc, char *argv[]) { + const bool verbose_log_requested = + (argc == 2 and std::strcmp(argv[1], "--verbose") == 0); + taranis::initialize_logging(verbose_log_requested); + InkViewMain(&event_handler); + return 0; } diff --git a/src/meson.build b/src/meson.build index 1e53d31..492ba13 100644 --- a/src/meson.build +++ b/src/meson.build @@ -23,9 +23,13 @@ sources = [about_cc] + files( 'app.cc', 'config.cc', 'currentconditionbox.cc', + 'events.cc', 'http.cc', 'locationbox.cc', + 'logging.cc', 'main.cc', + 'service.cc', + 'state.cc', 'ui.cc', 'util.cc' ) diff --git a/src/service.cc b/src/service.cc new file mode 100644 index 0000000..ff50562 --- /dev/null +++ b/src/service.cc @@ -0,0 +1,266 @@ +#include "service.h" + +#include "errors.h" + +namespace taranis { + +namespace openweather { +const std::string url{"https://api.openweathermap.org"}; +const std::string geo_path{"/geo/1.0/direct"}; +const std::string onecall_path{"/data/3.0/onecall"}; +} // namespace openweather + +void Service::fetch_data(const std::string &town, const std::string &country, + const std::string &language, + const std::string &units) { + this->returned_value = + this->request_onecall_api(town, country, language, units); + + if (not this->returned_value.isMember("current") or + not this->returned_value["current"].isObject()) { + throw ServiceError::get_unexpected_error(); + } + if (not this->returned_value.isMember("hourly") or + not this->returned_value["hourly"].isArray()) { + throw ServiceError::get_unexpected_error(); + } + + if (not this->returned_value.isMember("daily") or + not this->returned_value["daily"].isArray()) { + throw ServiceError::get_unexpected_error(); + } +} + +std::vector Service::get_hourly_forecast() const { + std::vector conditions; + conditions.reserve(Service::max_hourly_forecasts); + for (auto &value : this->returned_value["hourly"]) { + conditions.push_back(Service::extract_condition(value)); + + if (conditions.size() == Service::max_hourly_forecasts) { + break; + } + } + return conditions; +} + +std::vector Service::get_daily_forecast() const { + std::vector conditions; + conditions.reserve(Service::max_daily_forecasts); + for (auto &value : this->returned_value["daily"]) { + conditions.push_back(Service::extract_daily_condition(value)); + + if (conditions.size() == Service::max_daily_forecasts) { + break; + } + } + return conditions; +} + +std::vector Service::get_alerts() const { + if (not this->returned_value.isMember("alerts") or + not this->returned_value["alerts"].isArray()) { + return {}; + } + return Service::extract_alerts(this->returned_value["alerts"]); +} + +std::pair +Service::identify_lonlat(const std::string &town, const std::string &country) { + if (this->town != town or this->country != country) { + this->town = town; + this->country = country; + this->lonlat = {}; + } + if (not this->lonlat) { + this->request_lonlat(); + } + return this->lonlat.value(); +} + +void Service::request_lonlat() { + std::stringstream url; + url << openweather::url << openweather::geo_path << "?" + << "q=" << this->town; + if (not this->country.empty()) { + url << "," << this->country; + } + url << "&" + << "appid=" << this->api_key; + + auto returned_value = this->send_get_request(url.str()); + if (not returned_value.isArray() or returned_value.size() == 0) { + throw ServiceError::get_unknown_location_error(); + } + + Json::Value first_result = returned_value[0]; + const auto lon = first_result.get("lon", NAN).asDouble(); + const auto lat = first_result.get("lat", NAN).asDouble(); + if (std::isnan(lon) or std::isnan(lat)) { + throw ServiceError::get_unexpected_error(); + } + this->lonlat = std::make_pair(lon, lat); +} + +Json::Value Service::request_onecall_api(const std::string &town, + const std::string &country, + const std::string &language, + const std::string &units) { + const auto lonlat = this->identify_lonlat(town, country); + + std::stringstream url; + url << openweather::url << openweather::onecall_path << "?" + << "lon=" << lonlat.first << "&" + << "lat=" << lonlat.second << "&" + << "units=" << units << "&" + << "exclude=minutely" + << "&" + << "lang=" << language << "&" + << "appid=" << this->api_key; + + const auto returned_value = this->send_get_request(url.str()); + return returned_value; +} + +Json::Value Service::send_get_request(const std::string &url) { + try { + return this->client.get(url); + } catch (const HttpError &error) { + throw ServiceError::from_http_error(error); + } catch (const JsonParseError &error) { + throw ServiceError::get_unexpected_error(); + } +} + +Condition Service::extract_condition(const Json::Value &value) { + const auto date = static_cast(value.get("dt", 0).asInt()); + const auto sunrise = static_cast(value.get("sunrise", 0).asInt()); + const auto sunset = static_cast(value.get("sunset", 0).asInt()); + const auto temperature = value.get("temp", NAN).asDouble(); + const auto felt_temperature = value.get("feels_like", NAN).asDouble(); + const auto pressure = value.get("pressure", NAN).asInt(); + const auto humidity = value.get("humidity", NAN).asInt(); + const auto uv_index = value.get("uvi", NAN).asDouble(); + const auto clouds = value.get("clouds", NAN).asInt(); + const auto visibility = value.get("visibility", NAN).asInt(); + const auto probability_of_precipitation = value.get("pop", NAN).asDouble(); + const auto wind_speed = value.get("wind_speed", NAN).asDouble(); + const auto wind_degree = value.get("wind_deg", NAN).asInt(); + const auto wind_gust = value.get("wind_gust", NAN).asDouble(); + + Condition condition{date, + sunrise, + sunset, + temperature, + felt_temperature, + pressure, + humidity, + uv_index, + clouds, + visibility, + probability_of_precipitation, + wind_speed, + wind_degree, + wind_gust}; + + if (value.isMember("weather") and value["weather"].isArray() and + value["weather"].isValidIndex(0)) { + const auto weather_value = value["weather"][0]; + condition.weather = + static_cast(weather_value.get("id", CLEAR_SKY).asInt()); + condition.weather_description = + weather_value.get("description", "").asString(); + condition.weather_icon_name = weather_value.get("icon", "").asString(); + } + + if (value.isMember("rain") and value["rain"].isMember("1h")) { + const auto rain_value = value["rain"]; + condition.rain = rain_value.get("1h", NAN).asDouble(); + } else { + condition.rain = NAN; + } + + if (value.isMember("snow") and value["snow"].isMember("1h")) { + const auto snow_value = value["snow"]; + condition.snow = snow_value.get("1h", NAN).asDouble(); + } else { + condition.snow = NAN; + } + return condition; +} + +DailyCondition Service::extract_daily_condition(const Json::Value &value) { + const auto date = static_cast(value.get("dt", 0).asInt()); + const auto sunrise = static_cast(value.get("sunrise", 0).asInt()); + const auto sunset = static_cast(value.get("sunset", 0).asInt()); + const auto moonrise = static_cast(value.get("moonrise", 0).asInt()); + const auto moonset = static_cast(value.get("moonset", 0).asInt()); + const auto moon_phase = value.get("moon_phase", NAN).asDouble(); + const auto pressure = value.get("pressure", NAN).asInt(); + const auto humidity = value.get("humidity", NAN).asInt(); + const auto dew_point = value.get("dew_point", NAN).asDouble(); + const auto wind_speed = value.get("wind_speed", NAN).asDouble(); + const auto wind_degree = value.get("wind_deg", NAN).asInt(); + const auto wind_gust = value.get("wind_gust", NAN).asDouble(); + const auto clouds = value.get("clouds", NAN).asInt(); + const auto probability_of_precipitation = value.get("pop", NAN).asDouble(); + const auto uv_index = value.get("uvi", NAN).asDouble(); + const auto rain = value.get("rain", NAN).asDouble(); + const auto snow = value.get("snow", NAN).asDouble(); + + DailyCondition condition{date, sunrise, + sunset, moonrise, + moonset, moon_phase, + pressure, humidity, + dew_point, wind_speed, + wind_degree, wind_gust, + clouds, probability_of_precipitation, + uv_index, rain, + snow}; + + if (value.isMember("weather") and value["weather"].isArray() and + value["weather"].isValidIndex(0)) { + const auto weather_value = value["weather"][0]; + condition.weather = + static_cast(weather_value.get("id", CLEAR_SKY).asInt()); + condition.weather_description = + weather_value.get("description", "").asString(); + condition.weather_icon_name = weather_value.get("icon", "").asString(); + } + + if (value.isMember("temp")) { + const auto temp_value = value["temp"]; + condition.temperature_day = temp_value.get("day", NAN).asDouble(); + condition.temperature_min = temp_value.get("min", NAN).asDouble(); + condition.temperature_max = temp_value.get("max", NAN).asDouble(); + condition.temperature_night = temp_value.get("nigth", NAN).asDouble(); + condition.temperature_evening = temp_value.get("eve", NAN).asDouble(); + condition.temperature_morning = temp_value.get("morn", NAN).asDouble(); + } + + if (value.isMember("felt")) { + const auto felt_value = value["felt"]; + condition.temperature_day = felt_value.get("day", NAN).asDouble(); + condition.temperature_min = felt_value.get("min", NAN).asDouble(); + condition.temperature_max = felt_value.get("max", NAN).asDouble(); + condition.temperature_night = felt_value.get("nigth", NAN).asDouble(); + condition.temperature_evening = felt_value.get("eve", NAN).asDouble(); + condition.temperature_morning = felt_value.get("morn", NAN).asDouble(); + } + return condition; +} + +std::vector Service::extract_alerts(const Json::Value &value) { + std::vector alerts; + + for (auto &alert_value : value) { + const Alert alert{alert_value.get("sender_name", "").asString(), + alert_value.get("event", "").asString(), + static_cast(alert_value.get("start", 0).asInt()), + static_cast(alert_value.get("end", 0).asInt()), + alert_value.get("description", "").asString()}; + alerts.push_back(alert); + } + return alerts; +} +} // namespace taranis diff --git a/src/service.h b/src/service.h index fd60328..2c260bd 100644 --- a/src/service.h +++ b/src/service.h @@ -11,7 +11,6 @@ #include #include -#include "errors.h" #include "http.h" #include "model.h" @@ -23,12 +22,6 @@ template using optional = std::experimental::optional; namespace taranis { -namespace openweather { -const std::string url{"https://api.openweathermap.org"}; -const std::string geo_path{"/geo/1.0/direct"}; -const std::string onecall_path{"/data/3.0/onecall"}; -} // namespace openweather - class Service { public: Service() {} @@ -38,62 +31,17 @@ class Service { void set_api_key(const std::string &api_key) { this->api_key = api_key; } void fetch_data(const std::string &town, const std::string &country, - const std::string &language, const std::string &units) { - this->returned_value = - this->request_onecall_api(town, country, language, units); - - if (not this->returned_value.isMember("current") or - not this->returned_value["current"].isObject()) { - throw ServiceError::get_unexpected_error(); - } - if (not this->returned_value.isMember("hourly") or - not this->returned_value["hourly"].isArray()) { - throw ServiceError::get_unexpected_error(); - } - - if (not this->returned_value.isMember("daily") or - not this->returned_value["daily"].isArray()) { - throw ServiceError::get_unexpected_error(); - } - } + const std::string &language, const std::string &units); Condition get_current_condition() const { return Service::extract_condition(this->returned_value["current"]); } - std::vector get_hourly_forecast() const { - std::vector conditions; - conditions.reserve(Service::max_hourly_forecasts); - for (auto &value : this->returned_value["hourly"]) { - conditions.push_back(Service::extract_condition(value)); + std::vector get_hourly_forecast() const; - if (conditions.size() == Service::max_hourly_forecasts) { - break; - } - } - return conditions; - } - - std::vector get_daily_forecast() const { - std::vector conditions; - conditions.reserve(Service::max_daily_forecasts); - for (auto &value : this->returned_value["daily"]) { - conditions.push_back(Service::extract_daily_condition(value)); + std::vector get_daily_forecast() const; - if (conditions.size() == Service::max_daily_forecasts) { - break; - } - } - return conditions; - } - - std::vector get_alerts() const { - if (not this->returned_value.isMember("alerts") or - not this->returned_value["alerts"].isArray()) { - return {}; - } - return Service::extract_alerts(this->returned_value["alerts"]); - } + std::vector get_alerts() const; private: static const int max_hourly_forecasts = 24; @@ -111,203 +59,21 @@ class Service { Json::Value returned_value; std::pair - identify_lonlat(const std::string &town, const std::string &country) { - if (this->town != town or this->country != country) { - this->town = town; - this->country = country; - this->lonlat = {}; - } - if (not this->lonlat) { - this->request_lonlat(); - } - return this->lonlat.value(); - } + identify_lonlat(const std::string &town, const std::string &country); - void request_lonlat() { - std::stringstream url; - url << openweather::url << openweather::geo_path << "?" - << "q=" << this->town; - if (not this->country.empty()) { - url << "," << this->country; - } - url << "&" - << "appid=" << this->api_key; - - auto returned_value = this->send_get_request(url.str()); - if (not returned_value.isArray() or returned_value.size() == 0) { - throw ServiceError::get_unknown_location_error(); - } - - Json::Value first_result = returned_value[0]; - const auto lon = first_result.get("lon", NAN).asDouble(); - const auto lat = first_result.get("lat", NAN).asDouble(); - if (std::isnan(lon) or std::isnan(lat)) { - throw ServiceError::get_unexpected_error(); - } - this->lonlat = std::make_pair(lon, lat); - } + void request_lonlat(); Json::Value request_onecall_api(const std::string &town, const std::string &country, const std::string &language, - const std::string &units) { - const auto lonlat = this->identify_lonlat(town, country); + const std::string &units); - std::stringstream url; - url << openweather::url << openweather::onecall_path << "?" - << "lon=" << lonlat.first << "&" - << "lat=" << lonlat.second << "&" - << "units=" << units << "&" - << "exclude=minutely" - << "&" - << "lang=" << language << "&" - << "appid=" << this->api_key; + Json::Value send_get_request(const std::string &url); - const auto returned_value = this->send_get_request(url.str()); - return returned_value; - } + static Condition extract_condition(const Json::Value &value); - Json::Value send_get_request(const std::string &url) { - try { - return this->client.get(url); - } catch (const HttpError &error) { - throw ServiceError::from_http_error(error); - } catch (const JsonParseError &error) { - throw ServiceError::get_unexpected_error(); - } - } + static DailyCondition extract_daily_condition(const Json::Value &value); - static Condition extract_condition(const Json::Value &value) { - const auto date = static_cast(value.get("dt", 0).asInt()); - const auto sunrise = static_cast(value.get("sunrise", 0).asInt()); - const auto sunset = static_cast(value.get("sunset", 0).asInt()); - const auto temperature = value.get("temp", NAN).asDouble(); - const auto felt_temperature = value.get("feels_like", NAN).asDouble(); - const auto pressure = value.get("pressure", NAN).asInt(); - const auto humidity = value.get("humidity", NAN).asInt(); - const auto uv_index = value.get("uvi", NAN).asDouble(); - const auto clouds = value.get("clouds", NAN).asInt(); - const auto visibility = value.get("visibility", NAN).asInt(); - const auto probability_of_precipitation = value.get("pop", NAN).asDouble(); - const auto wind_speed = value.get("wind_speed", NAN).asDouble(); - const auto wind_degree = value.get("wind_deg", NAN).asInt(); - const auto wind_gust = value.get("wind_gust", NAN).asDouble(); - - Condition condition{date, - sunrise, - sunset, - temperature, - felt_temperature, - pressure, - humidity, - uv_index, - clouds, - visibility, - probability_of_precipitation, - wind_speed, - wind_degree, - wind_gust}; - - if (value.isMember("weather") and value["weather"].isArray() and - value["weather"].isValidIndex(0)) { - const auto weather_value = value["weather"][0]; - condition.weather = - static_cast(weather_value.get("id", CLEAR_SKY).asInt()); - condition.weather_description = - weather_value.get("description", "").asString(); - condition.weather_icon_name = weather_value.get("icon", "").asString(); - } - - if (value.isMember("rain") and value["rain"].isMember("1h")) { - const auto rain_value = value["rain"]; - condition.rain = rain_value.get("1h", NAN).asDouble(); - } else { - condition.rain = NAN; - } - - if (value.isMember("snow") and value["snow"].isMember("1h")) { - const auto snow_value = value["snow"]; - condition.snow = snow_value.get("1h", NAN).asDouble(); - } else { - condition.snow = NAN; - } - return condition; - } - - static DailyCondition extract_daily_condition(const Json::Value &value) { - const auto date = static_cast(value.get("dt", 0).asInt()); - const auto sunrise = static_cast(value.get("sunrise", 0).asInt()); - const auto sunset = static_cast(value.get("sunset", 0).asInt()); - const auto moonrise = static_cast(value.get("moonrise", 0).asInt()); - const auto moonset = static_cast(value.get("moonset", 0).asInt()); - const auto moon_phase = value.get("moon_phase", NAN).asDouble(); - const auto pressure = value.get("pressure", NAN).asInt(); - const auto humidity = value.get("humidity", NAN).asInt(); - const auto dew_point = value.get("dew_point", NAN).asDouble(); - const auto wind_speed = value.get("wind_speed", NAN).asDouble(); - const auto wind_degree = value.get("wind_deg", NAN).asInt(); - const auto wind_gust = value.get("wind_gust", NAN).asDouble(); - const auto clouds = value.get("clouds", NAN).asInt(); - const auto probability_of_precipitation = value.get("pop", NAN).asDouble(); - const auto uv_index = value.get("uvi", NAN).asDouble(); - const auto rain = value.get("rain", NAN).asDouble(); - const auto snow = value.get("snow", NAN).asDouble(); - - DailyCondition condition{date, sunrise, - sunset, moonrise, - moonset, moon_phase, - pressure, humidity, - dew_point, wind_speed, - wind_degree, wind_gust, - clouds, probability_of_precipitation, - uv_index, rain, - snow}; - - if (value.isMember("weather") and value["weather"].isArray() and - value["weather"].isValidIndex(0)) { - const auto weather_value = value["weather"][0]; - condition.weather = - static_cast(weather_value.get("id", CLEAR_SKY).asInt()); - condition.weather_description = - weather_value.get("description", "").asString(); - condition.weather_icon_name = weather_value.get("icon", "").asString(); - } - - if (value.isMember("temp")) { - const auto temp_value = value["temp"]; - condition.temperature_day = temp_value.get("day", NAN).asDouble(); - condition.temperature_min = temp_value.get("min", NAN).asDouble(); - condition.temperature_max = temp_value.get("max", NAN).asDouble(); - condition.temperature_night = temp_value.get("nigth", NAN).asDouble(); - condition.temperature_evening = temp_value.get("eve", NAN).asDouble(); - condition.temperature_morning = temp_value.get("morn", NAN).asDouble(); - } - - if (value.isMember("felt")) { - const auto felt_value = value["felt"]; - condition.temperature_day = felt_value.get("day", NAN).asDouble(); - condition.temperature_min = felt_value.get("min", NAN).asDouble(); - condition.temperature_max = felt_value.get("max", NAN).asDouble(); - condition.temperature_night = felt_value.get("nigth", NAN).asDouble(); - condition.temperature_evening = felt_value.get("eve", NAN).asDouble(); - condition.temperature_morning = felt_value.get("morn", NAN).asDouble(); - } - return condition; - } - - static std::vector extract_alerts(const Json::Value &value) { - std::vector alerts; - - for (auto &alert_value : value) { - const Alert alert{ - alert_value.get("sender_name", "").asString(), - alert_value.get("event", "").asString(), - static_cast(alert_value.get("start", 0).asInt()), - static_cast(alert_value.get("end", 0).asInt()), - alert_value.get("description", "").asString()}; - alerts.push_back(alert); - } - return alerts; - } + static std::vector extract_alerts(const Json::Value &value); }; } // namespace taranis diff --git a/src/state.cc b/src/state.cc new file mode 100644 index 0000000..f852b58 --- /dev/null +++ b/src/state.cc @@ -0,0 +1,94 @@ +#include "state.h" + +#include "json/writer.h" +#include +#include +#include + +#include "logging.h" + +constexpr char LOCATION_HISTORY_KEY[]{"location_history"}; + +namespace taranis { + +void ApplicationState::restore() { + BOOST_LOG_TRIVIAL(debug) << "Restoring application state"; + + const auto path = ApplicationState::get_application_state_path(); + std::ifstream input{path, std::ios_base::in}; + if (not input) { + BOOST_LOG_TRIVIAL(warning) + << "Failed to open file to restore application state"; + return; + } + + Json::Value root; + Json::Reader reader; + if (not reader.parse(input, root)) { + BOOST_LOG_TRIVIAL(error) << "Unexpected application state"; + return; + } + this->restore_location_history(root); +} + +void ApplicationState::dump() { + BOOST_LOG_TRIVIAL(debug) << "Dumping application state"; + + const auto path = ApplicationState::get_application_state_path(); + std::ofstream output{path, std::ios_base::out}; + if (not output) { + BOOST_LOG_TRIVIAL(warning) + << "Failed to open file to dump application state"; + return; + } + Json::Value root; + this->dump_location_history(root); + + output << root << std::endl; + if (not output) { + BOOST_LOG_TRIVIAL(error) << "Failed to stream application state"; + } + output.close(); +} + +std::string ApplicationState::get_application_state_path() { + iprofile profile = CreateProfileStruct(); + const auto failed = GetCurrentProfileEx(&profile); + const auto stem = not failed ? std::string{profile.path} + "/state" + : std::string{STATEPATH}; + const auto application_state_path = stem + "/taranis.json"; + + BOOST_LOG_TRIVIAL(info) << "Application state path " + << application_state_path; + + return application_state_path; +} + +void ApplicationState::restore_location_history(const Json::Value &root) { + this->model->location_history.clear(); + + for (const auto &value : root[LOCATION_HISTORY_KEY]) { + const auto town{value.get("town", "").asString()}; + if (town.empty()) { + return; + } + const HistoryItem item{town, value.get("country", "").asString(), + value.get("favorite", false).asBool()}; + this->model->location_history.push_back(item); + } +} + +void ApplicationState::dump_location_history(Json::Value &root) { + auto &history_value = root[LOCATION_HISTORY_KEY]; + const auto &history = this->model->location_history; + int location_index = 0; + for (const auto &item : history) { + history_value[location_index]["town"] = item.location.town; + history_value[location_index]["country"] = item.location.country; + history_value[location_index]["favorite"] = item.favorite; + + ++location_index; + } +} + +} // namespace taranis diff --git a/src/state.h b/src/state.h index 885b5e0..f5e5aba 100644 --- a/src/state.h +++ b/src/state.h @@ -1,8 +1,5 @@ #pragma once -#include -#include -#include #include #include #include @@ -19,72 +16,17 @@ class ApplicationState { ~ApplicationState() { this->dump(); } - void restore() { - const auto path = ApplicationState::get_application_state_path(); - std::ifstream input{path, std::ios_base::in}; - if (!input) { - return; - } + void restore(); - Json::Value root; - input >> root; - - this->restore_location_history(root); - } - - void dump() { - const auto path = ApplicationState::get_application_state_path(); - std::ofstream output{path, std::ios_base::out}; - if (!output) { - return; - } - Json::Value root; - - this->dump_location_history(root); - - output << root << std::endl; - output.close(); - } + void dump(); private: - static constexpr char LOCATION_HISTORY_KEY[]{"location_history"}; - std::shared_ptr model; - static std::string get_application_state_path() { - iprofile profile = CreateProfileStruct(); - const auto failed = GetCurrentProfileEx(&profile); - if (not failed) { - return std::string{profile.path} + "/state/taranis.json"; - } - return std::string{STATEPATH} + "/taranis.json"; - } - - void restore_location_history(const Json::Value &root) { - this->model->location_history.clear(); + static std::string get_application_state_path(); - for (const auto &value : root[ApplicationState::LOCATION_HISTORY_KEY]) { - const auto town{value.get("town", "").asString()}; - if (town.empty()) { - return; - } - const HistoryItem item{town, value.get("country", "").asString(), - value.get("favorite", false).asBool()}; - this->model->location_history.push_back(item); - } - } - - void dump_location_history(Json::Value &root) { - auto &history_value = root[ApplicationState::LOCATION_HISTORY_KEY]; - const auto &history = this->model->location_history; - int location_index = 0; - for (const auto &item : history) { - history_value[location_index]["town"] = item.location.town; - history_value[location_index]["country"] = item.location.country; - history_value[location_index]["favorite"] = item.favorite; + void restore_location_history(const Json::Value &root); - ++location_index; - } - } + void dump_location_history(Json::Value &root); }; } // namespace taranis diff --git a/src/ui.cc b/src/ui.cc index 541781e..4267a02 100644 --- a/src/ui.cc +++ b/src/ui.cc @@ -1,12 +1,16 @@ #include "ui.h" +#include "boost/log/trivial.hpp" #include "events.h" #include "history.h" #include "inkview.h" +#include "logging.h" #include "menu.h" #include "model.h" void taranis::handle_menu_item_selected(int item_index) { + BOOST_LOG_TRIVIAL(debug) << "Handling menu item selected " << item_index; + const auto event_handler = GetEventHandler(); if (item_index == MENU_ITEM_REFRESH) { SendEvent(event_handler, EVT_CUSTOM, CustomEvent::refresh_data, 0); @@ -33,5 +37,7 @@ void taranis::handle_menu_item_selected(int item_index) { CustomEvent::select_location_from_history, history_index); } else if (item_index == MENU_ITEM_QUIT) { SendEvent(event_handler, EVT_EXIT, 0, 0); + } else { + BOOST_LOG_TRIVIAL(error) << "Unexpected item index" << item_index; } }