├── .gitignore ├── src ├── ui │ ├── callback.cpp │ ├── callback.h │ ├── component.h │ ├── ui_functions.h │ ├── data_adapter.cpp │ ├── dynamic_helpers.h │ ├── component_factory.h │ ├── dynamic_gui.h │ ├── default_view_json.h │ ├── dynamic_label.h │ ├── dynamic_gauge.h │ ├── ui_ticker.h │ ├── dynamic_switch.h │ ├── dynamic_button.h │ ├── component_factory.cpp │ ├── message.h │ ├── view.h │ ├── themes.h │ ├── wifilist.h │ ├── loader.h │ ├── date_picker.h │ ├── data_adapter.h │ ├── dynamic_gui.cpp │ ├── dynamic_switch.cpp │ ├── dynamic_view.h │ ├── dynamic_button.cpp │ ├── navigationview.h │ ├── dynamic_label.cpp │ ├── watch_info.h │ ├── settings_view.h │ ├── wakeup_settings.h │ ├── dynamic_gauge.cpp │ ├── dynamic_helpers.cpp │ ├── localization.h │ ├── keyboard.h │ ├── statusbar.h │ ├── roller.h │ ├── display_settings.h │ ├── signalk_settings.h │ └── wifisettings.h ├── system │ ├── observer.h │ ├── async_dispatcher.h │ ├── systemobject.h │ ├── configurable.h │ ├── systemobject.cpp │ ├── observable.h │ ├── uuid.h │ ├── events.h │ ├── configurable.cpp │ ├── async_dispatcher.cpp │ └── events.cpp ├── config.h ├── json.h ├── hardware │ ├── touch.h │ ├── Wifi.h │ ├── hardware.h │ ├── touch.cpp │ └── Wifi.cpp ├── sounds │ ├── sound_player.h │ └── sound_player.cpp ├── CMakeLists.txt ├── networking │ ├── signalk_subscription.h │ ├── signalk_socket.h │ └── http_request.h ├── imgs │ └── sk_image_low.h ├── gui.h └── main.cpp ├── CMakeLists.txt ├── default_16MB.csv ├── git_rev_macro.py ├── test └── README ├── sdkconfig.defaults ├── LICENSE ├── lib └── README ├── README.rst ├── include └── README ├── .travis.yml ├── data └── sk_view.json └── platformio.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | build 3 | .vscode -------------------------------------------------------------------------------- /src/ui/callback.cpp: -------------------------------------------------------------------------------- 1 | #include "callback.h" 2 | -------------------------------------------------------------------------------- /src/ui/callback.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "functional" 3 | #include "config.h" -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13.4) 2 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 3 | project(twatchsk) 4 | -------------------------------------------------------------------------------- /src/system/observer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | template 4 | class Observer 5 | { 6 | public: 7 | virtual void notify_change(const T &value); 8 | }; -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #define ARDUINO 100 3 | //#define LV_USE_LOG 1 4 | // => Function select 5 | #define LILYGO_WATCH_LVGL //To use LVGL, you need to enable the macro LVGL 6 | //#define TWATCH_USE_PSRAM_ALLOC_LVGL 7 | #include 8 | 9 | -------------------------------------------------------------------------------- /default_16MB.csv: -------------------------------------------------------------------------------- 1 | # Name, Type, SubType, Offset, Size, Flags 2 | nvs, data, nvs, 0x9000, 0x5000, 3 | otadata, data, ota, 0xe000, 0x2000, 4 | app0, app, ota_0, 0x10000, 0x640000, 5 | app1, app, ota_1, 0x650000,0x640000, 6 | spiffs, data, spiffs, 0xc90000,0x370000, -------------------------------------------------------------------------------- /git_rev_macro.py: -------------------------------------------------------------------------------- 1 | Import("env") 2 | import subprocess 3 | 4 | revision = ( 5 | subprocess.check_output(["git", "rev-parse", "HEAD"]) 6 | .strip() 7 | .decode("utf-8") 8 | ) 9 | # General options that are passed to the C++ compiler 10 | env.Append(CXXFLAGS=["-DGIT_REV='\"%s\"'" % revision]) -------------------------------------------------------------------------------- /src/json.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | //allocate all JSON strings in SPI RAM 4 | struct SpiRamAllocator { 5 | void* allocate(size_t size) { 6 | return heap_caps_malloc(size, MALLOC_CAP_SPIRAM); 7 | } 8 | void deallocate(void* pointer) { 9 | heap_caps_free(pointer); 10 | } 11 | }; 12 | 13 | using SpiRamJsonDocument = BasicJsonDocument; -------------------------------------------------------------------------------- /src/system/async_dispatcher.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | struct AsyncTask_t 7 | { 8 | char name[32]; 9 | std::function callback; 10 | }; 11 | 12 | namespace twatchsk 13 | { 14 | void run_async(const char *name, std::function function); 15 | void initialize_async(); 16 | } -------------------------------------------------------------------------------- /src/system/systemobject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include "Arduino.h" 6 | 7 | class SystemObject 8 | { 9 | public: 10 | SystemObject(String name); 11 | static SystemObject*get_object(String name); 12 | String get_name() { return object_name; } 13 | private: 14 | String object_name; 15 | }; 16 | -------------------------------------------------------------------------------- /src/hardware/touch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../config.h" 3 | #include "FreeRTOS.h" 4 | 5 | typedef struct { 6 | bool touched; 7 | int16_t x_coor; 8 | int16_t y_coor; 9 | } touch_point; 10 | 11 | 12 | class Touch 13 | { 14 | public: 15 | bool initialize(EventGroupHandle_t wakeupEvents); 16 | void set_low_power(bool low_power); 17 | void allow_touch_wakeup(bool value); 18 | }; -------------------------------------------------------------------------------- /src/system/configurable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "SPIFFS.h" 3 | #include 4 | #include "json.h" 5 | 6 | class Configurable 7 | { 8 | public: 9 | Configurable(String path); 10 | void load(); 11 | void save(); 12 | protected: 13 | virtual void load_config_from_file(const JsonObject& json); 14 | virtual void save_config_to_file(JsonObject& json); 15 | private: 16 | String file_path; 17 | }; -------------------------------------------------------------------------------- /src/system/systemobject.cpp: -------------------------------------------------------------------------------- 1 | #include "systemobject.h" 2 | 3 | static std::map objectList; 4 | 5 | SystemObject::SystemObject(String name) 6 | { 7 | object_name = name; 8 | objectList[name] = this; 9 | } 10 | 11 | SystemObject* SystemObject::get_object(String name) 12 | { 13 | auto iterator = objectList.find(name); 14 | 15 | if(iterator == objectList.end()) 16 | { 17 | return NULL; 18 | } 19 | else 20 | { 21 | return iterator->second; 22 | } 23 | } -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | -------------------------------------------------------------------------------- /src/sounds/sound_player.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class SoundPlayer 9 | { 10 | public: 11 | SoundPlayer(); 12 | void play_raw_from_const(const char*name, const unsigned char*raw, int size, int repeat = 1); 13 | ~SoundPlayer(); 14 | private: 15 | QueueHandle_t player_queue_handle_ = NULL; 16 | TaskHandle_t player_task_ = NULL; 17 | static void player_task_func(void *pvParameter); 18 | }; -------------------------------------------------------------------------------- /src/ui/component.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "../config.h" 4 | 5 | class Component 6 | { 7 | public: 8 | void virtual load(const JsonObject &json); 9 | void virtual update(const JsonVariant &update); 10 | void virtual on_offline() { } 11 | void virtual destroy(); 12 | lv_obj_t* get_obj() 13 | { 14 | return obj_; 15 | } 16 | protected: 17 | Component(lv_obj_t*parent) 18 | { 19 | parent_ = parent; 20 | } 21 | 22 | lv_obj_t*obj_ = NULL; 23 | lv_obj_t*parent_ = NULL; 24 | }; -------------------------------------------------------------------------------- /src/ui/ui_functions.h: -------------------------------------------------------------------------------- 1 | #include "gui.h" 2 | 3 | namespace twatchsk 4 | { 5 | class UIFunctions 6 | { 7 | public: 8 | UIFunctions(Gui *gui) 9 | { 10 | gui_ = gui; 11 | } 12 | 13 | void show_home() 14 | { 15 | gui_->show_home(); 16 | }; 17 | void show_settings() 18 | { 19 | gui_->show_settings(); 20 | } 21 | void toggle_wifi() 22 | { 23 | gui_->toggle_wifi(); 24 | } 25 | 26 | 27 | private: 28 | Gui *gui_; 29 | }; 30 | 31 | static UIFunctions * UI_Functions; 32 | } -------------------------------------------------------------------------------- /src/ui/data_adapter.cpp: -------------------------------------------------------------------------------- 1 | #include "data_adapter.h" 2 | #include 3 | 4 | std::vector adapters; 5 | 6 | std::vector &DataAdapter::get_adapters() 7 | { 8 | return adapters; 9 | } 10 | 11 | DataAdapter::DataAdapter(String sk_path, int sk_subscription_period, Component*target) 12 | { 13 | targetObject_ = target; 14 | path = sk_path; 15 | subscription_period = sk_subscription_period; 16 | adapters.push_back(this); 17 | } 18 | 19 | DataAdapter::DataAdapter(Component*target) 20 | { 21 | path = ""; 22 | targetObject_ = target; 23 | sk_put_only_ = true; 24 | adapters.push_back(this); 25 | } -------------------------------------------------------------------------------- /src/ui/dynamic_helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../config.h" 3 | #include 4 | #include "ArduinoJson.h" 5 | 6 | class DynamicHelpers 7 | { 8 | public: 9 | static void set_layout(lv_obj_t *obj, lv_obj_t *parent, const JsonObject&json); 10 | static void set_location(lv_obj_t*obj, const JsonObject&json); 11 | static void set_size(lv_obj_t*obj, const JsonObject&json); 12 | static lv_color_t get_color(const String&value); 13 | static void set_container_layout(lv_obj_t*obj, String&value); 14 | static void set_font(lv_obj_t*obj, String&fontname); 15 | private: 16 | DynamicHelpers(); 17 | static uint8_t get_alignment(String value); 18 | }; -------------------------------------------------------------------------------- /src/ui/component_factory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../config.h" 3 | #include "map" 4 | #include "vector" 5 | #include "ArduinoJson.h" 6 | #include "functional" 7 | #include "data_adapter.h" 8 | #include "component.h" 9 | 10 | class ComponentFactory 11 | { 12 | public: 13 | Component*create_component(JsonObject& componentJson, lv_obj_t*parent); 14 | void layout_component(String layoutType, lv_obj_t* parent, lv_obj_t*obj); 15 | void register_constructor(String name, std::function factoryFunc); 16 | private: 17 | std::map> componentConstructors; 18 | }; -------------------------------------------------------------------------------- /src/ui/dynamic_gui.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "dynamic_view.h" 3 | #include "component_factory.h" 4 | #include "vector" 5 | #include "networking/signalk_socket.h" 6 | 7 | 8 | class DynamicGui 9 | { 10 | public: 11 | DynamicGui(); 12 | void initialize(); 13 | bool load_file(String path, lv_obj_t*parent, SignalKSocket*socket, int& count); 14 | void handle_signalk_update(const String& path, const JsonVariant&value); 15 | void update_online(bool online); 16 | lv_obj_t* get_tile_view() { return tile_view_; } 17 | private: 18 | ComponentFactory *factory; 19 | std::vector views; 20 | lv_obj_t* tile_view_; 21 | bool online_ = false; 22 | }; -------------------------------------------------------------------------------- /sdkconfig.defaults: -------------------------------------------------------------------------------- 1 | # Override some defaults to enable Arduino framework 2 | CONFIG_ENABLE_ARDUINO_DEPENDS=y 3 | CONFIG_AUTOSTART_ARDUINO=y 4 | CONFIG_ARDUINO_RUN_CORE1=y 5 | CONFIG_ARDUINO_RUNNING_CORE=1 6 | CONFIG_ARDUINO_EVENT_RUN_CORE1=y 7 | CONFIG_ARDUINO_EVENT_RUNNING_CORE=1 8 | CONFIG_ARDUINO_UDP_RUN_CORE1=y 9 | CONFIG_ARDUINO_UDP_RUNNING_CORE=1 10 | CONFIG_DISABLE_HAL_LOCKS=y 11 | CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL_ERROR=y 12 | CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL=1 13 | CONFIG_ARDUHAL_PARTITION_SCHEME_DEFAULT=y 14 | CONFIG_ARDUHAL_PARTITION_SCHEME="default" 15 | CONFIG_AUTOCONNECT_WIFI=y 16 | CONFIG_ARDUINO_SELECTIVE_WiFi=y 17 | CONFIG_MBEDTLS_PSK_MODES=y 18 | CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y -------------------------------------------------------------------------------- /src/ui/default_view_json.h: -------------------------------------------------------------------------------- 1 | #include 2 | const char JSON_default_view[] PROGMEM = R"=====( 3 | {"name":"Test view","views":[{"type":"normal","layout":"column_mid","components":[{"type":"label","text":"Speed","font":"roboto40"},{"type":"label","font":"roboto60","color":"blue","binding":{"path":"navigation.speedOverGround","multiply":3.6,"period":5000}},{"type":"label","text":"Km/h","font":"roboto40"}]},{"type":"normal","layout":"column_mid","components":[{"type":"label","text":"Depth","font":"roboto40"},{"type":"label","font":"roboto60","color":"blue","binding":{"path":"environment.depth.belowTransducer","multiply":1.0,"period":5000}},{"type":"label","text":"m","font":"roboto40"}]}]} 4 | )====="; -------------------------------------------------------------------------------- /src/ui/dynamic_label.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "component_factory.h" 3 | #include "dynamic_helpers.h" 4 | #include "component.h" 5 | #include "data_adapter.h" 6 | 7 | class DynamicLabel : public Component 8 | { 9 | public: 10 | DynamicLabel(lv_obj_t*parent) : Component(parent) 11 | { 12 | 13 | }; 14 | void load(const JsonObject &json) override; 15 | void update(const JsonVariant &update) override; 16 | void on_offline() override; 17 | void destroy() override; 18 | private: 19 | Data_formating_t formating; 20 | bool has_binding_ = false; 21 | }; 22 | 23 | class DynamicLabelBuilder 24 | { 25 | public: 26 | static void initialize(ComponentFactory *factory); 27 | 28 | private: 29 | DynamicLabelBuilder() {} 30 | }; -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register(SRCS "main.cpp" "system\\configurable.cpp" "system\\systemobject.cpp" "ui\\callback.cpp" "gui.cpp" "fonts\\roboto80.c" "fonts\\roboto70.c" "fonts\\roboto60.c" "fonts\\roboto40.c" "fonts\\roboto30.c" "imgs\\wifi_48px.c" "imgs\\info_48px.c" "imgs\\bg_default.c" "imgs\\sk_statusbar_icon.c" "imgs\\signalk_48px.c" "imgs\\time_48px.c" "imgs\\watch_48px.c" "hardware\\Wifi.cpp" "networking\\signalk_socket.cpp" "imgs\\exit_32px.c" "system\\events.cpp" "imgs\\display_48px.c" "ui\\dynamic_helpers.cpp" "ui\\component_factory.cpp" "ui\\dynamic_gui.cpp" "ui\\dynamic_label.cpp" "ui\\dynamic_gauge.cpp" "ui\\dynamic_switch.cpp" "ui\\dynamic_button.cpp" "hardware\\hardware.cpp" "system\\async_dispatcher.cpp" "imgs\\wakeup_48px.c" "sounds\\sound_player.cpp" "hardware\\touch.cpp" "ui\\data_adapter.cpp") -------------------------------------------------------------------------------- /src/ui/dynamic_gauge.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "component_factory.h" 3 | #include "dynamic_helpers.h" 4 | #include "component.h" 5 | #include "data_adapter.h" 6 | 7 | class DynamicGauge : public Component 8 | { 9 | public: 10 | DynamicGauge(lv_obj_t*parent) : Component(parent) 11 | { 12 | 13 | }; 14 | void load(const JsonObject &json) override; 15 | void update(const JsonVariant &update) override; 16 | void on_offline() override; 17 | void destroy() override; 18 | private: 19 | Data_formating_t formating; 20 | float minimum_ = 0.0f; 21 | float maximum_ = 100.0f; 22 | lv_obj_t* label_ = NULL; 23 | }; 24 | 25 | class DynamicGaugeBuilder 26 | { 27 | public: 28 | static void initialize(ComponentFactory *factory); 29 | 30 | private: 31 | DynamicGaugeBuilder() {} 32 | }; -------------------------------------------------------------------------------- /src/networking/signalk_subscription.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Arduino.h" 3 | 4 | class SignalKSubscription 5 | { 6 | public: 7 | SignalKSubscription(String subscription_path, uint subscription_period = 1000, bool subscription_low_power = false) 8 | { 9 | path_ = subscription_path; 10 | period_ = subscription_period; 11 | is_low_power_ = subscription_low_power; 12 | } 13 | bool get_low_power() { return is_low_power_; } 14 | String get_path() { return path_; } 15 | uint get_period() { return period_; } 16 | bool get_active() { return is_active_; } 17 | void set_active(bool value) { is_active_ = value; } 18 | private: 19 | String path_ = ""; 20 | uint period_ = 1000; 21 | bool is_low_power_ = false; 22 | bool is_active_ = false; 23 | }; -------------------------------------------------------------------------------- /src/ui/ui_ticker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "config.h" 4 | //Uses LVGL timers 5 | class UITicker 6 | { 7 | public: 8 | UITicker(uint32_t period_ms, std::function callback) 9 | { 10 | tickHandler = callback; 11 | task = lv_task_create(__task_handler, period_ms, LV_TASK_PRIO_LOW, this); 12 | } 13 | ~UITicker() 14 | { 15 | if(task != nullptr) 16 | { 17 | lv_task_del(task); 18 | task = nullptr; 19 | } 20 | } 21 | private: 22 | lv_task_t*task = nullptr; 23 | std::function tickHandler; 24 | static void __task_handler(lv_task_t * task) 25 | { 26 | auto ticker = (UITicker*)task->user_data; 27 | ticker->tickHandler(); 28 | } 29 | }; -------------------------------------------------------------------------------- /src/system/observable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "system/observer.h" 5 | 6 | template 7 | class Observable 8 | { 9 | public: 10 | Observable(T initialValue) 11 | { 12 | value = initialValue; 13 | } 14 | T get(); 15 | void attach(Observer* observer) 16 | { 17 | observers.push_front(observer); 18 | observer->notify_change(value); 19 | } 20 | protected: 21 | void emit(T value) 22 | { 23 | this->value = value; 24 | int index = 0; 25 | for(auto observer : observers) 26 | { 27 | observer->notify_change(value); 28 | index++; 29 | } 30 | } 31 | T value; 32 | private: 33 | std::forward_list*> observers; 34 | }; -------------------------------------------------------------------------------- /src/system/uuid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Arduino.h" 3 | #include "esp_system.h" 4 | #define UUID_BYTE_LEN 16 5 | #define UUID_STR_LEN 37 6 | 7 | class UUID 8 | { 9 | public: 10 | static const String new_id() 11 | { 12 | uint8_t id[UUID_BYTE_LEN]; 13 | /* generate idID bytes */ 14 | esp_fill_random(id, UUID_BYTE_LEN); 15 | /* idid version */ 16 | id[6] = 0x40 | (id[6] & 0xF); 17 | /* idid variant */ 18 | id[8] = (0x80 | id[8]) & ~0x40; 19 | static char out[37]; 20 | snprintf(out, UUID_STR_LEN, 21 | "%02x%02x%02x%02x-%02x%02x-%02x%02x-" 22 | "%02x%02x-%02x%02x%02x%02x%02x%02x", 23 | id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], id[9], id[10], id[11], 24 | id[12], id[13], id[14], id[15]); 25 | 26 | return String(out); 27 | } 28 | 29 | private: 30 | UUID() { } 31 | }; -------------------------------------------------------------------------------- /src/ui/dynamic_switch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "component_factory.h" 3 | #include "dynamic_helpers.h" 4 | #include "component.h" 5 | #include "data_adapter.h" 6 | 7 | class DynamicSwitch : public Component 8 | { 9 | public: 10 | DynamicSwitch(lv_obj_t*parent) : Component(parent) 11 | { 12 | 13 | }; 14 | void load(const JsonObject &json) override; 15 | void update(const JsonVariant &update) override; 16 | void destroy() override; 17 | bool IsChangeHandlerLocked() { return change_handler_locked_; } 18 | bool send_put_request(bool value); 19 | void on_offline() override; 20 | private: 21 | String path_; 22 | bool change_handler_locked_ = false; 23 | DataAdapter* adapter_ = NULL; 24 | }; 25 | 26 | class DynamicSwitchBuilder 27 | { 28 | public: 29 | static void initialize(ComponentFactory *factory); 30 | 31 | private: 32 | DynamicSwitchBuilder() {} 33 | 34 | }; -------------------------------------------------------------------------------- /src/ui/dynamic_button.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "component_factory.h" 3 | #include "dynamic_helpers.h" 4 | #include "component.h" 5 | #include "data_adapter.h" 6 | 7 | enum ButtonAction 8 | { 9 | SKPut, 10 | Settings, 11 | ToggleWifi, 12 | HomeTile 13 | }; 14 | 15 | class DynamicButton : public Component 16 | { 17 | public: 18 | DynamicButton(lv_obj_t*parent) : Component(parent) 19 | { 20 | 21 | }; 22 | void load(const JsonObject &json) override; 23 | void update(const JsonVariant &update) override; 24 | void destroy() override; 25 | bool send_put_request(); 26 | void on_clicked(); 27 | ButtonAction get_action(); 28 | private: 29 | ButtonAction button_action_; 30 | lv_obj_t * label_; 31 | String sk_put_json_; 32 | DataAdapter* adapter_; 33 | }; 34 | 35 | class DynamicButtonBuilder 36 | { 37 | public: 38 | static void initialize(ComponentFactory *factory); 39 | 40 | private: 41 | DynamicButtonBuilder() {} 42 | 43 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jan Dytrych 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ui/component_factory.cpp: -------------------------------------------------------------------------------- 1 | #include "component_factory.h" 2 | 3 | Component * ComponentFactory::create_component(JsonObject &componentJson, lv_obj_t *parent) 4 | { 5 | Component *ret = NULL; 6 | String type = componentJson["type"].as(); 7 | 8 | auto it = componentConstructors.find(type); 9 | 10 | if (it != componentConstructors.end()) 11 | { 12 | ret = it->second(componentJson, parent); 13 | if (componentJson.containsKey("layout")) 14 | { 15 | layout_component(componentJson["layout"].as(), parent, ret->get_obj()); 16 | } 17 | } 18 | else 19 | { 20 | ESP_LOGI("UI", "Unknown component type %s!", type.c_str()); 21 | } 22 | 23 | return ret; 24 | } 25 | 26 | void ComponentFactory::layout_component(String layoutType, lv_obj_t *parent, lv_obj_t *obj) 27 | { 28 | auto layout = LV_LAYOUT_CENTER; 29 | 30 | lv_obj_align(obj, parent, layout, 0, 0); 31 | } 32 | 33 | void ComponentFactory::register_constructor(String name, std::function factoryFunc) 34 | { 35 | componentConstructors[name] = factoryFunc; 36 | } -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. Copyright 2014-present PlatformIO 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | 12 | How to build PlatformIO based project 13 | ===================================== 14 | 15 | 1. `Install PlatformIO Core `_ 16 | 2. Download `development platform with examples `_ 17 | 3. Extract ZIP archive 18 | 4. Run these commands: 19 | 20 | .. code-block:: bash 21 | 22 | # Change directory to example 23 | > cd platform-espressif32/examples/espidf-arduino-blink 24 | 25 | # Build project 26 | > platformio run 27 | 28 | # Upload firmware 29 | > platformio run --target upload 30 | 31 | # Build specific environment 32 | > platformio run -e esp32dev 33 | 34 | # Upload firmware for the specific environment 35 | > platformio run -e esp32dev --target upload 36 | 37 | # Clean build files 38 | > platformio run --target clean 39 | -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /src/ui/message.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.h" 3 | 4 | /***************************************************************** 5 | * 6 | * ! MesBox Class 7 | * 8 | */ 9 | 10 | class MBox 11 | { 12 | public: 13 | MBox() 14 | { 15 | _mbox = nullptr; 16 | } 17 | ~MBox() 18 | { 19 | if (_mbox == nullptr) 20 | return; 21 | lv_obj_del(_mbox); 22 | _mbox = nullptr; 23 | } 24 | 25 | void create(const char *text, lv_event_cb_t event_cb, const char **btns = nullptr, lv_obj_t *par = nullptr) 26 | { 27 | if (_mbox != nullptr) 28 | return; 29 | lv_obj_t *p = par == nullptr ? lv_scr_act() : par; 30 | _mbox = lv_msgbox_create(p, NULL); 31 | lv_msgbox_set_text(_mbox, text); 32 | if (btns == nullptr) 33 | { 34 | static const char *defBtns[] = {"Ok", ""}; 35 | lv_msgbox_add_btns(_mbox, defBtns); 36 | } 37 | else 38 | { 39 | lv_msgbox_add_btns(_mbox, btns); 40 | } 41 | lv_obj_set_width(_mbox, LV_HOR_RES - 40); 42 | lv_obj_set_event_cb(_mbox, event_cb); 43 | lv_obj_align(_mbox, NULL, LV_ALIGN_CENTER, 0, 0); 44 | } 45 | 46 | void setData(void *data) 47 | { 48 | lv_obj_set_user_data(_mbox, data); 49 | } 50 | 51 | void *getData() 52 | { 53 | return lv_obj_get_user_data(_mbox); 54 | } 55 | 56 | void setBtn(const char **btns) 57 | { 58 | lv_msgbox_add_btns(_mbox, btns); 59 | } 60 | 61 | private: 62 | lv_obj_t *_mbox = nullptr; 63 | }; -------------------------------------------------------------------------------- /src/system/events.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "Arduino.h" 9 | 10 | #define G_EVENT_VBUS_PLUGIN _BV(0) 11 | #define G_EVENT_VBUS_REMOVE _BV(1) 12 | #define G_EVENT_CHARGE_DONE _BV(2) 13 | #define G_EVENT_TOUCH _BV(3) 14 | 15 | #define G_APP_STATE_LOW_POWER _BV(0) 16 | #define G_APP_STATE_WAKE_UP _BV(1) 17 | 18 | enum ApplicationEvents_T 19 | { 20 | Q_EVENT_BMA_INT, 21 | Q_EVENT_AXP_INT, 22 | Q_EVENT_UI_MESSAGE 23 | }; 24 | 25 | enum GuiEventType_t 26 | { 27 | GUI_SHOW_WARNING, 28 | GUI_SK_DV_UPDATE // SK_DV means "SignalK DynamicView" 29 | }; 30 | 31 | enum GuiMessageCode_t 32 | { 33 | NONE, 34 | GUI_WARN_SK_REJECTED, 35 | GUI_WARN_SK_LOST_CONNECTION, 36 | GUI_WARN_WIFI_DISCONNECTED, 37 | GUI_WARN_WIFI_CONNECTION_FAILED, 38 | GUI_INFO_BATTERY_CHARGE_COMPLETE 39 | }; 40 | 41 | struct GuiEvent_t 42 | { 43 | GuiEventType_t event_type; 44 | void*argument; 45 | GuiMessageCode_t message_code; 46 | }; 47 | 48 | extern QueueHandle_t g_event_queue_handle; 49 | extern EventGroupHandle_t g_app_state; 50 | 51 | void initialize_events(); 52 | void post_event(ApplicationEvents_T event); 53 | void post_gui_sk_dv_update(const String& json); // "sk_dv" means "SignalK DynamicView" 54 | void post_gui_warning(GuiMessageCode_t message); 55 | void post_gui_warning(const String& message); 56 | bool read_gui_update(GuiEvent_t& event); 57 | bool is_low_power(); 58 | void set_low_power(bool low_power); -------------------------------------------------------------------------------- /src/ui/view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../config.h" 3 | #include 4 | 5 | class View 6 | { 7 | private: 8 | inline static std::vector active_views_; 9 | bool is_active_ = false; 10 | 11 | public: 12 | View() 13 | { 14 | is_active_ = true; 15 | active_views_.push_back(this); // add each View descendant to a std::vector of active views 16 | ESP_LOGI("VIEW_CONSTRUCTOR", "Size of active_views_ is %d", active_views_.size()); 17 | } 18 | 19 | ~View() 20 | { 21 | if (is_active_) 22 | { 23 | is_active_ = false; 24 | active_views_.pop_back(); 25 | ESP_LOGI("VIEW_DESTRUCTOR", "Number of active_views_ is %d", active_views_.size()); 26 | } 27 | } 28 | 29 | void remove_from_active_list() 30 | { 31 | active_views_.pop_back(); 32 | ESP_LOGI("VIEW_POST_REMOVE", "Number of active_views_ is %d", active_views_.size()); 33 | } 34 | 35 | static int get_active_views_count() { return active_views_.size(); } 36 | 37 | virtual void show(lv_obj_t*parent) { } 38 | virtual void hide() 39 | { 40 | if (is_active_) 41 | { 42 | is_active_ = false; 43 | active_views_.pop_back(); 44 | ESP_LOGI("VIEW_HIDE", "Number of active_views_ is %d", active_views_.size()); 45 | } 46 | } 47 | virtual void theme_changed() { } 48 | static void invoke_theme_changed() 49 | { 50 | for (std::vector::iterator it = active_views_.begin(); it != active_views_.end(); it++) 51 | { 52 | (*it)->theme_changed(); 53 | } 54 | } 55 | 56 | }; -------------------------------------------------------------------------------- /src/system/configurable.cpp: -------------------------------------------------------------------------------- 1 | #include "configurable.h" 2 | const char TAG[] = "CONFIG"; 3 | 4 | Configurable::Configurable(String path) 5 | { 6 | this->file_path = path; 7 | } 8 | 9 | void Configurable::load() 10 | { 11 | StaticJsonDocument<512> doc; 12 | auto exists = SPIFFS.exists(file_path); 13 | ESP_LOGI(TAG, "Loading config %s (exists=%d)", file_path.c_str(), exists); 14 | 15 | if (exists) 16 | { 17 | auto file = SPIFFS.open(file_path); 18 | ESP_LOGI(TAG, "Config %s size=%d", file_path.c_str(), file.available()); 19 | auto error = deserializeJson(doc, file); 20 | if(error == DeserializationError::Ok) 21 | { 22 | load_config_from_file(doc.as()); 23 | } 24 | else 25 | { 26 | ESP_LOGI(TAG, "Failed to deserialize config %s! Error=%d", file_path.c_str(), (int)error.code()); 27 | } 28 | 29 | file.close(); 30 | } 31 | } 32 | 33 | void Configurable::save() 34 | { 35 | StaticJsonDocument<512> doc; 36 | auto file = SPIFFS.open(file_path, "w"); 37 | JsonObject obj = doc.createNestedObject("root"); 38 | save_config_to_file(obj); 39 | serializeJson(obj, file); 40 | file.flush(); 41 | file.close(); 42 | ESP_LOGI(TAG, "Saved config %s to SPIFFS!", file_path.c_str()); 43 | } 44 | 45 | void Configurable::load_config_from_file(const JsonObject &json) 46 | { 47 | ESP_LOGW(TAG, "load_config_from_file not overriden for %s", file_path.c_str()); 48 | } 49 | 50 | void Configurable::save_config_to_file(JsonObject &json) 51 | { 52 | ESP_LOGW(TAG, "save_config_to_file not overriden for %s", file_path.c_str()); 53 | } -------------------------------------------------------------------------------- /src/ui/themes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../config.h" 3 | 4 | namespace twatchsk 5 | { 6 | 7 | /** This file defines variables and functions for manipulating graphic elements of 8 | * the MATERIAL theme that's used for TWatchSK. They are declared in the twatchsk 9 | * namespace so that they can be used anwywhere throught the TWatchSK project. 10 | **/ 11 | 12 | static bool dark_theme_enabled = false; 13 | 14 | static lv_color_t get_text_color() 15 | { 16 | if(dark_theme_enabled) 17 | { 18 | return LV_COLOR_WHITE; 19 | } 20 | else 21 | { 22 | return LV_COLOR_BLACK; 23 | } 24 | } 25 | 26 | static void update_imgbtn_color(lv_obj_t *button) 27 | { 28 | if (dark_theme_enabled) 29 | { 30 | lv_obj_set_style_local_image_recolor_opa(button, LV_OBJ_PART_MAIN, LV_BTN_STATE_PRESSED, LV_OPA_COVER); 31 | lv_obj_set_style_local_image_recolor_opa(button, LV_OBJ_PART_MAIN, LV_BTN_STATE_RELEASED, LV_OPA_COVER); 32 | lv_obj_set_style_local_image_recolor(button, LV_OBJ_PART_MAIN, LV_BTN_STATE_PRESSED, LV_COLOR_WHITE); 33 | lv_obj_set_style_local_image_recolor(button, LV_OBJ_PART_MAIN, LV_BTN_STATE_RELEASED, LV_COLOR_WHITE); 34 | } 35 | else 36 | { 37 | lv_obj_set_style_local_image_recolor_opa(button, LV_OBJ_PART_MAIN, LV_BTN_STATE_PRESSED, LV_OPA_COVER); 38 | lv_obj_set_style_local_image_recolor_opa(button, LV_OBJ_PART_MAIN, LV_BTN_STATE_RELEASED, LV_OPA_COVER); 39 | lv_obj_set_style_local_image_recolor(button, LV_OBJ_PART_MAIN, LV_BTN_STATE_PRESSED, LV_COLOR_BLACK); 40 | lv_obj_set_style_local_image_recolor(button, LV_OBJ_PART_MAIN, LV_BTN_STATE_RELEASED, LV_COLOR_BLACK); 41 | } 42 | } 43 | 44 | } // namespace twatchsk -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < https://docs.platformio.org/page/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < https://docs.platformio.org/page/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < https://docs.platformio.org/page/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choose one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | # language: python 28 | # python: 29 | # - "2.7" 30 | # 31 | # sudo: false 32 | # cache: 33 | # directories: 34 | # - "~/.platformio" 35 | # 36 | # install: 37 | # - pip install -U platformio 38 | # - platformio update 39 | # 40 | # script: 41 | # - platformio run 42 | 43 | 44 | # 45 | # Template #2: The project is intended to be used as a library with examples. 46 | # 47 | 48 | # language: python 49 | # python: 50 | # - "2.7" 51 | # 52 | # sudo: false 53 | # cache: 54 | # directories: 55 | # - "~/.platformio" 56 | # 57 | # env: 58 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 59 | # - PLATFORMIO_CI_SRC=examples/file.ino 60 | # - PLATFORMIO_CI_SRC=path/to/test/directory 61 | # 62 | # install: 63 | # - pip install -U platformio 64 | # - platformio update 65 | # 66 | # script: 67 | # - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N 68 | -------------------------------------------------------------------------------- /src/system/async_dispatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "async_dispatcher.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | QueueHandle_t dispatcher_queue_handle = NULL; 12 | TaskHandle_t dispatcher_task = NULL; 13 | const char *ASYNC_TAG = "ASYNC"; 14 | 15 | /** This function is receiving tasks from queue and executing them one by one 16 | */ 17 | void dispatcher_task_func(void *pvParameter) 18 | { 19 | AsyncTask_t currentTask; 20 | 21 | ESP_LOGI(ASYNC_TAG, "Async task dispatcher started!"); 22 | 23 | while (true) 24 | { 25 | if (xQueueReceive(dispatcher_queue_handle, ¤tTask, portMAX_DELAY)) 26 | { 27 | ESP_LOGI(ASYNC_TAG, "Starting task %s", currentTask.name); 28 | currentTask.callback(); 29 | ESP_LOGI(ASYNC_TAG, "Task finished %s", currentTask.name); 30 | } 31 | } 32 | } 33 | 34 | /** Intialize async call dispatcher that allows serialized async method running 35 | */ 36 | void twatchsk::initialize_async() 37 | { 38 | ESP_LOGI(ASYNC_TAG, "Initializing async task dispatcher..."); 39 | dispatcher_queue_handle = xQueueCreate(32, sizeof(AsyncTask_t)); 40 | xTaskCreate(dispatcher_task_func, "async", CONFIG_MAIN_TASK_STACK_SIZE, NULL, 5, &dispatcher_task); 41 | } 42 | 43 | /** This method adds new task in async dispatcher queue, it will be run when all queued tasks prior this call are completed 44 | * @param name - name of the task (will be visible in log - start / stop) 45 | * @param function - lambda or std::binded function that will be executed on 1 code of ESP32 46 | */ 47 | void twatchsk::run_async(const char *name, std::function function) 48 | { 49 | AsyncTask_t task; 50 | strcpy(task.name, name); 51 | task.callback = function; 52 | xQueueSend(dispatcher_queue_handle, &task, portMAX_DELAY); 53 | } -------------------------------------------------------------------------------- /data/sk_view.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Test view", 3 | "views": [ 4 | { 5 | "type": "normal", 6 | "layout": "column_mid", 7 | "components": [ 8 | { 9 | "type": "label", 10 | "text": "Speed", 11 | "font": "roboto40", 12 | "color": "white" 13 | }, 14 | { 15 | "type": "label", 16 | "font": "roboto60", 17 | "color": "blue", 18 | "binding": { 19 | "path": "navigation.speedOverGround", 20 | "multiply": 3.6, 21 | "period": 2000 22 | } 23 | }, 24 | { 25 | "type": "label", 26 | "text": "Km/h", 27 | "font": "roboto40", 28 | "color": "white" 29 | } 30 | ] 31 | }, 32 | { 33 | "type": "normal", 34 | "layout": "column_mid", 35 | "components": [ 36 | { 37 | "type": "label", 38 | "text": "Depth", 39 | "font": "roboto40", 40 | "color": "white" 41 | }, 42 | { 43 | "type": "label", 44 | "font": "roboto60", 45 | "color": "blue", 46 | "binding": { 47 | "path": "environment.depth.belowTransducer", 48 | "multiply": 1.0, 49 | "period": 1000 50 | } 51 | }, 52 | { 53 | "type": "label", 54 | "text": "m", 55 | "font": "roboto40", 56 | "color": "white" 57 | } 58 | ] 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter, extra scripting 4 | ; Upload options: custom port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; 7 | ; Please visit documentation for the other options and examples 8 | ; http://docs.platformio.org/page/projectconf.html 9 | 10 | [env] 11 | platform = espressif32 12 | framework = espidf, arduino 13 | monitor_speed = 115200 14 | upload_speed = 400000 15 | extra_scripts = git_rev_macro.py 16 | platform_packages = 17 | ; use a special branch 18 | framework-arduinoespressif32@https://github.com/marcovannoord/arduino-esp32#idf-release/v4.0 19 | 20 | [env:ttgo-t-watch] 21 | board = ttgo-t-watch 22 | monitor_filters = esp32_exception_decoder 23 | board_build.partitions = default_16MB.csv 24 | build_flags = 25 | -D ARDUINO=165 26 | -D ESP32=1 27 | -D LV_USE_LOG=1 28 | -D LILYGO_WATCH_2020_V1 29 | -Wno-return-type 30 | -Wno-unused-const-variable 31 | -Wno-class-memaccess 32 | lib_deps = 33 | xinyuan-lilygo/TTGO TWatch Library@1.4.2 34 | ArduinoJson 35 | 36 | [env:ttgo-t-watch-v2] 37 | board = ttgo-t-watch 38 | monitor_filters = esp32_exception_decoder 39 | board_build.partitions = default_16MB.csv 40 | build_flags = 41 | -D ARDUINO=165 42 | -D ESP32=1 43 | -D LV_USE_LOG=1 44 | -D LILYGO_WATCH_2020_V2 45 | -Wno-return-type 46 | -Wno-unused-const-variable 47 | -Wno-class-memaccess 48 | lib_deps = 49 | xinyuan-lilygo/TTGO TWatch Library@1.4.2 50 | ArduinoJson 51 | 52 | [env:ttgo-t-watch-v3] 53 | board = ttgo-t-watch 54 | monitor_filters = esp32_exception_decoder 55 | board_build.partitions = default_16MB.csv 56 | build_flags = 57 | -D ARDUINO=165 58 | -D ESP32=1 59 | -D LV_USE_LOG=1 60 | -D LILYGO_WATCH_2020_V3 61 | -Wno-return-type 62 | -Wno-unused-const-variable 63 | -Wno-class-memaccess 64 | lib_deps = 65 | xinyuan-lilygo/TTGO TWatch Library@1.4.2 66 | ArduinoJson -------------------------------------------------------------------------------- /src/ui/wifilist.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.h" 3 | #include "ui/settings_view.h" 4 | #include "localization.h" 5 | 6 | LV_FONT_DECLARE(lv_font_montserrat_28); 7 | 8 | class WifiList : public SettingsView 9 | { 10 | public: 11 | WifiList() : SettingsView(LOC_WIFI_SELECT) {} 12 | 13 | ~WifiList() 14 | { 15 | if (wifi_list_ != NULL) 16 | { 17 | lv_obj_del(wifi_list_); 18 | wifi_list_ = NULL; 19 | } 20 | } 21 | 22 | virtual void show_internal(lv_obj_t *parent) override 23 | { 24 | lv_cont_set_layout(parent, LV_LAYOUT_OFF); 25 | 26 | wifi_list_ = lv_list_create(parent, NULL); 27 | lv_obj_set_pos(wifi_list_, 0, 0); 28 | lv_obj_set_size(wifi_list_, lv_obj_get_width(parent), lv_obj_get_height(parent)); 29 | lv_obj_set_style_local_scale_border_width(wifi_list_, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); 30 | lv_obj_set_style_local_radius(wifi_list_, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); 31 | lv_obj_set_style_local_text_font(wifi_list_, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, &lv_font_montserrat_28); 32 | } 33 | 34 | virtual bool hide_internal() override 35 | { 36 | lv_obj_del(wifi_list_); 37 | wifi_list_ = NULL; 38 | return true; 39 | } 40 | 41 | void add_ssid(const char *txt, void *imgsrc = (void *)LV_SYMBOL_WIFI) 42 | { 43 | lv_obj_t *btn = lv_list_add_btn(wifi_list_, imgsrc, txt); 44 | btn->user_data = this; 45 | lv_obj_set_event_cb(btn, __list_event_cb); 46 | } 47 | 48 | static void __list_event_cb(lv_obj_t *obj, lv_event_t event) 49 | { 50 | if (event == LV_EVENT_SHORT_CLICKED) 51 | { 52 | auto list = (WifiList *)obj->user_data; 53 | const char *ssid = lv_list_get_btn_text(obj); 54 | list->wifi_selected(ssid); 55 | } 56 | } 57 | 58 | const char *selected_ssid() 59 | { 60 | return selected_ssid_; 61 | } 62 | 63 | private: 64 | lv_obj_t *wifi_list_ = NULL; 65 | const char *selected_ssid_ = NULL; 66 | 67 | void wifi_selected(const char *ssid) 68 | { 69 | selected_ssid_ = ssid; 70 | hide(); 71 | } 72 | }; -------------------------------------------------------------------------------- /src/ui/loader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.h" 3 | 4 | class Loader 5 | { 6 | public: 7 | Loader(String text) 8 | { 9 | static lv_style_t plStyle; 10 | lv_style_init(&plStyle); 11 | lv_style_set_radius(&plStyle, LV_OBJ_PART_MAIN, 0); 12 | lv_style_set_border_width(&plStyle, LV_OBJ_PART_MAIN, 0); 13 | lv_style_set_bg_color(&plStyle, LV_OBJ_PART_MAIN, LV_COLOR_GRAY); 14 | lv_style_set_text_color(&plStyle, LV_OBJ_PART_MAIN, LV_COLOR_WHITE); 15 | lv_style_set_image_recolor(&plStyle, LV_OBJ_PART_MAIN, LV_COLOR_WHITE); 16 | 17 | static lv_style_t style; 18 | lv_style_init(&style); 19 | lv_style_set_radius(&style, LV_OBJ_PART_MAIN, 0); 20 | lv_style_set_border_width(&style, LV_OBJ_PART_MAIN, 0); 21 | lv_style_set_text_color(&style, LV_OBJ_PART_MAIN, LV_COLOR_WHITE); 22 | lv_style_set_image_recolor(&style, LV_OBJ_PART_MAIN, LV_COLOR_WHITE); 23 | 24 | loaderContainer = lv_cont_create(lv_scr_act(), NULL); 25 | lv_obj_set_pos(loaderContainer, 0, 0); 26 | lv_obj_set_size(loaderContainer, LV_HOR_RES, LV_VER_RES); 27 | //lv_obj_add_style(loaderContainer, LV_OBJ_PART_MAIN, &plStyle); 28 | lv_cont_set_layout(loaderContainer, LV_LAYOUT_COLUMN_MID); 29 | 30 | lv_obj_t *preload = lv_spinner_create(loaderContainer, NULL); 31 | lv_obj_set_size(preload, lv_obj_get_width(loaderContainer) / 2, lv_obj_get_height(loaderContainer) / 2); 32 | //lv_obj_add_style(preload, LV_OBJ_PART_MAIN, &style); 33 | 34 | lv_obj_t *label = lv_label_create(loaderContainer, NULL); 35 | lv_label_set_text(label, text.c_str()); 36 | //lv_obj_add_style(preload, LV_OBJ_PART_MAIN, &style); 37 | ESP_LOGI("LOADER", "Loader loaded %s", text.c_str()); 38 | } 39 | 40 | void update_status(String status) 41 | { 42 | lv_label_set_text(label, status.c_str()); 43 | } 44 | 45 | ~Loader() 46 | { 47 | if (loaderContainer != NULL) 48 | { 49 | lv_obj_del(loaderContainer); 50 | loaderContainer = NULL; 51 | label = NULL; 52 | ESP_LOGI("LOADER", "Loader destroyed"); 53 | } 54 | } 55 | 56 | private: 57 | lv_obj_t *loaderContainer = NULL; 58 | lv_obj_t *label = NULL; 59 | }; -------------------------------------------------------------------------------- /src/ui/date_picker.h: -------------------------------------------------------------------------------- 1 | #include "settings_view.h" 2 | #include "config.h" 3 | 4 | LV_FONT_DECLARE(lv_font_montserrat_14); 5 | 6 | class DatePicker : public SettingsView 7 | { 8 | public: 9 | DatePicker(char *title) : SettingsView(title) 10 | { 11 | } 12 | 13 | virtual void show_internal(lv_obj_t *parent) override 14 | { 15 | lv_cont_set_layout(parent, LV_LAYOUT_OFF); 16 | calendar = lv_calendar_create(parent, NULL); 17 | lv_obj_set_size(calendar, 200, 200); 18 | lv_obj_align(calendar, parent, LV_ALIGN_CENTER, 0, 0); 19 | lv_obj_set_style_local_text_font(calendar, LV_CALENDAR_PART_DATE, LV_STATE_DEFAULT, &lv_font_montserrat_14); 20 | 21 | auto today = TTGOClass::getWatch()->rtc->getDateTime(); 22 | lv_calendar_date_t date; 23 | date.year = today.year; 24 | date.month = today.month; 25 | date.day = today.day; 26 | selected_date = date; 27 | lv_calendar_set_today_date(calendar, &date); 28 | lv_calendar_set_showed_date(calendar, &date); 29 | calendar->user_data = this; 30 | lv_obj_set_event_cb(calendar, DatePicker::calendar_callback); 31 | } 32 | 33 | bool is_success() { return success; } 34 | lv_calendar_date_t get_date() { return selected_date; } 35 | 36 | private: 37 | lv_obj_t *calendar; 38 | lv_calendar_date_t selected_date; 39 | bool success = false; 40 | 41 | void date_selected(lv_calendar_date_t date) 42 | { 43 | success = true; 44 | selected_date = date; 45 | ESP_LOGI(SETTINGS_TAG, "User selected %d-%d-%d date in picker.", date.year, date.month, date.day); 46 | this->hide(); 47 | } 48 | 49 | static void calendar_callback(lv_obj_t*obj, lv_event_t event) 50 | { 51 | if(event == LV_EVENT_VALUE_CHANGED) 52 | { 53 | auto selectedDate = lv_calendar_get_pressed_date(obj); 54 | if(selectedDate) 55 | { 56 | 57 | DatePicker*picker = ((DatePicker*)obj->user_data); 58 | lv_calendar_date_t passDate; 59 | passDate.year = selectedDate->year; 60 | passDate.month = selectedDate->month; 61 | passDate.day = selectedDate->day; 62 | picker->date_selected(passDate); 63 | } 64 | } 65 | } 66 | }; -------------------------------------------------------------------------------- /src/system/events.cpp: -------------------------------------------------------------------------------- 1 | #include "events.h" 2 | 3 | QueueHandle_t g_event_queue_handle = NULL; 4 | EventGroupHandle_t g_app_state = NULL; 5 | QueueHandle_t gui_queue_handle = NULL; 6 | 7 | void initialize_events() 8 | { 9 | //Create a program that allows the required message objects and group flags 10 | g_event_queue_handle = xQueueCreate(20, sizeof(uint8_t)); 11 | gui_queue_handle = xQueueCreate(60, sizeof(GuiEvent_t)); 12 | g_app_state = xEventGroupCreate(); 13 | } 14 | 15 | void post_event(ApplicationEvents_T event) 16 | { 17 | xQueueSend(g_event_queue_handle, &event, 10); 18 | } 19 | 20 | void post_gui_update(GuiEvent_t event) 21 | { 22 | if (event.event_type == GuiEventType_t::GUI_SHOW_WARNING && is_low_power()) 23 | { 24 | xEventGroupSetBits(g_app_state, G_APP_STATE_WAKE_UP); 25 | } 26 | 27 | xQueueSend(gui_queue_handle, &event, 10); 28 | } 29 | 30 | void post_gui_warning(GuiMessageCode_t code) 31 | { 32 | GuiEvent_t event; 33 | event.event_type = GuiEventType_t::GUI_SHOW_WARNING; 34 | event.message_code = code; 35 | event.argument = NULL; 36 | 37 | post_gui_update(event); 38 | } 39 | 40 | void post_gui_warning(const String& message) 41 | { 42 | GuiEvent_t event; 43 | event.argument = malloc(message.length() + 1); 44 | strcpy((char *)event.argument, message.c_str()); 45 | event.event_type = GuiEventType_t::GUI_SHOW_WARNING; 46 | event.message_code = GuiMessageCode_t::NONE; 47 | post_gui_update(event); 48 | } 49 | 50 | void post_gui_sk_dv_update(const String& json) // "sk_dv" means "SignalK DynamicView" 51 | { 52 | GuiEvent_t event; 53 | event.argument = malloc(json.length() + 1); 54 | strcpy((char *)event.argument, json.c_str()); 55 | event.event_type = GuiEventType_t::GUI_SK_DV_UPDATE; 56 | event.message_code = GuiMessageCode_t::NONE; 57 | 58 | post_gui_update(event); 59 | } 60 | 61 | bool read_gui_update(GuiEvent_t &event) 62 | { 63 | return xQueueReceive(gui_queue_handle, &event, 10); 64 | } 65 | 66 | bool is_low_power() 67 | { 68 | return xEventGroupGetBits(g_app_state) & G_APP_STATE_LOW_POWER; 69 | } 70 | 71 | void set_low_power(bool low_power) 72 | { 73 | if (low_power) 74 | { 75 | xEventGroupSetBits(g_app_state, G_APP_STATE_LOW_POWER); 76 | } 77 | else 78 | { 79 | xEventGroupClearBits(g_app_state, G_APP_STATE_LOW_POWER); 80 | } 81 | } -------------------------------------------------------------------------------- /src/hardware/Wifi.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "system/configurable.h" 4 | #include "FreeRTOS.h" 5 | #include "system/systemobject.h" 6 | #include "system/observable.h" 7 | #include "esp_wifi.h" 8 | #define WIFI_AP_LIST_MAX_SIZE 32 9 | #define WIFI_RETRY_ARRAY_SIZE 8 10 | #define WIFI_RETRY_MAX_MINUTES 60.0 11 | 12 | enum WifiState_t 13 | { 14 | Wifi_Off, 15 | Wifi_Disconnected, 16 | Wifi_Connecting, 17 | Wifi_Connected 18 | }; 19 | 20 | struct KnownWifi_t 21 | { 22 | String known_ssid; 23 | String known_password; 24 | }; 25 | 26 | class WifiManager : public Configurable, public SystemObject, public Observable 27 | { 28 | public: 29 | WifiManager(); 30 | void on(); 31 | void off(bool force = false); 32 | void connect(); 33 | String get_ip() { return ip_; } 34 | String get_configured_ssid () 35 | { 36 | return ssid_; 37 | } 38 | WifiState_t get_status() { return value; } 39 | bool is_enabled() { return enabled_; } 40 | bool is_connected() { return value == WifiState_t::Wifi_Connected; } 41 | void set_ip(String ip) { this->ip_ = ip; } 42 | void update_status(WifiState_t value) { Observable::emit(value); } 43 | void setup(String ssid, String password); 44 | bool is_configured() { return configured_; } 45 | bool scan_wifi(); 46 | bool is_scan_complete(); 47 | int found_wifi_count(); 48 | const wifi_ap_record_t get_found_wifi(int index); 49 | bool is_known_wifi(const String ssid); 50 | bool get_known_wifi_password(const String ssid, String &password); 51 | int get_wifi_rssi(); 52 | private: 53 | void initialize(); 54 | virtual void load_config_from_file(const JsonObject &json) override final; 55 | virtual void save_config_to_file(JsonObject &json) override final; 56 | String ssid_; 57 | String password_; 58 | String ip_ = ""; 59 | bool enabled_ = false; 60 | bool connected_ = false; 61 | bool initialized_ = false; 62 | bool forced_disconnect_ = false; 63 | bool configured_ = false; 64 | static void wifi_event_handler(void *arg, esp_event_base_t event_base, 65 | int32_t event_id, void *event_data); 66 | void clear_wifi_list(); 67 | std::vector known_wifi_list_; 68 | static void wifi_reconnect_task(void *pvParameter); 69 | float wifi_retry_minutes_[WIFI_RETRY_ARRAY_SIZE] = {0.5, 1.0, 2.0, 3.0, 10.0, 20.0, 30.0, 60.0}; //{0.5, 1.0, 2.0, 3.0, 10.0, 20.0, 30.0, 60.0}; 70 | int wifi_retry_counter_ = 0; 71 | 72 | }; -------------------------------------------------------------------------------- /src/ui/data_adapter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ArduinoJson.h" 3 | #include "config.h" 4 | #include "functional" 5 | #include "component.h" 6 | #include 7 | #include "networking/signalk_socket.h" 8 | #include "system/uuid.h" 9 | 10 | struct Data_formating_t 11 | { 12 | float multiply = 1.0; 13 | float offset = 0.0; 14 | int decimal_places = 1; 15 | char *string_format = NULL; 16 | }; 17 | 18 | class DataAdapter 19 | { 20 | public: 21 | DataAdapter(String sk_path, int sk_subscription_period, Component *target); 22 | DataAdapter(Component *target); 23 | const String &get_path() { return path; } 24 | int get_subscription_period() { return subscription_period; } 25 | void on_updated(const JsonVariant &value) 26 | { 27 | targetObject_->update(value); 28 | } 29 | 30 | void on_offline() 31 | { 32 | targetObject_->on_offline(); 33 | } 34 | 35 | bool put_request(bool value) 36 | { 37 | ESP_LOGI("DataAdapter", "Put request %s with bool value %s", path.c_str(), value ? "true" : "false"); 38 | 39 | DynamicJsonDocument request(1024); 40 | JsonObject root = request.to(); 41 | root["requestId"] = UUID::new_id(); 42 | JsonObject put_data = root.createNestedObject("put"); 43 | put_data["path"] = path; 44 | put_data["value"] = value; 45 | 46 | return put_request(root); 47 | } 48 | 49 | bool put_request(String value) 50 | { 51 | ESP_LOGI("DataAdapter", "Put request %s with json value %s", path.c_str(), value.c_str()); 52 | DynamicJsonDocument putObj(512); 53 | deserializeJson(putObj, value); 54 | 55 | DynamicJsonDocument request(1024); 56 | JsonObject root = request.to(); 57 | root["requestId"] = UUID::new_id(); 58 | JsonObject put_data = root.createNestedObject("put"); 59 | put_data["path"] = putObj["path"]; 60 | put_data["value"] = putObj["value"]; 61 | 62 | return put_request(root); 63 | } 64 | 65 | bool put_request(JsonObject &obj) 66 | { 67 | if (ws_socket_->get_state() == WebsocketState_t::WS_Connected) 68 | { 69 | return ws_socket_->send_put_request(obj); 70 | } 71 | else 72 | { 73 | return false; 74 | } 75 | } 76 | 77 | void initialize(SignalKSocket *socket) 78 | { 79 | ws_socket_ = socket; 80 | if(!sk_put_only_) 81 | { 82 | socket->add_subscription(get_path(), get_subscription_period(), false); 83 | } 84 | } 85 | 86 | static std::vector &get_adapters(); 87 | 88 | protected: 89 | int subscription_period = 0; 90 | Data_formating_t formating_options_; 91 | String path = ""; 92 | Component *targetObject_ = NULL; 93 | SignalKSocket *ws_socket_ = NULL; 94 | bool sk_put_only_ = false; 95 | }; 96 | -------------------------------------------------------------------------------- /src/hardware/hardware.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../config.h" 3 | #include "system/configurable.h" 4 | #include "ui/themes.h" 5 | #include 6 | #include 7 | #include "system/async_dispatcher.h" 8 | #include "sounds/sound_player.h" 9 | #include "hardware/touch.h" 10 | 11 | enum PowerCode_t 12 | { 13 | POWER_ENTER_LOW_POWER, 14 | POWER_LEAVE_LOW_POWER, 15 | POWER_CHARGING_ON, 16 | POWER_CHARGING_OFF, 17 | POWER_CHARGING_DONE, 18 | WALK_STEP_COUNTER_UPDATED, 19 | POWER_LOW_TICK, 20 | DOUBLE_TAP_DETECTED 21 | }; 22 | 23 | enum WakeupSource_t 24 | { 25 | WAKEUP_BUTTON, 26 | WAKEUP_ACCELEROMETER, 27 | WAKEUP_TOUCH 28 | }; 29 | 30 | extern void IRAM_ATTR wakeup_from_isr(); 31 | 32 | typedef std::function low_power_callback; 33 | /** 34 | * @brief Hardware class purpose is to handle all hardware features of the watch, power managment setup, 35 | * power event callbacks and BMA interrupts, sound & vibrate stuff (coming soon) 36 | **/ 37 | class Hardware : public Configurable 38 | { 39 | public: 40 | Hardware(); 41 | 42 | void load_config_from_file(const JsonObject &json) override; 43 | void save_config_to_file(JsonObject &json) override; 44 | 45 | bool get_tilt_wakeup() 46 | { 47 | return tilt_wakeup_; 48 | } 49 | void set_tilt_wakeup(bool value) 50 | { 51 | tilt_wakeup_ = value; 52 | update_bma_wakeup(); 53 | } 54 | bool get_double_tap_wakeup() 55 | { 56 | return double_tap_wakeup_; 57 | } 58 | void set_double_tap_wakeup(bool value) 59 | { 60 | double_tap_wakeup_ = value; 61 | update_bma_wakeup(); 62 | } 63 | 64 | bool get_touch_wakeup() 65 | { 66 | return touch_wakeup_; 67 | } 68 | 69 | void set_touch_wakeup(bool value) 70 | { 71 | touch_wakeup_ = value; 72 | touch_->allow_touch_wakeup(value); 73 | } 74 | 75 | void initialize(TTGOClass *watch); 76 | void attach_power_callback(low_power_callback callback) 77 | { 78 | power_callbacks_.push_back(callback); 79 | } 80 | 81 | void loop(); 82 | void set_screen_timeout_func(std::function func) 83 | { 84 | get_screen_timeout_ = func; 85 | } 86 | void vibrate(int duration); 87 | void vibrate(int pattern[], int repeat = 1); 88 | void initialize_touch(); 89 | 90 | SoundPlayer*get_player() 91 | { 92 | return player_; 93 | } 94 | private: 95 | std::vector power_callbacks_; 96 | std::function get_screen_timeout_; 97 | bool double_tap_wakeup_ = false; 98 | bool tilt_wakeup_ = false; 99 | bool touch_wakeup_ = false; 100 | TTGOClass *watch_; 101 | bool lenergy_ = false; 102 | bool is_vibrating_ = false; 103 | void low_energy(); 104 | void invoke_power_callbacks(PowerCode_t code, uint32_t arg); 105 | void update_bma_wakeup(); 106 | void vibrate(bool status); 107 | SoundPlayer*player_ = NULL; 108 | Touch*touch_ = NULL; 109 | }; -------------------------------------------------------------------------------- /src/ui/dynamic_gui.cpp: -------------------------------------------------------------------------------- 1 | #include "dynamic_gui.h" 2 | #include "SPIFFS.h" 3 | #include "ArduinoJson.h" 4 | #include "default_view_json.h" 5 | #include "json.h" 6 | #include "networking/signalk_socket.h" 7 | #include "networking/signalk_subscription.h" 8 | #include "data_adapter.h" 9 | 10 | #include "dynamic_label.h" 11 | #include "dynamic_gauge.h" 12 | #include "dynamic_switch.h" 13 | #include "dynamic_button.h" 14 | 15 | const char *DGUI_TAG = "DGUI"; 16 | 17 | DynamicGui::DynamicGui() 18 | { 19 | factory = new ComponentFactory(); 20 | } 21 | 22 | void DynamicGui::initialize() 23 | { 24 | DynamicLabelBuilder::initialize(factory); 25 | DynamicGaugeBuilder::initialize(factory); 26 | DynamicSwitchBuilder::initialize(factory); 27 | DynamicButtonBuilder::initialize(factory); 28 | } 29 | 30 | bool DynamicGui::load_file(String path, lv_obj_t *parent, SignalKSocket *socket, int &count) 31 | { 32 | bool ret = false; 33 | tile_view_ = parent; 34 | SpiRamJsonDocument uiJson(20480); // allocate 20 kB in SPI RAM for JSON parsing 35 | DeserializationError result; 36 | 37 | if (SPIFFS.exists(path)) 38 | { 39 | auto file = SPIFFS.open(path); 40 | result = deserializeJson(uiJson, file); 41 | } 42 | else 43 | { 44 | ESP_LOGW(DGUI_TAG, "Dynamic GUI file %s definition not found, loading default views!", path.c_str()); 45 | result = deserializeJson(uiJson, JSON_default_view); 46 | } 47 | 48 | if (result == DeserializationError::Ok) 49 | { 50 | JsonArray views = uiJson["views"].as(); 51 | int x = 0; 52 | 53 | for (JsonObject viewJson : views) 54 | { 55 | x++; 56 | DynamicView *newView = new DynamicView(); 57 | newView->load(parent, viewJson, factory); 58 | this->views.push_back(newView); 59 | lv_obj_set_pos(newView->get_obj(), x * LV_HOR_RES, 0); 60 | lv_tileview_add_element(parent, newView->get_obj()); 61 | } 62 | 63 | count = x; 64 | ESP_LOGI(DGUI_TAG, "Loaded %d views.", count); 65 | 66 | for (auto adapter : DataAdapter::get_adapters()) 67 | { 68 | adapter->initialize(socket); 69 | } 70 | 71 | ret = true; 72 | } 73 | else 74 | { 75 | ESP_LOGW(DGUI_TAG, "Failed to load dynamic GUI file %s, error = %s!", path.c_str(), result.c_str()); 76 | } 77 | 78 | return ret; 79 | } 80 | 81 | void DynamicGui::handle_signalk_update(const String &path, const JsonVariant &value) 82 | { 83 | for (auto adapter : DataAdapter::get_adapters()) 84 | { 85 | if (adapter->get_path() == path) 86 | { 87 | adapter->on_updated(value); 88 | } 89 | } 90 | } 91 | 92 | void DynamicGui::update_online(bool online) 93 | { 94 | if (online_ != online) 95 | { 96 | online_ = online; 97 | } 98 | 99 | if (!online) 100 | { 101 | for (auto adapter : DataAdapter::get_adapters()) 102 | { 103 | adapter->on_offline(); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/ui/dynamic_switch.cpp: -------------------------------------------------------------------------------- 1 | #include "dynamic_switch.h" 2 | #include "localization.h" 3 | #include "data_adapter.h" 4 | #include "gui.h" 5 | #include "system/events.h" 6 | #include "localization.h" 7 | static void dynamic_switch_callback(lv_obj_t *obj, lv_event_t event) 8 | { 9 | if (event == LV_EVENT_VALUE_CHANGED) 10 | { 11 | DynamicSwitch *swtch = (DynamicSwitch *)obj->user_data; 12 | 13 | if (!swtch->IsChangeHandlerLocked()) 14 | { 15 | auto value = lv_switch_get_state(obj); 16 | 17 | if(!swtch->send_put_request(value)) 18 | { 19 | post_gui_warning(LOC_SK_PUT_SEND_FAIL); 20 | } 21 | } 22 | } 23 | } 24 | 25 | void DynamicSwitchBuilder::initialize(ComponentFactory *factory) 26 | { 27 | factory->register_constructor("switch", [factory](JsonObject &json, lv_obj_t *parent) -> Component * 28 | { 29 | auto swtch = new DynamicSwitch(parent); 30 | swtch->load(json); 31 | return swtch; 32 | }); 33 | } 34 | 35 | void DynamicSwitch::load(const JsonObject &json) 36 | { 37 | lv_obj_t *ui_switch = lv_switch_create(parent_, NULL); 38 | 39 | this->obj_ = ui_switch; 40 | 41 | lv_obj_set_user_data(ui_switch, this); 42 | lv_obj_set_event_cb(ui_switch, dynamic_switch_callback); 43 | 44 | if (json.containsKey("binding")) 45 | { 46 | JsonObject binding = json["binding"].as(); 47 | int period = 1000; 48 | 49 | if (binding.containsKey("period")) 50 | { 51 | period = binding["period"].as(); 52 | } 53 | 54 | path_ = binding["path"].as(); 55 | 56 | //register dataadapter that will connect SK receiver and this switch 57 | adapter_ = new DataAdapter(path_, period, this); 58 | } 59 | 60 | DynamicHelpers::set_location(ui_switch, json); 61 | DynamicHelpers::set_size(ui_switch, json); 62 | DynamicHelpers::set_layout(ui_switch, parent_, json); 63 | } 64 | 65 | void DynamicSwitch::update(const JsonVariant &json) 66 | { 67 | auto value = false; 68 | 69 | if (json.is()) 70 | { 71 | change_handler_locked_ = true; 72 | auto value = json.as(); 73 | if (value) 74 | { 75 | lv_switch_on(obj_, LV_ANIM_ON); 76 | } 77 | else 78 | { 79 | lv_switch_off(obj_, LV_ANIM_ON); 80 | } 81 | change_handler_locked_ = false; 82 | } 83 | else 84 | { 85 | log_i("Switch: value %s is invalid, switch accepts only bool values!", json.to()); 86 | } 87 | } 88 | 89 | bool DynamicSwitch::send_put_request(bool value) 90 | { 91 | if(adapter_ != NULL) 92 | { 93 | return adapter_->put_request(value); 94 | } 95 | else 96 | { 97 | return false; 98 | } 99 | } 100 | 101 | void DynamicSwitch::on_offline() 102 | { 103 | lv_switch_off(obj_, LV_ANIM_OFF); 104 | } 105 | 106 | void DynamicSwitch::destroy() 107 | { 108 | if (obj_ != NULL) 109 | { 110 | lv_obj_del(obj_); 111 | obj_ = NULL; 112 | } 113 | } -------------------------------------------------------------------------------- /src/imgs/sk_image_low.h: -------------------------------------------------------------------------------- 1 | #include // PROGMEM support header 2 | 3 | #define skIcon_width 64 4 | #define skIcon_height 64 5 | PROGMEM const unsigned char skIcon_bits[] = { 6 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 7 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 8 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 9 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 10 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 11 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 12 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13 | 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 14 | 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 15 | 0xFE, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 16 | 0x00, 0x00, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17 | 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0x00, 0x00, 0x00, 18 | 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 19 | 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 20 | 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 21 | 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xF2, 0xFF, 0x03, 0x00, 0x00, 22 | 0x00, 0x00, 0xFC, 0xA8, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC1, 23 | 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x7E, 0xEA, 0xFF, 0x1F, 0x00, 0x00, 24 | 0x00, 0x00, 0x7E, 0xC0, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x7E, 0xE2, 25 | 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xE4, 0xFF, 0x3F, 0x00, 0x00, 26 | 0x00, 0x00, 0x1F, 0xF0, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x8F, 0xF5, 27 | 0xFF, 0x1F, 0x00, 0x7F, 0x00, 0x80, 0xAF, 0xF0, 0xFF, 0x1F, 0xE0, 0x3F, 28 | 0x00, 0xC0, 0x0F, 0xFB, 0xFF, 0x0F, 0xF8, 0x0F, 0x00, 0xC0, 0x27, 0xF8, 29 | 0xFF, 0x0F, 0xFC, 0x07, 0x00, 0xC0, 0x47, 0xFC, 0xFF, 0x87, 0xFF, 0x01, 30 | 0x00, 0xE0, 0x07, 0xFD, 0xFF, 0xC3, 0x7F, 0x00, 0x00, 0xE0, 0x53, 0xFE, 31 | 0xFF, 0xE1, 0x3F, 0x00, 0x0C, 0xF0, 0x89, 0xFE, 0x7F, 0xF8, 0x0F, 0x00, 32 | 0x3C, 0xF0, 0x13, 0xFE, 0x3F, 0xFE, 0x07, 0x00, 0x78, 0xF0, 0x05, 0xFF, 33 | 0x0F, 0xFF, 0x01, 0x00, 0xF8, 0xF8, 0xA1, 0xFF, 0xC3, 0xFF, 0x00, 0x00, 34 | 0xF0, 0xE1, 0x80, 0xFF, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x87, 0x9A, 0x1F, 35 | 0xFC, 0x1F, 0x00, 0x00, 0xE0, 0x0F, 0x80, 0x01, 0xFF, 0x07, 0x00, 0x00, 36 | 0xC0, 0x7F, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x07, 0xFD, 37 | 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 38 | 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 39 | 0x01, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 40 | 0x00, 0x00, 0xF8, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 41 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 42 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 44 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 45 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 46 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 47 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; -------------------------------------------------------------------------------- /src/ui/dynamic_view.h: -------------------------------------------------------------------------------- 1 | #include "view.h" 2 | #include "ArduinoJson.h" 3 | #include "component_factory.h" 4 | #include "vector" 5 | #include "dynamic_helpers.h" 6 | #include "component.h" 7 | 8 | enum ViewType_t 9 | { 10 | WatchFace, 11 | NormalView 12 | }; 13 | 14 | class DynamicView 15 | { 16 | public: 17 | lv_obj_t *get_obj() { return container; } 18 | void load(lv_obj_t *parent, JsonObject viewObject, ComponentFactory *factory) 19 | { 20 | //! main 21 | static lv_style_t mainStyle; 22 | lv_style_init(&mainStyle); 23 | lv_style_set_radius(&mainStyle, LV_OBJ_PART_MAIN, 0); 24 | lv_style_set_bg_color(&mainStyle, LV_OBJ_PART_MAIN, LV_COLOR_GRAY); 25 | lv_style_set_bg_opa(&mainStyle, LV_OBJ_PART_MAIN, LV_OPA_0); 26 | lv_style_set_border_width(&mainStyle, LV_OBJ_PART_MAIN, 0); 27 | lv_style_set_text_color(&mainStyle, LV_OBJ_PART_MAIN, LV_COLOR_WHITE); 28 | lv_style_set_image_recolor(&mainStyle, LV_OBJ_PART_MAIN, LV_COLOR_WHITE); 29 | 30 | container = lv_cont_create(parent, NULL); 31 | lv_obj_add_style(container, LV_CONT_PART_MAIN, &mainStyle); 32 | lv_obj_set_size(container, LV_HOR_RES, LV_VER_RES - 30); 33 | 34 | name_ = viewObject["name"].as(); 35 | String viewType = viewObject["type"].as(); 36 | if (viewType == "watchface") 37 | { 38 | type = ViewType_t::WatchFace; 39 | } 40 | else if (viewType == "normal") 41 | { 42 | type = ViewType_t::NormalView; 43 | } 44 | else 45 | { 46 | ESP_LOGW("UI", "Invalid view type of %s", viewType.c_str()); 47 | type = ViewType_t::NormalView; 48 | } 49 | 50 | if(viewObject.containsKey("background")) 51 | { 52 | lv_color_t background = DynamicHelpers::get_color(viewObject["background"]); 53 | lv_obj_set_style_local_bg_color(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, background); 54 | lv_obj_set_style_local_bg_opa(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_100); 55 | } 56 | 57 | JsonArray components = viewObject["components"].as(); 58 | 59 | for (JsonObject component : components) 60 | { 61 | auto obj = factory->create_component(component, container); 62 | if (obj != NULL) 63 | { 64 | created_components.push_back(obj); 65 | } 66 | else 67 | { 68 | String unknownType = component["type"].as(); 69 | ESP_LOGW("DYNAMIC_VIEW", "View %s component type %s isn't supported!", name_.c_str(), unknownType.c_str()); 70 | } 71 | } 72 | 73 | if (viewObject.containsKey("layout")) 74 | { 75 | String layout = viewObject["layout"].as(); 76 | DynamicHelpers::set_container_layout(container, layout); 77 | lv_obj_set_style_local_pad_all(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 4); 78 | } 79 | else 80 | { 81 | lv_cont_set_layout(container, LV_LAYOUT_OFF); 82 | } 83 | } 84 | 85 | void on_visible() 86 | { 87 | for (auto view : created_components) 88 | { 89 | lv_obj_realign(view->get_obj()); 90 | } 91 | } 92 | 93 | private: 94 | ViewType_t type; 95 | lv_obj_t *container; 96 | std::vector created_components; 97 | String name_; 98 | }; -------------------------------------------------------------------------------- /src/ui/dynamic_button.cpp: -------------------------------------------------------------------------------- 1 | #include "dynamic_button.h" 2 | #include "localization.h" 3 | #include "data_adapter.h" 4 | #include "gui.h" 5 | #include "system/events.h" 6 | #include "localization.h" 7 | #include "ui_functions.h" 8 | 9 | static void dynamic_button_callback(lv_obj_t *obj, lv_event_t event) 10 | { 11 | if (event == LV_EVENT_CLICKED) 12 | { 13 | DynamicButton *button = (DynamicButton *)obj->user_data; 14 | button->on_clicked(); 15 | } 16 | } 17 | 18 | void DynamicButtonBuilder::initialize(ComponentFactory *factory) 19 | { 20 | factory->register_constructor("button", [factory](JsonObject &json, lv_obj_t *parent) -> Component * 21 | { 22 | auto btn = new DynamicButton(parent); 23 | btn->load(json); 24 | return btn; }); 25 | } 26 | 27 | void DynamicButton::load(const JsonObject &json) 28 | { 29 | lv_obj_t *btn = lv_btn_create(parent_, NULL); 30 | 31 | this->obj_ = btn; 32 | 33 | lv_obj_set_user_data(btn, this); 34 | lv_obj_set_event_cb(btn, dynamic_button_callback); 35 | 36 | this->label_ = lv_label_create(obj_, NULL); 37 | 38 | if (json.containsKey("put")) 39 | { 40 | auto push = json["put"].as(); 41 | serializeJson(push, sk_put_json_); 42 | ESP_LOGI("BTN", "SK Put %s", sk_put_json_.c_str()); 43 | button_action_ = ButtonAction::SKPut; 44 | adapter_ = new DataAdapter(this); 45 | } 46 | else if (json.containsKey("action")) 47 | { 48 | String action = json["action"].as(); 49 | 50 | if (action == "Settings") 51 | { 52 | button_action_ = ButtonAction::Settings; 53 | } 54 | else if (action == "ToggleWifi") 55 | { 56 | button_action_ = ButtonAction::ToggleWifi; 57 | } 58 | else if (action == "HomeTile") 59 | { 60 | button_action_ = ButtonAction::HomeTile; 61 | } 62 | else 63 | { 64 | log_w("Button action %s is invalid", action.c_str()); 65 | } 66 | } 67 | 68 | if (json.containsKey("text")) 69 | { 70 | lv_label_set_text(label_, json["text"].as()); 71 | } 72 | 73 | if (json.containsKey("font")) 74 | { 75 | auto fontName = json["font"].as(); 76 | DynamicHelpers::set_font(label_, fontName); 77 | } 78 | 79 | DynamicHelpers::set_location(btn, json); 80 | DynamicHelpers::set_size(btn, json); 81 | DynamicHelpers::set_layout(btn, parent_, json); 82 | } 83 | 84 | void DynamicButton::on_clicked() 85 | { 86 | switch (button_action_) 87 | { 88 | case ButtonAction::HomeTile: 89 | twatchsk::UI_Functions->show_home(); 90 | break; 91 | case ButtonAction::Settings: 92 | twatchsk::UI_Functions->show_settings(); 93 | break; 94 | case ButtonAction::ToggleWifi: 95 | twatchsk::UI_Functions->toggle_wifi(); 96 | break; 97 | case ButtonAction::SKPut: 98 | send_put_request(); 99 | break; 100 | } 101 | } 102 | 103 | bool DynamicButton::send_put_request() 104 | { 105 | return adapter_->put_request(sk_put_json_); 106 | } 107 | 108 | void DynamicButton::destroy() 109 | { 110 | if (obj_ != NULL) 111 | { 112 | lv_obj_del(obj_); 113 | obj_ = NULL; 114 | } 115 | } 116 | 117 | void DynamicButton::update(const JsonVariant &update) 118 | { 119 | 120 | } -------------------------------------------------------------------------------- /src/ui/navigationview.h: -------------------------------------------------------------------------------- 1 | #include "settings_view.h" 2 | #include 3 | #include 4 | 5 | LV_FONT_DECLARE(lv_font_montserrat_28); 6 | 7 | struct LinkData_t 8 | { 9 | const lv_img_dsc_t *img; 10 | bool color_img; 11 | char *title; 12 | std::function callback; 13 | }; 14 | 15 | class NavigationView : public SettingsView 16 | { 17 | public: 18 | NavigationView(char *title, std::function on_close) : SettingsView(title) 19 | { 20 | this->on_close(on_close); 21 | } 22 | 23 | void add_tile(char *title, const lv_img_dsc_t *img, bool img_is_color, std::function callback) 24 | { 25 | LinkData_t newTile; 26 | newTile.img = img; 27 | newTile.color_img = img_is_color; 28 | newTile.callback = callback; 29 | newTile.title = title; 30 | tiles.push_back(newTile); 31 | ESP_LOGI("SETTINGS", "Added tiles %d", tiles.size()); 32 | } 33 | 34 | void show_internal(lv_obj_t *parent) override 35 | { 36 | lv_cont_set_layout(parent, LV_LAYOUT_OFF); 37 | list = lv_list_create(parent, NULL); 38 | lv_obj_set_size(list, LV_HOR_RES, lv_obj_get_height(parent)); 39 | static lv_style_t tileStyle; 40 | lv_style_init(&tileStyle); 41 | lv_style_set_border_width(&tileStyle, LV_OBJ_PART_MAIN, 0); 42 | lv_style_set_radius(&tileStyle, LV_OBJ_PART_MAIN, 0); 43 | lv_style_set_text_font(&tileStyle, LV_STATE_DEFAULT, &lv_font_montserrat_28); 44 | lv_obj_add_style(list, LV_OBJ_PART_MAIN, &tileStyle); 45 | lv_page_set_edge_flash(list, true); 46 | for (auto it = tiles.begin(); it != tiles.end(); it++) 47 | { 48 | auto btn = lv_list_add_btn(list, it.base()->img, it.base()->title); 49 | auto img = lv_list_get_btn_img(btn); 50 | if (it.base()->color_img == false) // if the tile's icon is a monochrome image 51 | { 52 | twatchsk::update_imgbtn_color(img); // make it the correct color depending on LIGHT/DARK setting 53 | } 54 | btn->user_data = it.base(); 55 | lv_obj_set_event_cb(btn, NavigationView::tile_event); 56 | } 57 | 58 | ESP_LOGI("SETTINGS", "Tiles loaded!"); 59 | } 60 | void theme_changed() 61 | { 62 | twatchsk::update_imgbtn_color(back); // make it the correct color depending on LIGHT/DARK setting 63 | auto count = lv_list_get_size(list); 64 | lv_obj_t *btn = NULL; 65 | for (int i = 0; i < count; i++) 66 | { 67 | auto tile = tiles.at(i); 68 | btn = lv_list_get_next_btn(list, btn); 69 | if (!tile.color_img) 70 | { 71 | if (btn != NULL) 72 | { 73 | auto img = lv_list_get_btn_img(btn); 74 | twatchsk::update_imgbtn_color(img); 75 | } 76 | } 77 | } 78 | } 79 | 80 | bool hide_internal() override 81 | { 82 | return true; 83 | } 84 | 85 | private: 86 | lv_obj_t *list; 87 | lv_style_t buttonStyle; 88 | lv_style_t pageStyle; 89 | std::vector tiles; 90 | const int tile_width = 48; 91 | const int tile_height = 48; 92 | const int tile_spacing = 6; 93 | 94 | static void tile_event(lv_obj_t *btn, lv_event_t event) 95 | { 96 | if (event == LV_EVENT_CLICKED) 97 | { 98 | auto tile = (LinkData_t *)btn->user_data; 99 | ESP_LOGI("GUI", "Tile %s touched!", tile->title); 100 | tile->callback(); 101 | } 102 | } 103 | }; -------------------------------------------------------------------------------- /src/networking/signalk_socket.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ArduinoJson.h" 3 | #include "esp_websocket_client.h" 4 | #include "vector" 5 | #include "functional" 6 | #include "map" 7 | #include "system/configurable.h" 8 | #include "system/systemobject.h" 9 | #include "system/observable.h" 10 | #include "hardware/Wifi.h" 11 | #include "networking/signalk_subscription.h" 12 | #include "hardware/hardware.h" 13 | 14 | enum WebsocketState_t 15 | { 16 | WS_Offline = 0, 17 | WS_Connecting = 1, 18 | WS_Connected = 2 19 | }; 20 | 21 | class SignalKSocket : public Configurable, public SystemObject, public Observable, public Observer 22 | { 23 | public: 24 | SignalKSocket(WifiManager *wifi); 25 | bool connect(); 26 | bool disconnect(); 27 | bool reconnect(); 28 | void clear_token(); 29 | void parse_data(int length, const char *data); 30 | esp_websocket_client_handle_t get_ws() { return websocket; } 31 | void update_status(WebsocketState_t status) 32 | { 33 | Observable::emit(status); 34 | } 35 | WebsocketState_t get_state() { return value; } 36 | void notify_change(const WifiState_t &wifi) override; 37 | String get_server_address() { return server; } 38 | int get_server_port() { return port; } 39 | bool get_token_request_pending() { return token_request_pending; } 40 | bool get_sync_time_with_server() { return sync_time_with_server; } 41 | void set_sync_time_with_server(bool enabled) { sync_time_with_server = enabled; } 42 | uint get_handled_delta_count() { return delta_counter; } 43 | SignalKSubscription *add_subscription(String path, uint period, bool is_low_power); 44 | /** 45 | * Updates subscriptions depending on power mode to reduce power drain in low power mode. 46 | * In low power mode only active subscription is notifications.* 47 | * */ 48 | void update_subscriptions(bool force = false); 49 | ///This is intended to be wired with Hardware class power events 50 | void handle_power_event(PowerCode_t code, uint32_t arg); 51 | ///Updates server configuration (address and port) 52 | void set_server(String server_address, int port) 53 | { 54 | server = server_address; 55 | this->port = port; 56 | } 57 | 58 | void set_device_name(const char *device_name_ptr) 59 | { 60 | device_name_ = device_name_ptr; 61 | } 62 | 63 | String get_token() 64 | { 65 | return token; 66 | } 67 | 68 | bool send_put_request(JsonObject& request); 69 | private: 70 | const int reconnect_count_ = 3; 71 | static void ws_event_handler(void *arg, esp_event_base_t event_base, 72 | int32_t event_id, void *event_data); 73 | const char *device_name_ = NULL; 74 | String server = ""; 75 | int port = 0; 76 | uint delta_counter = 0; 77 | bool sync_time_with_server = false; 78 | String token = ""; 79 | String clientId = ""; 80 | bool token_request_pending = false; 81 | String pending_token_request_id = ""; 82 | esp_websocket_client_handle_t websocket; 83 | int reconnect_counter_ = reconnect_count_; 84 | bool websocket_initialized = false; 85 | bool low_power_subscriptions_ = false; 86 | std::map subscriptions; 87 | std::vector activeNotifications; 88 | WifiManager *wifi; 89 | void load_config_from_file(const JsonObject &json) override; 90 | void save_config_to_file(JsonObject &json) override; 91 | void send_token_permission(); 92 | void send_json(const JsonObject &json); 93 | bool is_notification_active(String path); 94 | void remove_active_notification(String path); 95 | void send_status_message(); 96 | }; -------------------------------------------------------------------------------- /src/sounds/sound_player.cpp: -------------------------------------------------------------------------------- 1 | #include "sound_player.h" 2 | #include "../config.h" 3 | #include "FreeRTOS.h" 4 | #include "driver/i2s.h" 5 | 6 | const char *PLAYER_TAG = "I2S"; 7 | 8 | struct SoundTask_t 9 | { 10 | unsigned char *sound; 11 | int len; 12 | char name[32]; 13 | int repeat; 14 | }; 15 | 16 | #ifdef LILYGO_WATCH_2020_V1 || LILYGO_WATCH_2020_V3 17 | 18 | SoundPlayer::SoundPlayer() 19 | { 20 | player_queue_handle_ = xQueueCreate(32, sizeof(SoundTask_t)); 21 | xTaskCreate(player_task_func, "sound_player", CONFIG_MAIN_TASK_STACK_SIZE, player_queue_handle_, 5, &player_task_); 22 | } 23 | 24 | void init_i2s() 25 | { 26 | auto ttgo = TTGOClass::getWatch(); 27 | ttgo->power->setLDO3Mode(AXP202_LDO3_MODE_DCIN); 28 | ttgo->power->setLDO3Voltage(3000); 29 | ttgo->power->setPowerOutPut(AXP202_LDO3, AXP202_ON); 30 | 31 | i2s_config_t i2s_config = { 32 | .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), 33 | .sample_rate = 44100, 34 | .bits_per_sample = (i2s_bits_per_sample_t)16, 35 | .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, 36 | .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), 37 | .dma_buf_count = 32, 38 | .dma_buf_len = 64, 39 | .use_apll = 0}; 40 | 41 | i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; 42 | 43 | i2s_pin_config_t pin_config = 44 | { 45 | .bck_io_num = TWATCH_DAC_IIS_BCK, 46 | .ws_io_num = TWATCH_DAC_IIS_WS, 47 | .data_out_num = TWATCH_DAC_IIS_DOUT, 48 | .data_in_num = I2S_PIN_NO_CHANGE}; 49 | 50 | if(i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) == ESP_OK) 51 | { 52 | if(i2s_set_pin(I2S_NUM_0, &pin_config) == ESP_OK) 53 | { 54 | ESP_LOGI(PLAYER_TAG, "I2S initialized OK!"); 55 | } 56 | } 57 | } 58 | 59 | void deinit_i2s() 60 | { 61 | auto ttgo = TTGOClass::getWatch(); 62 | ttgo->power->setLDO3Mode(AXP202_LDO3_MODE_DCIN); 63 | ttgo->power->setPowerOutPut(AXP202_LDO3, AXP202_OFF); 64 | i2s_stop(I2S_NUM_0); 65 | i2s_driver_uninstall(I2S_NUM_0); 66 | } 67 | 68 | void SoundPlayer::play_raw_from_const(const char *name, const unsigned char *raw, int size, int repeat) 69 | { 70 | SoundTask_t task; 71 | strcpy(task.name, name); 72 | task.sound = (unsigned char *)raw; 73 | task.len = size; 74 | task.repeat = repeat; 75 | 76 | xQueueSend(this->player_queue_handle_, &task, portMAX_DELAY); 77 | } 78 | 79 | void SoundPlayer::player_task_func(void *pvParameter) 80 | { 81 | SoundTask_t currentTask; 82 | QueueHandle_t queue = (QueueHandle_t)pvParameter; 83 | 84 | ESP_LOGI(PLAYER_TAG, "Async task dispatcher started!"); 85 | 86 | while (true) 87 | { 88 | if (xQueueReceive(queue, ¤tTask, portMAX_DELAY)) 89 | { 90 | ESP_LOGI(PLAYER_TAG, "Playing sound %s repeat=%d", currentTask.name, currentTask.repeat); 91 | init_i2s(); 92 | ssize_t nread; 93 | size_t nwritten; 94 | int res; 95 | i2s_zero_dma_buffer(I2S_NUM_0); 96 | i2s_start(I2S_NUM_0); 97 | 98 | for (int r = 0; r < currentTask.repeat; r++) 99 | { 100 | int bytes = currentTask.len; 101 | unsigned char *ptr = currentTask.sound; 102 | 103 | while (bytes > 0) 104 | { 105 | int batchSize = (bytes - 128) ? 128 : bytes; 106 | auto ret = i2s_write(I2S_NUM_0, ptr, batchSize, &nwritten, portMAX_DELAY); 107 | bytes -= nwritten; 108 | ptr += nwritten; 109 | 110 | if (ret != ESP_OK) 111 | { 112 | ESP_LOGI(PLAYER_TAG, "Unable to send I2S data, failed with result=%d", ret); 113 | } 114 | } 115 | } 116 | deinit_i2s(); 117 | ESP_LOGI(PLAYER_TAG, "Sound playing %s finished", currentTask.name); 118 | } 119 | } 120 | } 121 | #else 122 | 123 | SoundPlayer::SoundPlayer() 124 | { 125 | 126 | } 127 | 128 | void SoundPlayer::play_raw_from_const(const char *name, const unsigned char *raw, int size, int repeat) 129 | { 130 | log_w("TWatch 2020 V2 doesn't support playing sounds."); 131 | } 132 | 133 | #endif 134 | 135 | SoundPlayer::~SoundPlayer() 136 | { 137 | } -------------------------------------------------------------------------------- /src/networking/http_request.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.h" 3 | #include "esp_http_client.h" 4 | #include 5 | #include "ArduinoJson.h" 6 | #include "SPIFFS.h" 7 | 8 | class JsonHttpRequest 9 | { 10 | public: 11 | JsonHttpRequest(const char *requestUrl, const char *token) 12 | { 13 | requestUrl_ = requestUrl; 14 | token_ = token; 15 | } 16 | 17 | bool downloadFile(const char *path) 18 | { 19 | auto ret = false; 20 | auto file = SPIFFS.open(path, "w"); 21 | if (file) 22 | { 23 | ESP_LOGI("HTTP", "HTTP GET intializing client..."); 24 | esp_http_client_config_t config = { 25 | .url = requestUrl_, 26 | .event_handler = _http_event_handle}; 27 | 28 | esp_http_client_handle_t client = esp_http_client_init(&config); 29 | //set method 30 | esp_http_client_set_method(client, esp_http_client_method_t::HTTP_METHOD_GET); 31 | //set auth header 32 | char buffer[512]; 33 | sprintf(buffer, "Bearer %s", token_); 34 | esp_http_client_set_header(client, "Authorization", buffer); 35 | ESP_LOGI("HTTP", "HTTP GET opening connection to %s...", requestUrl_); 36 | esp_err_t err = esp_http_client_open(client, 0); 37 | ESP_LOGI("HTTP", "HTTP GET open connection result=%d.", err); 38 | if (err == ESP_OK) 39 | { 40 | auto len = esp_http_client_fetch_headers(client); 41 | auto status = esp_http_client_get_status_code(client); 42 | ESP_LOGI("HTTP", "HTTP GET %s got status %d with len %d", requestUrl_, status, len); 43 | 44 | if (status == 200) 45 | { 46 | int total = 0; 47 | int readLen = -1; 48 | while (readLen != 0) 49 | { 50 | readLen = esp_http_client_read(client, buffer, sizeof(buffer)); 51 | ESP_LOGI("HTTP", "HTTP GET read=%d", readLen); 52 | total += readLen; 53 | 54 | if (readLen > 0) 55 | { 56 | file.write((const uint8_t *)buffer, (size_t)readLen); 57 | } 58 | } 59 | if(total == len) 60 | { 61 | ret = true; 62 | } 63 | 64 | ESP_LOGI("HTTP", "HTTP GET DONE!"); 65 | } 66 | 67 | esp_http_client_cleanup(client); 68 | 69 | ESP_LOGI("HTTP", "Client cleanup."); 70 | } 71 | 72 | file.close(); 73 | ESP_LOGI("HTTP", "File closed!"); 74 | } 75 | else 76 | { 77 | ESP_LOGI("HTTP", "Unable to create file!"); 78 | } 79 | 80 | return ret; 81 | } 82 | 83 | ~JsonHttpRequest() 84 | { 85 | } 86 | 87 | private: 88 | const char *requestUrl_; 89 | const char *token_; 90 | 91 | static esp_err_t _http_event_handle(esp_http_client_event_t *evt) 92 | { 93 | switch (evt->event_id) 94 | { 95 | case HTTP_EVENT_ERROR: 96 | ESP_LOGI("HTTP", "HTTP_EVENT_ERROR"); 97 | break; 98 | case HTTP_EVENT_ON_CONNECTED: 99 | ESP_LOGI("HTTP", "HTTP_EVENT_ON_CONNECTED"); 100 | break; 101 | case HTTP_EVENT_HEADER_SENT: 102 | ESP_LOGI("HTTP", "HTTP_EVENT_HEADER_SENT"); 103 | break; 104 | case HTTP_EVENT_ON_HEADER: 105 | ESP_LOGI("HTTP", "HTTP_EVENT_ON_HEADER"); 106 | printf("%.*s", evt->data_len, (char *)evt->data); 107 | break; 108 | case HTTP_EVENT_ON_DATA: 109 | ESP_LOGI("HTTP", "HTTP_EVENT_ON_DATA, len=%d", evt->data_len); 110 | /*if (!esp_http_client_is_chunked_response(evt->client)) 111 | { 112 | printf("%.*s", evt->data_len, (char *)evt->data); 113 | }*/ 114 | 115 | break; 116 | case HTTP_EVENT_ON_FINISH: 117 | ESP_LOGI("HTTP", "HTTP_EVENT_ON_FINISH"); 118 | break; 119 | case HTTP_EVENT_DISCONNECTED: 120 | ESP_LOGI("HTTP", "HTTP_EVENT_DISCONNECTED"); 121 | break; 122 | } 123 | return ESP_OK; 124 | } 125 | }; -------------------------------------------------------------------------------- /src/ui/dynamic_label.cpp: -------------------------------------------------------------------------------- 1 | #include "dynamic_label.h" 2 | #include "localization.h" 3 | #include "data_adapter.h" 4 | 5 | void DynamicLabelBuilder::initialize(ComponentFactory *factory) 6 | { 7 | factory->register_constructor("label", [factory](JsonObject &json, lv_obj_t *parent) -> Component * 8 | { 9 | auto label = new DynamicLabel(parent); 10 | label->load(json); 11 | return label; }); 12 | } 13 | 14 | void DynamicLabel::load(const JsonObject &json) 15 | { 16 | lv_obj_t *label = lv_label_create(parent_, NULL); 17 | 18 | this->obj_ = label; 19 | 20 | if (json.containsKey("color")) 21 | { 22 | auto color = DynamicHelpers::get_color(json["color"]); 23 | lv_obj_set_style_local_text_color(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color); 24 | } 25 | 26 | auto textSet = false; 27 | 28 | if (json.containsKey("text")) 29 | { 30 | textSet = true; 31 | lv_label_set_text(label, json["text"].as().c_str()); 32 | } 33 | 34 | if (json.containsKey("binding")) 35 | { 36 | has_binding_ = true; 37 | JsonObject binding = json["binding"].as(); 38 | int period = 1000; 39 | formating.multiply = 1.0f; 40 | formating.offset = 0.0f; 41 | formating.decimal_places = 1; 42 | 43 | if (binding.containsKey("period")) 44 | { 45 | period = binding["period"].as(); 46 | } 47 | if (binding.containsKey("multiply")) 48 | { 49 | formating.multiply = binding["multiply"].as(); 50 | } 51 | if (binding.containsKey("offset")) 52 | { 53 | formating.offset = binding["offset"].as(); 54 | } 55 | 56 | if (binding.containsKey("decimals")) 57 | { 58 | formating.decimal_places = binding["decimals"].as(); 59 | } 60 | 61 | if (binding.containsKey("format")) 62 | { 63 | auto jsonFormating = binding["format"].as(); 64 | // allocate memory for format string + 1 for \0 char at the end 65 | formating.string_format = (char *)malloc(strlen(jsonFormating) + 1); 66 | strcpy(formating.string_format, jsonFormating); 67 | } 68 | // register dataadapter that will connect SK receiver and this label 69 | new DataAdapter(binding["path"].as(), period, this); 70 | 71 | if (!textSet) 72 | { 73 | lv_label_set_text(label, "--"); 74 | } 75 | } 76 | 77 | if (json.containsKey("font")) 78 | { 79 | auto styleName = json["font"].as(); 80 | DynamicHelpers::set_font(label, styleName); 81 | } 82 | 83 | DynamicHelpers::set_location(label, json); 84 | DynamicHelpers::set_size(label, json); 85 | DynamicHelpers::set_layout(label, parent_, json); 86 | } 87 | 88 | void DynamicLabel::update(const JsonVariant &value) 89 | { 90 | String stringValue; 91 | 92 | if (value.is()) 93 | { 94 | stringValue = value.as(); 95 | } 96 | else if (value.is()) 97 | { 98 | stringValue = String(((float)value.as() * formating.multiply) + formating.offset, formating.decimal_places); 99 | } 100 | else if (value.is()) 101 | { 102 | stringValue = String((value.as() * formating.multiply) + formating.offset, formating.decimal_places); 103 | } 104 | else if (value.is()) 105 | { 106 | stringValue = String(value.as() ? LOC_TRUE : LOC_FALSE); 107 | } 108 | else 109 | { 110 | String json; 111 | serializeJson(value, json); 112 | stringValue = json; 113 | } 114 | 115 | if (formating.string_format != NULL) 116 | { 117 | String text = String(formating.string_format); 118 | text.replace("$$", stringValue.c_str()); 119 | lv_label_set_text(obj_, text.c_str()); 120 | } 121 | else 122 | { 123 | lv_label_set_text(obj_, stringValue.c_str()); 124 | } 125 | } 126 | 127 | void DynamicLabel::on_offline() 128 | { 129 | if (has_binding_) 130 | { 131 | String stringValue = "--"; 132 | 133 | if (formating.string_format != NULL) 134 | { 135 | String text = String(formating.string_format); 136 | text.replace("$$", stringValue.c_str()); 137 | lv_label_set_text(obj_, text.c_str()); 138 | } 139 | else 140 | { 141 | lv_label_set_text(obj_, stringValue.c_str()); 142 | } 143 | } 144 | } 145 | 146 | void DynamicLabel::destroy() 147 | { 148 | if (obj_ != NULL) 149 | { 150 | lv_obj_del(obj_); 151 | obj_ = NULL; 152 | } 153 | } -------------------------------------------------------------------------------- /src/ui/watch_info.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "gui.h" 3 | #include "esp_timer.h" 4 | #include "settings_view.h" 5 | #include "localization.h" 6 | #include "ui_ticker.h" 7 | 8 | /** 9 | * @brief Shows (and sets) watch name, version, author, and uptime, number of wake-ups. 10 | * Future: number of handled SK deltas, more? 11 | **/ 12 | 13 | class WatchInfo : public SettingsView 14 | { 15 | public: 16 | WatchInfo(Gui* gui) : SettingsView(LOC_WATCH_INFO) 17 | { 18 | gui_ = gui; 19 | } 20 | 21 | void set_watch_name(const char* new_name) 22 | { 23 | strcpy(watch_name_, new_name); 24 | } 25 | 26 | char* get_watch_name() 27 | { 28 | return watch_name_; 29 | } 30 | 31 | protected: 32 | virtual void show_internal(lv_obj_t *parent) override 33 | { 34 | lv_cont_set_layout(parent, LV_LAYOUT_OFF); 35 | 36 | static lv_style_t buttonStyle; 37 | lv_style_init(&buttonStyle); 38 | lv_style_set_radius(&buttonStyle, LV_STATE_DEFAULT, 10); 39 | 40 | watchNameLabel_ = lv_label_create(parent, NULL); 41 | lv_obj_set_pos(watchNameLabel_, 4, 15); 42 | lv_label_set_text(watchNameLabel_, LOC_WATCH_NAME); 43 | watchNameButton_ = lv_btn_create(parent, NULL); 44 | watchNameButton_->user_data = this; 45 | lv_obj_align(watchNameButton_, watchNameLabel_, LV_ALIGN_OUT_RIGHT_MID, 7, 2); 46 | lv_obj_add_style(watchNameButton_, LV_OBJ_PART_MAIN, &buttonStyle); 47 | watchName_ = lv_label_create(watchNameButton_, NULL); 48 | lv_label_set_text(watchName_, watch_name_); 49 | lv_obj_set_event_cb(watchNameButton_, watch_name_button_callback); 50 | lv_obj_set_width(watchNameButton_, 160); 51 | lv_obj_set_height(watchNameButton_, 35); 52 | 53 | author_ = lv_label_create(parent, NULL); 54 | lv_obj_align(author_, watchNameLabel_, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10); 55 | lv_label_set_text(author_, LOC_AUTHOR); 56 | 57 | version_ = lv_label_create(parent, NULL); 58 | lv_obj_align(version_, author_, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10); 59 | lv_label_set_text(version_, LOC_WATCH_VERSION); 60 | 61 | uptime_ = lv_label_create(parent, NULL); 62 | lv_obj_align(uptime_, version_, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10); 63 | update_uptime(); 64 | 65 | uptimeTicker_ = new UITicker(1000, [this]() { 66 | this->update_uptime(); 67 | }); 68 | 69 | wakeup_count_ = lv_label_create(parent, NULL); 70 | lv_obj_align(wakeup_count_, uptime_, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10); 71 | lv_label_set_text_fmt(wakeup_count_, LOC_WAKEUP_COUNT, gui_->get_wakeup_count()); 72 | } 73 | 74 | virtual bool hide_internal() override 75 | { 76 | delete uptimeTicker_; 77 | uptimeTicker_ = NULL; 78 | return true; 79 | } 80 | 81 | void update_uptime() 82 | { 83 | int32_t elapsed_seconds = esp_timer_get_time() / 1000000; 84 | int hours = elapsed_seconds/3600; 85 | elapsed_seconds = elapsed_seconds%3600; 86 | int minutes = elapsed_seconds/60; 87 | elapsed_seconds = elapsed_seconds%60; 88 | int seconds = elapsed_seconds; 89 | 90 | lv_label_set_text_fmt(uptime_, LOC_UPTIME, hours, minutes, seconds); 91 | } 92 | 93 | void update_watch_name(const char *new_name) // for when user changes watch name 94 | { 95 | strcpy(watch_name_, new_name); 96 | if (strcmp(watch_name_, "") == 0) // new name is an empty string 97 | { 98 | lv_label_set_text(watchName_, LOC_WATCH_NAME_EMPTY); 99 | } 100 | else 101 | { 102 | lv_label_set_text(watchName_, watch_name_); 103 | } 104 | } 105 | 106 | private: 107 | Gui* gui_; 108 | lv_obj_t* version_; 109 | lv_obj_t* author_; 110 | lv_obj_t* uptime_; 111 | UITicker* uptimeTicker_; 112 | lv_obj_t* wakeup_count_; 113 | lv_obj_t* watchNameLabel_; 114 | lv_obj_t* watchNameButton_; 115 | lv_obj_t* watchName_; 116 | char watch_name_[16] = ""; 117 | 118 | static void watch_name_button_callback(lv_obj_t *obj, lv_event_t event) 119 | { 120 | if (event == LV_EVENT_CLICKED) 121 | { 122 | auto* watch_info = (WatchInfo* )obj->user_data; 123 | int max_chars = 15; 124 | auto keyboard = new Keyboard(LOC_INPUT_WATCH_NAME, KeyboardType_t::Normal, max_chars); 125 | keyboard->on_close([keyboard, watch_info]() 126 | { 127 | if (keyboard->is_success()) 128 | { 129 | const char* text = keyboard->get_text(); 130 | ESP_LOGI(SETTINGS_TAG, "keyboard->get_text() returned %s", text); //Jan: this is always executed twice - why? 131 | watch_info->update_watch_name(text); 132 | } 133 | delete keyboard; 134 | }); 135 | keyboard->show(lv_scr_act()); 136 | } 137 | } 138 | }; -------------------------------------------------------------------------------- /src/ui/settings_view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "view.h" 3 | #include "functional" 4 | #define SETTINGS_TOP_BAR_HEIGHT 42 5 | LV_IMG_DECLARE(exit_32px); 6 | const char *SETTINGS_TAG = "SETTINGS"; 7 | 8 | class SettingsView : public View 9 | { 10 | public: 11 | SettingsView(char *title) 12 | { 13 | this->titleText = title; 14 | } 15 | 16 | void on_close(std::function closeCallback) 17 | { 18 | this->callback = closeCallback; 19 | } 20 | 21 | void show(lv_obj_t *parent) override 22 | { 23 | static lv_style_t plStyle; 24 | lv_style_init(&plStyle); 25 | lv_style_set_radius(&plStyle, LV_OBJ_PART_MAIN, 0); 26 | lv_style_set_border_width(&plStyle, LV_OBJ_PART_MAIN, 0); 27 | container = lv_cont_create(parent, NULL); 28 | lv_obj_set_size(container, LV_HOR_RES, LV_VER_RES - SETTINGS_TOP_BAR_HEIGHT); 29 | lv_obj_set_pos(container, 0, SETTINGS_TOP_BAR_HEIGHT); 30 | lv_obj_add_style(container, LV_OBJ_PART_MAIN, &plStyle); 31 | lv_cont_set_layout(container, LV_LAYOUT_ROW_TOP); 32 | 33 | //define style of top bar - do not change colors - these are theme defined! 34 | static lv_style_t barStyle; 35 | lv_style_copy(&barStyle, &plStyle); 36 | lv_style_set_border_width(&barStyle, LV_OBJ_PART_MAIN, 2); 37 | lv_style_set_border_side(&barStyle, LV_OBJ_PART_MAIN, LV_BORDER_SIDE_BOTTOM); 38 | 39 | topBar = lv_cont_create(parent, NULL); 40 | lv_obj_set_size(topBar, LV_HOR_RES, SETTINGS_TOP_BAR_HEIGHT); 41 | lv_obj_set_pos(topBar, 0, 0); 42 | lv_obj_add_style(topBar, LV_OBJ_PART_MAIN, &barStyle); //BS: barStyle is currently just a copy of plStyle 43 | 44 | back = lv_imgbtn_create(topBar, NULL); 45 | lv_imgbtn_set_src(back, LV_BTN_STATE_RELEASED, &exit_32px); 46 | lv_imgbtn_set_src(back, LV_BTN_STATE_PRESSED, &exit_32px); 47 | lv_imgbtn_set_src(back, LV_BTN_STATE_CHECKED_RELEASED, &exit_32px); 48 | lv_imgbtn_set_src(back, LV_BTN_STATE_CHECKED_PRESSED, &exit_32px); 49 | twatchsk::update_imgbtn_color(back); 50 | 51 | lv_obj_set_click(back, true); 52 | lv_obj_set_ext_click_area(back, 0, 20, 0, 20); 53 | lv_obj_align(back, topBar, LV_ALIGN_IN_LEFT_MID, 6, 0); 54 | back->user_data = this; 55 | lv_obj_set_event_cb(back, __backButtonCallBack); 56 | 57 | title = lv_label_create(topBar, NULL); 58 | lv_label_set_text(title, titleText); 59 | lv_obj_align(title, topBar, LV_ALIGN_IN_LEFT_MID, 50, 0); 60 | 61 | show_internal(container); 62 | } 63 | 64 | void hide() override 65 | { 66 | if (hide_internal()) 67 | { 68 | cleanup(); 69 | callback(); 70 | } 71 | } 72 | void cleanup() 73 | { 74 | if (container != NULL) 75 | { 76 | lv_obj_del(container); 77 | lv_obj_del(topBar); 78 | container = topBar = back = title = NULL; 79 | } 80 | } 81 | virtual ~SettingsView() 82 | { 83 | cleanup(); 84 | } 85 | 86 | virtual void theme_changed() // updates graphic elements for a theme change; override for every descendant of SettingsView with specific theme change needs 87 | { 88 | twatchsk::update_imgbtn_color(back); // make the "Back" button the correct color depending on the theme 89 | } 90 | 91 | protected: 92 | virtual void show_internal(lv_obj_t *parent){}; 93 | virtual bool hide_internal() { return true; } 94 | 95 | /** 96 | * shows message box with message and if close_delay != 0 it will auto dismiss the message box in miliseconds. 97 | */ 98 | void show_message(const char *message, int close_delay = 0) 99 | { 100 | static const char *btns[] = {LOC_MESSAGEBOX_OK, ""}; 101 | lv_obj_t *mbox = lv_msgbox_create(lv_scr_act(), NULL); 102 | lv_msgbox_set_text(mbox, message); 103 | lv_msgbox_add_btns(mbox, btns); 104 | if(close_delay > 0) 105 | { 106 | lv_msgbox_start_auto_close(mbox, close_delay); 107 | } 108 | lv_obj_set_width(mbox, 200); 109 | lv_obj_align(mbox, NULL, LV_ALIGN_CENTER, 0, 0); 110 | //trigger activity on main screen to avoid message is displayed and device goes into sleep 111 | lv_disp_trig_activity(NULL); 112 | } 113 | lv_obj_t *container; 114 | lv_obj_t *topBar; 115 | lv_obj_t *back; 116 | const int spacing = 6; //defines default spacing between elements in layout, to make all spacing same on every page 117 | private: 118 | lv_obj_t *title; 119 | char *titleText; 120 | std::function callback; 121 | 122 | static void __backButtonCallBack(lv_obj_t *obj, lv_event_t event) 123 | { 124 | if (event == LV_EVENT_CLICKED) 125 | { 126 | auto view = (SettingsView *)obj->user_data; 127 | ESP_LOGI("SETTINGS", "Closing view %s", view->titleText); 128 | view->hide(); 129 | } 130 | } 131 | }; -------------------------------------------------------------------------------- /src/ui/wakeup_settings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "gui.h" 3 | #include "settings_view.h" 4 | #include "localization.h" 5 | #include "hardware/hardware.h" 6 | 7 | /** 8 | * @brief Allows user to configure wake up sources (double tap, watch tilt). 9 | * Future: Signal K notifications? 10 | **/ 11 | 12 | class WakeupSettings : public SettingsView 13 | { 14 | public: 15 | WakeupSettings(Gui *gui, Hardware *hardware) : SettingsView(LOC_WAKEUP_SETTINGS) 16 | { 17 | gui_ = gui; 18 | hardware_ = hardware; 19 | } 20 | 21 | protected: 22 | virtual void show_internal(lv_obj_t *parent) override 23 | { 24 | const lv_coord_t padding = 10; 25 | lv_cont_set_layout(parent, LV_LAYOUT_OFF); 26 | 27 | title_ = lv_label_create(parent, NULL); 28 | lv_obj_set_pos(title_, padding, padding); 29 | lv_label_set_text(title_, LOC_WAKEUP_TITLE); 30 | //double tap switch + label + callback 31 | double_tap_switch_ = lv_switch_create(parent, NULL); 32 | double_tap_wakeup_ = hardware_->get_double_tap_wakeup(); 33 | if (double_tap_wakeup_) 34 | { 35 | lv_switch_on(double_tap_switch_, LV_ANIM_OFF); 36 | } 37 | lv_obj_align(double_tap_switch_, title_, LV_ALIGN_OUT_BOTTOM_LEFT, 0, padding); 38 | double_tap_label_ = lv_label_create(parent, NULL); 39 | lv_label_set_text(double_tap_label_, LOC_WAKEUP_DOUBLE_TAP); 40 | lv_obj_align(double_tap_label_, double_tap_switch_, LV_ALIGN_OUT_RIGHT_MID, padding, 0); 41 | double_tap_switch_->user_data = this; 42 | lv_obj_set_event_cb(double_tap_switch_, WakeupSettings::double_tap_switch_callback); 43 | //tilt switch + label + callback 44 | tilt_switch_ = lv_switch_create(parent, NULL); 45 | tilt_wakeup_ = hardware_->get_tilt_wakeup(); 46 | if (tilt_wakeup_) 47 | { 48 | lv_switch_on(tilt_switch_, LV_ANIM_OFF); 49 | } 50 | 51 | lv_obj_align(tilt_switch_, double_tap_switch_, LV_ALIGN_OUT_BOTTOM_LEFT, 0, padding); 52 | tilt_label_ = lv_label_create(parent, NULL); 53 | lv_label_set_text(tilt_label_, LOC_WAKEUP_TILT); 54 | lv_obj_align(tilt_label_, tilt_switch_, LV_ALIGN_OUT_RIGHT_MID, padding, 0); 55 | tilt_switch_->user_data = this; 56 | lv_obj_set_event_cb(tilt_switch_, WakeupSettings::tilt_switch_callback); 57 | //touch switch + label + callback 58 | touch_switch_ = lv_switch_create(parent, NULL); 59 | touch_wakeup_ = hardware_->get_touch_wakeup(); 60 | if (touch_wakeup_) 61 | { 62 | lv_switch_on(touch_switch_, LV_ANIM_OFF); 63 | } 64 | 65 | lv_obj_align(touch_switch_, tilt_switch_, LV_ALIGN_OUT_BOTTOM_LEFT, 0, padding); 66 | touch_label_ = lv_label_create(parent, NULL); 67 | lv_label_set_text(touch_label_, LOC_WAKEUP_TOUCH); 68 | lv_obj_align(touch_label_, touch_switch_, LV_ALIGN_OUT_RIGHT_MID, padding, 0); 69 | touch_switch_->user_data = this; 70 | lv_obj_set_event_cb(touch_switch_, WakeupSettings::touch_switch_callback); 71 | } 72 | 73 | virtual bool hide_internal() override 74 | { 75 | if (update_hardware_) 76 | { 77 | ESP_LOGI(SETTINGS_TAG, "Updating double tap=%d, tilt=%d, touch wakeup=%d", double_tap_wakeup_, tilt_wakeup_, touch_wakeup_); 78 | hardware_->set_double_tap_wakeup(double_tap_wakeup_); 79 | hardware_->set_tilt_wakeup(tilt_wakeup_); 80 | hardware_->set_touch_wakeup(touch_wakeup_); 81 | hardware_->save(); 82 | } 83 | return true; 84 | } 85 | 86 | private: 87 | Gui *gui_; 88 | Hardware *hardware_; 89 | bool double_tap_wakeup_; 90 | bool tilt_wakeup_; 91 | bool touch_wakeup_; 92 | bool update_hardware_ = false; 93 | lv_obj_t *title_; 94 | lv_obj_t *double_tap_switch_; 95 | lv_obj_t *double_tap_label_; 96 | lv_obj_t *tilt_switch_; 97 | lv_obj_t *tilt_label_; 98 | lv_obj_t *touch_switch_; 99 | lv_obj_t *touch_label_; 100 | 101 | static void double_tap_switch_callback(lv_obj_t *obj, lv_event_t event) 102 | { 103 | if (event == LV_EVENT_VALUE_CHANGED) 104 | { 105 | auto settings = (WakeupSettings *)obj->user_data; 106 | auto value = lv_switch_get_state(obj); 107 | settings->double_tap_wakeup_ = value; 108 | settings->update_hardware_ = true; 109 | } 110 | } 111 | 112 | static void tilt_switch_callback(lv_obj_t *obj, lv_event_t event) 113 | { 114 | if (event == LV_EVENT_VALUE_CHANGED) 115 | { 116 | auto settings = (WakeupSettings *)obj->user_data; 117 | auto value = lv_switch_get_state(obj); 118 | settings->tilt_wakeup_ = value; 119 | settings->update_hardware_ = true; 120 | } 121 | } 122 | 123 | static void touch_switch_callback(lv_obj_t *obj, lv_event_t event) 124 | { 125 | if (event == LV_EVENT_VALUE_CHANGED) 126 | { 127 | auto settings = (WakeupSettings *)obj->user_data; 128 | auto value = lv_switch_get_state(obj); 129 | settings->touch_wakeup_ = value; 130 | settings->update_hardware_ = true; 131 | } 132 | } 133 | }; -------------------------------------------------------------------------------- /src/ui/dynamic_gauge.cpp: -------------------------------------------------------------------------------- 1 | #include "dynamic_gauge.h" 2 | #include "localization.h" 3 | #include "data_adapter.h" 4 | 5 | void DynamicGaugeBuilder::initialize(ComponentFactory *factory) 6 | { 7 | factory->register_constructor("gauge", [factory](JsonObject &json, lv_obj_t *parent) -> Component * 8 | { 9 | auto gauge = new DynamicGauge(parent); 10 | gauge->load(json); 11 | return gauge; 12 | }); 13 | } 14 | 15 | void DynamicGauge::load(const JsonObject &json) 16 | { 17 | lv_obj_t *arc = lv_arc_create(parent_, NULL); 18 | 19 | this->obj_ = arc; 20 | 21 | lv_arc_set_range(arc, 0, 300); 22 | lv_arc_set_bg_angles(arc, 0, 300); 23 | lv_arc_set_angles(arc, 0, 300); 24 | lv_arc_set_rotation(arc, 120); 25 | lv_arc_set_value(arc, 20); 26 | lv_arc_set_adjustable(arc, false); 27 | lv_obj_set_click(arc, false); 28 | //remove border 29 | lv_obj_set_style_local_pad_all(arc, LV_ARC_PART_BG, LV_STATE_DEFAULT, 0); 30 | lv_obj_set_style_local_border_side(arc, LV_ARC_PART_BG, LV_STATE_DEFAULT, LV_BORDER_SIDE_NONE); 31 | //set opacity to transparent 32 | lv_obj_set_style_local_bg_opa(arc, LV_ARC_PART_BG, LV_STATE_DEFAULT, LV_OPA_TRANSP); 33 | 34 | if (json.containsKey("color")) 35 | { 36 | auto color = DynamicHelpers::get_color(json["color"]); 37 | lv_obj_set_style_local_line_color(arc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, color); 38 | } 39 | 40 | if(json.containsKey("minimum")) 41 | { 42 | minimum_ = json["minimum"].as(); 43 | } 44 | 45 | if(json.containsKey("maximum")) 46 | { 47 | maximum_ = json["maximum"].as(); 48 | } 49 | 50 | if (json.containsKey("binding")) 51 | { 52 | JsonObject binding = json["binding"].as(); 53 | int period = 1000; 54 | formating.multiply = 1.0f; 55 | formating.offset = 0.0f; 56 | formating.decimal_places = 1; 57 | 58 | if (binding.containsKey("period")) 59 | { 60 | period = binding["period"].as(); 61 | } 62 | if (binding.containsKey("multiply")) 63 | { 64 | formating.multiply = binding["multiply"].as(); 65 | } 66 | if (binding.containsKey("offset")) 67 | { 68 | formating.offset = binding["offset"].as(); 69 | } 70 | 71 | if (binding.containsKey("decimals")) 72 | { 73 | formating.decimal_places = binding["decimals"].as(); 74 | } 75 | 76 | if (binding.containsKey("format")) 77 | { 78 | auto jsonFormating = binding["format"].as(); 79 | //allocate memory for format string + 1 for \0 char at the end 80 | formating.string_format = (char *)malloc(strlen(jsonFormating) + 1); 81 | strcpy(formating.string_format, jsonFormating); 82 | } 83 | 84 | //register dataadapter that will connect SK receiver and this arc 85 | new DataAdapter(binding["path"].as(), period, this); 86 | } 87 | 88 | DynamicHelpers::set_location(arc, json); 89 | DynamicHelpers::set_size(arc, json); 90 | DynamicHelpers::set_layout(arc, parent_, json); 91 | 92 | label_ = lv_label_create(arc, NULL); 93 | lv_label_set_text(label_, "--"); 94 | lv_obj_align(label_, arc, LV_ALIGN_CENTER, 0, 0); 95 | 96 | if(json.containsKey("text-color")) 97 | { 98 | auto textColor = DynamicHelpers::get_color(json["text-color"]); 99 | 100 | lv_obj_set_style_local_text_color(label_, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, textColor); 101 | } 102 | } 103 | 104 | void DynamicGauge::update(const JsonVariant &json) 105 | { 106 | float value = 0.0f; 107 | 108 | if (json.is()) 109 | { 110 | value = ((json.as() + formating.offset) * formating.multiply); 111 | } 112 | else if (json.is()) 113 | { 114 | value = ((json.as() + formating.offset) * formating.multiply); 115 | } 116 | else if (json.is()) 117 | { 118 | value = json.as() ? minimum_ : maximum_; 119 | } 120 | else 121 | { 122 | value = minimum_; 123 | } 124 | 125 | lv_arc_set_value(obj_, (int)(300.0f * ((value - minimum_) / (maximum_ - minimum_)))); 126 | 127 | if (formating.string_format != NULL) 128 | { 129 | String labelText = String(formating.string_format); 130 | labelText.replace("$$", String(value, formating.decimal_places)); 131 | lv_label_set_text(label_, labelText.c_str()); 132 | } 133 | else 134 | { 135 | String labelText = String(value,1); 136 | lv_label_set_text(obj_, labelText.c_str()); 137 | } 138 | lv_obj_align(label_, obj_, LV_ALIGN_CENTER, 0, 0); 139 | } 140 | 141 | void DynamicGauge::on_offline() 142 | { 143 | lv_arc_set_value(obj_, 0); 144 | lv_label_set_text(label_, "--"); 145 | lv_obj_align(label_, obj_, LV_ALIGN_CENTER, 0, 0); 146 | } 147 | 148 | void DynamicGauge::destroy() 149 | { 150 | if (label_ != NULL) 151 | { 152 | lv_obj_del(label_); 153 | label_ = NULL; 154 | } 155 | 156 | if (obj_ != NULL) 157 | { 158 | lv_obj_del(obj_); 159 | obj_ = NULL; 160 | } 161 | } -------------------------------------------------------------------------------- /src/ui/dynamic_helpers.cpp: -------------------------------------------------------------------------------- 1 | #include "dynamic_helpers.h" 2 | LV_FONT_DECLARE(Geometr); 3 | LV_FONT_DECLARE(Ubuntu); 4 | LV_FONT_DECLARE(roboto80); 5 | LV_FONT_DECLARE(roboto60); 6 | LV_FONT_DECLARE(roboto40); 7 | LV_FONT_DECLARE(lv_font_montserrat_14) 8 | LV_FONT_DECLARE(lv_font_montserrat_28) 9 | LV_FONT_DECLARE(lv_font_montserrat_32) 10 | 11 | static String alignments[9] = { 12 | "center", 13 | "top-left", 14 | "top-midle", 15 | "top_right", 16 | "bottom_left", 17 | "bottom_mid", 18 | "bottom_right", 19 | "left_mid", 20 | "right_mid"}; 21 | 22 | static String layouts[12] = { 23 | "off", 24 | "center", 25 | "column_left", /**< column left align*/ 26 | "column_mid", /**< column middle align*/ 27 | "column_right", /**< column right align*/ 28 | "row_top", /**< row top align*/ 29 | "row_mid", /**< row middle align*/ 30 | "row_bottom", /**< row bottom align*/ 31 | "pretty_top", /**< row top align*/ 32 | "pretty_mid", /**< row middle align*/ 33 | "pretty_bottom", /**< row bottom align*/ 34 | "grid"}; 35 | 36 | uint8_t DynamicHelpers::get_alignment(String value) 37 | { 38 | for (int i = 0; i < 9; i++) 39 | { 40 | if (alignments[i].equals(value)) 41 | { 42 | return (uint8_t)i; 43 | } 44 | } 45 | 46 | return LV_ALIGN_CENTER; 47 | } 48 | 49 | void DynamicHelpers::set_container_layout(lv_obj_t *obj, String &value) 50 | { 51 | lv_layout_t layout = LV_LAYOUT_OFF; 52 | for (int i = 0; i < 12; i++) 53 | { 54 | if (layouts[i].equals(value)) 55 | { 56 | layout = (lv_layout_t)i; 57 | } 58 | } 59 | lv_cont_set_layout(obj, layout); 60 | } 61 | 62 | void DynamicHelpers::set_layout(lv_obj_t *obj, lv_obj_t *parent, const JsonObject &json) 63 | { 64 | if (json.containsKey("layout")) 65 | { 66 | String layout = json["layout"].as(); 67 | auto alignment = DynamicHelpers::get_alignment(layout); 68 | lv_obj_align(obj, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0); 69 | ESP_LOGI("UI", "Object layout %s=%d", layout.c_str(), alignment); 70 | } 71 | } 72 | 73 | void DynamicHelpers::set_location(lv_obj_t *obj, const JsonObject &json) 74 | { 75 | if (json.containsKey("location")) 76 | { 77 | JsonArray array = json["location"]; 78 | if (array && array.size() == 2) 79 | { 80 | lv_obj_set_pos(obj, array[0].as(), array[1].as()); 81 | } 82 | } 83 | } 84 | 85 | void DynamicHelpers::set_size(lv_obj_t *obj, const JsonObject &json) 86 | { 87 | if (json.containsKey("size")) 88 | { 89 | JsonArray array = json["size"]; 90 | if (array && array.size() == 2) 91 | { 92 | lv_obj_set_size(obj, array[0].as(), array[1].as()); 93 | } 94 | } 95 | } 96 | 97 | lv_color_t DynamicHelpers::get_color(const String &value) 98 | { 99 | lv_color_t ret = LV_COLOR_BLACK; 100 | 101 | if (value.startsWith("#")) 102 | { 103 | ret = lv_color_hex(strtol(value.substring(1).c_str(), NULL, 16)); 104 | } 105 | else if (value == "white") 106 | { 107 | ret = LV_COLOR_WHITE; 108 | } 109 | else if (value == "black") 110 | { 111 | ret = LV_COLOR_BLACK; 112 | } 113 | else if (value == "blue") 114 | { 115 | ret = LV_COLOR_BLUE; 116 | } 117 | else if (value == "red") 118 | { 119 | ret = LV_COLOR_RED; 120 | } 121 | else if (value == "green") 122 | { 123 | ret = LV_COLOR_GREEN; 124 | } 125 | else if (value == "gray") 126 | { 127 | ret = LV_COLOR_GRAY; 128 | } 129 | else if (value == "primary") 130 | { 131 | ret = LV_THEME_DEFAULT_COLOR_PRIMARY; 132 | } 133 | else if (value == "secondary") 134 | { 135 | ret = LV_THEME_DEFAULT_COLOR_SECONDARY; 136 | } 137 | else 138 | { 139 | ESP_LOGW("HELPERS", "Color %s not found!", value.c_str()); 140 | } 141 | 142 | return ret; 143 | } 144 | 145 | void DynamicHelpers::set_font(lv_obj_t *obj, String &styleName) 146 | { 147 | if (styleName == "montserrat14") 148 | { 149 | lv_obj_set_style_local_text_font(obj, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_montserrat_14); 150 | } 151 | else if (styleName == "montserrat28") 152 | { 153 | lv_obj_set_style_local_text_font(obj, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_montserrat_28); 154 | } 155 | else if (styleName == "montserrat32") 156 | { 157 | lv_obj_set_style_local_text_font(obj, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_montserrat_32); 158 | } 159 | else if (styleName == "ubuntu50") 160 | { 161 | lv_obj_set_style_local_text_font(obj, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &Ubuntu); 162 | } 163 | else if (styleName == "roboto40") 164 | { 165 | lv_obj_set_style_local_text_font(obj, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &roboto40); 166 | } 167 | else if (styleName == "roboto60") 168 | { 169 | lv_obj_set_style_local_text_font(obj, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &roboto60); 170 | } 171 | else if (styleName == "roboto80") 172 | { 173 | lv_obj_set_style_local_text_font(obj, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &roboto80); 174 | } 175 | else 176 | { 177 | ESP_LOGW("LABEL", "Font %s not found!", styleName.c_str()); 178 | } 179 | } -------------------------------------------------------------------------------- /src/gui.h: -------------------------------------------------------------------------------- 1 | #include "hardware/Wifi.h" 2 | #include "networking/signalk_socket.h" 3 | #include "system/configurable.h" 4 | #include "system/events.h" 5 | #include "ui/statusbar.h" 6 | #include "ui/dynamic_gui.h" 7 | #include "ui/themes.h" 8 | #include "hardware/hardware.h" 9 | #include 10 | 11 | #ifndef __GUI_H 12 | #define __GUI_H 13 | 14 | #define ARROWS_DISAPPEAR_TIME 4000 15 | 16 | enum MsgTopic_t 17 | { 18 | Wifi_Problem 19 | }; 20 | 21 | class Gui : public Configurable 22 | { 23 | public: 24 | Gui() : Configurable("/config/gui") 25 | { 26 | //set default name 27 | strcpy(watch_name, "TWatchSK"); 28 | load(); 29 | } 30 | void setup_gui(WifiManager *wifi, SignalKSocket *socket, Hardware *hardware); 31 | void update_step_counter(uint32_t counter); 32 | void update_battery_level(); 33 | void hide_status_bar(bool hidden); 34 | void hide_main_bar(bool hidden); 35 | void load_config_from_file(const JsonObject &json) override; 36 | void save_config_to_file(JsonObject &json) override; 37 | WifiManager *get_wifi_manager() { return wifiManager; } 38 | SignalKSocket *get_sk_socket() { return ws_socket; } 39 | Hardware *get_hardware() { return hardware_; } 40 | bool get_time_24hour_format() { return time_24hour_format; } 41 | void set_time_24hour_format(bool value) { time_24hour_format = value; } 42 | int get_screen_timeout() { return screen_timeout; } 43 | void set_screen_timeout(int value) { screen_timeout = value; } 44 | void set_temporary_screen_timeout(int value); 45 | void clear_temporary_screen_timeout(); 46 | int get_wakeup_count() { return wakeup_count; } 47 | void increment_wakeup_count() { wakeup_count++; } 48 | uint8_t get_display_brightness() { return display_brightness; } 49 | uint8_t get_adjusted_display_brightness(); 50 | void set_display_brightness(uint8_t value) { display_brightness = value; } 51 | void on_power_event(PowerCode_t code, uint32_t arg); 52 | int8_t get_timezone_id() { return timezone_id; } 53 | void set_timezone_id(int8_t new_tz_id) { timezone_id = new_tz_id; } 54 | StatusBar *get_bar() 55 | { 56 | return bar; 57 | } 58 | void theme_changed(); 59 | const char* get_watch_name() { return watch_name; } 60 | void set_watch_name(const char *new_name) { strcpy(watch_name, new_name); } 61 | bool is_active_view_dynamic() { return is_active_view_dynamic_; } 62 | void set_is_active_view_dynamic(bool new_value); 63 | bool get_gui_needs_saved() { return gui_needs_saved; } 64 | void set_gui_needs_saved(bool new_value) { gui_needs_saved = new_value; } 65 | void set_display_next_pending_message(bool value) { display_next_pending_message_ = value; } 66 | void display_next_message(bool delete_first_message); 67 | void update_pending_messages(); 68 | void handle_gui_queue(); 69 | void show_home(); 70 | void show_settings(); 71 | void toggle_wifi(); 72 | private: 73 | static void lv_update_task(struct _lv_task_t *); 74 | static void lv_battery_task(struct _lv_task_t *); 75 | static void lv_mainbar_callback(lv_obj_t*obj, lv_event_t event); 76 | void update_time(); 77 | void update_tiles_valid_points(int count); 78 | char *message_from_code(GuiMessageCode_t code); 79 | void update_gui(); 80 | String current_time(); 81 | void update_arrows_visibility(); 82 | void update_arrows_visibility(bool left, bool right); 83 | static void msg_box_callback(lv_obj_t * obj, lv_event_t event); 84 | static void hide_arrows_task_cb(lv_task_t * task); 85 | 86 | WifiManager *wifiManager = NULL; 87 | SignalKSocket *ws_socket = NULL; 88 | Hardware *hardware_ = NULL; 89 | lv_obj_t *mainBar = NULL; 90 | lv_obj_t *timeLabel = NULL; 91 | lv_obj_t *timeSuffixLabel = NULL; 92 | lv_obj_t *secondsLabel = NULL; 93 | lv_obj_t *menuBtn = NULL; 94 | lv_obj_t *watch_face = NULL; 95 | lv_obj_t *dayDateLabel = NULL; 96 | lv_obj_t *watchNameLabel = NULL; 97 | lv_obj_t *msgBox = NULL; 98 | lv_obj_t *arrow_left = NULL; 99 | lv_obj_t *arrow_right = NULL; 100 | StatusBar *bar = NULL; 101 | DynamicGui*dynamic_gui = NULL; 102 | 103 | struct PendingMsg_t 104 | { 105 | String msg_text; 106 | MsgTopic_t msg_topic; 107 | String msg_time;; 108 | int msg_count = 0; 109 | }; 110 | 111 | std::list pending_messages_; 112 | 113 | bool time_24hour_format = false; 114 | int screen_timeout = 30; // only until it's first changed 115 | int saved_screen_timeout = 30; 116 | bool screen_timeout_is_temporary = false; 117 | int8_t timezone_id = 0; // Index of the array of timezones in the timezone Roller selector widget 118 | int wakeup_count = 0; // restarts at zero at each startup 119 | uint8_t display_brightness = 5; // only until it's first changed 120 | lv_point_t*tile_valid_points = NULL; //this is for tile navigation matrix to allow user navigation in multiple directions 121 | int tile_valid_points_count = 0; //number of matrix points 122 | char watch_name[16] = ""; 123 | bool is_active_view_dynamic_ = false; 124 | bool gui_needs_saved = false; 125 | bool display_next_pending_message_ = true; 126 | long arrows_hide_at_time = -1; 127 | 128 | StatusBarIcon* batteryPercent_; 129 | StatusBarIcon* stepCounterIcon_; 130 | StatusBarIcon* stepCounterSteps_; 131 | StatusBarIcon* WifiIcon_; 132 | StatusBarIcon* SKIcon_; 133 | StatusBarIcon* pendingMessagesIcon_; 134 | }; 135 | #endif /*__GUI_H */ -------------------------------------------------------------------------------- /src/ui/localization.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef LILYGO_WATCH_2020_V1 3 | #define LOC_WATCH_MODEL "V1" 4 | #elif LILYGO_WATCH_2020_V2 5 | #define LOC_WATCH_MODEL "V2" 6 | #elif LILYGO_WATCH_2020_V3 7 | #define LOC_WATCH_MODEL "V3" 8 | #endif 9 | #define LOC_LANGUAGE "English" 10 | #define LOC_WIFI "Wifi" 11 | #define LOC_WIFI_SETTINGS "Wifi settings" 12 | #define LOC_WIFI_ENABLE "Enabled" 13 | #define LOC_WIFI_CONNECTED "Connected" 14 | #define LOC_WIFI_CONNECT "Connect" 15 | #define LOC_WIFI_CONFIG_SSID_FMT "Wifi: %s" 16 | #define LOC_WIFI_IP_RSSI_FMT "IP: %s, RSSI: %d dB" 17 | #define LOC_WIFI_CONNECTING "Connecting" 18 | #define LOC_WIFI_CONNECT_SUCCESS "Success! You're now connected to Wifi." 19 | #define LOC_WIFI_DISCONNECTED "Disconnected" 20 | #define LOC_WIFI_OFF "Off" 21 | #define LOC_WIFI_NO_CONFIG "Not configured" 22 | #define LOC_WIFI_PLEASE_CONFIGURE "Please configure Wifi" 23 | #define LOC_WIFI_SCAN_LABEL "Scan" 24 | #define LOC_WIFI_SCANNING_PROGRESS "Scanning Wifi..." 25 | #define LOC_WIFI_SELECT "Select Wifi" 26 | #define LOC_KEYBOARD_DEL "Del" 27 | #define LOC_WIFI_PASSWORD_PROMPT "Enter password" 28 | #define LOC_WIFI_LOST_CONNECTION "Wifi has lost connection to AP!" 29 | #define LOC_WIFI_ASSOC_FAIL "Can't connect to Wifi. Incorrect password, or out of range." 30 | #define LOC_MESSAGEBOX_OK "OK" 31 | #define LOC_MESSAGEBOX_DISABLE_WIFI "Disable Wifi" 32 | #define LOC_SETTINGS_MENU "Watch settings" 33 | #define LOC_SIGNALK_SETTINGS "Signal K settings" 34 | #define LOC_SIGNALK_CONNECTED_FMT "Connected\nDeltas Rx'd: %d" 35 | #define LOC_SIGNALK_DISCONNECTED "Disconnected" 36 | #define LOC_SIGNALK_CONNECTING "Connecting" 37 | #define LOC_SIGNALK_TOKEN_OK "Token OK" 38 | #define LOC_SIGNALK_TOKEN_NONE "No token!" 39 | #define LOC_SIGNALK_TOKEN_PENDING "Pending authorization" 40 | #define LOC_SIGNALK_TOKEN_RESET "Reset token" 41 | #define LOC_SIGNALK_REQUEST_REJECTED "Signal K server rejected authorization!" 42 | #define LOC_SIGNALK_CONNECTION_LOST "Signal K server connection lost! Please reconnect manually!" 43 | #define LOC_SIGNALK_ADDRESS_EMPTY "Set address" 44 | #define LOC_SIGNALK_INPUT_ADDRESS "Enter server address" 45 | #define LOC_SIGNALK_INPUT_PORT "Enter server port (default: 3000)" 46 | #define LOC_SIGNALK_FIND_SERVER "Find SK Server w/ mDNS" 47 | #define LOC_SIGNALK_FINDING_SERVER "Detecting SK server..." 48 | #define LOC_SIGNALK_SYNC_TIME "Sync time with SK Server" 49 | #define LOC_SIGNALK_MDNS_NOT_FOUND "Unable to find SK server!" 50 | #define LOC_SIGNALK_MDNS_ERORR "Unable to initialize mDNS service!" 51 | #define LOC_TIME_SETTINGS "Time settings" 52 | #define LOC_TIME "Time" 53 | #define LOC_DATE "Date" 54 | #define LOC_DATE_FORMAT "%d. %s %d" 55 | #define LOC_INPUT_HOUR "Enter current hour" 56 | #define LOC_INPUT_MINUTE "Enter current minute" 57 | #define LOC_SELECT_YEAR "Select year" 58 | #define LOC_SELECT_DAY "Select day" 59 | #define LOC_SELECT_MONTH "Select month" 60 | #define LOC_MONTHS {"???","Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} 61 | #define LOC_MONTHS_FULL "January\n" "February\n" "March\n" "April\n" "May\n" "June\n" "July\n" "August\n" "September\n" "October\n" "November\n" "December\n" 62 | #define LOC_WATCH_INFO "Watch info" 63 | #define LOC_WATCH_VERSION "Version 1.0 " LOC_WATCH_MODEL 64 | #define LOC_AUTHOR "Author: Jan Dytrych,\nBrian Smith and contributors" 65 | #define LOC_UPTIME "Uptime: %d:%.2d:%.2d " 66 | #define LOC_WATCH_NAME "Watch\nname:" 67 | #define LOC_INPUT_WATCH_NAME "Watch name (1 - 16 chars)" 68 | #define LOC_WATCH_NAME_EMPTY "Enter watch name" 69 | #define LOC_TIMEZONE "Timezone: " 70 | #define LOC_24HOUR_TIME "Show 24 hour time" 71 | #define LOC_DISPLAY_SETTINGS "Display settings" 72 | #define LOC_SCREEN_TIMEOUT "Screen\ntimeout: " 73 | #define LOC_INPUT_SCREEN_TIMEOUT "Scrn timeout (>=5 sec.)" 74 | #define LOC_WAKEUP_COUNT "Wake-up count: %d" 75 | #define LOC_DISPLAY_BRIGHTNESS "Display\nbrightness: " 76 | #define LOC_DISPLAY_DOWNLOAD_UI "Download DynamicViews" 77 | #define LOC_DISPLAY_DOWNLOADING_UI "Downloading UI from SK server..." 78 | #define LOC_DISPLAY_DOWNLOAD_UI_DONE "The DynamicViews download was successful. Watch will reboot in 5 seconds!" 79 | #define LOC_DISPLAY_DOWNLOAD_UI_NO_CONNECTION "Unable to download DynamicViews!\r\nUnable to connect to SK server!" 80 | #define LOC_DISPLAY_DOWNLOAD_UI_ERROR "Unable to download DynamicViews!\r\nThere was an error in transfer!" 81 | #define LOC_INPUT_DISPLAY_BRIGHTNESS "Disp brightness (1 to 5)" 82 | #define LOC_ON "On" 83 | #define LOC_OFF "Off" 84 | #define LOC_TRUE "True" 85 | #define LOC_FALSE "False" 86 | #define LOC_INPUT_TIMEZONE "Select timezone" 87 | #define LOC_DARK_SWITCH_LABEL "Enable Dark theme" 88 | #define LOC_WAKEUP_SETTINGS_MENU "Wake-up" 89 | #define LOC_WAKEUP_SETTINGS "Wake-up settings" 90 | #define LOC_WAKEUP_TITLE "Select wake-up options" 91 | #define LOC_WAKEUP_DOUBLE_TAP "Double tap" 92 | #define LOC_WAKEUP_TILT "Watch tilt" 93 | #define LOC_WAKEUP_TOUCH "Screen touch" 94 | #define LOC_CLOCK_SETTINGS_MENU "Clock" 95 | #define LOC_DISPLAY_SETTINGS_MENU "Display" 96 | #define LOC_WIFI_SETTINGS_MENU "Wifi" 97 | #define LOC_SIGNALK_SETTING_MENU "Signal K" 98 | #define LOC_WATCH_INFO_MENU "Watch info" 99 | #define LOC_MSG_COUNT " of this msg" 100 | #define LOC_UNREAD_MSGS " unread msgs" 101 | #define LOC_POWER_BATTERY_CHARGED "Battery charging is now complete!" 102 | #define LOC_STARTUP_SPIFFS "SPIFFS init...please wait!" 103 | #define LOC_STARTUP_HW_GUI "Hardware & LVGL init..." 104 | #define LOC_STARTUP_NETWORKING "Wifi & networking init..." 105 | #define LOC_SK_PUT_SEND_FAIL "Unable to send SK put request!" 106 | #define LOC_SK_PUT_SEND_FAIL "Unable to send Signal K put request!" 107 | -------------------------------------------------------------------------------- /src/hardware/touch.cpp: -------------------------------------------------------------------------------- 1 | #include "hardware/touch.h" 2 | // Took from code by sharandac, uri: https://github.com/sharandac/My-TTGO-Watch/blob/master/src/hardware/touch.cpp 3 | #define TOUCH_TAG "TOUCH" 4 | static SemaphoreHandle_t xTouchSemaphore = NULL; 5 | lv_indev_t *touch_dev = NULL; 6 | static bool DRAM_ATTR low_power_mode = false; 7 | static bool touch_down = false; 8 | volatile bool DRAM_ATTR wakeup_from_sleep_ = true; 9 | // Touch IRQ flags and Mutex 10 | volatile bool DRAM_ATTR touch_irq_flag = false; 11 | portMUX_TYPE DRAM_ATTR Touch_IRQ_Mux = portMUX_INITIALIZER_UNLOCKED; 12 | EventGroupHandle_t watch_isr_group = NULL; 13 | 14 | #define WATCH_FLAG_SLEEP_MODE _BV(1) // in sleep mode 15 | #define WATCH_FLAG_SLEEP_EXIT _BV(2) // leaving sleep mode because of any kind of interrupt 16 | #define WATCH_FLAG_TOUCH_IRQ _BV(5) // leaving sleep mode because of touch 17 | 18 | bool touch_lock_take(void) 19 | { 20 | return xSemaphoreTake(xTouchSemaphore, portMAX_DELAY) == pdTRUE; 21 | } 22 | 23 | void touch_lock_give(void) 24 | { 25 | xSemaphoreGive(xTouchSemaphore); 26 | } 27 | 28 | static bool touch_getXY(int16_t &x, int16_t &y) 29 | { 30 | TTGOClass *ttgo = TTGOClass::getWatch(); 31 | 32 | 33 | if (!low_power_mode) 34 | { 35 | /* 36 | * get touchstate from touchcontroller if not taken 37 | * by other task/thread 38 | */ 39 | bool getTouchResult = false; 40 | if (touch_lock_take()) 41 | { 42 | getTouchResult = ttgo->getTouch(x, y); 43 | touch_lock_give(); 44 | } 45 | /* 46 | * if touched? 47 | */ 48 | if (!getTouchResult) 49 | { 50 | return false; 51 | } 52 | 53 | /* 54 | * issue https://github.com/sharandac/My-TTGO-Watch/issues/18 fix 55 | */ 56 | float temp_x = (x - (lv_disp_get_hor_res(NULL) / 2)) * 1.15; 57 | float temp_y = (y - (lv_disp_get_ver_res(NULL) / 2)) * 1.0; 58 | x = temp_x + (lv_disp_get_hor_res(NULL) / 2); 59 | y = temp_y + (lv_disp_get_ver_res(NULL) / 2); 60 | 61 | x = min((int16_t)(LV_HOR_RES-5), max((int16_t)0, x)); 62 | y = min((int16_t)(LV_VER_RES-5), max((int16_t)0, y)); 63 | 64 | ESP_LOGI(TOUCH_TAG, "Touch=(%d,%d)", x,y); 65 | 66 | return true; 67 | } 68 | else 69 | { 70 | return false; 71 | } 72 | } 73 | 74 | static bool touch_read(lv_indev_drv_t *drv, lv_indev_data_t *data) 75 | { 76 | /* 77 | * We use two flags, one changes in the interrupt handler 78 | * the other controls whether we poll the sensor, 79 | * and gets cleared when the level is no longer low, 80 | * meaning the touch has finished 81 | */ 82 | portENTER_CRITICAL(&Touch_IRQ_Mux); 83 | bool temp_touch_irq_flag = touch_irq_flag; 84 | touch_irq_flag = false; 85 | portEXIT_CRITICAL(&Touch_IRQ_Mux); 86 | touch_down |= temp_touch_irq_flag; 87 | /* 88 | * check for an touch interrupt 89 | */ 90 | if (touch_down) 91 | { 92 | data->state = touch_getXY(data->point.x, data->point.y) ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL; 93 | touch_down = digitalRead(TOUCH_INT) == LOW; 94 | if (!touch_down) 95 | { 96 | /* 97 | * Save power by switching to monitor mode now instead of waiting for 30 seconds. 98 | */ 99 | if (touch_lock_take()) 100 | { 101 | TTGOClass::getWatch()->touchToMonitor(); 102 | touch_lock_give(); 103 | } 104 | } 105 | } 106 | else 107 | { 108 | data->state = LV_INDEV_STATE_REL; 109 | } 110 | return false; 111 | } 112 | 113 | void IRAM_ATTR touch_irq(void) 114 | { 115 | /* 116 | * enter critical section and set interrupt flag 117 | */ 118 | portENTER_CRITICAL_ISR(&Touch_IRQ_Mux); 119 | touch_irq_flag = true; 120 | /* 121 | * leave critical section 122 | */ 123 | portEXIT_CRITICAL_ISR(&Touch_IRQ_Mux); 124 | 125 | BaseType_t xHigherPriorityTaskWoken = pdFALSE; 126 | EventBits_t bits = xEventGroupGetBitsFromISR(watch_isr_group); 127 | if (bits & WATCH_FLAG_SLEEP_MODE && wakeup_from_sleep_) 128 | { 129 | //! For quick wake up, use the group flag 130 | xEventGroupSetBitsFromISR(watch_isr_group, WATCH_FLAG_SLEEP_EXIT | WATCH_FLAG_TOUCH_IRQ, &xHigherPriorityTaskWoken); 131 | } 132 | } 133 | 134 | void Touch::set_low_power(bool low_power) 135 | { 136 | low_power_mode = low_power; 137 | #if defined(LILYGO_WATCH_2020_V2) || defined(LILYGO_WATCH_2020_V3) 138 | if (!low_power) 139 | { 140 | TTGOClass *ttgo = TTGOClass::getWatch(); 141 | ttgo->touchWakup(); 142 | ESP_LOGI(TOUCH_TAG, "irq disable"); 143 | // we will handle TOUCH interupts on our own 144 | ttgo->touch->disableINT(); 145 | ttgo->disableTouchIRQ(); 146 | ESP_LOGI(TOUCH_TAG, "touch monitoring config"); 147 | // configure touch monitoring time 148 | ttgo->touch->setMonitorTime(0x01); 149 | ttgo->touch->setMonitorPeriod(125); 150 | ESP_LOGI(TOUCH_TAG, "driver init"); 151 | xTouchSemaphore = xSemaphoreCreateMutex(); 152 | ESP_LOGI(TOUCH_TAG, "interrup attach"); 153 | attachInterrupt(TOUCH_INT, &touch_irq, FALLING); 154 | } 155 | #endif 156 | } 157 | 158 | bool Touch::initialize(EventGroupHandle_t wakeupEvents) 159 | { 160 | TTGOClass *ttgo = TTGOClass::getWatch(); 161 | 162 | #if defined(LILYGO_WATCH_2020_V2) || defined(LILYGO_WATCH_2020_V3) 163 | ttgo->touchWakup(); 164 | #endif 165 | ESP_LOGI(TOUCH_TAG, "irq disable"); 166 | 167 | // we will handle TOUCH interupts on our own 168 | ttgo->touch->disableINT(); 169 | ttgo->disableTouchIRQ(); 170 | ESP_LOGI(TOUCH_TAG, "touch monitoring config"); 171 | // configure touch monitoring time 172 | ttgo->touch->setMonitorTime(0x01); 173 | ttgo->touch->setMonitorPeriod(125); 174 | ESP_LOGI(TOUCH_TAG, "driver init"); 175 | xTouchSemaphore = xSemaphoreCreateMutex(); 176 | touch_dev = lv_indev_get_next(NULL); 177 | touch_dev->driver.read_cb = touch_read; 178 | ESP_LOGI(TOUCH_TAG, "interup attach"); 179 | attachInterrupt(TOUCH_INT, &touch_irq, FALLING); 180 | watch_isr_group = wakeupEvents; 181 | 182 | return true; 183 | } 184 | 185 | void Touch::allow_touch_wakeup(bool value) 186 | { 187 | wakeup_from_sleep_ = value; 188 | } -------------------------------------------------------------------------------- /src/ui/keyboard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "localization.h" 4 | #include "config.h" 5 | #include "settings_view.h" 6 | 7 | /***************************************************************** 8 | * 9 | * ! Keyboard Class 10 | * 11 | */ 12 | 13 | enum KeyboardType_t 14 | { 15 | Normal, 16 | Number, 17 | Brightness 18 | }; 19 | 20 | class Keyboard : public SettingsView 21 | { 22 | public: 23 | Keyboard(char *title, KeyboardType_t type = KeyboardType_t::Normal, int maxTextLength = 0) : SettingsView(title) 24 | { 25 | keyboardType = type; 26 | maxLength = maxTextLength; 27 | }; 28 | 29 | ~Keyboard(){ 30 | 31 | }; 32 | 33 | virtual void show_internal(lv_obj_t *parent) override 34 | { 35 | lv_cont_set_layout(parent, LV_LAYOUT_OFF); 36 | lv_obj_t *ta = lv_textarea_create(parent, NULL); 37 | lv_obj_set_height(ta, 40); 38 | lv_textarea_set_one_line(ta, true); 39 | lv_textarea_set_pwd_mode(ta, false); 40 | lv_textarea_set_text(ta, ""); 41 | lv_textarea_set_max_length(ta, 128); 42 | 43 | lv_obj_align(ta, parent, LV_ALIGN_IN_TOP_MID, 10, 10); 44 | lv_obj_t *kb = lv_keyboard_create(parent, NULL); 45 | lv_keyboard_set_cursor_manage(kb, true); 46 | lv_keyboard_set_textarea(kb, ta); 47 | lv_obj_set_pos(kb, 0, 50); 48 | lv_obj_set_size(kb, LV_HOR_RES, lv_obj_get_height(parent) - 50); 49 | if (keyboardType == KeyboardType_t::Normal) 50 | { 51 | lv_keyboard_set_map(kb, LV_KEYBOARD_MODE_TEXT_LOWER, btnm_mapplus[0]); 52 | } 53 | else if (keyboardType == KeyboardType_t::Number) 54 | { 55 | lv_keyboard_set_map(kb, LV_KEYBOARD_MODE_TEXT_LOWER, btnm_numeric[0]); 56 | lv_textarea_set_accepted_chars(ta, "0123456789"); 57 | } 58 | else // type == Brightness) 59 | { 60 | lv_keyboard_set_map(kb, LV_KEYBOARD_MODE_TEXT_LOWER, btnm_brightness[0]); 61 | lv_textarea_set_accepted_chars(ta, "12345"); 62 | } 63 | if (maxLength != 0) 64 | { 65 | lv_textarea_set_max_length(ta, maxLength); 66 | } 67 | kb->user_data = this; 68 | lv_obj_set_event_cb(kb, __kb_event_cb); 69 | } 70 | 71 | virtual bool hide_internal() override 72 | { 73 | return true; 74 | } 75 | 76 | static void __kb_event_cb(lv_obj_t *kb, lv_event_t event) 77 | { 78 | if (event == LV_EVENT_VALUE_CHANGED || event == LV_EVENT_LONG_PRESSED_REPEAT) 79 | { 80 | lv_keyboard_ext_t *ext = (lv_keyboard_ext_t *)lv_obj_get_ext_attr(kb); 81 | Keyboard *keyboard = (Keyboard *)kb->user_data; 82 | const char *txt = lv_btnmatrix_get_active_btn_text(kb); 83 | if (txt != NULL) 84 | { 85 | static int index = 0; 86 | if (strcmp(txt, LV_SYMBOL_OK) == 0) 87 | { 88 | strcpy(keyboard->__buf, lv_textarea_get_text(ext->ta)); 89 | keyboard->close_and_result(true); 90 | return; 91 | } 92 | else if (strcmp(txt, LV_SYMBOL_LEFT) == 0) 93 | { 94 | index = index - 1 < 0 ? 9 : index - 1; 95 | lv_keyboard_set_map(kb, LV_KEYBOARD_MODE_TEXT_LOWER, btnm_mapplus[index]); 96 | return; 97 | } 98 | else if (strcmp(txt, LV_SYMBOL_RIGHT) == 0) 99 | { 100 | index = index + 1 >= sizeof(btnm_mapplus) / sizeof(btnm_mapplus[0]) ? 0 : index + 1; 101 | lv_keyboard_set_map(kb, LV_KEYBOARD_MODE_TEXT_LOWER, btnm_mapplus[index]); 102 | return; 103 | } 104 | else if (strcmp(txt, LOC_KEYBOARD_DEL) == 0) 105 | { 106 | lv_textarea_del_char(ext->ta); 107 | } 108 | else 109 | { 110 | lv_textarea_add_text(ext->ta, txt); 111 | } 112 | } 113 | } 114 | } 115 | 116 | const char *get_text() 117 | { 118 | return (const char *)__buf; 119 | } 120 | 121 | bool is_success() 122 | { 123 | return _isSuccess; 124 | } 125 | 126 | private: 127 | static const char *btnm_mapplus[10][23]; 128 | static const char *btnm_numeric[1][23]; 129 | static const char *btnm_brightness[1][23]; 130 | char __buf[128]; 131 | bool _isSuccess = false; 132 | KeyboardType_t keyboardType; 133 | int maxLength; 134 | 135 | void close_and_result(bool is_success) 136 | { 137 | _isSuccess = is_success; 138 | lv_async_call(async_hide, this); 139 | } 140 | 141 | static void async_hide(void*arg) 142 | { 143 | auto keyboard = (Keyboard*)arg; 144 | keyboard->hide(); 145 | } 146 | }; 147 | 148 | const char *Keyboard::btnm_numeric[1][23] = { 149 | {"1", "2", "3", "\n", 150 | "4", "5", "6", "\n", 151 | "7", "8", "9", "\n", 152 | "0", LV_SYMBOL_OK, LOC_KEYBOARD_DEL, "", ""}}; 153 | 154 | const char *Keyboard::btnm_brightness[1][23] = { 155 | {"1", "2", "3", "\n", 156 | "4", "5", LV_SYMBOL_OK, LOC_KEYBOARD_DEL, "", ""}}; 157 | 158 | const char *Keyboard::btnm_mapplus[10][23] = { 159 | {"a", "b", "c", "\n", 160 | "d", "e", "f", "\n", 161 | "g", "h", "i", "\n", 162 | LV_SYMBOL_OK, LOC_KEYBOARD_DEL, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT, ""}, 163 | {"j", "k", "l", "\n", 164 | "m", "n", "o", "\n", 165 | "p", "q", "r", "\n", 166 | LV_SYMBOL_OK, LOC_KEYBOARD_DEL, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT, ""}, 167 | {"s", "t", "u", "\n", 168 | "v", "w", "x", "\n", 169 | "y", "z", " ", "\n", 170 | LV_SYMBOL_OK, LOC_KEYBOARD_DEL, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT, ""}, 171 | {"A", "B", "C", "\n", 172 | "D", "E", "F", "\n", 173 | "G", "H", "I", "\n", 174 | LV_SYMBOL_OK, LOC_KEYBOARD_DEL, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT, ""}, 175 | {"J", "K", "L", "\n", 176 | "N", "M", "O", "\n", 177 | "P", "Q", "R", "\n", 178 | LV_SYMBOL_OK, LOC_KEYBOARD_DEL, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT, ""}, 179 | {"S", "T", "U", "\n", 180 | "V", "W", "X", "\n", 181 | "Y", "Z", " ", "\n", 182 | LV_SYMBOL_OK, LOC_KEYBOARD_DEL, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT, ""}, 183 | {"1", "2", "3", "\n", 184 | "4", "5", "6", "\n", 185 | "7", "8", "9", "\n", 186 | LV_SYMBOL_OK, LOC_KEYBOARD_DEL, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT, ""}, 187 | {"0", "+", "-", "\n", 188 | "/", "*", "=", "\n", 189 | "!", "?", "#", "\n", 190 | LV_SYMBOL_OK, LOC_KEYBOARD_DEL, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT, ""}, 191 | {"<", ">", "@", "\n", 192 | "%", "$", "(", "\n", 193 | ")", "{", "}", "\n", 194 | LV_SYMBOL_OK, LOC_KEYBOARD_DEL, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT, ""}, 195 | {"[", "]", ";", "\n", 196 | "\"", "'", ".", "\n", 197 | ",", ":", " ", "\n", 198 | LV_SYMBOL_OK, LOC_KEYBOARD_DEL, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT, ""}}; 199 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* Blink Example 2 | This example code is in the Public Domain (or CC0 licensed, at your option.) 3 | Unless required by applicable law or agreed to in writing, this 4 | software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 5 | CONDITIONS OF ANY KIND, either express or implied. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include "freertos/timers.h" 12 | #include "freertos/queue.h" 13 | #include 14 | #include "sdkconfig.h" 15 | #include 16 | #include "config.h" 17 | #include 18 | #include "esp_wifi.h" 19 | #include "esp_sleep.h" 20 | #include 21 | #include "gui.h" 22 | #include "SPIFFS.h" 23 | #include "hardware/Wifi.h" 24 | #include "networking/signalk_socket.h" 25 | #include "esp_int_wdt.h" 26 | #include "esp_pm.h" 27 | #include "system/events.h" 28 | #include "hardware/hardware.h" 29 | #include 30 | using std::placeholders::_1; 31 | using std::placeholders::_2; 32 | #include "system/async_dispatcher.h" 33 | #include "sounds/sound_player.h" 34 | #include "sounds/beep.h" 35 | #include "ui/localization.h" 36 | #include "imgs/sk_image_low.h" 37 | 38 | const char *TAG = "APP"; 39 | TTGOClass *ttgo; 40 | WifiManager *wifiManager; 41 | SignalKSocket *sk_socket; 42 | Hardware *hardware; 43 | Gui *gui; 44 | 45 | #if LV_USE_LOG 46 | void lv_log_cb(lv_log_level_t level, const char * file, uint32_t line, const char * func, const char * dsc) 47 | { 48 | /*Send the logs via serial port*/ 49 | 50 | if(level == LV_LOG_LEVEL_ERROR) 51 | { 52 | ESP_LOGE("LVGL", "%s in %s:%d", dsc, file, line); 53 | } 54 | else if(level == LV_LOG_LEVEL_INFO) 55 | { 56 | ESP_LOGI("LVGL", "%s in %s:%d", dsc, file, line); 57 | } 58 | else if(level == LV_LOG_LEVEL_WARN) 59 | { 60 | ESP_LOGW("LVGL", "%s in %s:%d", dsc, file, line); 61 | } 62 | else 63 | { 64 | ESP_LOGI("LVGL", "%s in %s:%d", dsc, file, line); 65 | } 66 | } 67 | #endif 68 | 69 | void set_splash_screen_status(TTGOClass* watch, int percent, char*message = NULL) 70 | { 71 | auto y = 160; 72 | if(percent > 100) 73 | { 74 | percent = 100; 75 | } 76 | watch->tft->fillRect(21, y, percent * 2, 28, watch->tft->color565(0, 51, 153)); 77 | watch->tft->drawRect(19, y, 202, 30, TFT_WHITE); 78 | if(message != NULL) 79 | { 80 | watch->tft->fillRect(0, TFT_HEIGHT - 20, TFT_WIDTH, 20, TFT_BLACK); 81 | watch->tft->setTextColor(TFT_WHITE); 82 | watch->tft->setTextFont(2); 83 | watch->tft->setCursor(0, TFT_HEIGHT - 20); 84 | watch->tft->println(message); 85 | } 86 | 87 | ESP_LOGI(TAG, "Loader status=%d%%", percent); 88 | } 89 | 90 | void init_splash_screen(TTGOClass* watch) 91 | { 92 | watch->tft->setTextColor(TFT_WHITE); 93 | watch->tft->setTextFont(2); 94 | watch->tft->setCursor(0, TFT_HEIGHT - 20); 95 | watch->tft->println("Initializing..."); 96 | watch->tft->setCursor((TFT_WIDTH / 2) - 30, 130); 97 | watch->tft->println("TWatchSK"); 98 | watch->tft->setCursor(0, 0); 99 | watch->tft->print(LOC_WATCH_VERSION); 100 | watch->tft->drawXBitmap((TFT_WIDTH / 2) - (skIcon_width / 2), 60, skIcon_bits, skIcon_width, skIcon_height, watch->tft->color565(0, 51, 153)); 101 | 102 | set_splash_screen_status(watch, 10); 103 | } 104 | 105 | 106 | void setup() 107 | { 108 | #if !CONFIG_PM_ENABLE 109 | #error "CONFIG_PM_ENABLE missing" 110 | #endif 111 | #if !CONFIG_FREERTOS_USE_TICKLESS_IDLE 112 | #error "CONFIG_FREERTOS_USE_TICKLESS_IDLE missing" 113 | #endif 114 | #if LV_USE_LOG 115 | lv_log_register_print_cb(lv_log_cb); 116 | #endif 117 | 118 | ttgo = TTGOClass::getWatch(); 119 | initialize_events(); 120 | twatchsk::initialize_async(); 121 | //Initialize TWatch 122 | ttgo->begin(); 123 | #ifdef LILYGO_WATCH_2020_V2 124 | ttgo->power->setLDO2Voltage(3300); 125 | ttgo->power->setLDO3Voltage(3300); 126 | ttgo->power->setPowerOutPut(AXP202_LDO2, true); 127 | ttgo->power->setPowerOutPut(AXP202_LDO3, true); 128 | #endif 129 | ttgo->bl->on(); 130 | init_splash_screen(ttgo); 131 | ESP_LOGI(TAG, "Initializing SPIFFS..."); 132 | set_splash_screen_status(ttgo, 10, LOC_STARTUP_SPIFFS); 133 | if (!SPIFFS.begin(true)) 134 | { 135 | ESP_LOGE(TAG, "Failed to initialize SPIFFS!"); 136 | } 137 | set_splash_screen_status(ttgo, 30, LOC_STARTUP_HW_GUI); 138 | 139 | //initalize Hardware (power management, sensors and interupts) 140 | hardware = new Hardware(); 141 | hardware->initialize(ttgo); 142 | //Initialize lvgl 143 | if(ttgo->lvgl_begin()) 144 | { 145 | ESP_LOGI(TAG, "LVGL initialized!"); 146 | } 147 | else 148 | { 149 | ESP_LOGE(TAG, "Failed to initialize LVGL!"); 150 | return; 151 | } 152 | 153 | hardware->initialize_touch(); 154 | 155 | ESP_LOGI(TAG, "Touch initialized!"); 156 | //Synchronize time to system time 157 | ttgo->rtc->syncToSystem(); 158 | ESP_LOGI(TAG, "Time synced with RTC!"); 159 | set_splash_screen_status(ttgo, 40, LOC_STARTUP_NETWORKING); 160 | //Setting up the network 161 | wifiManager = new WifiManager(); 162 | //Setting up websocket 163 | sk_socket = new SignalKSocket(wifiManager); 164 | sk_socket->add_subscription("notifications.*", 1000, true); 165 | sk_socket->add_subscription("environment.mode", 5000, false); 166 | //Attach power management events to sk_socket 167 | hardware->attach_power_callback(std::bind(&SignalKSocket::handle_power_event, sk_socket, _1, _2)); 168 | set_splash_screen_status(ttgo, 60); 169 | //Intialize watch GUI 170 | gui = new Gui(); 171 | //Setup GUI 172 | gui->setup_gui(wifiManager, sk_socket, hardware); 173 | //set SK socket pointer to device name in gui 174 | sk_socket->set_device_name(gui->get_watch_name()); 175 | //Clear lvgl counter 176 | lv_disp_trig_activity(NULL); 177 | //When the initialization is complete, turn on the backlight 178 | ttgo->bl->adjust(gui->get_adjusted_display_brightness()); 179 | hardware->get_player()->play_raw_from_const("beep", beep_sound, beep_sound_len); 180 | set_splash_screen_status(ttgo, 90); 181 | 182 | #if CONFIG_PM_ENABLE 183 | // Configure dynamic frequency scaling: 184 | // maximum and minimum frequencies are set in sdkconfig, 185 | // automatic light sleep is enabled if tickless idle support is enabled. 186 | esp_pm_config_esp32_t pm_config = { 187 | .max_freq_mhz = 80, 188 | .min_freq_mhz = 10, 189 | #if CONFIG_FREERTOS_USE_TICKLESS_IDLE 190 | .light_sleep_enable = true 191 | #endif 192 | }; 193 | ESP_ERROR_CHECK(esp_pm_configure(&pm_config)); 194 | #endif // CONFIG_PM_ENABLE 195 | 196 | ESP_LOGI(TAG, "TWatch SK app initialized, Build date " __DATE__ ", GIT revision "); 197 | } 198 | void loop() 199 | { 200 | hardware->loop(); 201 | } 202 | 203 | void arduinoTask(void *pvParameter) 204 | { 205 | while (1) 206 | { 207 | loop(); 208 | } 209 | } 210 | 211 | void app_main() 212 | { 213 | esp_log_level_set("*", ESP_LOG_INFO); 214 | esp_log_level_set("WEBSOCKET_CLIENT", ESP_LOG_DEBUG); 215 | esp_log_level_set("TRANS_TCP", ESP_LOG_DEBUG); 216 | // initialize arduino library before we start the tasks 217 | initArduino(); 218 | setup(); 219 | xTaskCreate(&arduinoTask, "app_task", configMINIMAL_STACK_SIZE * 2, NULL, 5, NULL); 220 | } -------------------------------------------------------------------------------- /src/ui/statusbar.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.h" 3 | #include "system/observable.h" 4 | #include "ui/themes.h" 5 | #include 6 | #include 7 | 8 | enum StatusBarIconType 9 | { 10 | Text, 11 | Image 12 | }; 13 | 14 | enum StatusBarLocation 15 | { 16 | Left, 17 | Right 18 | }; 19 | 20 | enum StatusBarIconStatus 21 | { 22 | Visible, 23 | Warning, 24 | Hidden 25 | }; 26 | 27 | class StatusBarIcon 28 | { 29 | public: 30 | StatusBarIcon(lv_obj_t *parent, const char *text, StatusBarLocation location, std::function update_func, StatusBarIconStatus status) 31 | { 32 | type_ = StatusBarIconType::Text; 33 | obj_ = lv_label_create(parent, NULL); 34 | lv_label_set_text(obj_, text); 35 | update_func_ = update_func; 36 | location_ = location; 37 | set_status(status); 38 | } 39 | 40 | StatusBarIcon(lv_obj_t *parent, const void *image, StatusBarLocation location, std::function update_func, StatusBarIconStatus status) 41 | { 42 | type_ = StatusBarIconType::Image; 43 | obj_ = lv_img_create(parent, NULL); 44 | lv_img_set_src(obj_, image); 45 | twatchsk::update_imgbtn_color(obj_); 46 | update_func_ = update_func; 47 | location_ = location; 48 | set_status(status); 49 | } 50 | 51 | lv_obj_t *get_obj() 52 | { 53 | return obj_; 54 | } 55 | 56 | StatusBarLocation get_location() 57 | { 58 | return location_; 59 | } 60 | 61 | StatusBarIconStatus get_status() 62 | { 63 | return status_; 64 | } 65 | 66 | StatusBarIconType get_type() 67 | { 68 | return type_; 69 | } 70 | 71 | void set_text(const char *text) 72 | { 73 | if (type_ != StatusBarIconType::Image) 74 | { 75 | lv_label_set_text(obj_, text); 76 | update_func_(); 77 | } 78 | } 79 | 80 | void set_status(StatusBarIconStatus status) 81 | { 82 | if (status_ != status) 83 | { 84 | bool call_update = (status == StatusBarIconStatus::Hidden || status_ == StatusBarIconStatus::Hidden); 85 | 86 | if (status == StatusBarIconStatus::Hidden) 87 | { 88 | lv_obj_set_hidden(obj_, true); 89 | } 90 | else if (status == StatusBarIconStatus::Warning) 91 | { 92 | lv_obj_set_hidden(obj_, false); 93 | set_color(LV_COLOR_RED); 94 | } 95 | else //normal 96 | { 97 | lv_obj_set_hidden(obj_, false); 98 | set_color(twatchsk::get_text_color()); 99 | } 100 | 101 | status_ = status; 102 | 103 | if (call_update) //when status is changing from normal <--> warning we don't need update locations 104 | { 105 | update_func_(); 106 | } 107 | } 108 | } 109 | 110 | void set_color(lv_color_t color) 111 | { 112 | if (type_ == StatusBarIconType::Image) 113 | { 114 | lv_obj_set_style_local_image_recolor(obj_, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, color); 115 | } 116 | else 117 | { 118 | lv_obj_set_style_local_text_color(obj_, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, color); 119 | } 120 | } 121 | 122 | private: 123 | lv_obj_t *obj_ = NULL; 124 | StatusBarIconType type_; 125 | std::function update_func_; 126 | StatusBarLocation location_; 127 | StatusBarIconStatus status_; 128 | }; 129 | 130 | class StatusBar 131 | { 132 | public: 133 | StatusBar() 134 | { 135 | } 136 | 137 | StatusBarIcon *create_text_icon(char *initial, StatusBarLocation location, StatusBarIconStatus status = StatusBarIconStatus::Visible) 138 | { 139 | auto ret = new StatusBarIcon(_par, initial, location, std::bind(&StatusBar::refresh, this), status); 140 | icons_.push_back(ret); 141 | 142 | return ret; 143 | } 144 | 145 | StatusBarIcon *create_image_icon(const void *image_bytes, StatusBarLocation location, StatusBarIconStatus status = StatusBarIconStatus::Visible) 146 | { 147 | auto ret = new StatusBarIcon(_par, image_bytes, location, std::bind(&StatusBar::refresh, this), status); 148 | icons_.push_back(ret); 149 | return ret; 150 | } 151 | 152 | void setup(lv_obj_t *par) 153 | { 154 | _par = par; 155 | 156 | _bar = lv_cont_create(_par, NULL); 157 | lv_obj_set_size(_bar, LV_HOR_RES, _barHeight); 158 | lv_obj_set_style_local_radius(_bar, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); 159 | lv_obj_set_style_local_border_width(_bar, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); 160 | //lv_obj_set_style_local_border_side(_bar, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_BORDER_SIDE_BOTTOM); 161 | 162 | refresh(); 163 | theme_changed(); 164 | } 165 | 166 | void theme_changed() 167 | { 168 | for (int i = 0; i < icons_.size(); i++) 169 | { 170 | auto icon = icons_.at(i); 171 | if (icon->get_type() == StatusBarIconType::Image) 172 | { 173 | twatchsk::update_imgbtn_color(icon->get_obj()); 174 | } 175 | else if(icon->get_type() == StatusBarIconType::Text && icon->get_status() == StatusBarIconStatus::Visible) 176 | { 177 | icon->set_color(twatchsk::get_text_color()); 178 | } 179 | } 180 | } 181 | 182 | uint8_t height() 183 | { 184 | return _barHeight; 185 | } 186 | 187 | lv_obj_t *self() 188 | { 189 | return _bar; 190 | } 191 | 192 | void set_hidden(bool hidden) 193 | { 194 | lv_obj_set_hidden(_bar, hidden); 195 | } 196 | 197 | void dont_refresh() 198 | { 199 | refresh_hold_ = true; 200 | } 201 | 202 | void do_refresh() 203 | { 204 | refresh_hold_ = false; 205 | refresh(); 206 | } 207 | 208 | private: 209 | void refresh() 210 | { 211 | if (!refresh_hold_) 212 | { 213 | lv_obj_t *lastLeft = NULL; 214 | lv_obj_t *lastRight = NULL; 215 | for (int i = 0; i < icons_.size(); i++) 216 | { 217 | auto icon = icons_.at(i); 218 | if (icon->get_status() != StatusBarIconStatus::Hidden) 219 | { 220 | if (icon->get_location() == StatusBarLocation::Left) 221 | { 222 | if (lastLeft == NULL) 223 | { 224 | lv_obj_align(icon->get_obj(), _bar, LV_ALIGN_IN_LEFT_MID, iconOffset, 0); 225 | } 226 | else 227 | { 228 | lv_obj_align(icon->get_obj(), lastLeft, LV_ALIGN_OUT_RIGHT_MID, iconOffset, 0); 229 | } 230 | 231 | lastLeft = icon->get_obj(); 232 | } 233 | else 234 | { 235 | if (lastRight == NULL) 236 | { 237 | lv_obj_align(icon->get_obj(), _bar, LV_ALIGN_IN_RIGHT_MID, -iconOffset, 0); 238 | } 239 | else 240 | { 241 | lv_obj_align(icon->get_obj(), lastRight, LV_ALIGN_OUT_LEFT_MID, -iconOffset, 0); 242 | } 243 | 244 | lastRight = icon->get_obj(); 245 | } 246 | } 247 | } 248 | } 249 | }; 250 | lv_obj_t *_bar = nullptr; 251 | lv_obj_t *_par = nullptr; 252 | uint8_t _barHeight = 30; 253 | std::vector icons_; 254 | 255 | const int8_t iconOffset = 5; 256 | bool refresh_hold_ = false; 257 | }; 258 | -------------------------------------------------------------------------------- /src/ui/roller.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "localization.h" 4 | #include "config.h" 5 | #include "settings_view.h" 6 | 7 | /** 8 | * @brief Roller is a class for selecting an item from a scrolling list of items. 9 | * 10 | * Initially used to select the watch's timezone, it's now available for anything else. 11 | * 12 | * @param title The title at the top of the page. Set in localization.h 13 | * 14 | * @param type What the roller is used to select. To add a type, add it to the 15 | * enum just below, and define it similar to the private data member timezone_option_list_. 16 | * 17 | * @param starting_id The internal ID of the item in the list that you want to be displayed 18 | * when theh roller is created. 0 is the first item in the list, 1 is the second, etc. 19 | * 20 | * All of the code to set, save, and retrieve the timezone uses the internal ID. Only the 21 | * display of the timezone to the user converts it to human-friendly format. For example, 22 | * timezone ID 0 is displayed as "GMT-12:00". This is done with a function in time_settings.h 23 | * called get_timezone_string(). 24 | **/ 25 | 26 | enum RollerType_t 27 | { 28 | Timezone, 29 | Year, 30 | Month, 31 | Day28, 32 | Day30, 33 | Day31, 34 | Hours24, 35 | Hours12, 36 | Minutes 37 | }; 38 | 39 | class Roller : public SettingsView 40 | { 41 | public: 42 | Roller(char *title, RollerType_t type, uint16_t starting_id) : SettingsView(title) 43 | { 44 | roller_type_ = type; 45 | starting_id_ = starting_id; 46 | }; 47 | 48 | ~Roller() { }; 49 | 50 | virtual void show_internal(lv_obj_t *parent) override 51 | { 52 | lv_cont_set_layout(parent, LV_LAYOUT_CENTER); 53 | 54 | /*Create a roller and apply the styles*/ 55 | roller_widget_ = lv_roller_create(parent, NULL); 56 | lv_obj_align(roller_widget_, parent, LV_ALIGN_IN_TOP_MID, 10, 10); 57 | const char * options = NULL; 58 | 59 | if (roller_type_ == RollerType_t::Timezone) 60 | { 61 | options = timezone_option_list_; 62 | 63 | } 64 | else if(roller_type_ == RollerType_t::Year) 65 | { 66 | options = year_option_list_; 67 | } 68 | else if(roller_type_ == RollerType_t::Month) 69 | { 70 | options = LOC_MONTHS_FULL; 71 | } 72 | else if(roller_type_ == RollerType_t::Day31) 73 | { 74 | options = day31_option_list_; 75 | } 76 | else if(roller_type_ == RollerType_t::Day30) 77 | { 78 | options = day30_option_list_; 79 | } 80 | else if(roller_type_ == RollerType_t::Day28) 81 | { 82 | options = day28_option_list_; 83 | } 84 | else if(roller_type_ == RollerType_t::Hours12) 85 | { 86 | options = hours12_option_list_; 87 | } 88 | else if(roller_type_ == RollerType_t::Hours24) 89 | { 90 | options = hours24_option_list_; 91 | } 92 | else if(roller_type_ == RollerType_t::Minutes) 93 | { 94 | options = minutes_option_list_; 95 | } 96 | 97 | lv_roller_set_options(roller_widget_, options, LV_ROLLER_MODE_INIFINITE); 98 | // add more RollerType_t checking here as they are added 99 | lv_roller_set_selected(roller_widget_, starting_id_, LV_ANIM_OFF); 100 | //set selected id to starting_id_ 101 | selected_id_ = starting_id_; 102 | lv_roller_set_visible_row_count(roller_widget_, 5); // number of rows to make visible in the widget 103 | lv_obj_set_event_cb(roller_widget_, roller_widget_cb); 104 | //add OK button 105 | ok_button_ = lv_btn_create(topBar, NULL); 106 | lv_obj_set_style_local_border_width(ok_button_, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, 0); 107 | lv_obj_set_style_local_border_width(ok_button_, LV_BTN_PART_MAIN, LV_STATE_FOCUSED, 0); 108 | lv_obj_set_style_local_border_width(ok_button_, LV_BTN_PART_MAIN, LV_STATE_PRESSED, 0); 109 | lv_obj_set_style_local_radius(ok_button_, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, 0); 110 | lv_obj_set_style_local_radius(ok_button_, LV_BTN_PART_MAIN, LV_STATE_FOCUSED, 0); 111 | lv_obj_set_style_local_radius(ok_button_, LV_BTN_PART_MAIN, LV_STATE_PRESSED, 0); 112 | lv_obj_set_size(ok_button_, 40, lv_obj_get_height(topBar)); 113 | ok_button_->user_data = this; 114 | lv_obj_set_event_cb(ok_button_, ok_button_cb); 115 | //OK label on button 116 | auto lbl = lv_label_create(ok_button_, NULL); 117 | lv_label_set_text(lbl, LOC_MESSAGEBOX_OK); 118 | 119 | lv_obj_align(ok_button_, topBar, LV_ALIGN_IN_TOP_RIGHT, 0, 0); 120 | 121 | roller_widget_->user_data = this; 122 | } 123 | 124 | virtual bool hide_internal() override 125 | { 126 | return true; 127 | } 128 | 129 | bool is_success() 130 | { 131 | return success_; 132 | } 133 | 134 | uint16_t get_selected_id() 135 | { 136 | return selected_id_; 137 | } 138 | 139 | void set_selected_id(uint16_t new_selected_id) 140 | { 141 | selected_id_ = new_selected_id; 142 | } 143 | 144 | void theme_changed() override 145 | { 146 | twatchsk::update_imgbtn_color(back); // make the "Back" button the correct color depending on the theme 147 | 148 | } 149 | 150 | private: 151 | lv_obj_t *roller_widget_; 152 | lv_obj_t *ok_button_; 153 | RollerType_t roller_type_; 154 | uint16_t selected_id_; 155 | bool success_ = false; 156 | const char* timezone_option_list_ = 157 | "GMT-12:00\n" "GMT-11:00\n" "GMT-10:00\n" "GMT-9:00\n" "GMT-8:00\n" "GMT-7:00\n" "GMT-6:00\n" 158 | "GMT-5:00\n" "GMT-4:00\n" "GMT-3:00\n" "GMT-2:00\n" "GMT-1:00\n" "GMT0\n" "GMT+1:00\n" "GMT+2:00\n" 159 | "GMT+3:00\n" "GMT+4:00\n" "GMT+5:00\n" "GMT+6:00\n" "GMT+7:00\n" "GMT+8:00\n" "GMT+9:00\n" 160 | "GMT+10:00\n" "GMT+11:00\n" "GMT+12:00"; 161 | uint16_t starting_id_ = 12; // equates to GMT0 162 | 163 | const char* year_option_list_ = 164 | "2022\n" "2023\n" "2024\n" 165 | "2025\n" "2026\n" "2027\n" 166 | "2028\n" "2029\n" "2030"; 167 | 168 | const char * day31_option_list_ = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31"; 169 | const char * day30_option_list_ = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30"; 170 | const char * day28_option_list_ = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29"; 171 | const char * minutes_option_list_ = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59"; 172 | const char * hours24_option_list_ = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23"; 173 | const char * hours12_option_list_ = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12"; 174 | 175 | static void roller_widget_cb(lv_obj_t *obj, lv_event_t event) 176 | { 177 | if (event == LV_EVENT_VALUE_CHANGED) 178 | { 179 | const uint16_t sel_id = lv_roller_get_selected(obj); 180 | Roller* roller_ptr = ((Roller*)obj->user_data); 181 | roller_ptr->selected_id_ = sel_id; 182 | 183 | ESP_LOGI("ROLLER", "SelectedId=%d", sel_id); 184 | } 185 | } 186 | 187 | static void ok_button_cb(lv_obj_t *obj, lv_event_t event) 188 | { 189 | ESP_LOGI("ROLLER", "OK_BUTTON event=%d", event); 190 | 191 | if (event == LV_EVENT_CLICKED) 192 | { 193 | Roller* roller_ptr = ((Roller*)obj->user_data); 194 | roller_ptr->success_ = true; 195 | roller_ptr->hide(); 196 | } 197 | } 198 | }; 199 | 200 | 201 | -------------------------------------------------------------------------------- /src/ui/display_settings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "settings_view.h" 3 | #include "localization.h" 4 | #include "keyboard.h" 5 | #include "themes.h" 6 | #include "ui/loader.h" 7 | #include "networking/http_request.h" 8 | #include "networking/signalk_socket.h" 9 | #include "system/async_dispatcher.h" 10 | #include "ui_ticker.h" 11 | 12 | /** 13 | * @brief Used to set the screen timeout (sleep time) and 14 | * turn the Dark Theme on or off. 15 | **/ 16 | 17 | class DisplaySettings : public SettingsView 18 | { 19 | public: 20 | DisplaySettings(TTGOClass *watch, SignalKSocket *socket) : SettingsView(LOC_DISPLAY_SETTINGS) 21 | { 22 | watch_ = watch; 23 | socket_ = socket; 24 | } 25 | 26 | void update_timeout(int timeout_seconds) // for when user changes the screen timeout value 27 | { 28 | screen_timeout_ = timeout_seconds; 29 | lv_label_set_text_fmt(timeoutLabel_, "%d", screen_timeout_); 30 | ESP_LOGI(SETTINGS_TAG, "User set screen timeout to %d", screen_timeout_); 31 | } 32 | 33 | void update_brightness(uint8_t new_brightness_level) // for when the display brightness value changes 34 | { 35 | display_brightness_ = new_brightness_level; 36 | uint8_t adjusted_brightness = display_brightness_; 37 | if (adjusted_brightness == 1) 38 | { 39 | adjusted_brightness = 10; // minimum readable level in bright light 40 | } 41 | else 42 | { 43 | adjusted_brightness = (adjusted_brightness - 1) * 63; 44 | } 45 | watch_->bl->adjust(adjusted_brightness); 46 | ESP_LOGI(SETTINGS_TAG, "User set display brightness to %d", display_brightness_); 47 | } 48 | 49 | int get_screen_timeout() { return screen_timeout_; } 50 | void set_screen_timeout(int value) 51 | { 52 | screen_timeout_ = value; 53 | } 54 | 55 | int get_display_brightness() { return display_brightness_; } 56 | void set_display_brightness(uint8_t value) 57 | { 58 | display_brightness_ = value; 59 | } 60 | 61 | void theme_changed() override 62 | { 63 | twatchsk::update_imgbtn_color(back); 64 | update_brightness(twatchsk::dark_theme_enabled ? 1 : 5); 65 | if (twatchsk::dark_theme_enabled) 66 | { 67 | lv_switch_on(dark_switch_, LV_ANIM_OFF); 68 | } 69 | else 70 | { 71 | lv_switch_off(dark_switch_, LV_ANIM_OFF); 72 | } 73 | } 74 | 75 | protected: 76 | virtual void show_internal(lv_obj_t *parent) override 77 | { 78 | lv_cont_set_layout(parent, LV_LAYOUT_OFF); 79 | 80 | static lv_style_t buttonStyle; 81 | lv_style_init(&buttonStyle); 82 | lv_style_set_radius(&buttonStyle, LV_STATE_DEFAULT, 10); 83 | 84 | screenTimeoutLabel_ = lv_label_create(parent, NULL); 85 | lv_obj_set_pos(screenTimeoutLabel_, 4, 10); 86 | lv_label_set_text(screenTimeoutLabel_, LOC_SCREEN_TIMEOUT); 87 | timeoutButton_ = lv_btn_create(parent, NULL); 88 | lv_obj_add_style(timeoutButton_, LV_OBJ_PART_MAIN, &buttonStyle); 89 | lv_obj_align(timeoutButton_, screenTimeoutLabel_, LV_ALIGN_OUT_RIGHT_MID, 10, 0); 90 | timeoutLabel_ = lv_label_create(timeoutButton_, NULL); 91 | lv_label_set_text_fmt(timeoutLabel_, "%d", screen_timeout_); 92 | lv_obj_set_event_cb(timeoutButton_, timeout_button_callback); 93 | lv_obj_set_width(timeoutButton_, 50); 94 | 95 | dark_switch_ = lv_switch_create(parent, NULL); 96 | lv_obj_align(dark_switch_, screenTimeoutLabel_, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 12); 97 | if (lv_theme_get_flags() & LV_THEME_MATERIAL_FLAG_DARK) // is the theme's "dark" version currently on? 98 | { 99 | lv_switch_on(dark_switch_, LV_ANIM_OFF); // set the switch widget to "ON" 100 | } 101 | dark_switch_label_ = lv_label_create(parent, NULL); 102 | lv_obj_align(dark_switch_label_, dark_switch_, LV_ALIGN_OUT_RIGHT_MID, 4, 0); 103 | lv_label_set_text(dark_switch_label_, LOC_DARK_SWITCH_LABEL); 104 | if (twatchsk::dark_theme_enabled) 105 | { 106 | lv_switch_on(dark_switch_, LV_ANIM_OFF); 107 | uint32_t flag = LV_THEME_MATERIAL_FLAG_DARK; 108 | LV_THEME_DEFAULT_INIT(lv_theme_get_color_primary(), lv_theme_get_color_secondary(), flag, 109 | lv_theme_get_font_small(), lv_theme_get_font_normal(), lv_theme_get_font_subtitle(), 110 | lv_theme_get_font_title()); 111 | } 112 | lv_obj_set_event_cb(dark_switch_, dark_switch_cb); 113 | 114 | download_ui_button_ = lv_btn_create(parent, NULL); 115 | lv_obj_t *downloadLabel = lv_label_create(download_ui_button_, NULL); 116 | lv_obj_set_width(download_ui_button_, 230); 117 | lv_obj_align(download_ui_button_, dark_switch_, LV_ALIGN_OUT_BOTTOM_LEFT, 1, 8); 118 | lv_obj_add_style(download_ui_button_, LV_OBJ_PART_MAIN, &buttonStyle); 119 | lv_label_set_text(downloadLabel, LOC_DISPLAY_DOWNLOAD_UI); 120 | lv_obj_set_event_cb(download_ui_button_, download_button_cb); 121 | 122 | download_ui_button_->user_data = this; 123 | timeoutButton_->user_data = this; 124 | dark_switch_->user_data = this; 125 | } 126 | 127 | virtual bool hide_internal() override 128 | { 129 | return true; 130 | } 131 | 132 | private: 133 | TTGOClass *watch_; 134 | SignalKSocket *socket_; 135 | lv_obj_t *screenTimeoutLabel_; 136 | lv_obj_t *timeoutButton_; 137 | lv_obj_t *timeoutLabel_; 138 | int screen_timeout_ = 10; // multiply by 1000 in main.cpp to make this "seconds" 139 | uint8_t display_brightness_; 140 | lv_obj_t *dark_switch_; 141 | lv_obj_t *dark_switch_label_; 142 | lv_obj_t *download_ui_button_; 143 | 144 | static void timeout_button_callback(lv_obj_t *obj, lv_event_t event) 145 | { 146 | if (event == LV_EVENT_CLICKED) 147 | { 148 | DisplaySettings *settings = (DisplaySettings *)obj->user_data; 149 | auto keyboard = new Keyboard(LOC_INPUT_SCREEN_TIMEOUT, KeyboardType_t::Number, 2); 150 | keyboard->on_close([keyboard, settings]() { 151 | if (keyboard->is_success()) 152 | { 153 | const char *text = keyboard->get_text(); 154 | 155 | int timeout = atoi(text); 156 | if (timeout >= 5) 157 | { 158 | settings->update_timeout(timeout); 159 | } 160 | } 161 | delete keyboard; 162 | }); 163 | keyboard->show(lv_scr_act()); 164 | } 165 | } 166 | 167 | static void dark_switch_cb(lv_obj_t *obj, lv_event_t event) 168 | { 169 | if (event == LV_EVENT_VALUE_CHANGED) 170 | { 171 | DisplaySettings* settings = (DisplaySettings* )obj->user_data; 172 | uint32_t flag = lv_switch_get_state(obj) ? LV_THEME_MATERIAL_FLAG_DARK : LV_THEME_MATERIAL_FLAG_LIGHT; // create theme flag that matches current state of the Dark Theme switch 173 | twatchsk::dark_theme_enabled = !twatchsk::dark_theme_enabled; // switch value changed, so save the changed value 174 | LV_THEME_DEFAULT_INIT(lv_theme_get_color_primary(), lv_theme_get_color_secondary(), flag, 175 | lv_theme_get_font_small(), lv_theme_get_font_normal(), lv_theme_get_font_subtitle(), 176 | lv_theme_get_font_title()); 177 | twatchsk::update_imgbtn_color(settings->back); 178 | uint8_t new_brightness_level = (twatchsk::dark_theme_enabled == true ? 1 : 5); 179 | settings->update_brightness(new_brightness_level); 180 | } 181 | } 182 | 183 | static void download_button_cb(lv_obj_t *obj, lv_event_t event) 184 | { 185 | if (event == LV_EVENT_CLICKED) 186 | { 187 | auto settings = (DisplaySettings *)obj->user_data; 188 | 189 | settings->download_ui_from_server(); 190 | } 191 | } 192 | 193 | void download_ui_from_server() 194 | { 195 | if (socket_->get_token() != "" && socket_->get_state() == WebsocketState_t::WS_Connected) 196 | { 197 | ESP_LOGI(SETTINGS_TAG, "About to start downloading Dynamic UI from SK Server..."); 198 | 199 | //auto loader = new Loader(LOC_DISPLAY_DOWNLOADING_UI); 200 | bool done = false; 201 | /*UITicker* ticker = NULL; 202 | ticker = new UITicker(1000, [loader, &done, ticker]() 203 | { 204 | ESP_LOGI(SETTINGS_TAG, "Downloading status=%d", done); 205 | if(done) 206 | { 207 | delete loader; 208 | delete ticker; 209 | } 210 | });*/ 211 | auto address = socket_->get_server_address(); 212 | int port = socket_->get_server_port(); 213 | auto token = socket_->get_token(); 214 | 215 | /*twatchsk::run_async("Dynamic UI download", [address, port, token, &done]() 216 | {*/ 217 | char requestUri[192]; 218 | ESP_LOGI(SETTINGS_TAG, "Server: %s:%d", address.c_str(), port); 219 | sprintf(requestUri, "http://%s:%d/signalk/v1/applicationData/global/twatch/1.0/ui/default", address.c_str(), port); 220 | ESP_LOGI(SETTINGS_TAG, "Will be downloading from URI: %s", requestUri); 221 | auto http = new JsonHttpRequest(requestUri, token.c_str()); 222 | if (http->downloadFile("/sk_view.json")) 223 | { 224 | show_message(LOC_DISPLAY_DOWNLOAD_UI_DONE); 225 | auto ticker = new UITicker(1000, []() { 226 | static int countDown = 5; 227 | countDown--; 228 | if (countDown < 0) 229 | { 230 | esp_restart(); 231 | } 232 | }); 233 | } 234 | else 235 | { 236 | show_message(LOC_DISPLAY_DOWNLOAD_UI_ERROR); 237 | } 238 | //done = true; 239 | //}); 240 | } 241 | else 242 | { 243 | show_message(LOC_DISPLAY_DOWNLOAD_UI_NO_CONNECTION); 244 | } 245 | } 246 | }; -------------------------------------------------------------------------------- /src/hardware/Wifi.cpp: -------------------------------------------------------------------------------- 1 | #include "Wifi.h" 2 | #include 3 | #include "freertos/FreeRTOS.h" 4 | #include "freertos/task.h" 5 | #include "freertos/event_groups.h" 6 | #include "esp_system.h" 7 | #include "esp_event.h" 8 | #include "esp_log.h" 9 | #include "nvs_flash.h" 10 | 11 | #include "lwip/err.h" 12 | #include "lwip/sys.h" 13 | #include "system/events.h" 14 | #include "system/async_dispatcher.h" 15 | 16 | const char *WIFI_TAG = "WIFI"; 17 | static bool scan_done = false; 18 | static bool scan_running = false; 19 | static wifi_ap_record_t ap_info[WIFI_AP_LIST_MAX_SIZE]; 20 | static uint16_t ap_count = 0; 21 | 22 | void WifiManager::wifi_event_handler(void *arg, esp_event_base_t event_base, 23 | int32_t event_id, void *event_data) 24 | { 25 | auto manager = (WifiManager *)arg; 26 | 27 | if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) 28 | { 29 | esp_wifi_connect(); 30 | } 31 | else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) 32 | { 33 | wifi_event_sta_disconnected_t *disconnect_event = (wifi_event_sta_disconnected_t *)event_data; 34 | auto status = manager->value; 35 | ESP_LOGI(WIFI_TAG, "wifi_event_handler(): Wifi disconnected with reason=%d, status=%d", disconnect_event->reason, status); 36 | if (!manager->forced_disconnect_) // this is not an intentional wifi disconnect 37 | { 38 | if (manager->is_enabled()) 39 | { 40 | if (manager->value == WifiState_t::Wifi_Connecting) 41 | { 42 | post_gui_warning(GuiMessageCode_t::GUI_WARN_WIFI_CONNECTION_FAILED); 43 | } 44 | else // manager->value is WifiState_t::Connected 45 | { 46 | post_gui_warning(GuiMessageCode_t::GUI_WARN_WIFI_DISCONNECTED); 47 | } 48 | xTaskCreate(&wifi_reconnect_task, "wifi reconnect task", 2048, manager, 5, NULL); 49 | } 50 | } 51 | 52 | manager->update_status(Wifi_Disconnected); 53 | manager->forced_disconnect_ = false; // reset to the normal "waiting for the next disconnect" state 54 | } 55 | else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) 56 | { 57 | ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; 58 | ESP_LOGI(WIFI_TAG, "wifi_event_handler(): Got IP: " IPSTR, IP2STR(&event->ip_info.ip)); 59 | char buff[24]; 60 | sprintf(buff, IPSTR, IP2STR(&event->ip_info.ip)); 61 | manager->set_ip(String(buff)); 62 | manager->update_status(Wifi_Connected); 63 | //BS: add a new message here: "Wifi reconnected after X attempts" 64 | //post_gui_warning(GuiMessageCode_t::GUI_WARN_WIFI_RECONNECTED); 65 | manager->wifi_retry_counter_ = 0; // to be ready for the next disconnect 66 | 67 | if (!manager->is_known_wifi(manager->ssid_)) //add wifi to known list - we need to save it later on 68 | { 69 | KnownWifi_t wifi; 70 | wifi.known_ssid = manager->ssid_; 71 | wifi.known_password = manager->password_; 72 | manager->known_wifi_list_.push_back(wifi); 73 | } 74 | } 75 | else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) 76 | { 77 | ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_count)); 78 | ESP_LOGI(WIFI_TAG, "wifi_event_handler(): Scan complete. Total APs found = %u", ap_count); 79 | ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_count, ap_info)); 80 | scan_running = false; 81 | scan_done = true; 82 | } 83 | } 84 | 85 | void WifiManager::wifi_reconnect_task(void *pvParameter) 86 | { 87 | auto manager = (WifiManager*)pvParameter; 88 | esp_wifi_disconnect(); 89 | esp_wifi_stop(); 90 | //manager->off(); //if wifi is turned off this way, the "Disable Wifi" button on the message box doesn't work 91 | manager->update_status(Wifi_Disconnected); 92 | ESP_LOGI(WIFI_TAG, "wifi_reconnect_task(): wifi_retry_counter is now %d", manager->wifi_retry_counter_); 93 | float wifi_retry_time = manager->wifi_retry_counter_ < WIFI_RETRY_ARRAY_SIZE ? manager->wifi_retry_minutes_[manager->wifi_retry_counter_] : WIFI_RETRY_MAX_MINUTES; 94 | ESP_LOGI(WIFI_TAG, "wifi_reconnect_task(): Will try to reconnect to wifi in %.1f minutes...", wifi_retry_time); 95 | vTaskDelay((60000 / portTICK_RATE_MS) * wifi_retry_time); 96 | if (manager->is_enabled()) 97 | { 98 | manager->wifi_retry_counter_++; 99 | ESP_LOGI(WIFI_TAG, "wifi_reconnect_task(): Reconnect attempt %d", manager->wifi_retry_counter_); 100 | manager->connect(); 101 | } 102 | //remove this task 103 | vTaskDelete(NULL); 104 | } 105 | 106 | int WifiManager::found_wifi_count() 107 | { 108 | return ap_count; 109 | } 110 | 111 | bool WifiManager::is_scan_complete() 112 | { 113 | return scan_done; 114 | } 115 | 116 | void WifiManager::initialize() 117 | { 118 | if (!initialized_) 119 | { 120 | ESP_LOGI(WIFI_TAG, "WifiManager::initialize(): Initializing wifi..."); 121 | tcpip_adapter_init(); 122 | ESP_ERROR_CHECK(esp_event_loop_create_default()); 123 | wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 124 | cfg.nvs_enable = false; 125 | ESP_ERROR_CHECK(esp_wifi_init(&cfg)); 126 | ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiManager::wifi_event_handler, this)); 127 | ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &WifiManager::wifi_event_handler, this)); 128 | ESP_LOGI(WIFI_TAG, "WifiManager::initialize(): Wifi is initialized!"); 129 | initialized_ = true; 130 | } 131 | } 132 | 133 | static void wifi_enable(const char *ssid, const char *password) 134 | { 135 | wifi_config_t wifi_config; 136 | memset(&wifi_config, 0, sizeof(wifi_config_t)); 137 | strcpy(reinterpret_cast(wifi_config.sta.ssid), ssid); 138 | strcpy(reinterpret_cast(wifi_config.sta.password), password); 139 | wifi_config.sta.listen_interval = 9; 140 | 141 | ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); 142 | ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); 143 | ESP_ERROR_CHECK(esp_wifi_start()); 144 | 145 | ESP_LOGI(WIFI_TAG, "wifi_enable(): esp_wifi_set_ps()."); 146 | esp_wifi_set_ps(WIFI_PS_MAX_MODEM); 147 | } 148 | 149 | static void disable_wifi() 150 | { 151 | esp_wifi_disconnect(); 152 | esp_wifi_stop(); 153 | //esp_wifi_deinit(); 154 | } 155 | 156 | void WifiManager::clear_wifi_list() 157 | { 158 | ap_count = 0; 159 | } 160 | 161 | WifiManager::WifiManager() : Configurable("/config/wifi"), SystemObject("wifi"), Observable(Wifi_Off) 162 | { 163 | load(); 164 | initialize(); 165 | 166 | if (enabled_) 167 | { 168 | on(); 169 | } 170 | } 171 | 172 | void WifiManager::on() 173 | { 174 | if (!ssid_.isEmpty()) 175 | { 176 | enabled_ = true; 177 | wifi_enable(ssid_.c_str(), password_.c_str()); 178 | ESP_LOGI(WIFI_TAG, "WifiManager::on(): Wifi has been enabled, SSID=%s.", ssid_.c_str()); 179 | update_status(Wifi_Connecting); 180 | } 181 | else 182 | { 183 | ESP_LOGW(WIFI_TAG, "WifiManager::on(): No SSID is configured!"); 184 | this->off(); 185 | this->configured_ = false; 186 | } 187 | } 188 | 189 | void WifiManager::off(bool force) 190 | { 191 | if (enabled_ || force) 192 | { 193 | enabled_ = false; 194 | disable_wifi(); 195 | ESP_LOGI(WIFI_TAG, "WifiManager::off(): Wifi has been disabled."); 196 | this->update_status(Wifi_Off); 197 | } 198 | } 199 | 200 | void WifiManager::save_config_to_file(JsonObject &json) 201 | { 202 | ESP_LOGI(WIFI_TAG, "Storing SSID %s to JSON.", ssid_.c_str()); 203 | json["enabled"] = enabled_; 204 | json["ssid"] = ssid_; 205 | json["password"] = password_; 206 | JsonArray knownList = json.createNestedArray("known"); 207 | for (int i = 0; i < known_wifi_list_.size(); i++) 208 | { 209 | JsonObject known = knownList.createNestedObject(); 210 | auto wifiInfo = known_wifi_list_.at(i); 211 | known["ssid"] = wifiInfo.known_ssid; 212 | known["password"] = wifiInfo.known_password; 213 | } 214 | } 215 | 216 | void WifiManager::load_config_from_file(const JsonObject &json) 217 | { 218 | enabled_ = json["enabled"].as(); 219 | setup(json["ssid"].as(), json["password"].as()); 220 | 221 | if (json.containsKey("known")) 222 | { 223 | JsonArray knownList = json["known"].as(); 224 | 225 | for (JsonObject known : knownList) 226 | { 227 | KnownWifi_t wifiInfo; 228 | wifiInfo.known_ssid = known["ssid"].as(); 229 | wifiInfo.known_password = known["password"].as(); 230 | known_wifi_list_.push_back(wifiInfo); 231 | } 232 | } 233 | } 234 | 235 | void WifiManager::setup(String ssid, String password) 236 | { 237 | ssid_ = ssid; 238 | password_ = password; 239 | ESP_LOGI(WIFI_TAG, "SSID has been updated to %s with password ******.", ssid.c_str()); 240 | configured_ = !ssid.isEmpty(); 241 | } 242 | 243 | bool WifiManager::scan_wifi() 244 | { 245 | bool ret = false; 246 | if (!scan_running) 247 | { 248 | scan_running = true; 249 | initialize(); 250 | clear_wifi_list(); 251 | scan_done = false; 252 | ESP_LOGI(WIFI_TAG, "WifiManager::scan_wifi(): Scanning nearby Wifi state=%d...", (int)value); 253 | if (value == WifiState_t::Wifi_Connected) 254 | { 255 | ESP_ERROR_CHECK(esp_wifi_scan_start(NULL, false)); 256 | ret = true; 257 | } 258 | else if (value == WifiState_t::Wifi_Disconnected || value == WifiState_t::Wifi_Off) 259 | { 260 | ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); 261 | ESP_ERROR_CHECK(esp_wifi_start()); 262 | delay(150); 263 | ESP_ERROR_CHECK(esp_wifi_disconnect()); 264 | delay(150); 265 | ESP_ERROR_CHECK(esp_wifi_scan_start(NULL, false)); 266 | ret = true; 267 | } 268 | } 269 | else 270 | { 271 | ESP_LOGE(WIFI_TAG, "Scan is already in progress!"); 272 | } 273 | 274 | return ret; 275 | } 276 | 277 | const wifi_ap_record_t WifiManager::get_found_wifi(int index) 278 | { 279 | return ap_info[index]; 280 | } 281 | 282 | void WifiManager::connect() 283 | { 284 | if (ssid_ != "") 285 | { 286 | if (get_status() == WifiState_t::Wifi_Connecting || get_status() == WifiState_t::Wifi_Connected) 287 | { 288 | forced_disconnect_ = true; // to prevent "Wifi disconnected" messages from appearing on the watch for an "on-purpose" disconnect 289 | } 290 | 291 | off(true); //let's force calling disable wifi 292 | delay(100); //let the driver stop wifi 293 | on(); 294 | } 295 | else 296 | { 297 | configured_ = false; 298 | } 299 | } 300 | 301 | bool WifiManager::is_known_wifi(const String ssid) 302 | { 303 | bool ret = false; 304 | 305 | for (KnownWifi_t wifi : known_wifi_list_) 306 | { 307 | if (wifi.known_ssid == ssid) 308 | { 309 | ret = true; 310 | break; 311 | } 312 | } 313 | 314 | return ret; 315 | } 316 | 317 | bool WifiManager::get_known_wifi_password(const String ssid, String &password) 318 | { 319 | bool ret = false; 320 | 321 | for (KnownWifi_t wifi : known_wifi_list_) 322 | { 323 | if (wifi.known_ssid == ssid) 324 | { 325 | password = wifi.known_password; 326 | ret = true; 327 | break; 328 | } 329 | } 330 | 331 | return ret; 332 | } 333 | 334 | int WifiManager::get_wifi_rssi() 335 | { 336 | int ret = 0; 337 | wifi_ap_record_t info; 338 | if (esp_wifi_sta_get_ap_info(&info) == ESP_OK) 339 | { 340 | ret = info.rssi; 341 | } 342 | 343 | return ret; 344 | } -------------------------------------------------------------------------------- /src/ui/signalk_settings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "settings_view.h" 3 | #include "localization.h" 4 | #include "networking/signalk_socket.h" 5 | #include "ui/keyboard.h" 6 | #include "loader.h" 7 | #include "ui_ticker.h" 8 | #include "mdns.h" 9 | #include "system/async_dispatcher.h" 10 | #include "system/events.h" 11 | 12 | class SignalKSettings : public SettingsView 13 | { 14 | public: 15 | SignalKSettings(SignalKSocket *socket) : SettingsView(LOC_SIGNALK_SETTINGS) 16 | { 17 | sk_socket_ = socket; 18 | } 19 | 20 | protected: 21 | virtual void show_internal(lv_obj_t *parent) override 22 | { 23 | //enable layout into column on left side of the screen 24 | lv_cont_set_layout(parent, LV_LAYOUT_OFF); 25 | //show status label - connected/connecting/authorization pending/disconnected 26 | status_label_ = lv_label_create(parent, NULL); 27 | lv_obj_set_pos(status_label_, 10, 6); 28 | 29 | static lv_style_t buttonStyle; 30 | lv_style_init(&buttonStyle); 31 | lv_style_set_radius(&buttonStyle, LV_STATE_DEFAULT, 10); 32 | 33 | //add address and port changing buttons - we will use keyboard for input! 34 | server_address_button_ = lv_btn_create(parent, NULL); 35 | server_address_button_->user_data = this; 36 | lv_obj_set_event_cb(server_address_button_, change_server_address_event); 37 | lv_obj_add_style(server_address_button_, LV_OBJ_PART_MAIN, &buttonStyle); 38 | lv_obj_set_size(server_address_button_, 140, 38); 39 | lv_obj_align(server_address_button_, status_label_, LV_ALIGN_OUT_BOTTOM_LEFT, 6, 27); 40 | server_address_label_ = lv_label_create(server_address_button_, NULL); 41 | 42 | //server port 43 | server_port_button_ = lv_btn_create(parent, NULL); 44 | server_port_label_ = lv_label_create(server_port_button_, NULL); 45 | lv_obj_add_style(server_port_button_, LV_OBJ_PART_MAIN, &buttonStyle); 46 | lv_obj_set_size(server_port_button_, 60, 38); 47 | lv_obj_set_event_cb(server_port_button_, change_server_port_event); 48 | server_port_button_->user_data = this; 49 | 50 | //find sk with mDNS service 51 | find_button_ = lv_btn_create(parent, NULL); 52 | lv_obj_add_style(find_button_, LV_OBJ_PART_MAIN, &buttonStyle); 53 | lv_obj_set_size(find_button_, 215, 38); 54 | lv_obj_align(find_button_, server_address_button_, LV_ALIGN_OUT_BOTTOM_LEFT, -4, 8); 55 | find_button_label_ = lv_label_create(find_button_, NULL); 56 | lv_label_set_text(find_button_label_, LOC_SIGNALK_FIND_SERVER); 57 | find_button_->user_data = this; 58 | lv_obj_set_event_cb(find_button_, __scan_event); 59 | 60 | //reset token button - only visible if token is present 61 | token_reset_button_ = lv_btn_create(parent, NULL); 62 | lv_obj_add_style(token_reset_button_, LV_OBJ_PART_MAIN, &buttonStyle); 63 | lv_obj_set_size(token_reset_button_, 140, 38); 64 | lv_obj_align(token_reset_button_, find_button_, LV_ALIGN_OUT_BOTTOM_MID, 0, 8); 65 | auto resetLabel = lv_label_create(token_reset_button_, NULL); 66 | lv_label_set_text(resetLabel, LOC_SIGNALK_TOKEN_RESET); 67 | token_reset_button_->user_data = this; 68 | lv_obj_set_event_cb(token_reset_button_, __token_reset_event); 69 | 70 | //update current configuration to class variables 71 | server_address_ = sk_socket_->get_server_address(); 72 | server_port_ = sk_socket_->get_server_port(); 73 | 74 | //update current status 75 | update_sk_info(); 76 | update_server_info(); 77 | 78 | status_update_ticker_ = new UITicker(1000, [this]() { 79 | this->update_sk_info(); 80 | }); 81 | } 82 | 83 | virtual bool hide_internal() override 84 | { 85 | delete status_update_ticker_; 86 | status_update_ticker_ = NULL; 87 | return true; 88 | } 89 | 90 | void update_server_info() 91 | { 92 | if (server_address_ == "") 93 | { 94 | lv_label_set_text_fmt(server_address_label_, LOC_SIGNALK_ADDRESS_EMPTY); 95 | } 96 | else 97 | { 98 | lv_label_set_text(server_address_label_, server_address_.c_str()); 99 | } 100 | 101 | lv_label_set_text_fmt(server_port_label_, "%d", server_port_); 102 | lv_obj_align(server_port_button_, server_address_button_, LV_ALIGN_OUT_RIGHT_MID, 4, 0); 103 | } 104 | 105 | void set_server(const char *server) 106 | { 107 | server_address_ = String(server); 108 | signalk_changed_ = true; 109 | update_server_info(); 110 | if(server_port_ != 0 && server_address_ != "") 111 | { 112 | apply_signalk_settings(); 113 | } 114 | } 115 | 116 | void set_port(int port) 117 | { 118 | server_port_ = port; 119 | signalk_changed_ = true; 120 | update_server_info(); 121 | if(server_port_ != 0 && server_address_ != "") 122 | { 123 | apply_signalk_settings(); 124 | } 125 | } 126 | 127 | private: 128 | SignalKSocket *sk_socket_; 129 | lv_obj_t *status_label_; 130 | lv_obj_t *find_button_; 131 | lv_obj_t *find_button_label_; 132 | lv_obj_t *server_address_button_; 133 | lv_obj_t *server_address_label_; 134 | lv_obj_t *server_port_button_; 135 | lv_obj_t *server_port_label_; 136 | lv_obj_t *token_reset_button_; 137 | bool server_search_running_ = false; 138 | bool server_search_completed_ = false; 139 | Loader *search_loader_ = NULL; 140 | UITicker *status_update_ticker_; 141 | String server_address_; 142 | int server_port_ = 3000; 143 | bool searching_sk_ = false; 144 | bool signalk_changed_ = false; 145 | 146 | static void change_server_address_event(lv_obj_t *obj, lv_event_t event) 147 | { 148 | if (event == LV_EVENT_CLICKED) 149 | { 150 | auto *settings = (SignalKSettings *)obj->user_data; 151 | auto keyboard = new Keyboard(LOC_SIGNALK_INPUT_ADDRESS, KeyboardType_t::Normal); 152 | keyboard->on_close([keyboard, settings]() { 153 | if (keyboard->is_success()) 154 | { 155 | settings->set_server(keyboard->get_text()); 156 | } 157 | delete keyboard; 158 | }); 159 | keyboard->show(lv_scr_act()); 160 | } 161 | } 162 | 163 | static void change_server_port_event(lv_obj_t *obj, lv_event_t event) 164 | { 165 | if (event == LV_EVENT_CLICKED) 166 | { 167 | auto *settings = (SignalKSettings *)obj->user_data; 168 | auto keyboard = new Keyboard(LOC_SIGNALK_INPUT_PORT, KeyboardType_t::Number, 5); 169 | keyboard->on_close([keyboard, settings]() { 170 | if (keyboard->is_success()) 171 | { 172 | int port = atoi(keyboard->get_text()); 173 | if (port > 0 && port < 65536) 174 | { 175 | settings->set_port(port); 176 | } 177 | } 178 | delete keyboard; 179 | }); 180 | keyboard->show(lv_scr_act()); 181 | } 182 | } 183 | 184 | static void __scan_event(lv_obj_t *obj, lv_event_t event) 185 | { 186 | if (event == LV_EVENT_CLICKED) 187 | { 188 | auto settings = ((SignalKSettings *)obj->user_data); 189 | settings->find_sk_server(); 190 | } 191 | } 192 | 193 | static void __token_reset_event(lv_obj_t *obj, lv_event_t event) 194 | { 195 | if (event == LV_EVENT_CLICKED) 196 | { 197 | auto settings = ((SignalKSettings *)obj->user_data); 198 | settings->clear_token(); 199 | } 200 | } 201 | 202 | void find_sk_server() 203 | { 204 | ESP_LOGI(SETTINGS_TAG, "Starting SK mDNS search..."); 205 | if (!server_search_running_) 206 | { 207 | server_search_completed_ = false; 208 | server_search_running_ = true; 209 | search_loader_ = new Loader(LOC_SIGNALK_FINDING_SERVER); 210 | twatchsk::run_async("mDNS search", [this]() { 211 | const char *service_name = "_signalk-ws"; 212 | const char *service_proto = "_tcp"; 213 | mdns_result_t *results = NULL; 214 | if (mdns_init() == ESP_OK) 215 | { 216 | ESP_LOGI(SETTINGS_TAG, "Query PTR: %s.%s.local", service_name, service_proto); 217 | esp_err_t err = mdns_query_ptr(service_name, service_proto, 5000, 5, &results); 218 | if (err == ESP_OK) 219 | { 220 | if (results != NULL) 221 | { 222 | mdns_result_t *r = results; 223 | 224 | while (r) 225 | { 226 | ESP_LOGI(SETTINGS_TAG, "mDNS search result %s:%d", r->hostname, r->port); 227 | server_address_ = String(r->hostname); 228 | server_port_ = r->port; 229 | signalk_changed_ = true; 230 | break; 231 | } 232 | } 233 | else 234 | { 235 | ESP_LOGW(SETTINGS_TAG, "No results found!"); 236 | post_gui_warning(String(LOC_SIGNALK_MDNS_NOT_FOUND)); 237 | } 238 | } 239 | else 240 | { 241 | ESP_LOGE(SETTINGS_TAG, "mDNS SK query failed with %d", (int)err); 242 | post_gui_warning(String(LOC_SIGNALK_MDNS_ERORR)); 243 | } 244 | 245 | mdns_free(); 246 | } 247 | 248 | server_search_completed_ = true; 249 | }); 250 | } 251 | } 252 | 253 | void stop_find_sk_server() 254 | { 255 | if (server_search_running_) 256 | { 257 | delete search_loader_; 258 | search_loader_ = NULL; 259 | server_search_running_ = false; 260 | } 261 | } 262 | 263 | void update_sk_info() 264 | { 265 | if (!server_search_running_) 266 | { 267 | auto socketStatus = sk_socket_->get_state(); 268 | 269 | if (socketStatus == WebsocketState_t::WS_Connected) 270 | { 271 | if (!sk_socket_->get_token_request_pending()) 272 | { 273 | lv_label_set_text_fmt(status_label_, LOC_SIGNALK_CONNECTED_FMT, sk_socket_->get_handled_delta_count()); 274 | lv_obj_set_hidden(token_reset_button_, false); 275 | } 276 | else 277 | { 278 | lv_label_set_text(status_label_, LOC_SIGNALK_TOKEN_PENDING); 279 | lv_obj_set_hidden(token_reset_button_, true); 280 | } 281 | 282 | } 283 | else if (socketStatus == WebsocketState_t::WS_Offline) 284 | { 285 | lv_obj_set_hidden(find_button_, false); 286 | lv_label_set_text(status_label_, LOC_SIGNALK_DISCONNECTED); 287 | lv_obj_set_hidden(token_reset_button_, false); 288 | } 289 | else if (socketStatus == WebsocketState_t::WS_Connecting) 290 | { 291 | lv_label_set_text(status_label_, LOC_SIGNALK_CONNECTING); 292 | lv_obj_set_hidden(token_reset_button_, false); 293 | } 294 | 295 | ESP_LOGI(SETTINGS_TAG, "Status update %d", (int)socketStatus); 296 | } 297 | else if (server_search_completed_) 298 | { 299 | ESP_LOGI(SETTINGS_TAG, "mDNS search is complete!"); 300 | 301 | stop_find_sk_server(); 302 | } 303 | } 304 | 305 | void apply_signalk_settings() 306 | { 307 | ESP_LOGI(SETTINGS_TAG, "Saving SignalK settings (server=%s,port=%d)...", server_address_.c_str(), server_port_); 308 | sk_socket_->set_server(server_address_, server_port_); 309 | twatchsk::run_async("SK Settings save", [this]() { 310 | delay(100); 311 | this->sk_socket_->save(); 312 | }); 313 | if (sk_socket_->get_state() == WebsocketState_t::WS_Connected) 314 | { 315 | ESP_LOGI(SETTINGS_TAG, "SK websocket will be reconnected."); 316 | auto result = sk_socket_->reconnect(); 317 | ESP_LOGI(SETTINGS_TAG, "SK websocket reconnect result=%d.", result); 318 | } 319 | else if (sk_socket_->get_state() == WebsocketState_t::WS_Offline) 320 | { 321 | if (!server_address_.isEmpty() && server_port_ != 0) 322 | { 323 | sk_socket_->connect(); 324 | } 325 | } 326 | } 327 | 328 | void clear_token() 329 | { 330 | ESP_LOGI(SETTINGS_TAG, "User is resetting SK authorization token."); 331 | sk_socket_->clear_token(); 332 | ESP_LOGI(SETTINGS_TAG, "Initiating SK reconnection."); 333 | auto result = sk_socket_->reconnect(); 334 | ESP_LOGI(SETTINGS_TAG, "SK reconnection result=%d", result); 335 | } 336 | }; -------------------------------------------------------------------------------- /src/ui/wifisettings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "settings_view.h" 3 | #include "localization.h" 4 | #include "hardware/Wifi.h" 5 | #include "loader.h" 6 | #include "wifilist.h" 7 | #include "ui_ticker.h" 8 | #include "system/async_dispatcher.h" 9 | 10 | enum WifiSettingsState_t 11 | { 12 | WiS_Normal, //showing status 13 | WiS_WifiScan, //showing loader with scanning wifi 14 | WiS_ConnectingToWifi //connecting to wifi network 15 | }; 16 | 17 | class WifiSettings : public SettingsView 18 | { 19 | public: 20 | WifiSettings(WifiManager *wifi) : SettingsView(LOC_WIFI_SETTINGS) 21 | { 22 | wifi_manager_ = wifi; 23 | } 24 | 25 | protected: 26 | virtual void show_internal(lv_obj_t *parent) override 27 | { 28 | lv_cont_set_layout(parent, LV_LAYOUT_OFF); 29 | //Create on / off switch for WiFi control 30 | enable_switch_ = lv_switch_create(topBar, NULL); 31 | lv_obj_align(enable_switch_, topBar, LV_ALIGN_IN_RIGHT_MID, -6, 0); 32 | enable_switch_->user_data = this; 33 | 34 | if (wifi_manager_->get_status() == WifiState_t::Wifi_Off) 35 | { 36 | lv_switch_off(enable_switch_, LV_ANIM_OFF); 37 | } 38 | else 39 | { 40 | lv_switch_on(enable_switch_, LV_ANIM_OFF); 41 | } 42 | 43 | lv_obj_set_event_cb(enable_switch_, __enable_switch_event); 44 | 45 | //show configured wifi name 46 | wifi_name_ = lv_label_create(parent, NULL); 47 | lv_label_set_text_fmt(wifi_name_, LOC_WIFI_CONFIG_SSID_FMT, wifi_manager_->get_configured_ssid().c_str()); 48 | lv_obj_set_pos(wifi_name_, spacing, spacing); 49 | 50 | //show wifi current status 51 | status_ = lv_label_create(parent, NULL); 52 | lv_obj_align(status_, wifi_name_, LV_ALIGN_OUT_BOTTOM_LEFT, 0, spacing); 53 | 54 | //show current IP address (if any) 55 | wifi_ip_ = lv_label_create(parent, NULL); 56 | lv_obj_align(wifi_ip_, status_, LV_ALIGN_OUT_BOTTOM_LEFT, 0, spacing); 57 | 58 | static lv_style_t buttonStyle; 59 | lv_style_init(&buttonStyle); 60 | lv_style_set_radius(&buttonStyle, LV_STATE_DEFAULT, 10); 61 | 62 | //connect button - visible only if not connected - just guide for user to enable wifi 63 | connect_button_ = lv_btn_create(parent, NULL); 64 | lv_obj_add_style(connect_button_, LV_OBJ_PART_MAIN, &buttonStyle); 65 | lv_obj_align_y(connect_button_, wifi_ip_, LV_ALIGN_OUT_BOTTOM_LEFT, spacing * 2); //first align the top with label above + 4pix 66 | lv_obj_align_x(connect_button_, parent, LV_ALIGN_IN_BOTTOM_MID, 0); //then center the button in parent 67 | auto connectLabel = lv_label_create(connect_button_, NULL); 68 | lv_label_set_text(connectLabel, LOC_WIFI_CONNECT); 69 | connect_button_->user_data = this; 70 | lv_obj_set_event_cb(connect_button_, __connect_event); 71 | 72 | //scan button 73 | scan_button_ = lv_btn_create(parent, NULL); 74 | lv_obj_add_style(scan_button_, LV_OBJ_PART_MAIN, &buttonStyle); 75 | lv_obj_align(scan_button_, connect_button_, LV_ALIGN_OUT_BOTTOM_MID, 0, spacing); 76 | auto scanLabel = lv_label_create(scan_button_, NULL); 77 | lv_label_set_text(scanLabel, LOC_WIFI_SCAN_LABEL); 78 | scan_button_->user_data = this; 79 | lv_obj_set_event_cb(scan_button_, __scan_event); 80 | 81 | update_wifi_info(true); 82 | 83 | status_update_ticker_ = new UITicker(1000, [this]() { 84 | if (state_ == WifiSettingsState_t::WiS_Normal) 85 | { 86 | update_wifi_info(); 87 | } 88 | else if (state_ == WifiSettingsState_t::WiS_WifiScan) 89 | { 90 | scan_wifi_check(); 91 | } 92 | else if (state_ == WifiSettingsState_t::WiS_ConnectingToWifi) 93 | { 94 | wifi_connect_check(); 95 | } 96 | }); 97 | } 98 | 99 | virtual bool hide_internal() override 100 | { 101 | delete status_update_ticker_; 102 | status_update_ticker_ = NULL; 103 | if(wifi_changed_) 104 | { 105 | save_wifi_settings(); 106 | } 107 | return true; 108 | } 109 | 110 | void set_ssid(const char *ssid) 111 | { 112 | strcpy(selected_ap_, ssid); 113 | lv_label_set_text_fmt(wifi_name_, LOC_WIFI_CONFIG_SSID_FMT, ssid); 114 | } 115 | 116 | void set_password_and_connect(const char *password) 117 | { 118 | if (state_ != WifiSettingsState_t::WiS_ConnectingToWifi) //JD: Workaround, something is wrong as this is called 2x, will fix that later 119 | // BS: I think it was a bug with keyboards, and may be fixed now? 120 | // See changes to keyboard.h in https://github.com/JohnySeven/TWatchSK/commit/551d1be1fce5e36144c4c00131db384b8cce88ae# 121 | { 122 | strcpy(password_, password); 123 | wifi_changed_ = true; 124 | 125 | ESP_LOGI(SETTINGS_TAG, "Connecting to wifi %s", selected_ap_); 126 | 127 | wifi_manager_->setup(String(selected_ap_), String(password)); 128 | wifi_manager_->connect(); 129 | 130 | state_ = WifiSettingsState_t::WiS_ConnectingToWifi; 131 | loader_ = new Loader(LOC_WIFI_CONNECTING); 132 | } 133 | } 134 | 135 | private: 136 | WifiManager *wifi_manager_; 137 | lv_obj_t *enable_switch_; 138 | lv_obj_t *wifi_name_; 139 | lv_obj_t *wifi_connection_; 140 | lv_obj_t *wifi_ip_; 141 | lv_obj_t *status_; 142 | lv_obj_t *connect_button_; 143 | lv_obj_t *scan_button_; 144 | Loader *loader_; 145 | UITicker *status_update_ticker_; 146 | char selected_ap_[64]; 147 | char password_[64]; 148 | bool wifi_changed_ = false; 149 | WifiSettingsState_t state_ = WifiSettingsState_t::WiS_Normal; 150 | 151 | static void __enable_switch_event(lv_obj_t *obj, lv_event_t event) 152 | { 153 | if (event == LV_EVENT_VALUE_CHANGED) 154 | { 155 | ((WifiSettings *)obj->user_data)->enable_switch_updated(); 156 | } 157 | } 158 | 159 | void enable_switch_updated() 160 | { 161 | if (lv_switch_get_state(enable_switch_)) 162 | { 163 | wifi_manager_->on(); 164 | } 165 | else 166 | { 167 | wifi_manager_->off(); 168 | } 169 | } 170 | 171 | static void __scan_event(lv_obj_t *obj, lv_event_t event) 172 | { 173 | if (event == LV_EVENT_CLICKED) 174 | { 175 | ((WifiSettings *)obj->user_data)->scan_wifi_list(); 176 | } 177 | } 178 | 179 | void scan_wifi_list() 180 | { 181 | if (wifi_manager_->scan_wifi()) 182 | { 183 | loader_ = new Loader(LOC_WIFI_SCANNING_PROGRESS); 184 | state_ = WifiSettingsState_t::WiS_WifiScan; 185 | } 186 | } 187 | 188 | void scan_wifi_check() 189 | { 190 | if (wifi_manager_->is_scan_complete()) 191 | { 192 | delete loader_; 193 | loader_ = NULL; 194 | state_ = WifiSettingsState_t::WiS_Normal; 195 | ESP_LOGI(SETTINGS_TAG, "Scan completed with %d found WiFi APs", wifi_manager_->found_wifi_count()); 196 | 197 | auto wifiList = new WifiList(); 198 | wifiList->show(lv_scr_act()); 199 | 200 | for (int i = 0; i < wifi_manager_->found_wifi_count(); i++) 201 | { 202 | auto ap = wifi_manager_->get_found_wifi(i); 203 | auto isknown = wifi_manager_->is_known_wifi(String((char *)ap.ssid)); 204 | wifiList->add_ssid((char *)ap.ssid, isknown ? (void *)LV_SYMBOL_SAVE : (void *)LV_SYMBOL_WIFI); 205 | } 206 | 207 | wifiList->on_close([this, wifiList]() { 208 | auto ssid = wifiList->selected_ssid(); 209 | 210 | if (ssid != nullptr) 211 | { 212 | ESP_LOGI(SETTINGS_TAG, "User has selected %s SSID", ssid); 213 | set_ssid(ssid); 214 | delete wifiList; 215 | String password; 216 | if (wifi_manager_->get_known_wifi_password(ssid, password)) 217 | { 218 | set_password_and_connect(password.c_str()); 219 | } 220 | else 221 | { 222 | auto passwordPicker = new Keyboard(LOC_WIFI_PASSWORD_PROMPT); 223 | passwordPicker->on_close([this, passwordPicker]() { 224 | if (passwordPicker->is_success()) 225 | { 226 | ESP_LOGI(SETTINGS_TAG, "User password input."); 227 | set_password_and_connect(passwordPicker->get_text()); 228 | } 229 | else 230 | { 231 | ESP_LOGI(SETTINGS_TAG, "User canceled password input."); 232 | } 233 | delete passwordPicker; 234 | }); 235 | 236 | passwordPicker->show(lv_scr_act()); 237 | } 238 | } 239 | }); 240 | } 241 | } 242 | 243 | void wifi_connect_check() 244 | { 245 | auto wifi_status = wifi_manager_->get_status(); 246 | ESP_LOGI(SETTINGS_TAG, "Connecting to Wifi status=%d", (int)wifi_status); 247 | 248 | if (wifi_status != WifiState_t::Wifi_Connecting) 249 | { 250 | delete loader_; 251 | loader_ = NULL; 252 | state_ = WifiSettingsState_t::WiS_Normal; 253 | 254 | if (wifi_status == WifiState_t::Wifi_Connected) 255 | { 256 | show_message(LOC_WIFI_CONNECT_SUCCESS); 257 | } 258 | else 259 | { 260 | //erase wifi settings 261 | wifi_manager_->setup("", ""); 262 | set_ssid(""); 263 | strcpy(password_, ""); 264 | update_wifi_info(true); 265 | } 266 | } 267 | } 268 | 269 | void update_wifi_info(bool force = false) 270 | { 271 | static WifiState_t lastState = WifiState_t::Wifi_Off; 272 | auto wifiStatus = wifi_manager_->get_status(); 273 | auto isConfigured = wifi_manager_->is_configured(); 274 | 275 | if (wifiStatus == WifiState_t::Wifi_Connected) //update IP and RSSI regardless of the other values 276 | { 277 | lv_label_set_text_fmt(wifi_ip_, LOC_WIFI_IP_RSSI_FMT, wifi_manager_->get_ip().c_str(), wifi_manager_->get_wifi_rssi()); 278 | } 279 | 280 | if (lastState != wifiStatus || force) //just update the UI if status has been changed or force = true 281 | { 282 | ESP_LOGI(SETTINGS_TAG, "Wifi status update %d=>%d (force:%d)", lastState, wifiStatus, force); 283 | 284 | lastState = wifiStatus; 285 | if (wifiStatus == WifiState_t::Wifi_Connected) 286 | { 287 | lv_label_set_text_fmt(status_, LOC_WIFI_CONNECTED); 288 | lv_obj_set_hidden(connect_button_, true); 289 | lv_obj_set_hidden(scan_button_, false); 290 | update_silently_enable_switch(true); 291 | } 292 | else if (wifiStatus == WifiState_t::Wifi_Disconnected) 293 | { 294 | lv_label_set_text(status_, LOC_WIFI_DISCONNECTED); 295 | lv_label_set_text_fmt(wifi_ip_, LOC_WIFI_IP_RSSI_FMT, "--", 0); 296 | lv_obj_set_hidden(connect_button_, !isConfigured); //if wifi isn't configured connect will not happen 297 | lv_obj_set_hidden(scan_button_, false); 298 | update_silently_enable_switch(true); 299 | } 300 | else if (wifiStatus == WifiState_t::Wifi_Connecting) 301 | { 302 | lv_label_set_text(status_, LOC_WIFI_CONNECTING); 303 | lv_label_set_text_fmt(wifi_ip_, LOC_WIFI_IP_RSSI_FMT, "--", 0); 304 | lv_obj_set_hidden(connect_button_, true); 305 | lv_obj_set_hidden(scan_button_, true); //if Wifi is connecting to AP it's not allowed to do scan at all 306 | update_silently_enable_switch(true); 307 | } 308 | else if (wifiStatus == WifiState_t::Wifi_Off) 309 | { 310 | if (isConfigured) 311 | { 312 | lv_label_set_text(status_, LOC_WIFI_OFF); 313 | lv_label_set_text_fmt(wifi_ip_, LOC_WIFI_IP_RSSI_FMT, "--", 0); 314 | lv_obj_set_hidden(connect_button_, false); //if wifi isn't configured connect will not happen 315 | lv_obj_set_hidden(scan_button_, false); 316 | update_silently_enable_switch(false); 317 | } 318 | else 319 | { 320 | lv_label_set_text(status_, LOC_WIFI_NO_CONFIG); 321 | lv_label_set_text(wifi_ip_, LOC_WIFI_PLEASE_CONFIGURE); 322 | lv_obj_set_hidden(connect_button_, false); //if wifi isn't configured connect will not happen 323 | lv_obj_set_hidden(scan_button_, true); 324 | update_silently_enable_switch(false); 325 | } 326 | } 327 | 328 | ESP_LOGI("WIFI", "Status update %d", wifiStatus); 329 | } 330 | } 331 | 332 | void save_wifi_settings() 333 | { 334 | twatchsk::run_async("Wifi save", [this]() { 335 | ESP_LOGI(SETTINGS_TAG, "Saving WiFi settings..."); 336 | this->wifi_manager_->save(); 337 | ESP_LOGI(SETTINGS_TAG, "WiFi settings saved!"); 338 | }); 339 | } 340 | 341 | /** 342 | * Updates Enable switch to value, for update time it disables callback (will not trigger on or off on wifi manager) 343 | **/ 344 | void update_silently_enable_switch(bool value) 345 | { 346 | if (lv_switch_get_state(enable_switch_) != value) 347 | { 348 | //we need to bypass enable switch callback 349 | lv_obj_set_event_cb(enable_switch_, NULL); 350 | if (value) 351 | { 352 | lv_switch_on(enable_switch_, LV_ANIM_ON); 353 | } 354 | else 355 | { 356 | lv_switch_off(enable_switch_, LV_ANIM_OFF); 357 | } 358 | lv_obj_set_event_cb(enable_switch_, __enable_switch_event); 359 | } 360 | } 361 | 362 | static void __connect_event(lv_obj_t *obj, lv_event_t event) 363 | { 364 | if (event == LV_EVENT_CLICKED) 365 | { 366 | auto settings = ((WifiSettings *)obj->user_data); 367 | if (settings->wifi_manager_->is_configured()) 368 | { 369 | settings->wifi_manager_->connect(); 370 | } 371 | else 372 | { 373 | settings->scan_wifi_list(); 374 | } 375 | } 376 | } 377 | }; --------------------------------------------------------------------------------