Time & date, system events and much more (#152)

## Time & Date
- Added time to statusbar widget
- Added Time & Date Settings app
- Added TimeZone app for selecting TimeZone
- Added `tt::time` namespace with timezone code

## Other changes

- Added `SystemEvent` to publish/subscribe to system wide (e.g. for init code, but also for time settings changes)
- Changed the way the statusbar widget works: now there's only 1 that gets shown/hidden, instead of 1 instance per app instance.
- Moved `lowercase()` function to new namespace: `tt::string`
- Increased T-Deck flash & PSRAM SPI frequencies to 120 MHz (from 80 MHz)
- Temporary work-around (+ TODO item) for LVGL stack size (issue with WiFi app)
- Suppress T-Deck keystroke debugging to debug level (privacy issue)
- Improved SDL dependency wiring in various `CMakeLists.txt`
- `Loader` service had some variables renamed to the newer C++ style (from previous C style)
This commit is contained in:
Ken Van Hoeylandt 2025-01-10 23:44:32 +01:00 committed by GitHub
parent 4f360741a1
commit bf91e7530d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 1498 additions and 153 deletions

View File

@ -24,6 +24,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
)
else()
file(GLOB_RECURSE SOURCES "Source/*.c*")
add_executable(AppSim ${SOURCES})
target_link_libraries(AppSim
@ -33,9 +34,11 @@ else()
PRIVATE Simulator
)
if (NOT DEFINED ENV{SKIP_SDL})
find_package(SDL2 REQUIRED CONFIG)
target_link_libraries(AppSim PRIVATE ${SDL2_LIBRARIES})
include_directories(${SDL2_INCLUDE_DIRS})
target_link_libraries(AppSim PRIVATE ${SDL2_LIBRARIES})
endif()
add_definitions(-D_Nullable=)
add_definitions(-D_Nonnull=)

View File

@ -8,8 +8,8 @@
// LVGL
// The minimum task stack seems to be about 3500, but that crashes the wifi app in some scenarios
// At 4000, it crashes when the fps renderer is available
#define TDECK_LVGL_TASK_STACK_DEPTH 8192
// At 8192, it sometimes crashes when wifi-auto enables and is busy connecting and then you open WifiManage
#define TDECK_LVGL_TASK_STACK_DEPTH 9216
bool tdeck_init_lvgl() {
static lv_disp_t* display = nullptr;

View File

@ -4,7 +4,7 @@
#define TDECK_LCD_PIN_CS GPIO_NUM_12
#define TDECK_LCD_PIN_DC GPIO_NUM_11 // RS
#define TDECK_LCD_PIN_BACKLIGHT GPIO_NUM_42
#define TDECK_LCD_SPI_FREQUENCY 40000000
#define TDECK_LCD_SPI_FREQUENCY 80000000
#define TDECK_LCD_HORIZONTAL_RESOLUTION 320
#define TDECK_LCD_VERTICAL_RESOLUTION 240
#define TDECK_LCD_BITS_PER_PIXEL 16

View File

@ -30,11 +30,11 @@ static void keyboard_read_callback(TT_UNUSED lv_indev_t* indev, lv_indev_data_t*
if (keyboard_i2c_read(&read_buffer)) {
if (read_buffer == 0 && read_buffer != last_buffer) {
TT_LOG_I(TAG, "Released %d", last_buffer);
TT_LOG_D(TAG, "Released %d", last_buffer);
data->key = last_buffer;
data->state = LV_INDEV_STATE_RELEASED;
} else if (read_buffer != 0) {
TT_LOG_I(TAG, "Pressed %d", read_buffer);
TT_LOG_D(TAG, "Pressed %d", read_buffer);
data->key = read_buffer;
data->state = LV_INDEV_STATE_PRESSED;
}

View File

@ -8,7 +8,7 @@
// LVGL
// The minimum task stack seems to be about 3500, but that crashes the wifi app in some scenarios
// At 4000, it crashes when the fps renderer is available
#define CORE2_LVGL_TASK_STACK_DEPTH 8192
#define CORE2_LVGL_TASK_STACK_DEPTH 9216
bool initLvgl() {
const lvgl_port_cfg_t lvgl_cfg = {

View File

@ -8,7 +8,7 @@
// LVGL
// The minimum task stack seems to be about 3500, but that crashes the wifi app in some scenarios
// At 4000, it crashes when the fps renderer is available
#define CORE2_LVGL_TASK_STACK_DEPTH 8192
#define CORE2_LVGL_TASK_STACK_DEPTH 9216
bool initLvgl() {
const lvgl_port_cfg_t lvgl_cfg = {

View File

@ -11,13 +11,20 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
PRIVATE ${SOURCES}
PUBLIC ${HEADERS}
)
target_link_libraries(Simulator
PRIVATE Tactility
PRIVATE TactilityCore
PRIVATE TactilityHeadless
PRIVATE lvgl
PRIVATE ${SDL2_LIBRARIES}
)
if (NOT DEFINED ENV{SKIP_SDL})
find_package(SDL2 REQUIRED CONFIG)
target_link_libraries(Simulator PRIVATE ${SDL2_LIBRARIES})
endif()
add_definitions(-D_Nullable=)
add_definitions(-D_Nonnull=)

View File

@ -2,7 +2,6 @@
#include "lvgl.h"
#include "Log.h"
#include "TactilityCore.h"
#include "Thread.h"
#include "lvgl/LvglSync.h"

View File

@ -64,7 +64,6 @@ project(Tactility)
# Defined as regular project for PC and component for ESP
if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Tactility)
add_subdirectory(TactilityC)
add_subdirectory(TactilityCore)
add_subdirectory(TactilityHeadless)
add_subdirectory(Boards/Simulator)
@ -96,65 +95,14 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Tests)
# LVGL
add_subdirectory(Libraries/lvgl) # Added as idf component for ESP and as library for other targets
include_directories(lvgl PUBLIC ${PROJECT_SOURCE_DIR}/Libraries/lvgl_conf)
if (NOT DEFINED ENV{SKIP_SDL})
target_include_directories(lvgl PUBLIC ${SDL2_IMAGE_INCLUDE_DIRS})
find_package(SDL2 REQUIRED CONFIG)
target_include_directories(lvgl PUBLIC ${SDL2_INCLUDE_DIRS})
target_link_libraries(lvgl
PRIVATE ${SDL2_LIBRARIES}
)
endif()
target_compile_definitions(lvgl PUBLIC "-DLV_CONF_PATH=\"${PROJECT_SOURCE_DIR}/Libraries/lvgl_conf/lv_conf_simulator.h\"")
# SDL
# TODO: This is a temporary skipping option for running unit tests
# TODO: Remove when github action for SDL is working again
if (NOT DEFINED ENV{SKIP_SDL})
find_package(SDL2 REQUIRED CONFIG)
option(LV_USE_DRAW_SDL "Use SDL draw unit" OFF)
option(LV_USE_LIBPNG "Use libpng to decode PNG" OFF)
option(LV_USE_LIBJPEG_TURBO "Use libjpeg turbo to decode JPEG" OFF)
option(LV_USE_FFMPEG "Use libffmpeg to display video using lv_ffmpeg" OFF)
option(LV_USE_FREETYPE "Use freetype lib" OFF)
add_compile_definitions($<$<BOOL:${LV_USE_DRAW_SDL}>:LV_USE_DRAW_SDL=1>)
add_compile_definitions($<$<BOOL:${LV_USE_LIBPNG}>:LV_USE_LIBPNG=1>)
add_compile_definitions($<$<BOOL:${LV_USE_LIBJPEG_TURBO}>:LV_USE_LIBJPEG_TURBO=1>)
add_compile_definitions($<$<BOOL:${LV_USE_FFMPEG}>:LV_USE_FFMPEG=1>)
if (LV_USE_DRAW_SDL)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
# Need to install libsdl2-image-dev
# `sudo apt install libsdl2-image-dev`
# `brew install sdl2_image`
find_package(SDL2_image REQUIRED)
include_directories(lvgl PUBLIC ${PROJECT_SOURCE_DIR}/Libraries/lvgl_conf)
target_link_libraries(AppSim ${SDL2_IMAGE_LIBRARIES})
target_link_libraries(Simulator ${SDL2_IMAGE_LIBRARIES})
endif(LV_USE_DRAW_SDL)
if (LV_USE_LIBPNG)
find_package(PNG REQUIRED)
target_include_directories(lvgl PUBLIC ${PNG_INCLUDE_DIR})
target_link_libraries(AppSim ${PNG_LIBRARY})
endif(LV_USE_LIBPNG)
if (LV_USE_LIBJPEG_TURBO)
# Need to install libjpeg-turbo8-dev
# `sudo apt install libjpeg-turbo8-dev`
# `brew install libjpeg-turbo`
find_package(JPEG REQUIRED)
target_include_directories(lvgl PUBLIC ${JPEG_INCLUDE_DIRS})
target_link_libraries(AppSim ${JPEG_LIBRARIES})
endif(LV_USE_LIBJPEG_TURBO)
if (LV_USE_FFMPEG)
target_link_libraries(main avformat avcodec avutil swscale)
endif(LV_USE_FFMPEG)
if (LV_USE_FREETYPE)
find_package(Freetype REQUIRED)
target_link_libraries(AppSim ${FREETYPE_LIBRARIES})
target_include_directories(lvgl PUBLIC ${FREETYPE_INCLUDE_DIRS})
endif(LV_USE_FREETYPE)
endif()
endif()

View File

@ -33,6 +33,12 @@ Website: https://fonts.google.com/icons
License: Multiple (SIL Open Font License, Apache License, Ubuntu Font License): https://developers.google.com/fonts/faq
### Timezones CSV
Website: https://github.com/nayarsystems/posix_tz_db
License: [MIT](https://github.com/nayarsystems/posix_tz_db/blob/master/LICENSE)
### Other Components
See `/components` for the respective projects and their licenses.

BIN
Data/screenshot-AppList.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

461
Data/system/timezones.csv Normal file
View File

@ -0,0 +1,461 @@
"Africa/Abidjan","GMT0"
"Africa/Accra","GMT0"
"Africa/Addis_Ababa","EAT-3"
"Africa/Algiers","CET-1"
"Africa/Asmara","EAT-3"
"Africa/Bamako","GMT0"
"Africa/Bangui","WAT-1"
"Africa/Banjul","GMT0"
"Africa/Bissau","GMT0"
"Africa/Blantyre","CAT-2"
"Africa/Brazzaville","WAT-1"
"Africa/Bujumbura","CAT-2"
"Africa/Cairo","EET-2EEST,M4.5.5/0,M10.5.4/24"
"Africa/Casablanca","<+01>-1"
"Africa/Ceuta","CET-1CEST,M3.5.0,M10.5.0/3"
"Africa/Conakry","GMT0"
"Africa/Dakar","GMT0"
"Africa/Dar_es_Salaam","EAT-3"
"Africa/Djibouti","EAT-3"
"Africa/Douala","WAT-1"
"Africa/El_Aaiun","<+01>-1"
"Africa/Freetown","GMT0"
"Africa/Gaborone","CAT-2"
"Africa/Harare","CAT-2"
"Africa/Johannesburg","SAST-2"
"Africa/Juba","CAT-2"
"Africa/Kampala","EAT-3"
"Africa/Khartoum","CAT-2"
"Africa/Kigali","CAT-2"
"Africa/Kinshasa","WAT-1"
"Africa/Lagos","WAT-1"
"Africa/Libreville","WAT-1"
"Africa/Lome","GMT0"
"Africa/Luanda","WAT-1"
"Africa/Lubumbashi","CAT-2"
"Africa/Lusaka","CAT-2"
"Africa/Malabo","WAT-1"
"Africa/Maputo","CAT-2"
"Africa/Maseru","SAST-2"
"Africa/Mbabane","SAST-2"
"Africa/Mogadishu","EAT-3"
"Africa/Monrovia","GMT0"
"Africa/Nairobi","EAT-3"
"Africa/Ndjamena","WAT-1"
"Africa/Niamey","WAT-1"
"Africa/Nouakchott","GMT0"
"Africa/Ouagadougou","GMT0"
"Africa/Porto-Novo","WAT-1"
"Africa/Sao_Tome","GMT0"
"Africa/Tripoli","EET-2"
"Africa/Tunis","CET-1"
"Africa/Windhoek","CAT-2"
"America/Adak","HST10HDT,M3.2.0,M11.1.0"
"America/Anchorage","AKST9AKDT,M3.2.0,M11.1.0"
"America/Anguilla","AST4"
"America/Antigua","AST4"
"America/Araguaina","<-03>3"
"America/Argentina/Buenos_Aires","<-03>3"
"America/Argentina/Catamarca","<-03>3"
"America/Argentina/Cordoba","<-03>3"
"America/Argentina/Jujuy","<-03>3"
"America/Argentina/La_Rioja","<-03>3"
"America/Argentina/Mendoza","<-03>3"
"America/Argentina/Rio_Gallegos","<-03>3"
"America/Argentina/Salta","<-03>3"
"America/Argentina/San_Juan","<-03>3"
"America/Argentina/San_Luis","<-03>3"
"America/Argentina/Tucuman","<-03>3"
"America/Argentina/Ushuaia","<-03>3"
"America/Aruba","AST4"
"America/Asuncion","<-04>4<-03>,M10.1.0/0,M3.4.0/0"
"America/Atikokan","EST5"
"America/Bahia","<-03>3"
"America/Bahia_Banderas","CST6"
"America/Barbados","AST4"
"America/Belem","<-03>3"
"America/Belize","CST6"
"America/Blanc-Sablon","AST4"
"America/Boa_Vista","<-04>4"
"America/Bogota","<-05>5"
"America/Boise","MST7MDT,M3.2.0,M11.1.0"
"America/Cambridge_Bay","MST7MDT,M3.2.0,M11.1.0"
"America/Campo_Grande","<-04>4"
"America/Cancun","EST5"
"America/Caracas","<-04>4"
"America/Cayenne","<-03>3"
"America/Cayman","EST5"
"America/Chicago","CST6CDT,M3.2.0,M11.1.0"
"America/Chihuahua","CST6"
"America/Costa_Rica","CST6"
"America/Creston","MST7"
"America/Cuiaba","<-04>4"
"America/Curacao","AST4"
"America/Danmarkshavn","GMT0"
"America/Dawson","MST7"
"America/Dawson_Creek","MST7"
"America/Denver","MST7MDT,M3.2.0,M11.1.0"
"America/Detroit","EST5EDT,M3.2.0,M11.1.0"
"America/Dominica","AST4"
"America/Edmonton","MST7MDT,M3.2.0,M11.1.0"
"America/Eirunepe","<-05>5"
"America/El_Salvador","CST6"
"America/Fortaleza","<-03>3"
"America/Fort_Nelson","MST7"
"America/Glace_Bay","AST4ADT,M3.2.0,M11.1.0"
"America/Godthab","<-02>2<-01>,M3.5.0/-1,M10.5.0/0"
"America/Goose_Bay","AST4ADT,M3.2.0,M11.1.0"
"America/Grand_Turk","EST5EDT,M3.2.0,M11.1.0"
"America/Grenada","AST4"
"America/Guadeloupe","AST4"
"America/Guatemala","CST6"
"America/Guayaquil","<-05>5"
"America/Guyana","<-04>4"
"America/Halifax","AST4ADT,M3.2.0,M11.1.0"
"America/Havana","CST5CDT,M3.2.0/0,M11.1.0/1"
"America/Hermosillo","MST7"
"America/Indiana/Indianapolis","EST5EDT,M3.2.0,M11.1.0"
"America/Indiana/Knox","CST6CDT,M3.2.0,M11.1.0"
"America/Indiana/Marengo","EST5EDT,M3.2.0,M11.1.0"
"America/Indiana/Petersburg","EST5EDT,M3.2.0,M11.1.0"
"America/Indiana/Tell_City","CST6CDT,M3.2.0,M11.1.0"
"America/Indiana/Vevay","EST5EDT,M3.2.0,M11.1.0"
"America/Indiana/Vincennes","EST5EDT,M3.2.0,M11.1.0"
"America/Indiana/Winamac","EST5EDT,M3.2.0,M11.1.0"
"America/Inuvik","MST7MDT,M3.2.0,M11.1.0"
"America/Iqaluit","EST5EDT,M3.2.0,M11.1.0"
"America/Jamaica","EST5"
"America/Juneau","AKST9AKDT,M3.2.0,M11.1.0"
"America/Kentucky/Louisville","EST5EDT,M3.2.0,M11.1.0"
"America/Kentucky/Monticello","EST5EDT,M3.2.0,M11.1.0"
"America/Kralendijk","AST4"
"America/La_Paz","<-04>4"
"America/Lima","<-05>5"
"America/Los_Angeles","PST8PDT,M3.2.0,M11.1.0"
"America/Lower_Princes","AST4"
"America/Maceio","<-03>3"
"America/Managua","CST6"
"America/Manaus","<-04>4"
"America/Marigot","AST4"
"America/Martinique","AST4"
"America/Matamoros","CST6CDT,M3.2.0,M11.1.0"
"America/Mazatlan","MST7"
"America/Menominee","CST6CDT,M3.2.0,M11.1.0"
"America/Merida","CST6"
"America/Metlakatla","AKST9AKDT,M3.2.0,M11.1.0"
"America/Mexico_City","CST6"
"America/Miquelon","<-03>3<-02>,M3.2.0,M11.1.0"
"America/Moncton","AST4ADT,M3.2.0,M11.1.0"
"America/Monterrey","CST6"
"America/Montevideo","<-03>3"
"America/Montreal","EST5EDT,M3.2.0,M11.1.0"
"America/Montserrat","AST4"
"America/Nassau","EST5EDT,M3.2.0,M11.1.0"
"America/New_York","EST5EDT,M3.2.0,M11.1.0"
"America/Nipigon","EST5EDT,M3.2.0,M11.1.0"
"America/Nome","AKST9AKDT,M3.2.0,M11.1.0"
"America/Noronha","<-02>2"
"America/North_Dakota/Beulah","CST6CDT,M3.2.0,M11.1.0"
"America/North_Dakota/Center","CST6CDT,M3.2.0,M11.1.0"
"America/North_Dakota/New_Salem","CST6CDT,M3.2.0,M11.1.0"
"America/Nuuk","<-02>2<-01>,M3.5.0/-1,M10.5.0/0"
"America/Ojinaga","CST6CDT,M3.2.0,M11.1.0"
"America/Panama","EST5"
"America/Pangnirtung","EST5EDT,M3.2.0,M11.1.0"
"America/Paramaribo","<-03>3"
"America/Phoenix","MST7"
"America/Port-au-Prince","EST5EDT,M3.2.0,M11.1.0"
"America/Port_of_Spain","AST4"
"America/Porto_Velho","<-04>4"
"America/Puerto_Rico","AST4"
"America/Punta_Arenas","<-03>3"
"America/Rainy_River","CST6CDT,M3.2.0,M11.1.0"
"America/Rankin_Inlet","CST6CDT,M3.2.0,M11.1.0"
"America/Recife","<-03>3"
"America/Regina","CST6"
"America/Resolute","CST6CDT,M3.2.0,M11.1.0"
"America/Rio_Branco","<-05>5"
"America/Santarem","<-03>3"
"America/Santiago","<-04>4<-03>,M9.1.6/24,M4.1.6/24"
"America/Santo_Domingo","AST4"
"America/Sao_Paulo","<-03>3"
"America/Scoresbysund","<-02>2<-01>,M3.5.0/-1,M10.5.0/0"
"America/Sitka","AKST9AKDT,M3.2.0,M11.1.0"
"America/St_Barthelemy","AST4"
"America/St_Johns","NST3:30NDT,M3.2.0,M11.1.0"
"America/St_Kitts","AST4"
"America/St_Lucia","AST4"
"America/St_Thomas","AST4"
"America/St_Vincent","AST4"
"America/Swift_Current","CST6"
"America/Tegucigalpa","CST6"
"America/Thule","AST4ADT,M3.2.0,M11.1.0"
"America/Thunder_Bay","EST5EDT,M3.2.0,M11.1.0"
"America/Tijuana","PST8PDT,M3.2.0,M11.1.0"
"America/Toronto","EST5EDT,M3.2.0,M11.1.0"
"America/Tortola","AST4"
"America/Vancouver","PST8PDT,M3.2.0,M11.1.0"
"America/Whitehorse","MST7"
"America/Winnipeg","CST6CDT,M3.2.0,M11.1.0"
"America/Yakutat","AKST9AKDT,M3.2.0,M11.1.0"
"America/Yellowknife","MST7MDT,M3.2.0,M11.1.0"
"Antarctica/Casey","<+08>-8"
"Antarctica/Davis","<+07>-7"
"Antarctica/DumontDUrville","<+10>-10"
"Antarctica/Macquarie","AEST-10AEDT,M10.1.0,M4.1.0/3"
"Antarctica/Mawson","<+05>-5"
"Antarctica/McMurdo","NZST-12NZDT,M9.5.0,M4.1.0/3"
"Antarctica/Palmer","<-03>3"
"Antarctica/Rothera","<-03>3"
"Antarctica/Syowa","<+03>-3"
"Antarctica/Troll","<+00>0<+02>-2,M3.5.0/1,M10.5.0/3"
"Antarctica/Vostok","<+05>-5"
"Arctic/Longyearbyen","CET-1CEST,M3.5.0,M10.5.0/3"
"Asia/Aden","<+03>-3"
"Asia/Almaty","<+05>-5"
"Asia/Amman","<+03>-3"
"Asia/Anadyr","<+12>-12"
"Asia/Aqtau","<+05>-5"
"Asia/Aqtobe","<+05>-5"
"Asia/Ashgabat","<+05>-5"
"Asia/Atyrau","<+05>-5"
"Asia/Baghdad","<+03>-3"
"Asia/Bahrain","<+03>-3"
"Asia/Baku","<+04>-4"
"Asia/Bangkok","<+07>-7"
"Asia/Barnaul","<+07>-7"
"Asia/Beirut","EET-2EEST,M3.5.0/0,M10.5.0/0"
"Asia/Bishkek","<+06>-6"
"Asia/Brunei","<+08>-8"
"Asia/Chita","<+09>-9"
"Asia/Choibalsan","<+08>-8"
"Asia/Colombo","<+0530>-5:30"
"Asia/Damascus","<+03>-3"
"Asia/Dhaka","<+06>-6"
"Asia/Dili","<+09>-9"
"Asia/Dubai","<+04>-4"
"Asia/Dushanbe","<+05>-5"
"Asia/Famagusta","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Asia/Gaza","EET-2EEST,M3.4.4/50,M10.4.4/50"
"Asia/Hebron","EET-2EEST,M3.4.4/50,M10.4.4/50"
"Asia/Ho_Chi_Minh","<+07>-7"
"Asia/Hong_Kong","HKT-8"
"Asia/Hovd","<+07>-7"
"Asia/Irkutsk","<+08>-8"
"Asia/Jakarta","WIB-7"
"Asia/Jayapura","WIT-9"
"Asia/Jerusalem","IST-2IDT,M3.4.4/26,M10.5.0"
"Asia/Kabul","<+0430>-4:30"
"Asia/Kamchatka","<+12>-12"
"Asia/Karachi","PKT-5"
"Asia/Kathmandu","<+0545>-5:45"
"Asia/Khandyga","<+09>-9"
"Asia/Kolkata","IST-5:30"
"Asia/Krasnoyarsk","<+07>-7"
"Asia/Kuala_Lumpur","<+08>-8"
"Asia/Kuching","<+08>-8"
"Asia/Kuwait","<+03>-3"
"Asia/Macau","CST-8"
"Asia/Magadan","<+11>-11"
"Asia/Makassar","WITA-8"
"Asia/Manila","PST-8"
"Asia/Muscat","<+04>-4"
"Asia/Nicosia","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Asia/Novokuznetsk","<+07>-7"
"Asia/Novosibirsk","<+07>-7"
"Asia/Omsk","<+06>-6"
"Asia/Oral","<+05>-5"
"Asia/Phnom_Penh","<+07>-7"
"Asia/Pontianak","WIB-7"
"Asia/Pyongyang","KST-9"
"Asia/Qatar","<+03>-3"
"Asia/Qyzylorda","<+05>-5"
"Asia/Riyadh","<+03>-3"
"Asia/Sakhalin","<+11>-11"
"Asia/Samarkand","<+05>-5"
"Asia/Seoul","KST-9"
"Asia/Shanghai","CST-8"
"Asia/Singapore","<+08>-8"
"Asia/Srednekolymsk","<+11>-11"
"Asia/Taipei","CST-8"
"Asia/Tashkent","<+05>-5"
"Asia/Tbilisi","<+04>-4"
"Asia/Tehran","<+0330>-3:30"
"Asia/Thimphu","<+06>-6"
"Asia/Tokyo","JST-9"
"Asia/Tomsk","<+07>-7"
"Asia/Ulaanbaatar","<+08>-8"
"Asia/Urumqi","<+06>-6"
"Asia/Ust-Nera","<+10>-10"
"Asia/Vientiane","<+07>-7"
"Asia/Vladivostok","<+10>-10"
"Asia/Yakutsk","<+09>-9"
"Asia/Yangon","<+0630>-6:30"
"Asia/Yekaterinburg","<+05>-5"
"Asia/Yerevan","<+04>-4"
"Atlantic/Azores","<-01>1<+00>,M3.5.0/0,M10.5.0/1"
"Atlantic/Bermuda","AST4ADT,M3.2.0,M11.1.0"
"Atlantic/Canary","WET0WEST,M3.5.0/1,M10.5.0"
"Atlantic/Cape_Verde","<-01>1"
"Atlantic/Faroe","WET0WEST,M3.5.0/1,M10.5.0"
"Atlantic/Madeira","WET0WEST,M3.5.0/1,M10.5.0"
"Atlantic/Reykjavik","GMT0"
"Atlantic/South_Georgia","<-02>2"
"Atlantic/Stanley","<-03>3"
"Atlantic/St_Helena","GMT0"
"Australia/Adelaide","ACST-9:30ACDT,M10.1.0,M4.1.0/3"
"Australia/Brisbane","AEST-10"
"Australia/Broken_Hill","ACST-9:30ACDT,M10.1.0,M4.1.0/3"
"Australia/Currie","AEST-10AEDT,M10.1.0,M4.1.0/3"
"Australia/Darwin","ACST-9:30"
"Australia/Eucla","<+0845>-8:45"
"Australia/Hobart","AEST-10AEDT,M10.1.0,M4.1.0/3"
"Australia/Lindeman","AEST-10"
"Australia/Lord_Howe","<+1030>-10:30<+11>-11,M10.1.0,M4.1.0"
"Australia/Melbourne","AEST-10AEDT,M10.1.0,M4.1.0/3"
"Australia/Perth","AWST-8"
"Australia/Sydney","AEST-10AEDT,M10.1.0,M4.1.0/3"
"Europe/Amsterdam","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Andorra","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Astrakhan","<+04>-4"
"Europe/Athens","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Belgrade","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Berlin","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Bratislava","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Brussels","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Bucharest","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Budapest","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Busingen","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Chisinau","EET-2EEST,M3.5.0,M10.5.0/3"
"Europe/Copenhagen","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Dublin","IST-1GMT0,M10.5.0,M3.5.0/1"
"Europe/Gibraltar","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Guernsey","GMT0BST,M3.5.0/1,M10.5.0"
"Europe/Helsinki","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Isle_of_Man","GMT0BST,M3.5.0/1,M10.5.0"
"Europe/Istanbul","<+03>-3"
"Europe/Jersey","GMT0BST,M3.5.0/1,M10.5.0"
"Europe/Kaliningrad","EET-2"
"Europe/Kiev","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Kirov","MSK-3"
"Europe/Lisbon","WET0WEST,M3.5.0/1,M10.5.0"
"Europe/Ljubljana","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/London","GMT0BST,M3.5.0/1,M10.5.0"
"Europe/Luxembourg","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Madrid","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Malta","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Mariehamn","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Minsk","<+03>-3"
"Europe/Monaco","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Moscow","MSK-3"
"Europe/Oslo","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Paris","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Podgorica","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Prague","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Riga","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Rome","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Samara","<+04>-4"
"Europe/San_Marino","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Sarajevo","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Saratov","<+04>-4"
"Europe/Simferopol","MSK-3"
"Europe/Skopje","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Sofia","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Stockholm","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Tallinn","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Tirane","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Ulyanovsk","<+04>-4"
"Europe/Uzhgorod","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Vaduz","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Vatican","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Vienna","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Vilnius","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Volgograd","MSK-3"
"Europe/Warsaw","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Zagreb","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Zaporozhye","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Zurich","CET-1CEST,M3.5.0,M10.5.0/3"
"Indian/Antananarivo","EAT-3"
"Indian/Chagos","<+06>-6"
"Indian/Christmas","<+07>-7"
"Indian/Cocos","<+0630>-6:30"
"Indian/Comoro","EAT-3"
"Indian/Kerguelen","<+05>-5"
"Indian/Mahe","<+04>-4"
"Indian/Maldives","<+05>-5"
"Indian/Mauritius","<+04>-4"
"Indian/Mayotte","EAT-3"
"Indian/Reunion","<+04>-4"
"Pacific/Apia","<+13>-13"
"Pacific/Auckland","NZST-12NZDT,M9.5.0,M4.1.0/3"
"Pacific/Bougainville","<+11>-11"
"Pacific/Chatham","<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45"
"Pacific/Chuuk","<+10>-10"
"Pacific/Easter","<-06>6<-05>,M9.1.6/22,M4.1.6/22"
"Pacific/Efate","<+11>-11"
"Pacific/Enderbury","<+13>-13"
"Pacific/Fakaofo","<+13>-13"
"Pacific/Fiji","<+12>-12"
"Pacific/Funafuti","<+12>-12"
"Pacific/Galapagos","<-06>6"
"Pacific/Gambier","<-09>9"
"Pacific/Guadalcanal","<+11>-11"
"Pacific/Guam","ChST-10"
"Pacific/Honolulu","HST10"
"Pacific/Kiritimati","<+14>-14"
"Pacific/Kosrae","<+11>-11"
"Pacific/Kwajalein","<+12>-12"
"Pacific/Majuro","<+12>-12"
"Pacific/Marquesas","<-0930>9:30"
"Pacific/Midway","SST11"
"Pacific/Nauru","<+12>-12"
"Pacific/Niue","<-11>11"
"Pacific/Norfolk","<+11>-11<+12>,M10.1.0,M4.1.0/3"
"Pacific/Noumea","<+11>-11"
"Pacific/Pago_Pago","SST11"
"Pacific/Palau","<+09>-9"
"Pacific/Pitcairn","<-08>8"
"Pacific/Pohnpei","<+11>-11"
"Pacific/Port_Moresby","<+10>-10"
"Pacific/Rarotonga","<-10>10"
"Pacific/Saipan","ChST-10"
"Pacific/Tahiti","<-10>10"
"Pacific/Tarawa","<+12>-12"
"Pacific/Tongatapu","<+13>-13"
"Pacific/Wake","<+12>-12"
"Pacific/Wallis","<+12>-12"
"Etc/GMT","GMT0"
"Etc/GMT-0","GMT0"
"Etc/GMT-1","<+01>-1"
"Etc/GMT-2","<+02>-2"
"Etc/GMT-3","<+03>-3"
"Etc/GMT-4","<+04>-4"
"Etc/GMT-5","<+05>-5"
"Etc/GMT-6","<+06>-6"
"Etc/GMT-7","<+07>-7"
"Etc/GMT-8","<+08>-8"
"Etc/GMT-9","<+09>-9"
"Etc/GMT-10","<+10>-10"
"Etc/GMT-11","<+11>-11"
"Etc/GMT-12","<+12>-12"
"Etc/GMT-13","<+13>-13"
"Etc/GMT-14","<+14>-14"
"Etc/GMT0","GMT0"
"Etc/GMT+0","GMT0"
"Etc/GMT+1","<-01>1"
"Etc/GMT+2","<-02>2"
"Etc/GMT+3","<-03>3"
"Etc/GMT+4","<-04>4"
"Etc/GMT+5","<-05>5"
"Etc/GMT+6","<-06>6"
"Etc/GMT+7","<-07>7"
"Etc/GMT+8","<-08>8"
"Etc/GMT+9","<-09>9"
"Etc/GMT+10","<-10>10"
"Etc/GMT+11","<-11>11"
"Etc/GMT+12","<-12>12"
"Etc/UCT","UTC0"
"Etc/UTC","UTC0"
"Etc/Greenwich","GMT0"
"Etc/Universal","UTC0"
"Etc/Zulu","UTC0"
1 Africa/Abidjan GMT0
2 Africa/Accra GMT0
3 Africa/Addis_Ababa EAT-3
4 Africa/Algiers CET-1
5 Africa/Asmara EAT-3
6 Africa/Bamako GMT0
7 Africa/Bangui WAT-1
8 Africa/Banjul GMT0
9 Africa/Bissau GMT0
10 Africa/Blantyre CAT-2
11 Africa/Brazzaville WAT-1
12 Africa/Bujumbura CAT-2
13 Africa/Cairo EET-2EEST,M4.5.5/0,M10.5.4/24
14 Africa/Casablanca <+01>-1
15 Africa/Ceuta CET-1CEST,M3.5.0,M10.5.0/3
16 Africa/Conakry GMT0
17 Africa/Dakar GMT0
18 Africa/Dar_es_Salaam EAT-3
19 Africa/Djibouti EAT-3
20 Africa/Douala WAT-1
21 Africa/El_Aaiun <+01>-1
22 Africa/Freetown GMT0
23 Africa/Gaborone CAT-2
24 Africa/Harare CAT-2
25 Africa/Johannesburg SAST-2
26 Africa/Juba CAT-2
27 Africa/Kampala EAT-3
28 Africa/Khartoum CAT-2
29 Africa/Kigali CAT-2
30 Africa/Kinshasa WAT-1
31 Africa/Lagos WAT-1
32 Africa/Libreville WAT-1
33 Africa/Lome GMT0
34 Africa/Luanda WAT-1
35 Africa/Lubumbashi CAT-2
36 Africa/Lusaka CAT-2
37 Africa/Malabo WAT-1
38 Africa/Maputo CAT-2
39 Africa/Maseru SAST-2
40 Africa/Mbabane SAST-2
41 Africa/Mogadishu EAT-3
42 Africa/Monrovia GMT0
43 Africa/Nairobi EAT-3
44 Africa/Ndjamena WAT-1
45 Africa/Niamey WAT-1
46 Africa/Nouakchott GMT0
47 Africa/Ouagadougou GMT0
48 Africa/Porto-Novo WAT-1
49 Africa/Sao_Tome GMT0
50 Africa/Tripoli EET-2
51 Africa/Tunis CET-1
52 Africa/Windhoek CAT-2
53 America/Adak HST10HDT,M3.2.0,M11.1.0
54 America/Anchorage AKST9AKDT,M3.2.0,M11.1.0
55 America/Anguilla AST4
56 America/Antigua AST4
57 America/Araguaina <-03>3
58 America/Argentina/Buenos_Aires <-03>3
59 America/Argentina/Catamarca <-03>3
60 America/Argentina/Cordoba <-03>3
61 America/Argentina/Jujuy <-03>3
62 America/Argentina/La_Rioja <-03>3
63 America/Argentina/Mendoza <-03>3
64 America/Argentina/Rio_Gallegos <-03>3
65 America/Argentina/Salta <-03>3
66 America/Argentina/San_Juan <-03>3
67 America/Argentina/San_Luis <-03>3
68 America/Argentina/Tucuman <-03>3
69 America/Argentina/Ushuaia <-03>3
70 America/Aruba AST4
71 America/Asuncion <-04>4<-03>,M10.1.0/0,M3.4.0/0
72 America/Atikokan EST5
73 America/Bahia <-03>3
74 America/Bahia_Banderas CST6
75 America/Barbados AST4
76 America/Belem <-03>3
77 America/Belize CST6
78 America/Blanc-Sablon AST4
79 America/Boa_Vista <-04>4
80 America/Bogota <-05>5
81 America/Boise MST7MDT,M3.2.0,M11.1.0
82 America/Cambridge_Bay MST7MDT,M3.2.0,M11.1.0
83 America/Campo_Grande <-04>4
84 America/Cancun EST5
85 America/Caracas <-04>4
86 America/Cayenne <-03>3
87 America/Cayman EST5
88 America/Chicago CST6CDT,M3.2.0,M11.1.0
89 America/Chihuahua CST6
90 America/Costa_Rica CST6
91 America/Creston MST7
92 America/Cuiaba <-04>4
93 America/Curacao AST4
94 America/Danmarkshavn GMT0
95 America/Dawson MST7
96 America/Dawson_Creek MST7
97 America/Denver MST7MDT,M3.2.0,M11.1.0
98 America/Detroit EST5EDT,M3.2.0,M11.1.0
99 America/Dominica AST4
100 America/Edmonton MST7MDT,M3.2.0,M11.1.0
101 America/Eirunepe <-05>5
102 America/El_Salvador CST6
103 America/Fortaleza <-03>3
104 America/Fort_Nelson MST7
105 America/Glace_Bay AST4ADT,M3.2.0,M11.1.0
106 America/Godthab <-02>2<-01>,M3.5.0/-1,M10.5.0/0
107 America/Goose_Bay AST4ADT,M3.2.0,M11.1.0
108 America/Grand_Turk EST5EDT,M3.2.0,M11.1.0
109 America/Grenada AST4
110 America/Guadeloupe AST4
111 America/Guatemala CST6
112 America/Guayaquil <-05>5
113 America/Guyana <-04>4
114 America/Halifax AST4ADT,M3.2.0,M11.1.0
115 America/Havana CST5CDT,M3.2.0/0,M11.1.0/1
116 America/Hermosillo MST7
117 America/Indiana/Indianapolis EST5EDT,M3.2.0,M11.1.0
118 America/Indiana/Knox CST6CDT,M3.2.0,M11.1.0
119 America/Indiana/Marengo EST5EDT,M3.2.0,M11.1.0
120 America/Indiana/Petersburg EST5EDT,M3.2.0,M11.1.0
121 America/Indiana/Tell_City CST6CDT,M3.2.0,M11.1.0
122 America/Indiana/Vevay EST5EDT,M3.2.0,M11.1.0
123 America/Indiana/Vincennes EST5EDT,M3.2.0,M11.1.0
124 America/Indiana/Winamac EST5EDT,M3.2.0,M11.1.0
125 America/Inuvik MST7MDT,M3.2.0,M11.1.0
126 America/Iqaluit EST5EDT,M3.2.0,M11.1.0
127 America/Jamaica EST5
128 America/Juneau AKST9AKDT,M3.2.0,M11.1.0
129 America/Kentucky/Louisville EST5EDT,M3.2.0,M11.1.0
130 America/Kentucky/Monticello EST5EDT,M3.2.0,M11.1.0
131 America/Kralendijk AST4
132 America/La_Paz <-04>4
133 America/Lima <-05>5
134 America/Los_Angeles PST8PDT,M3.2.0,M11.1.0
135 America/Lower_Princes AST4
136 America/Maceio <-03>3
137 America/Managua CST6
138 America/Manaus <-04>4
139 America/Marigot AST4
140 America/Martinique AST4
141 America/Matamoros CST6CDT,M3.2.0,M11.1.0
142 America/Mazatlan MST7
143 America/Menominee CST6CDT,M3.2.0,M11.1.0
144 America/Merida CST6
145 America/Metlakatla AKST9AKDT,M3.2.0,M11.1.0
146 America/Mexico_City CST6
147 America/Miquelon <-03>3<-02>,M3.2.0,M11.1.0
148 America/Moncton AST4ADT,M3.2.0,M11.1.0
149 America/Monterrey CST6
150 America/Montevideo <-03>3
151 America/Montreal EST5EDT,M3.2.0,M11.1.0
152 America/Montserrat AST4
153 America/Nassau EST5EDT,M3.2.0,M11.1.0
154 America/New_York EST5EDT,M3.2.0,M11.1.0
155 America/Nipigon EST5EDT,M3.2.0,M11.1.0
156 America/Nome AKST9AKDT,M3.2.0,M11.1.0
157 America/Noronha <-02>2
158 America/North_Dakota/Beulah CST6CDT,M3.2.0,M11.1.0
159 America/North_Dakota/Center CST6CDT,M3.2.0,M11.1.0
160 America/North_Dakota/New_Salem CST6CDT,M3.2.0,M11.1.0
161 America/Nuuk <-02>2<-01>,M3.5.0/-1,M10.5.0/0
162 America/Ojinaga CST6CDT,M3.2.0,M11.1.0
163 America/Panama EST5
164 America/Pangnirtung EST5EDT,M3.2.0,M11.1.0
165 America/Paramaribo <-03>3
166 America/Phoenix MST7
167 America/Port-au-Prince EST5EDT,M3.2.0,M11.1.0
168 America/Port_of_Spain AST4
169 America/Porto_Velho <-04>4
170 America/Puerto_Rico AST4
171 America/Punta_Arenas <-03>3
172 America/Rainy_River CST6CDT,M3.2.0,M11.1.0
173 America/Rankin_Inlet CST6CDT,M3.2.0,M11.1.0
174 America/Recife <-03>3
175 America/Regina CST6
176 America/Resolute CST6CDT,M3.2.0,M11.1.0
177 America/Rio_Branco <-05>5
178 America/Santarem <-03>3
179 America/Santiago <-04>4<-03>,M9.1.6/24,M4.1.6/24
180 America/Santo_Domingo AST4
181 America/Sao_Paulo <-03>3
182 America/Scoresbysund <-02>2<-01>,M3.5.0/-1,M10.5.0/0
183 America/Sitka AKST9AKDT,M3.2.0,M11.1.0
184 America/St_Barthelemy AST4
185 America/St_Johns NST3:30NDT,M3.2.0,M11.1.0
186 America/St_Kitts AST4
187 America/St_Lucia AST4
188 America/St_Thomas AST4
189 America/St_Vincent AST4
190 America/Swift_Current CST6
191 America/Tegucigalpa CST6
192 America/Thule AST4ADT,M3.2.0,M11.1.0
193 America/Thunder_Bay EST5EDT,M3.2.0,M11.1.0
194 America/Tijuana PST8PDT,M3.2.0,M11.1.0
195 America/Toronto EST5EDT,M3.2.0,M11.1.0
196 America/Tortola AST4
197 America/Vancouver PST8PDT,M3.2.0,M11.1.0
198 America/Whitehorse MST7
199 America/Winnipeg CST6CDT,M3.2.0,M11.1.0
200 America/Yakutat AKST9AKDT,M3.2.0,M11.1.0
201 America/Yellowknife MST7MDT,M3.2.0,M11.1.0
202 Antarctica/Casey <+08>-8
203 Antarctica/Davis <+07>-7
204 Antarctica/DumontDUrville <+10>-10
205 Antarctica/Macquarie AEST-10AEDT,M10.1.0,M4.1.0/3
206 Antarctica/Mawson <+05>-5
207 Antarctica/McMurdo NZST-12NZDT,M9.5.0,M4.1.0/3
208 Antarctica/Palmer <-03>3
209 Antarctica/Rothera <-03>3
210 Antarctica/Syowa <+03>-3
211 Antarctica/Troll <+00>0<+02>-2,M3.5.0/1,M10.5.0/3
212 Antarctica/Vostok <+05>-5
213 Arctic/Longyearbyen CET-1CEST,M3.5.0,M10.5.0/3
214 Asia/Aden <+03>-3
215 Asia/Almaty <+05>-5
216 Asia/Amman <+03>-3
217 Asia/Anadyr <+12>-12
218 Asia/Aqtau <+05>-5
219 Asia/Aqtobe <+05>-5
220 Asia/Ashgabat <+05>-5
221 Asia/Atyrau <+05>-5
222 Asia/Baghdad <+03>-3
223 Asia/Bahrain <+03>-3
224 Asia/Baku <+04>-4
225 Asia/Bangkok <+07>-7
226 Asia/Barnaul <+07>-7
227 Asia/Beirut EET-2EEST,M3.5.0/0,M10.5.0/0
228 Asia/Bishkek <+06>-6
229 Asia/Brunei <+08>-8
230 Asia/Chita <+09>-9
231 Asia/Choibalsan <+08>-8
232 Asia/Colombo <+0530>-5:30
233 Asia/Damascus <+03>-3
234 Asia/Dhaka <+06>-6
235 Asia/Dili <+09>-9
236 Asia/Dubai <+04>-4
237 Asia/Dushanbe <+05>-5
238 Asia/Famagusta EET-2EEST,M3.5.0/3,M10.5.0/4
239 Asia/Gaza EET-2EEST,M3.4.4/50,M10.4.4/50
240 Asia/Hebron EET-2EEST,M3.4.4/50,M10.4.4/50
241 Asia/Ho_Chi_Minh <+07>-7
242 Asia/Hong_Kong HKT-8
243 Asia/Hovd <+07>-7
244 Asia/Irkutsk <+08>-8
245 Asia/Jakarta WIB-7
246 Asia/Jayapura WIT-9
247 Asia/Jerusalem IST-2IDT,M3.4.4/26,M10.5.0
248 Asia/Kabul <+0430>-4:30
249 Asia/Kamchatka <+12>-12
250 Asia/Karachi PKT-5
251 Asia/Kathmandu <+0545>-5:45
252 Asia/Khandyga <+09>-9
253 Asia/Kolkata IST-5:30
254 Asia/Krasnoyarsk <+07>-7
255 Asia/Kuala_Lumpur <+08>-8
256 Asia/Kuching <+08>-8
257 Asia/Kuwait <+03>-3
258 Asia/Macau CST-8
259 Asia/Magadan <+11>-11
260 Asia/Makassar WITA-8
261 Asia/Manila PST-8
262 Asia/Muscat <+04>-4
263 Asia/Nicosia EET-2EEST,M3.5.0/3,M10.5.0/4
264 Asia/Novokuznetsk <+07>-7
265 Asia/Novosibirsk <+07>-7
266 Asia/Omsk <+06>-6
267 Asia/Oral <+05>-5
268 Asia/Phnom_Penh <+07>-7
269 Asia/Pontianak WIB-7
270 Asia/Pyongyang KST-9
271 Asia/Qatar <+03>-3
272 Asia/Qyzylorda <+05>-5
273 Asia/Riyadh <+03>-3
274 Asia/Sakhalin <+11>-11
275 Asia/Samarkand <+05>-5
276 Asia/Seoul KST-9
277 Asia/Shanghai CST-8
278 Asia/Singapore <+08>-8
279 Asia/Srednekolymsk <+11>-11
280 Asia/Taipei CST-8
281 Asia/Tashkent <+05>-5
282 Asia/Tbilisi <+04>-4
283 Asia/Tehran <+0330>-3:30
284 Asia/Thimphu <+06>-6
285 Asia/Tokyo JST-9
286 Asia/Tomsk <+07>-7
287 Asia/Ulaanbaatar <+08>-8
288 Asia/Urumqi <+06>-6
289 Asia/Ust-Nera <+10>-10
290 Asia/Vientiane <+07>-7
291 Asia/Vladivostok <+10>-10
292 Asia/Yakutsk <+09>-9
293 Asia/Yangon <+0630>-6:30
294 Asia/Yekaterinburg <+05>-5
295 Asia/Yerevan <+04>-4
296 Atlantic/Azores <-01>1<+00>,M3.5.0/0,M10.5.0/1
297 Atlantic/Bermuda AST4ADT,M3.2.0,M11.1.0
298 Atlantic/Canary WET0WEST,M3.5.0/1,M10.5.0
299 Atlantic/Cape_Verde <-01>1
300 Atlantic/Faroe WET0WEST,M3.5.0/1,M10.5.0
301 Atlantic/Madeira WET0WEST,M3.5.0/1,M10.5.0
302 Atlantic/Reykjavik GMT0
303 Atlantic/South_Georgia <-02>2
304 Atlantic/Stanley <-03>3
305 Atlantic/St_Helena GMT0
306 Australia/Adelaide ACST-9:30ACDT,M10.1.0,M4.1.0/3
307 Australia/Brisbane AEST-10
308 Australia/Broken_Hill ACST-9:30ACDT,M10.1.0,M4.1.0/3
309 Australia/Currie AEST-10AEDT,M10.1.0,M4.1.0/3
310 Australia/Darwin ACST-9:30
311 Australia/Eucla <+0845>-8:45
312 Australia/Hobart AEST-10AEDT,M10.1.0,M4.1.0/3
313 Australia/Lindeman AEST-10
314 Australia/Lord_Howe <+1030>-10:30<+11>-11,M10.1.0,M4.1.0
315 Australia/Melbourne AEST-10AEDT,M10.1.0,M4.1.0/3
316 Australia/Perth AWST-8
317 Australia/Sydney AEST-10AEDT,M10.1.0,M4.1.0/3
318 Europe/Amsterdam CET-1CEST,M3.5.0,M10.5.0/3
319 Europe/Andorra CET-1CEST,M3.5.0,M10.5.0/3
320 Europe/Astrakhan <+04>-4
321 Europe/Athens EET-2EEST,M3.5.0/3,M10.5.0/4
322 Europe/Belgrade CET-1CEST,M3.5.0,M10.5.0/3
323 Europe/Berlin CET-1CEST,M3.5.0,M10.5.0/3
324 Europe/Bratislava CET-1CEST,M3.5.0,M10.5.0/3
325 Europe/Brussels CET-1CEST,M3.5.0,M10.5.0/3
326 Europe/Bucharest EET-2EEST,M3.5.0/3,M10.5.0/4
327 Europe/Budapest CET-1CEST,M3.5.0,M10.5.0/3
328 Europe/Busingen CET-1CEST,M3.5.0,M10.5.0/3
329 Europe/Chisinau EET-2EEST,M3.5.0,M10.5.0/3
330 Europe/Copenhagen CET-1CEST,M3.5.0,M10.5.0/3
331 Europe/Dublin IST-1GMT0,M10.5.0,M3.5.0/1
332 Europe/Gibraltar CET-1CEST,M3.5.0,M10.5.0/3
333 Europe/Guernsey GMT0BST,M3.5.0/1,M10.5.0
334 Europe/Helsinki EET-2EEST,M3.5.0/3,M10.5.0/4
335 Europe/Isle_of_Man GMT0BST,M3.5.0/1,M10.5.0
336 Europe/Istanbul <+03>-3
337 Europe/Jersey GMT0BST,M3.5.0/1,M10.5.0
338 Europe/Kaliningrad EET-2
339 Europe/Kiev EET-2EEST,M3.5.0/3,M10.5.0/4
340 Europe/Kirov MSK-3
341 Europe/Lisbon WET0WEST,M3.5.0/1,M10.5.0
342 Europe/Ljubljana CET-1CEST,M3.5.0,M10.5.0/3
343 Europe/London GMT0BST,M3.5.0/1,M10.5.0
344 Europe/Luxembourg CET-1CEST,M3.5.0,M10.5.0/3
345 Europe/Madrid CET-1CEST,M3.5.0,M10.5.0/3
346 Europe/Malta CET-1CEST,M3.5.0,M10.5.0/3
347 Europe/Mariehamn EET-2EEST,M3.5.0/3,M10.5.0/4
348 Europe/Minsk <+03>-3
349 Europe/Monaco CET-1CEST,M3.5.0,M10.5.0/3
350 Europe/Moscow MSK-3
351 Europe/Oslo CET-1CEST,M3.5.0,M10.5.0/3
352 Europe/Paris CET-1CEST,M3.5.0,M10.5.0/3
353 Europe/Podgorica CET-1CEST,M3.5.0,M10.5.0/3
354 Europe/Prague CET-1CEST,M3.5.0,M10.5.0/3
355 Europe/Riga EET-2EEST,M3.5.0/3,M10.5.0/4
356 Europe/Rome CET-1CEST,M3.5.0,M10.5.0/3
357 Europe/Samara <+04>-4
358 Europe/San_Marino CET-1CEST,M3.5.0,M10.5.0/3
359 Europe/Sarajevo CET-1CEST,M3.5.0,M10.5.0/3
360 Europe/Saratov <+04>-4
361 Europe/Simferopol MSK-3
362 Europe/Skopje CET-1CEST,M3.5.0,M10.5.0/3
363 Europe/Sofia EET-2EEST,M3.5.0/3,M10.5.0/4
364 Europe/Stockholm CET-1CEST,M3.5.0,M10.5.0/3
365 Europe/Tallinn EET-2EEST,M3.5.0/3,M10.5.0/4
366 Europe/Tirane CET-1CEST,M3.5.0,M10.5.0/3
367 Europe/Ulyanovsk <+04>-4
368 Europe/Uzhgorod EET-2EEST,M3.5.0/3,M10.5.0/4
369 Europe/Vaduz CET-1CEST,M3.5.0,M10.5.0/3
370 Europe/Vatican CET-1CEST,M3.5.0,M10.5.0/3
371 Europe/Vienna CET-1CEST,M3.5.0,M10.5.0/3
372 Europe/Vilnius EET-2EEST,M3.5.0/3,M10.5.0/4
373 Europe/Volgograd MSK-3
374 Europe/Warsaw CET-1CEST,M3.5.0,M10.5.0/3
375 Europe/Zagreb CET-1CEST,M3.5.0,M10.5.0/3
376 Europe/Zaporozhye EET-2EEST,M3.5.0/3,M10.5.0/4
377 Europe/Zurich CET-1CEST,M3.5.0,M10.5.0/3
378 Indian/Antananarivo EAT-3
379 Indian/Chagos <+06>-6
380 Indian/Christmas <+07>-7
381 Indian/Cocos <+0630>-6:30
382 Indian/Comoro EAT-3
383 Indian/Kerguelen <+05>-5
384 Indian/Mahe <+04>-4
385 Indian/Maldives <+05>-5
386 Indian/Mauritius <+04>-4
387 Indian/Mayotte EAT-3
388 Indian/Reunion <+04>-4
389 Pacific/Apia <+13>-13
390 Pacific/Auckland NZST-12NZDT,M9.5.0,M4.1.0/3
391 Pacific/Bougainville <+11>-11
392 Pacific/Chatham <+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45
393 Pacific/Chuuk <+10>-10
394 Pacific/Easter <-06>6<-05>,M9.1.6/22,M4.1.6/22
395 Pacific/Efate <+11>-11
396 Pacific/Enderbury <+13>-13
397 Pacific/Fakaofo <+13>-13
398 Pacific/Fiji <+12>-12
399 Pacific/Funafuti <+12>-12
400 Pacific/Galapagos <-06>6
401 Pacific/Gambier <-09>9
402 Pacific/Guadalcanal <+11>-11
403 Pacific/Guam ChST-10
404 Pacific/Honolulu HST10
405 Pacific/Kiritimati <+14>-14
406 Pacific/Kosrae <+11>-11
407 Pacific/Kwajalein <+12>-12
408 Pacific/Majuro <+12>-12
409 Pacific/Marquesas <-0930>9:30
410 Pacific/Midway SST11
411 Pacific/Nauru <+12>-12
412 Pacific/Niue <-11>11
413 Pacific/Norfolk <+11>-11<+12>,M10.1.0,M4.1.0/3
414 Pacific/Noumea <+11>-11
415 Pacific/Pago_Pago SST11
416 Pacific/Palau <+09>-9
417 Pacific/Pitcairn <-08>8
418 Pacific/Pohnpei <+11>-11
419 Pacific/Port_Moresby <+10>-10
420 Pacific/Rarotonga <-10>10
421 Pacific/Saipan ChST-10
422 Pacific/Tahiti <-10>10
423 Pacific/Tarawa <+12>-12
424 Pacific/Tongatapu <+13>-13
425 Pacific/Wake <+12>-12
426 Pacific/Wallis <+12>-12
427 Etc/GMT GMT0
428 Etc/GMT-0 GMT0
429 Etc/GMT-1 <+01>-1
430 Etc/GMT-2 <+02>-2
431 Etc/GMT-3 <+03>-3
432 Etc/GMT-4 <+04>-4
433 Etc/GMT-5 <+05>-5
434 Etc/GMT-6 <+06>-6
435 Etc/GMT-7 <+07>-7
436 Etc/GMT-8 <+08>-8
437 Etc/GMT-9 <+09>-9
438 Etc/GMT-10 <+10>-10
439 Etc/GMT-11 <+11>-11
440 Etc/GMT-12 <+12>-12
441 Etc/GMT-13 <+13>-13
442 Etc/GMT-14 <+14>-14
443 Etc/GMT0 GMT0
444 Etc/GMT+0 GMT0
445 Etc/GMT+1 <-01>1
446 Etc/GMT+2 <-02>2
447 Etc/GMT+3 <-03>3
448 Etc/GMT+4 <-04>4
449 Etc/GMT+5 <-05>5
450 Etc/GMT+6 <-06>6
451 Etc/GMT+7 <-07>7
452 Etc/GMT+8 <-08>8
453 Etc/GMT+9 <-09>9
454 Etc/GMT+10 <-10>10
455 Etc/GMT+11 <-11>11
456 Etc/GMT+12 <-12>12
457 Etc/UCT UTC0
458 Etc/UTC UTC0
459 Etc/Greenwich GMT0
460 Etc/Universal UTC0
461 Etc/Zulu UTC0

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="search.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
inkscape:export-filename="search.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="47.5"
inkscape:cx="12"
inkscape:cy="10.989474"
inkscape:window-width="2560"
inkscape:window-height="1368"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 884.20117,0.42105034 548.79064,-334.98948 q -39.92982,31.94386 -91.8386,50.57778 -51.90877,18.63392 -110.47251,18.63392 -145.07836,0 -245.56842,-100.49006 Q 0.42105263,-466.7579 0.42105263,-611.83626 q 0,-145.07836 100.49005737,-245.56842 100.49006,-100.49006 245.56842,-100.49006 145.07836,0 245.56842,100.49006 100.49006,100.49006 100.49006,245.56842 0,58.56374 -18.63392,110.47251 -18.63392,51.90878 -50.57778,91.8386 L 958.73684,-74.114622 Z M 346.47953,-372.25731 q 99.82456,0 169.70175,-69.8772 69.8772,-69.87719 69.8772,-169.70175 0,-99.82456 -69.8772,-169.70175 -69.87719,-69.8772 -169.70175,-69.8772 -99.82456,0 -169.70176,69.8772 -69.87718,69.87719 -69.87718,169.70175 0,99.82456 69.87718,169.70175 69.8772,69.8772 169.70176,69.8772 z"
id="path1"
style="stroke-width:1.33099" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="16"
viewBox="0 -960 640 640"
width="16"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="calendar_clock_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.svg"
inkscape:export-filename="calendar_clock_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.png"
inkscape:export-xdpi="96.18351"
inkscape:export-ydpi="96.18351"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="47.5"
inkscape:cx="10.842105"
inkscape:cy="10.821053"
inkscape:window-width="2560"
inkscape:window-height="1368"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 61.257143,-775.80752 H 487.10977 v -60.83609 H 61.257143 Z m 0,0 v -60.83609 z m 0,425.85263 q -25.094888,0 -42.965489,-17.8706 -17.870601,-17.8706 -17.870601,-42.96549 v -425.85263 q 0,-25.09489 17.870601,-42.96549 17.870601,-17.8706 42.965489,-17.8706 H 91.67518 v -60.83609 h 60.83609 v 60.83609 h 243.34437 v -60.83609 h 60.83609 v 60.83609 h 30.41804 q 25.09489,0 42.96549,17.8706 17.8706,17.8706 17.8706,42.96549 v 172.62241 q -14.44857,-6.84406 -29.6576,-11.40677 -15.20901,-4.56271 -31.17849,-6.84406 v -32.6994 H 61.257143 v 304.18045 H 252.89083 q 5.32315,16.72993 12.54744,31.93895 7.22428,15.20902 17.11015,28.89714 z m 395.434587,30.41805 q -63.11745,0 -107.60384,-44.48639 -44.48639,-44.4864 -44.48639,-107.60384 0,-63.11744 44.48639,-107.60384 44.48639,-44.48638 107.60384,-44.48638 63.11744,0 107.60383,44.48638 44.48639,44.4864 44.48639,107.60384 0,63.11744 -44.48639,107.60384 -44.48639,44.48639 -107.60383,44.48639 z m 50.95022,-79.84737 21.29264,-21.29263 -57.03384,-57.03384 V -562.8812 H 441.4827 v 97.33774 z"
id="path1"
style="stroke-width:0.760454;fill:#000000" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -12,8 +12,16 @@
- M5Stack CoreS3 SD card mounts, but cannot be read. There is currently a notice about it [here](https://github.com/espressif/esp-bsp/blob/master/bsp/m5stack_core_s3/README.md).
- EventFlag: Fix return value of set/get/wait (the errors are weirdly mixed in)
- Consistently use either ESP_TARGET or ESP_PLATFORM
- tt_check() failure during app argument bundle nullptr check seems to trigger SIGSEGV
- Fix bug in T-Deck/etc: esp_lvgl_port settings has a large stack depth (~9kB) to fix an issue where the T-Deck would get a stackoverflow. This sometimes happens when WiFi is auto-enabled and you open the app while it is still connecting.
# TODOs
- Try to fix SDL pipeline issue with apt-get:
```yaml
- name: Install SDL
run: sudo apt-get install -y libsdl2-dev cmake
```
- Make "blocking" argument the last one, and put it default to false (or remove it entirely?): void startApp(const std::string& id, bool blocking, std::shared_ptr<const Bundle> parameters) {
- Boot hooks instead of a single boot method in config. Define different boot phases/levels in enum.
- Add toggle to Display app for sysmon overlay: https://docs.lvgl.io/master/API/others/sysmon/index.html
- CrashHandler: use "corrupted" flag

View File

@ -23,13 +23,14 @@ struct Gui {
PubSubSubscription* loader_pubsub_subscription = nullptr;
// Layers and Canvas
lv_obj_t* lvgl_parent = nullptr;
lv_obj_t* appRootWidget = nullptr;
lv_obj_t* statusbarWidget = nullptr;
// App-specific
ViewPort* app_view_port = nullptr;
ViewPort* appViewPort = nullptr;
lv_obj_t* _Nullable keyboard = nullptr;
lv_group_t* keyboard_group = nullptr;
lv_group_t* keyboardGroup = nullptr;
};
/** Update GUI, request redraw */

View File

@ -90,10 +90,13 @@ public:
// endregion LoaderMessage
struct Loader {
std::shared_ptr<PubSub> pubsub_internal = std::make_shared<PubSub>();
std::shared_ptr<PubSub> pubsub_external = std::make_shared<PubSub>();
std::shared_ptr<PubSub> pubsubInternal = std::make_shared<PubSub>();
std::shared_ptr<PubSub> pubsubExternal = std::make_shared<PubSub>();
Mutex mutex = Mutex(Mutex::TypeRecursive);
std::stack<app::AppInstance*> app_stack;
std::stack<app::AppInstance*> appStack;
/** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods.
* This includes full LVGL redraw via Gui::redraw()
*/
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
};

View File

@ -54,6 +54,8 @@ namespace app {
namespace settings { extern const AppManifest manifest; }
namespace systeminfo { extern const AppManifest manifest; }
namespace textviewer { extern const AppManifest manifest; }
namespace timedatesettings { extern const AppManifest manifest; }
namespace timezone { extern const AppManifest manifest; }
namespace usbsettings { extern const AppManifest manifest; }
namespace wifiapsettings { extern const AppManifest manifest; }
namespace wificonnect { extern const AppManifest manifest; }
@ -87,6 +89,8 @@ static const std::vector<const app::AppManifest*> system_apps = {
&app::selectiondialog::manifest,
&app::systeminfo::manifest,
&app::textviewer::manifest,
&app::timedatesettings::manifest,
&app::timezone::manifest,
&app::usbsettings::manifest,
&app::wifiapsettings::manifest,
&app::wificonnect::manifest,

View File

@ -10,6 +10,7 @@
#include "lvgl.h"
#include "Tactility.h"
#include "hal/usb/Usb.h"
#include "kernel/SystemEvents.h"
#ifdef ESP_PLATFORM
#include "kernel/PanicHandler.h"
@ -35,6 +36,8 @@ struct Data {
static int32_t bootThreadCallback(TT_UNUSED void* context) {
TickType_t start_time = kernel::getTicks();
kernel::systemEventPublish(kernel::SystemEvent::BootSplash);
auto* lvgl_display = lv_display_get_default();
tt_assert(lvgl_display != nullptr);
auto* hal_display = (hal::Display*)lv_display_get_user_data(lvgl_display);

View File

@ -2,6 +2,7 @@
#include "TactilityCore.h"
#include <cstring>
#include <bits/stdc++.h>
#include <StringUtils.h>
namespace tt::app::files {
@ -69,25 +70,13 @@ bool isSupportedExecutableFile(const std::string& filename) {
#endif
}
template <typename T>
std::basic_string<T> lowercase(const std::basic_string<T>& input) {
std::basic_string<T> output = input;
std::transform(
output.begin(),
output.end(),
output.begin(),
[](const T character) { return static_cast<T>(std::tolower(character)); }
);
return std::move(output);
}
bool isSupportedImageFile(const std::string& filename) {
// Currently only the PNG library is built into Tactility
return lowercase(filename).ends_with(".png");
return string::lowercase(filename).ends_with(".png");
}
bool isSupportedTextFile(const std::string& filename) {
std::string filename_lower = lowercase(filename);
std::string filename_lower = string::lowercase(filename);
return filename_lower.ends_with(".txt") ||
filename_lower.ends_with(".ini") ||
filename_lower.ends_with(".json") ||

View File

@ -0,0 +1,136 @@
#include <StringUtils.h>
#include "lvgl.h"
#include "lvgl/Toolbar.h"
#include "service/loader/Loader.h"
#include "app/timezone/TimeZone.h"
#include "Assets.h"
#include "Tactility.h"
#include "time/Time.h"
#include "lvgl/LvglSync.h"
#define TAG "text_viewer"
namespace tt::app::timedatesettings {
extern const AppManifest manifest;
struct Data {
Mutex mutex = Mutex(Mutex::TypeRecursive);
lv_obj_t* regionLabelWidget = nullptr;
};
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
std::shared_ptr<Data> _Nullable optData() {
app::AppContext* app = service::loader::getCurrentApp();
if (app->getManifest().id == manifest.id) {
return std::static_pointer_cast<Data>(app->getData());
} else {
return nullptr;
}
}
static void onConfigureTimeZonePressed(TT_UNUSED lv_event_t* event) {
timezone::start();
}
static void onTimeFormatChanged(lv_event_t* event) {
auto* widget = lv_event_get_target_obj(event);
bool show_24 = lv_obj_has_state(widget, LV_STATE_CHECKED);
time::setTimeFormat24Hour(show_24);
}
static void onShow(AppContext& app, lv_obj_t* parent) {
auto data = std::static_pointer_cast<Data>(app.getData());
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app);
auto* main_wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(main_wrapper, LV_PCT(100));
lv_obj_set_flex_grow(main_wrapper, 1);
auto* region_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_width(region_wrapper, LV_PCT(100));
lv_obj_set_height(region_wrapper, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(region_wrapper, 0, 0);
lv_obj_set_style_border_width(region_wrapper, 0, 0);
auto* region_prefix_label = lv_label_create(region_wrapper);
lv_label_set_text(region_prefix_label, "Region: ");
lv_obj_align(region_prefix_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* region_label = lv_label_create(region_wrapper);
std::string timeZoneName = time::getTimeZoneName();
if (timeZoneName.empty()) {
timeZoneName = "not set";
}
data->regionLabelWidget = region_label;
lv_label_set_text(region_label, timeZoneName.c_str());
// TODO: Find out why Y offset is needed
lv_obj_align_to(region_label, region_prefix_label, LV_ALIGN_OUT_RIGHT_MID, 0, 8);
auto* region_button = lv_button_create(region_wrapper);
lv_obj_align(region_button, LV_ALIGN_TOP_RIGHT, 0, 0);
auto* region_button_image = lv_image_create(region_button);
lv_obj_add_event_cb(region_button, onConfigureTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr);
lv_image_set_src(region_button_image, LV_SYMBOL_SETTINGS);
auto* time_format_wrapper= lv_obj_create(main_wrapper);
lv_obj_set_width(time_format_wrapper, LV_PCT(100));
lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(time_format_wrapper, 0, 0);
lv_obj_set_style_border_width(time_format_wrapper, 0, 0);
auto* time_24h_label = lv_label_create(time_format_wrapper);
lv_label_set_text(time_24h_label, "24-hour clock");
lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* time_24h_switch = lv_switch_create(time_format_wrapper);
lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(time_24h_switch, onTimeFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr);
if (time::isTimeFormat24Hour()) {
lv_obj_add_state(time_24h_switch, LV_STATE_CHECKED);
} else {
lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED);
}
}
static void onStart(AppContext& app) {
auto data = std::make_shared<Data>();
app.setData(data);
}
static void onResult(AppContext& app, Result result, const Bundle& bundle) {
if (result == ResultOk) {
auto data = std::static_pointer_cast<Data>(app.getData());
auto name = timezone::getResultName(bundle);
auto code = timezone::getResultCode(bundle);
TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str());
time::setTimeZone(name, code);
if (!name.empty()) {
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
lv_label_set_text(data->regionLabelWidget, name.c_str());
lvgl::unlock();
}
}
}
}
extern const AppManifest manifest = {
.id = "TimeDateSettings",
.name = "Time & Date",
.icon = TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS,
.type = TypeSettings,
.onStart = onStart,
.onShow = onShow,
.onResult = onResult
};
void start() {
service::loader::startApp(manifest.id);
}
} // namespace

View File

@ -0,0 +1,7 @@
#pragma once
namespace tt::app::timedatesettings {
void start();
}

View File

@ -0,0 +1,243 @@
#include "TimeZone.h"
#include "app/AppManifest.h"
#include "app/AppContext.h"
#include "service/loader/Loader.h"
#include "lvgl.h"
#include "lvgl/Toolbar.h"
#include "Partitions.h"
#include "TactilityHeadless.h"
#include "lvgl/LvglSync.h"
#include "service/gui/Gui.h"
#include <memory>
#include <StringUtils.h>
#include <Timer.h>
namespace tt::app::timezone {
#define TAG "timezone_select"
#define RESULT_BUNDLE_CODE_INDEX "code"
#define RESULT_BUNDLE_NAME_INDEX "name"
extern const AppManifest manifest;
struct TimeZoneEntry {
std::string name;
std::string code;
};
struct Data {
Mutex mutex;
std::vector<TimeZoneEntry> entries;
std::unique_ptr<Timer> updateTimer;
lv_obj_t* listWidget = nullptr;
lv_obj_t* filterTextareaWidget = nullptr;
};
static void updateList(std::shared_ptr<Data>& data);
static bool parseEntry(const std::string& input, std::string& outName, std::string& outCode) {
std::string partial_strip = input.substr(1, input.size() - 3);
auto first_end_quote = partial_strip.find('"');
if (first_end_quote == std::string::npos) {
return false;
} else {
outName = partial_strip.substr(0, first_end_quote);
outCode = partial_strip.substr(first_end_quote + 3);
return true;
}
}
// region Result
std::string getResultName(const Bundle& bundle) {
std::string result;
bundle.optString(RESULT_BUNDLE_NAME_INDEX, result);
return result;
}
std::string getResultCode(const Bundle& bundle) {
std::string result;
bundle.optString(RESULT_BUNDLE_CODE_INDEX, result);
return result;
}
void setResultName(std::shared_ptr<Bundle>& bundle, const std::string& name) {
bundle->putString(RESULT_BUNDLE_NAME_INDEX, name);
}
void setResultCode(std::shared_ptr<Bundle>& bundle, const std::string& code) {
bundle->putString(RESULT_BUNDLE_CODE_INDEX, code);
}
// endregion
static void onUpdateTimer(std::shared_ptr<void> context) {
auto data = std::static_pointer_cast<Data>(context);
updateList(data);
}
static void onTextareaValueChanged(TT_UNUSED lv_event_t* e) {
auto* app = service::loader::getCurrentApp();
auto app_data = app->getData();
auto data = std::static_pointer_cast<Data>(app_data);
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) {
if (data->updateTimer->isRunning()) {
data->updateTimer->stop();
}
data->updateTimer->start(500 / portTICK_PERIOD_MS);
data->mutex.unlock();
}
}
static void onListItemSelected(lv_event_t* e) {
auto index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e));
TT_LOG_I(TAG, "Selected item at index %zu", index);
auto* app = service::loader::getCurrentApp();
auto data = std::static_pointer_cast<Data>(app->getData());
auto& entry = data->entries[index];
auto bundle = std::make_shared<Bundle>();
setResultName(bundle, entry.name);
setResultCode(bundle, entry.code);
app->setResult(app::ResultOk, bundle);
service::loader::stopApp();
}
static void createListItem(lv_obj_t* list, const std::string& title, size_t index) {
lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str());
lv_obj_add_event_cb(btn, &onListItemSelected, LV_EVENT_SHORT_CLICKED, (void*)index);
}
static void readTimeZones(const std::shared_ptr<Data>& data, std::string filter) {
auto path = std::string(MOUNT_POINT_SYSTEM) + "/timezones.csv";
auto* file = fopen(path.c_str(), "rb");
if (file == nullptr) {
TT_LOG_E(TAG, "Failed to open %s", path.c_str());
return;
}
char line[96];
std::string name;
std::string code;
uint32_t count = 0;
std::vector<TimeZoneEntry> entries;
while (fgets(line, 96, file)) {
if (parseEntry(line, name, code)) {
if (tt::string::lowercase(name).find(filter) != std::string::npos) {
count++;
entries.push_back({
.name = name,
.code = code
});
// Safety guard
if (count > 50) {
// TODO: Show warning that we're not displaying a complete list
break;
}
}
} else {
TT_LOG_E(TAG, "Parse error at line %lu", count);
}
}
fclose(file);
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) {
data->entries = std::move(entries);
data->mutex.unlock();
} else {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
}
TT_LOG_I(TAG, "Processed %lu entries", count);
}
static void updateList(std::shared_ptr<Data>& data) {
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
std::string filter = tt::string::lowercase(std::string(lv_textarea_get_text(data->filterTextareaWidget)));
readTimeZones(data, filter);
lvgl::unlock();
} else {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
return;
}
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) {
lv_obj_clean(data->listWidget);
uint32_t index = 0;
for (auto& entry : data->entries) {
createListItem(data->listWidget, entry.name, index);
index++;
}
data->mutex.unlock();
}
lvgl::unlock();
}
}
static void onShow(AppContext& app, lv_obj_t* parent) {
auto data = std::static_pointer_cast<Data>(app.getData());
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app);
auto* search_wrapper = lv_obj_create(parent);
lv_obj_set_size(search_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(search_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(search_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START);
lv_obj_set_style_pad_all(search_wrapper, 0, 0);
lv_obj_set_style_border_width(search_wrapper, 0, 0);
auto* icon = lv_image_create(search_wrapper);
lv_obj_set_style_margin_left(icon, 8, 0);
lv_obj_set_style_image_recolor_opa(icon, 255, 0);
lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0);
std::string icon_path = app.getPaths()->getSystemPathLvgl("search.png");
lv_image_set_src(icon, icon_path.c_str());
lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0);
auto* textarea = lv_textarea_create(search_wrapper);
lv_textarea_set_placeholder_text(textarea, "e.g. Europe/Amsterdam");
lv_textarea_set_one_line(textarea, true);
lv_obj_add_event_cb(textarea, onTextareaValueChanged, LV_EVENT_VALUE_CHANGED, nullptr);
data->filterTextareaWidget = textarea;
lv_obj_set_flex_grow(textarea, 1);
service::gui::keyboardAddTextArea(textarea);
auto* list = lv_list_create(parent);
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
lv_obj_set_style_border_width(list, 0, 0);
data->listWidget = list;
}
static void onStart(AppContext& app) {
auto data = std::make_shared<Data>();
data->updateTimer = std::make_unique<Timer>(Timer::TypeOnce, onUpdateTimer, data);
app.setData(data);
}
extern const AppManifest manifest = {
.id = "TimeZone",
.name = "Select timezone",
.type = TypeHidden,
.onStart = onStart,
.onShow = onShow,
};
void start() {
service::loader::startApp(manifest.id);
}
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "Bundle.h"
namespace tt::app::timezone {
void start();
std::string getResultName(const Bundle& bundle);
std::string getResultCode(const Bundle& bundle);
}

View File

@ -6,6 +6,7 @@
#include "hal/Keyboard.h"
#include "lvgl/LvglKeypad.h"
#include "lvgl/Lvgl.h"
#include "kernel/SystemEvents.h"
namespace tt::lvgl {
@ -76,6 +77,8 @@ bool initKeyboard(hal::Display* display, hal::Keyboard* keyboard) {
void init(const hal::Configuration& config) {
TT_LOG_I(TAG, "Starting");
kernel::systemEventPublish(kernel::SystemEvent::BootInitLvglBegin);
if (config.initLvgl != nullptr && !config.initLvgl()) {
TT_LOG_E(TAG, "LVGL init failed");
return;
@ -98,6 +101,8 @@ void init(const hal::Configuration& config) {
}
TT_LOG_I(TAG, "Finished");
kernel::systemEventPublish(kernel::SystemEvent::BootInitLvglEnd);
}
} // namespace

View File

@ -1,4 +1,6 @@
#define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration
#include <Timer.h>
#include "Statusbar.h"
#include "Mutex.h"
@ -8,11 +10,15 @@
#include "LvglSync.h"
#include "lvgl.h"
#include "kernel/SystemEvents.h"
#include "time/Time.h"
namespace tt::lvgl {
#define TAG "statusbar"
static void onUpdateTime(TT_UNUSED std::shared_ptr<void> context);
struct StatusbarIcon {
std::string image;
bool visible = false;
@ -23,12 +29,18 @@ struct StatusbarData {
Mutex mutex = Mutex(Mutex::TypeRecursive);
std::shared_ptr<PubSub> pubsub = std::make_shared<PubSub>();
StatusbarIcon icons[STATUSBAR_ICON_LIMIT] = {};
Timer* time_update_timer = new Timer(Timer::TypeOnce, onUpdateTime, nullptr);
uint8_t time_hours = 0;
uint8_t time_minutes = 0;
bool time_set = false;
kernel::SystemEventSubscription systemEventSubscription = 0;
};
static StatusbarData statusbar_data;
typedef struct {
lv_obj_t obj;
lv_obj_t* time;
lv_obj_t* icons[STATUSBAR_ICON_LIMIT];
lv_obj_t* battery_icon;
PubSubSubscription* pubsub_subscription;
@ -46,8 +58,40 @@ static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj);
static void statusbar_destructor(const lv_obj_class_t* class_p, lv_obj_t* obj);
static void statusbar_event(const lv_obj_class_t* class_p, lv_event_t* event);
static void update_time(Statusbar* statusbar);
static void update_main(Statusbar* statusbar);
static TickType_t getNextUpdateTime() {
time_t now = ::time(nullptr);
struct tm* tm_struct = localtime(&now);
uint32_t seconds_to_wait = 60U - tm_struct->tm_sec;
TT_LOG_I(TAG, "Update in %lu s", seconds_to_wait);
return pdMS_TO_TICKS(seconds_to_wait * 1000U);
}
static void onUpdateTime(TT_UNUSED std::shared_ptr<void> context) {
time_t now = ::time(nullptr);
struct tm* tm_struct = localtime(&now);
if (statusbar_data.mutex.lock(100 / portTICK_PERIOD_MS)) {
if (tm_struct->tm_year >= (2025 - 1900)) {
statusbar_data.time_hours = tm_struct->tm_hour;
statusbar_data.time_minutes = tm_struct->tm_min;
statusbar_data.time_set = true;
// Reschedule
statusbar_data.time_update_timer->start(getNextUpdateTime());
// Notify widget
tt_pubsub_publish(statusbar_data.pubsub, nullptr);
} else {
statusbar_data.time_update_timer->start(pdMS_TO_TICKS(60000U));
}
statusbar_data.mutex.unlock();
}
}
static const lv_obj_class_t statusbar_class = {
.base_class = &lv_obj_class,
.constructor_cb = &statusbar_constructor,
@ -73,6 +117,15 @@ static void statusbar_pubsub_event(TT_UNUSED const void* message, void* obj) {
}
}
static void onNetworkConnected(TT_UNUSED kernel::SystemEvent event) {
if (statusbar_data.mutex.lock(100 / portTICK_PERIOD_MS)) {
statusbar_data.time_update_timer->stop();
statusbar_data.time_update_timer->start(5);
statusbar_data.mutex.unlock();
}
}
static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
LV_UNUSED(class_p);
LV_TRACE_OBJ_CREATE("begin");
@ -80,6 +133,14 @@ static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj)
LV_TRACE_OBJ_CREATE("finished");
auto* statusbar = (Statusbar*)obj;
statusbar->pubsub_subscription = tt_pubsub_subscribe(statusbar_data.pubsub, &statusbar_pubsub_event, statusbar);
if (!statusbar_data.time_update_timer->isRunning()) {
statusbar_data.time_update_timer->start(50 / portTICK_PERIOD_MS);
statusbar_data.systemEventSubscription = kernel::systemEventAddListener(
kernel::SystemEvent::Time,
onNetworkConnected
);
}
}
static void statusbar_destructor(TT_UNUSED const lv_obj_class_t* class_p, lv_obj_t* obj) {
@ -108,15 +169,21 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
obj_set_style_no_padding(obj);
lv_obj_center(obj);
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t* left_spacer = lv_obj_create(obj);
statusbar->time = lv_label_create(obj);
lv_obj_set_style_text_color(statusbar->time, lv_color_white(), 0);
lv_obj_set_style_margin_left(statusbar->time, 4, 0);
update_time(statusbar);
auto* left_spacer = lv_obj_create(obj);
lv_obj_set_size(left_spacer, 1, 1);
obj_set_style_bg_invisible(left_spacer);
lv_obj_set_flex_grow(left_spacer, 1);
statusbar_lock(TtWaitForever);
for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) {
lv_obj_t* image = lv_image_create(obj);
auto* image = lv_image_create(obj);
lv_obj_set_size(image, STATUSBAR_ICON_SIZE, STATUSBAR_ICON_SIZE);
obj_set_style_no_padding(image);
obj_set_style_bg_blacken(image);
@ -129,7 +196,19 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
return obj;
}
static void update_time(Statusbar* statusbar) {
if (statusbar_data.time_set) {
bool format24 = time::isTimeFormat24Hour();
int hours = format24 ? statusbar_data.time_hours : statusbar_data.time_hours % 12;
lv_label_set_text_fmt(statusbar->time, "%d:%02d", hours, statusbar_data.time_minutes);
} else {
lv_label_set_text(statusbar->time, "");
}
}
static void update_main(Statusbar* statusbar) {
update_time(statusbar);
if (statusbar_lock(50 / portTICK_PERIOD_MS)) {
for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) {
update_icon(statusbar->icons[i], &(statusbar_data.icons[i]));
@ -150,8 +229,6 @@ static void statusbar_event(TT_UNUSED const lv_obj_class_t* class_p, lv_event_t*
if (code == LV_EVENT_VALUE_CHANGED) {
lv_obj_invalidate(obj);
} else if (code == LV_EVENT_DRAW_MAIN) {
// NO-OP
}
}

View File

@ -1,9 +1,10 @@
#include "Tactility.h"
#include "service/gui/Gui_i.h"
#include "service/loader/Loader_i.h"
#include "lvgl/LvglKeypad.h"
#include "lvgl/LvglSync.h"
#include "RtosCompat.h"
#include "lvgl/Style.h"
#include "lvgl/Statusbar.h"
namespace tt::service::gui {
@ -11,11 +12,11 @@ namespace tt::service::gui {
// Forward declarations
void redraw(Gui*);
static int32_t gui_main(void*);
static int32_t guiMain(TT_UNUSED void* p);
Gui* gui = nullptr;
void loader_callback(const void* message, TT_UNUSED void* context) {
void onLoaderMessage(const void* message, TT_UNUSED void* context) {
auto* event = static_cast<const loader::LoaderEvent*>(message);
if (event->type == loader::LoaderEventTypeApplicationShowing) {
app::AppContext& app = event->app_showing.app;
@ -32,13 +33,34 @@ Gui* gui_alloc() {
instance->thread = new Thread(
"gui",
4096, // Last known minimum was 2800 for launching desktop
&gui_main,
&guiMain,
nullptr
);
instance->loader_pubsub_subscription = tt_pubsub_subscribe(loader::getPubsub(), &loader_callback, instance);
instance->loader_pubsub_subscription = tt_pubsub_subscribe(loader::getPubsub(), &onLoaderMessage, instance);
tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS));
instance->keyboard_group = lv_group_create();
instance->lvgl_parent = lv_scr_act();
instance->keyboardGroup = lv_group_create();
auto* screen_root = lv_scr_act();
lvgl::obj_set_style_bg_blacken(screen_root);
lv_obj_t* vertical_container = lv_obj_create(screen_root);
lv_obj_set_size(vertical_container, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(vertical_container);
lvgl::obj_set_style_bg_blacken(vertical_container);
instance->statusbarWidget = lvgl::statusbar_create(vertical_container);
auto* app_container = lv_obj_create(vertical_container);
lvgl::obj_set_style_no_padding(app_container);
lv_obj_set_style_border_width(app_container, 0, 0);
lvgl::obj_set_style_bg_blacken(app_container);
lv_obj_set_width(app_container, LV_PCT(100));
lv_obj_set_flex_grow(app_container, 1);
lv_obj_set_flex_flow(app_container, LV_FLEX_FLOW_COLUMN);
instance->appRootWidget = app_container;
lvgl::unlock();
return instance;
@ -48,9 +70,9 @@ void gui_free(Gui* instance) {
tt_assert(instance != nullptr);
delete instance->thread;
lv_group_delete(instance->keyboard_group);
lv_group_delete(instance->keyboardGroup);
tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS));
lv_group_del(instance->keyboard_group);
lv_group_del(instance->keyboardGroup);
lvgl::unlock();
delete instance;
@ -74,15 +96,15 @@ void requestDraw() {
void showApp(app::AppContext& app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide) {
lock();
tt_check(gui->app_view_port == nullptr);
gui->app_view_port = view_port_alloc(app, on_show, on_hide);
tt_check(gui->appViewPort == nullptr);
gui->appViewPort = view_port_alloc(app, on_show, on_hide);
unlock();
requestDraw();
}
void hideApp() {
lock();
ViewPort* view_port = gui->app_view_port;
ViewPort* view_port = gui->appViewPort;
tt_check(view_port != nullptr);
// We must lock the LVGL port, because the viewport hide callbacks
@ -92,11 +114,11 @@ void hideApp() {
lvgl::unlock();
view_port_free(view_port);
gui->app_view_port = nullptr;
gui->appViewPort = nullptr;
unlock();
}
static int32_t gui_main(TT_UNUSED void* p) {
static int32_t guiMain(TT_UNUSED void* p) {
tt_check(gui);
Gui* local_gui = gui;

View File

@ -9,27 +9,14 @@ namespace tt::service::gui {
#define TAG "gui"
static lv_obj_t* create_app_views(Gui* gui, lv_obj_t* parent, app::AppContext& app) {
lvgl::obj_set_style_bg_blacken(parent);
lv_obj_t* vertical_container = lv_obj_create(parent);
lv_obj_set_size(vertical_container, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(vertical_container);
lvgl::obj_set_style_bg_blacken(vertical_container);
// TODO: Move statusbar into separate ViewPort
app::Flags flags = app.getFlags();
if (flags.showStatusbar) {
lvgl::statusbar_create(vertical_container);
}
lv_obj_t* child_container = lv_obj_create(vertical_container);
static lv_obj_t* createAppViews(Gui* gui, lv_obj_t* parent, app::AppContext& app) {
lv_obj_send_event(gui->statusbarWidget, LV_EVENT_DRAW_MAIN, nullptr);
lv_obj_t* child_container = lv_obj_create(parent);
lv_obj_set_width(child_container, LV_PCT(100));
lv_obj_set_flex_grow(child_container, 1);
if (keyboardIsEnabled()) {
gui->keyboard = lv_keyboard_create(vertical_container);
gui->keyboard = lv_keyboard_create(parent);
lv_obj_add_flag(gui->keyboard, LV_OBJ_FLAG_HIDDEN);
} else {
gui->keyboard = nullptr;
@ -45,12 +32,20 @@ void redraw(Gui* gui) {
lock();
if (lvgl::lock(1000)) {
lv_obj_clean(gui->lvgl_parent);
lv_obj_clean(gui->appRootWidget);
ViewPort* view_port = gui->app_view_port;
ViewPort* view_port = gui->appViewPort;
if (view_port != nullptr) {
app::AppContext& app = view_port->app;
lv_obj_t* container = create_app_views(gui, gui->lvgl_parent, app);
app::Flags flags = app.getFlags();
if (flags.showStatusbar) {
lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t* container = createAppViews(gui, gui->appRootWidget, app);
view_port_show(view_port, container);
} else {
TT_LOG_W(TAG, "nothing to draw");

View File

@ -54,9 +54,9 @@ void keyboardAddTextArea(lv_obj_t* textarea) {
}
// lv_obj_t auto-remove themselves from the group when they are destroyed (last checked in LVGL 8.3)
lv_group_add_obj(gui->keyboard_group, textarea);
lv_group_add_obj(gui->keyboardGroup, textarea);
lvgl::keypad_activate(gui->keyboard_group);
lvgl::keypad_activate(gui->keyboardGroup);
lvgl::unlock();
unlock();

View File

@ -11,7 +11,6 @@
#else
#include "lvgl/LvglSync.h"
#include "TactilityHeadless.h"
#endif
namespace tt::service::loader {
@ -42,7 +41,7 @@ static void loader_free() {
loader_singleton = nullptr;
}
void startApp(const std::string& id, bool blocking, std::shared_ptr<const Bundle> parameters) {
void startApp(const std::string& id, bool blocking, const std::shared_ptr<const Bundle>& parameters) {
TT_LOG_I(TAG, "Start app %s", id.c_str());
tt_assert(loader_singleton);
@ -67,7 +66,7 @@ void stopApp() {
app::AppContext* _Nullable getCurrentApp() {
tt_assert(loader_singleton);
if (loader_singleton->mutex.lock(10 / portTICK_PERIOD_MS)) {
app::AppInstance* app = loader_singleton->app_stack.top();
app::AppInstance* app = loader_singleton->appStack.top();
loader_singleton->mutex.unlock();
return dynamic_cast<app::AppContext*>(app);
} else {
@ -80,7 +79,7 @@ std::shared_ptr<PubSub> getPubsub() {
// it's safe to return pubsub without locking
// because it's never freed and loader is never exited
// also the loader instance cannot be obtained until the pubsub is created
return loader_singleton->pubsub_external;
return loader_singleton->pubsubExternal;
}
static const char* appStateToString(app::State state) {
@ -129,7 +128,7 @@ static void transitionAppToState(app::AppInstance& app, app::State state) {
.app = app
}
};
tt_pubsub_publish(loader_singleton->pubsub_external, &event_showing);
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_showing);
app.setState(app::StateShowing);
break;
}
@ -140,7 +139,7 @@ static void transitionAppToState(app::AppInstance& app, app::State state) {
.app = app
}
};
tt_pubsub_publish(loader_singleton->pubsub_external, &event_hiding);
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_hiding);
app.setState(app::StateHiding);
break;
}
@ -167,11 +166,11 @@ static LoaderStatus startAppWithManifestInternal(
return LoaderStatusErrorInternal;
}
auto previous_app = !loader_singleton->app_stack.empty() ? loader_singleton->app_stack.top() : nullptr;
auto previous_app = !loader_singleton->appStack.empty() ? loader_singleton->appStack.top() : nullptr;
auto new_app = new app::AppInstance(*manifest, parameters);
new_app->mutableFlags().showStatusbar = (manifest->type != app::TypeBoot);
loader_singleton->app_stack.push(new_app);
loader_singleton->appStack.push(new_app);
transitionAppToState(*new_app, app::StateInitial);
transitionAppToState(*new_app, app::StateStarted);
@ -183,7 +182,7 @@ static LoaderStatus startAppWithManifestInternal(
transitionAppToState(*new_app, app::StateShowing);
LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStarted};
tt_pubsub_publish(loader_singleton->pubsub_internal, &event_internal);
tt_pubsub_publish(loader_singleton->pubsubInternal, &event_internal);
LoaderEvent event_external = {
.type = LoaderEventTypeApplicationStarted,
@ -191,7 +190,7 @@ static LoaderStatus startAppWithManifestInternal(
.app = *new_app
}
};
tt_pubsub_publish(loader_singleton->pubsub_external, &event_external);
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_external);
return LoaderStatusOk;
}
@ -228,7 +227,7 @@ static void stopAppInternal() {
return;
}
size_t original_stack_size = loader_singleton->app_stack.size();
size_t original_stack_size = loader_singleton->appStack.size();
if (original_stack_size == 0) {
TT_LOG_E(TAG, "Stop app: no app running");
@ -236,7 +235,7 @@ static void stopAppInternal() {
}
// Stop current app
app::AppInstance* app_to_stop = loader_singleton->app_stack.top();
app::AppInstance* app_to_stop = loader_singleton->appStack.top();
if (original_stack_size == 1 && app_to_stop->getManifest().type != app::TypeBoot) {
TT_LOG_E(TAG, "Stop app: can't stop root app");
@ -249,7 +248,7 @@ static void stopAppInternal() {
transitionAppToState(*app_to_stop, app::StateHiding);
transitionAppToState(*app_to_stop, app::StateStopped);
loader_singleton->app_stack.pop();
loader_singleton->appStack.pop();
delete app_to_stop;
#ifdef ESP_PLATFORM
@ -259,8 +258,8 @@ static void stopAppInternal() {
app::AppOnResult on_result = nullptr;
app::AppInstance* app_to_resume = nullptr;
// If there's a previous app, resume it
if (!loader_singleton->app_stack.empty()) {
app_to_resume = loader_singleton->app_stack.top();
if (!loader_singleton->appStack.empty()) {
app_to_resume = loader_singleton->appStack.top();
tt_assert(app_to_resume);
transitionAppToState(*app_to_resume, app::StateShowing);
@ -272,7 +271,7 @@ static void stopAppInternal() {
// WARNING: After this point we cannot change the app states from this method directly anymore as we don't have a lock!
LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStopped};
tt_pubsub_publish(loader_singleton->pubsub_internal, &event_internal);
tt_pubsub_publish(loader_singleton->pubsubInternal, &event_internal);
LoaderEvent event_external = {
.type = LoaderEventTypeApplicationStopped,
@ -280,7 +279,7 @@ static void stopAppInternal() {
.manifest = manifest
}
};
tt_pubsub_publish(loader_singleton->pubsub_external, &event_external);
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_external);
if (on_result != nullptr && app_to_resume != nullptr) {
if (result_holder != nullptr) {

View File

@ -24,7 +24,7 @@ typedef enum {
* @param[in] blocking whether this call is blocking or not. You cannot call this from an LVGL thread.
* @param[in] parameters optional parameters to pass onto the application
*/
void startApp(const std::string& id, bool blocking = false, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
void startApp(const std::string& id, bool blocking = false, const std::shared_ptr<const Bundle>& _Nullable parameters = nullptr);
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
void stopApp();

View File

@ -33,3 +33,5 @@
* @param[in] lower lower bounds for x
*/
#define TT_CLAMP(x, upper, lower) (TT_MIN(upper, TT_MAX(x, lower)))
#define TT_STRINGIFY(x) #x

View File

@ -1,5 +1,6 @@
#pragma once
#include <bits/stdc++.h>
#include <cstdio>
#include <string>
#include <vector>
@ -48,4 +49,22 @@ std::vector<std::string> split(const std::string& input, const std::string& deli
*/
std::string join(const std::vector<std::string>& input, const std::string& delimiter);
/**
* Returns the lowercase value of a string.
* @param[in] the string with lower and/or uppercase characters
* @return a string with only lowercase characters
*/
template <typename T>
std::basic_string<T> lowercase(const std::basic_string<T>& input) {
std::basic_string<T> output = input;
std::transform(
output.begin(),
output.end(),
output.begin(),
[](const T character) { return static_cast<T>(std::tolower(character)); }
);
return std::move(output);
}
} // namespace

View File

@ -2,7 +2,6 @@
#include <utility>
#include "Check.h"
#include "kernel/Kernel.h"
#include "RtosCompat.h"
namespace tt {

View File

@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
if (DEFINED ENV{ESP_IDF_VERSION})
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
list(APPEND REQUIRES_LIST TactilityCore esp_wifi nvs_flash driver spiffs vfs fatfs)
list(APPEND REQUIRES_LIST TactilityCore esp_wifi nvs_flash driver spiffs vfs fatfs lwip)
if("${IDF_TARGET}" STREQUAL "esp32s3")
list(APPEND REQUIRES_LIST esp_tinyusb)
endif()

View File

@ -0,0 +1,7 @@
#pragma once
namespace tt::network::ntp {
void init();
}

View File

@ -0,0 +1,7 @@
#pragma once
namespace tt::time {
void init();
}

View File

@ -14,3 +14,4 @@
#define TT_ASSETS_APP_ICON_I2C_SETTINGS TT_ASSET("app_icon_i2c.png")
#define TT_ASSETS_APP_ICON_SETTINGS TT_ASSET("app_icon_settings.png")
#define TT_ASSETS_APP_ICON_SYSTEM_INFO TT_ASSET("app_icon_system_info.png")
#define TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS TT_ASSET("app_icon_time_date_settings.png")

View File

@ -4,6 +4,9 @@
#include "hal/Hal_i.h"
#include "service/ServiceManifest.h"
#include "service/ServiceRegistry.h"
#include "kernel/SystemEvents.h"
#include "network/NtpPrivate.h"
#include "time/TimePrivate.h"
#ifdef ESP_PLATFORM
#include "EspInit.h"
@ -39,7 +42,9 @@ void initHeadless(const hal::Configuration& config) {
initEsp();
#endif
hardwareConfig = &config;
time::init();
hal::init(config);
network::ntp::init();
register_and_start_system_services();
}

View File

@ -10,8 +10,6 @@ typedef bool (*InitBoot)();
typedef bool (*InitHardware)();
typedef bool (*InitLvgl)();
typedef void (*SetBacklightDuty)(uint8_t);
class Display;
class Keyboard;
typedef Display* (*CreateDisplay)();
@ -57,7 +55,7 @@ struct Configuration {
const CreatePower _Nullable power = nullptr;
/**
* A list of i2c devices (can be empty, but preferably accurately represents the device capabilities)
* A list of i2c interfaces
*/
const std::vector<i2c::Configuration> i2c = {};
};

View File

@ -1,16 +1,21 @@
#include "hal/Hal_i.h"
#include "hal/i2c/I2c.h"
#include "kernel/SystemEvents.h"
#define TAG "hal"
namespace tt::hal {
void init(const Configuration& configuration) {
kernel::systemEventPublish(kernel::SystemEvent::BootInitHalBegin);
kernel::systemEventPublish(kernel::SystemEvent::BootInitI2cBegin);
tt_check(i2c::init(configuration.i2c), "I2C init failed");
if (configuration.initHardware != nullptr) {
TT_LOG_I(TAG, "Init hardware");
tt_check(configuration.initHardware(), "Hardware init failed");
}
kernel::systemEventPublish(kernel::SystemEvent::BootInitI2cEnd);
if (configuration.initBoot != nullptr) {
TT_LOG_I(TAG, "Init power");
@ -23,6 +28,8 @@ void init(const Configuration& configuration) {
TT_LOG_W(TAG, "SD card mount failed (init can continue)");
}
}
kernel::systemEventPublish(kernel::SystemEvent::BootInitHalEnd);
}
} // namespace

View File

@ -0,0 +1,87 @@
#include "SystemEvents.h"
#include "Mutex.h"
#include "CoreExtraDefines.h"
#include <list>
#define TAG "system_event"
namespace tt::kernel {
struct SubscriptionData {
SystemEventSubscription id;
SystemEvent event;
OnSystemEvent handler;
};
static Mutex mutex;
static SystemEventSubscription subscriptionCounter = 0;
static std::list<SubscriptionData> subscriptions;
static const char* getEventName(SystemEvent event) {
switch (event) {
case SystemEvent::BootInitHalBegin:
return TT_STRINGIFY(SystemEvent::BootInitHalBegin);
case SystemEvent::BootInitHalEnd:
return TT_STRINGIFY(SystemEvent::BootInitHalEnd);
case SystemEvent::BootInitI2cBegin:
return TT_STRINGIFY(SystemEvent::BootInitI2cBegin);
case SystemEvent::BootInitI2cEnd:
return TT_STRINGIFY(SystemEvent::BootInitI2cEnd);
case SystemEvent::BootInitLvglBegin:
return TT_STRINGIFY(SystemEvent::BootInitLvglBegin);
case SystemEvent::BootInitLvglEnd:
return TT_STRINGIFY(SystemEvent::BootInitLvglEnd);
case SystemEvent::BootSplash:
return TT_STRINGIFY(SystemEvent::BootSplash);
case SystemEvent::NetworkConnected:
return TT_STRINGIFY(SystemEvent::NetworkConnected);
case SystemEvent::NetworkDisconnected:
return TT_STRINGIFY(SystemEvent::NetworkDisconnected);
case SystemEvent::Time:
return TT_STRINGIFY(SystemEvent::Time);
}
tt_crash(); // Missing case above
}
void systemEventPublish(SystemEvent event) {
TT_LOG_I(TAG, "%s", getEventName(event));
if (mutex.lock(portMAX_DELAY)) {
for (auto& subscription : subscriptions) {
if (subscription.event == event) {
subscription.handler(event);
}
}
mutex.unlock();
}
}
SystemEventSubscription systemEventAddListener(SystemEvent event, OnSystemEvent handler) {
if (mutex.lock(portMAX_DELAY)) {
auto id = ++subscriptionCounter;
subscriptions.push_back({
.id = id,
.event = event,
.handler = handler
});
mutex.unlock();
return id;
} else {
tt_crash();
}
}
void systemEventRemoveListener(SystemEventSubscription subscription) {
if (mutex.lock(portMAX_DELAY)) {
std::erase_if(subscriptions, [subscription](auto& item) {
return (item.id == subscription);
});
mutex.unlock();
}
}
}

View File

@ -0,0 +1,31 @@
#pragma once
#include <cstdint>
namespace tt::kernel {
enum class SystemEvent {
BootInitHalBegin,
BootInitHalEnd,
BootInitI2cBegin,
BootInitI2cEnd,
BootInitLvglBegin,
BootInitLvglEnd,
BootSplash,
/** Gained IP address */
NetworkConnected,
NetworkDisconnected,
/** An important system time-related event, such as NTP update or time-zone change */
Time,
};
/** Value 0 mean "no subscription" */
typedef uint32_t SystemEventSubscription;
typedef void (*OnSystemEvent)(SystemEvent event);
void systemEventPublish(SystemEvent event);
SystemEventSubscription systemEventAddListener(SystemEvent event, OnSystemEvent handler);
void systemEventRemoveListener(SystemEventSubscription subscription);
}

View File

@ -0,0 +1,34 @@
#include "network/NtpPrivate.h"
#ifdef ESP_PLATFORM
#include "kernel/SystemEvents.h"
#include "TactilityCore.h"
#include <esp_netif_sntp.h>
#include <esp_sntp.h>
#endif
#define TAG "ntp"
namespace tt::network::ntp {
#ifdef ESP_PLATFORM
static void onTimeSynced(struct timeval* tv) {
TT_LOG_I(TAG, "Time synced (%llu)", tv->tv_sec);
kernel::systemEventPublish(kernel::SystemEvent::Time);
}
void init() {
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org");
config.sync_cb = onTimeSynced;
esp_netif_sntp_init(&config);
}
#else
void init() {
}
#endif
}

View File

@ -0,0 +1,101 @@
#include <ctime>
#include "Time.h"
#include "Preferences.h"
#include "kernel/SystemEvents.h"
namespace tt::time {
#ifdef ESP_PLATFORM
#define TIME_SETTINGS_NAMESPACE "time"
#define TIMEZONE_PREFERENCES_KEY_NAME "tz_name"
#define TIMEZONE_PREFERENCES_KEY_CODE "tz_code"
#define TIMEZONE_PREFERENCES_KEY_TIME24 "tz_time24"
void init() {
auto code= getTimeZoneCode();
if (!code.empty()) {
setenv("TZ", code.c_str(), 1);
tzset();
}
}
void setTimeZone(const std::string& name, const std::string& code) {
Preferences preferences(TIME_SETTINGS_NAMESPACE);
preferences.putString(TIMEZONE_PREFERENCES_KEY_NAME, name);
preferences.putString(TIMEZONE_PREFERENCES_KEY_CODE, code);
setenv("TZ", code.c_str(), 1);
tzset();
kernel::systemEventPublish(kernel::SystemEvent::Time);
}
std::string getTimeZoneName() {
Preferences preferences(TIME_SETTINGS_NAMESPACE);
std::string result;
if (preferences.optString(TIMEZONE_PREFERENCES_KEY_NAME, result)) {
return result;
} else {
return {};
}
}
std::string getTimeZoneCode() {
Preferences preferences(TIME_SETTINGS_NAMESPACE);
std::string result;
if (preferences.optString(TIMEZONE_PREFERENCES_KEY_CODE, result)) {
return result;
} else {
return {};
}
}
bool isTimeFormat24Hour() {
Preferences preferences(TIME_SETTINGS_NAMESPACE);
bool show24Hour = true;
preferences.optBool(TIMEZONE_PREFERENCES_KEY_TIME24, show24Hour);
return show24Hour;
}
void setTimeFormat24Hour(bool show24Hour) {
Preferences preferences(TIME_SETTINGS_NAMESPACE);
preferences.putBool(TIMEZONE_PREFERENCES_KEY_TIME24, show24Hour);
kernel::systemEventPublish(kernel::SystemEvent::Time);
}
#else
static std::string timeZoneName;
static std::string timeZoneCode;
static bool show24Hour = true;
void init() {}
void setTimeZone(const std::string& name, const std::string& code) {
timeZoneName = name;
timeZoneCode = code;
kernel::systemEventPublish(kernel::SystemEvent::Time);
}
std::string getTimeZoneName() {
return timeZoneName;
}
std::string getTimeZoneCode() {
return timeZoneCode;
}
bool isTimeFormat24Hour() {
return show24Hour;
}
void setTimeFormat24Hour(bool enabled) {
show24Hour = enabled;
kernel::systemEventPublish(kernel::SystemEvent::Time);
}
#endif
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <string>
namespace tt::time {
/**
* Set the timezone
* @param[in] name human-readable name
* @param[in] code the technical code (from timezones.csv)
*/
void setTimeZone(const std::string& name, const std::string& code);
/**
* Get the name of the timezone
*/
std::string getTimeZoneName();
/**
* Get the code of the timezone (see timezones.csv)
*/
std::string getTimeZoneCode();
/** @return true when clocks should be shown as a 24 hours one instead of 12 hours */
bool isTimeFormat24Hour();
/** Set whether clocks should be shown as a 24 hours instead of 12 hours
* @param[in] show24Hour
*/
void setTimeFormat24Hour(bool show24Hour);
}

View File

@ -29,6 +29,7 @@ CONFIG_FATFS_LFN_HEAP=y
CONFIG_FATFS_VOLUME_COUNT=3
# Hardware: Main
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_TT_BOARD_LILYGO_TDECK=y
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
@ -38,9 +39,11 @@ CONFIG_FLASHMODE_QIO=y
# Hardware: SPI RAM
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_SPEED_120M=y
CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
# SPI Flash (can set back to 80MHz after ESP-IDF bug is resolved)
CONFIG_ESPTOOLPY_FLASHFREQ_120M=y
# LVGL
CONFIG_LV_DISP_DEF_REFR_PERIOD=17
CONFIG_LV_INDEV_DEF_READ_PERIOD=17