diff options
Diffstat (limited to 'src')
183 files changed, 6196 insertions, 4071 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a6a5bf54..86510590 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -46,7 +46,6 @@ set(SDK_SOURCE_FILES # FreeRTOS ${NRF5_SDK_PATH}/external/freertos/source/croutine.c ${NRF5_SDK_PATH}/external/freertos/source/event_groups.c - ${NRF5_SDK_PATH}/external/freertos/source/portable/MemMang/heap_4.c ${NRF5_SDK_PATH}/external/freertos/source/list.c ${NRF5_SDK_PATH}/external/freertos/source/queue.c ${NRF5_SDK_PATH}/external/freertos/source/stream_buffer.c @@ -356,23 +355,16 @@ set(LVGL_SRC libs/lvgl/src/lv_widgets/lv_win.c ) -set(QCBOR_SRC - libs/QCBOR/src/ieee754.c - libs/QCBOR/src/qcbor_decode.c - libs/QCBOR/src/qcbor_encode.c - libs/QCBOR/src/qcbor_err_to_str.c - libs/QCBOR/src/UsefulBuf.c - ) - list(APPEND IMAGE_FILES displayapp/icons/battery/batteryicon.c ) list(APPEND SOURCE_FILES + stdlib.c + FreeRTOS/heap_4_infinitime.c BootloaderVersion.cpp logging/NrfLogger.cpp displayapp/DisplayApp.cpp displayapp/screens/Screen.cpp - displayapp/screens/Clock.cpp displayapp/screens/Tile.cpp displayapp/screens/InfiniPaint.cpp displayapp/screens/Paddle.cpp @@ -384,26 +376,28 @@ list(APPEND SOURCE_FILES displayapp/screens/Label.cpp displayapp/screens/FirmwareUpdate.cpp displayapp/screens/Music.cpp - displayapp/screens/Weather.cpp displayapp/screens/Navigation.cpp displayapp/screens/Metronome.cpp displayapp/screens/Motion.cpp + displayapp/screens/Weather.cpp + displayapp/screens/Calculator.cpp displayapp/screens/FirmwareValidation.cpp displayapp/screens/ApplicationList.cpp displayapp/screens/Notifications.cpp displayapp/screens/Twos.cpp displayapp/screens/HeartRate.cpp - displayapp/screens/Motion.cpp displayapp/screens/FlashLight.cpp displayapp/screens/List.cpp displayapp/screens/CheckboxList.cpp displayapp/screens/BatteryInfo.cpp displayapp/screens/Steps.cpp displayapp/screens/Timer.cpp + displayapp/screens/Dice.cpp displayapp/screens/PassKey.cpp displayapp/screens/Error.cpp displayapp/screens/Alarm.cpp displayapp/screens/Styles.cpp + displayapp/screens/WeatherSymbols.cpp displayapp/Colors.cpp displayapp/widgets/Counter.cpp displayapp/widgets/PageIndicator.cpp @@ -415,6 +409,7 @@ list(APPEND SOURCE_FILES displayapp/screens/settings/Settings.cpp displayapp/screens/settings/SettingWatchFace.cpp displayapp/screens/settings/SettingTimeFormat.cpp + displayapp/screens/settings/SettingWeatherFormat.cpp displayapp/screens/settings/SettingWakeUp.cpp displayapp/screens/settings/SettingDisplay.cpp displayapp/screens/settings/SettingSteps.cpp @@ -426,7 +421,6 @@ list(APPEND SOURCE_FILES displayapp/screens/settings/SettingBluetooth.cpp ## Watch faces - displayapp/icons/bg_clock.c displayapp/screens/WatchFaceAnalog.cpp displayapp/screens/WatchFaceDigital.cpp displayapp/screens/WatchFaceInfineat.cpp @@ -461,7 +455,7 @@ list(APPEND SOURCE_FILES components/ble/CurrentTimeService.cpp components/ble/AlertNotificationService.cpp components/ble/MusicService.cpp - components/ble/weather/WeatherService.cpp + components/ble/SimpleWeatherService.cpp components/ble/NavigationService.cpp components/ble/BatteryInformationService.cpp components/ble/FSService.cpp @@ -472,7 +466,7 @@ list(APPEND SOURCE_FILES components/firmwarevalidator/FirmwareValidator.cpp components/motor/MotorController.cpp components/settings/Settings.cpp - components/timer/TimerController.cpp + components/timer/Timer.cpp components/alarm/AlarmController.cpp components/fs/FS.cpp drivers/Cst816s.cpp @@ -485,19 +479,23 @@ list(APPEND SOURCE_FILES systemtask/SystemTask.cpp systemtask/SystemMonitor.cpp + systemtask/WakeLock.cpp drivers/TwiMaster.cpp heartratetask/HeartRateTask.cpp - components/heartrate/Ppg.cpp - components/heartrate/Biquad.cpp - components/heartrate/Ptagc.cpp components/heartrate/HeartRateController.cpp + components/heartrate/Ppg.cpp buttonhandler/ButtonHandler.cpp touchhandler/TouchHandler.cpp + + utility/Math.cpp ) list(APPEND RECOVERY_SOURCE_FILES + stdlib.c + FreeRTOS/heap_4_infinitime.c + BootloaderVersion.cpp logging/NrfLogger.cpp displayapp/DisplayAppRecovery.cpp @@ -527,7 +525,7 @@ list(APPEND RECOVERY_SOURCE_FILES components/ble/CurrentTimeService.cpp components/ble/AlertNotificationService.cpp components/ble/MusicService.cpp - components/ble/weather/WeatherService.cpp + components/ble/SimpleWeatherService.cpp components/ble/BatteryInformationService.cpp components/ble/FSService.cpp components/ble/ImmediateAlertService.cpp @@ -537,7 +535,7 @@ list(APPEND RECOVERY_SOURCE_FILES components/ble/MotionService.cpp components/firmwarevalidator/FirmwareValidator.cpp components/settings/Settings.cpp - components/timer/TimerController.cpp + components/timer/Timer.cpp components/alarm/AlarmController.cpp drivers/Cst816s.cpp FreeRTOS/port.c @@ -546,21 +544,25 @@ list(APPEND RECOVERY_SOURCE_FILES systemtask/SystemTask.cpp systemtask/SystemMonitor.cpp + systemtask/WakeLock.cpp drivers/TwiMaster.cpp - components/gfx/Gfx.cpp components/rle/RleDecoder.cpp components/heartrate/HeartRateController.cpp heartratetask/HeartRateTask.cpp components/heartrate/Ppg.cpp - components/heartrate/Biquad.cpp - components/heartrate/Ptagc.cpp + components/motor/MotorController.cpp components/fs/FS.cpp buttonhandler/ButtonHandler.cpp touchhandler/TouchHandler.cpp + + utility/Math.cpp ) list(APPEND RECOVERYLOADER_SOURCE_FILES + stdlib.c + FreeRTOS/heap_4_infinitime.c + # FreeRTOS FreeRTOS/port.c FreeRTOS/port_cmsis_systick.c @@ -573,7 +575,6 @@ list(APPEND RECOVERYLOADER_SOURCE_FILES components/rle/RleDecoder.cpp - components/gfx/Gfx.cpp drivers/St7789.cpp components/brightness/BrightnessController.cpp @@ -591,7 +592,6 @@ set(INCLUDE_FILES displayapp/Messages.h displayapp/TouchEvents.h displayapp/screens/Screen.h - displayapp/screens/Clock.h displayapp/screens/Tile.h displayapp/screens/InfiniPaint.h displayapp/screens/StopWatch.h @@ -612,6 +612,7 @@ set(INCLUDE_FILES displayapp/screens/Metronome.h displayapp/screens/Motion.h displayapp/screens/Timer.h + displayapp/screens/Dice.h displayapp/screens/Alarm.h displayapp/Colors.h displayapp/widgets/Counter.h @@ -651,9 +652,9 @@ set(INCLUDE_FILES components/ble/BleClient.h components/ble/HeartRateService.h components/ble/MotionService.h - components/ble/weather/WeatherService.h + components/ble/SimpleWeatherService.h components/settings/Settings.h - components/timer/TimerController.h + components/timer/Timer.h components/alarm/AlarmController.h drivers/Cst816s.h FreeRTOS/portmacro.h @@ -662,16 +663,19 @@ set(INCLUDE_FILES displayapp/InfiniTimeTheme.h systemtask/SystemTask.h systemtask/SystemMonitor.h + systemtask/WakeLock.h displayapp/screens/Symbols.h drivers/TwiMaster.h heartratetask/HeartRateTask.h components/heartrate/Ppg.h - components/heartrate/Biquad.h - components/heartrate/Ptagc.h components/heartrate/HeartRateController.h + libs/arduinoFFT/src/arduinoFFT.h + libs/arduinoFFT/src/defs.h + libs/arduinoFFT/src/types.h components/motor/MotorController.h buttonhandler/ButtonHandler.h touchhandler/TouchHandler.h + utility/Math.h ) include_directories( @@ -783,8 +787,12 @@ add_definitions(-DOS_CPUTIME_FREQ) add_definitions(-DNRF52 -DNRF52832 -DNRF52832_XXAA -DNRF52_PAN_74 -DNRF52_PAN_64 -DNRF52_PAN_12 -DNRF52_PAN_58 -DNRF52_PAN_54 -DNRF52_PAN_31 -DNRF52_PAN_51 -DNRF52_PAN_36 -DNRF52_PAN_15 -DNRF52_PAN_20 -DNRF52_PAN_55 -DBOARD_PCA10040) add_definitions(-DFREERTOS) add_definitions(-D__STACK_SIZE=1024) -add_definitions(-D__HEAP_SIZE=4096) +add_definitions(-D__HEAP_SIZE=0) add_definitions(-DMYNEWT_VAL_BLE_LL_RFMGMT_ENABLE_TIME=1500) +add_definitions(-DLFS_CONFIG=libs/lfs_config.h) + +# _sbrk is purposefully not implemented so that builds fail when it is used +add_link_options(-Wl,-wrap=malloc -Wl,-wrap=free -Wl,-wrap=calloc -Wl,-wrap=realloc -Wl,-wrap=_malloc_r -Wl,-wrap=_sbrk) # Note: Only use this for debugging # Derive the low frequency clock from the main clock (SYNT) @@ -796,18 +804,18 @@ add_definitions(-DTARGET_DEVICE_NAME="${TARGET_DEVICE}") if(TARGET_DEVICE STREQUAL "PINETIME") add_definitions(-DDRIVER_PINMAP_PINETIME) add_definitions(-DCLOCK_CONFIG_LF_SRC=1) # XTAL -elseif(TARGET_DEVICE STREQUAL "MOY-TFK5") # P8a +elseif(TARGET_DEVICE STREQUAL "MOY_TFK5") # P8a add_definitions(-DDRIVER_PINMAP_P8) add_definitions(-DCLOCK_CONFIG_LF_SRC=1) # XTAL -elseif(TARGET_DEVICE STREQUAL "MOY-TIN5") # P8a variant 2 +elseif(TARGET_DEVICE STREQUAL "MOY_TIN5") # P8a variant 2 add_definitions(-DDRIVER_PINMAP_P8) add_definitions(-DCLOCK_CONFIG_LF_SRC=1) # XTAL -elseif(TARGET_DEVICE STREQUAL "MOY-TON5") # P8b +elseif(TARGET_DEVICE STREQUAL "MOY_TON5") # P8b add_definitions(-DDRIVER_PINMAP_P8) add_definitions(-DCLOCK_CONFIG_LF_SRC=0) # RC add_definitions(-DMYNEWT_VAL_BLE_LL_SCA=500) add_definitions(-DCLOCK_CONFIG_LF_CAL_ENABLED=1) -elseif(TARGET_DEVICE STREQUAL "MOY-UNK") # P8b mirrored +elseif(TARGET_DEVICE STREQUAL "MOY_UNK") # P8b mirrored add_definitions(-DDRIVER_PINMAP_P8) add_definitions(-DCLOCK_CONFIG_LF_SRC=0) # RC add_definitions(-DMYNEWT_VAL_BLE_LL_SCA=500) @@ -848,6 +856,8 @@ target_compile_options(infinitime_fonts PUBLIC $<$<COMPILE_LANGUAGE:ASM>: ${ASM_FLAGS}> ) +add_subdirectory(displayapp/apps) + # NRF SDK add_library(nrf-sdk STATIC ${SDK_SOURCE_FILES}) target_include_directories(nrf-sdk SYSTEM PUBLIC . ../) @@ -858,7 +868,6 @@ target_compile_options(nrf-sdk PRIVATE $<$<CONFIG:RELEASE>: ${RELEASE_FLAGS}> $<$<COMPILE_LANGUAGE:CXX>: ${CXX_FLAGS}> $<$<COMPILE_LANGUAGE:ASM>: ${ASM_FLAGS}> - -O3 ) # NimBLE @@ -885,27 +894,6 @@ target_compile_options(lvgl PRIVATE $<$<COMPILE_LANGUAGE:ASM>: ${ASM_FLAGS}> ) -# QCBOR -add_library(QCBOR STATIC ${QCBOR_SRC}) -target_include_directories(QCBOR SYSTEM PUBLIC libs/QCBOR/inc) -# This is required with the current configuration -target_compile_definitions(QCBOR PUBLIC QCBOR_DISABLE_FLOAT_HW_USE) -# These are for space-saving -target_compile_definitions(QCBOR PUBLIC QCBOR_DISABLE_PREFERRED_FLOAT) -target_compile_definitions(QCBOR PUBLIC QCBOR_DISABLE_EXP_AND_MANTISSA) -target_compile_definitions(QCBOR PUBLIC QCBOR_DISABLE_INDEFINITE_LENGTH_STRINGS) -#target_compile_definitions(QCBOR PUBLIC QCBOR_DISABLE_INDEFINITE_LENGTH_ARRAYS) -target_compile_definitions(QCBOR PUBLIC QCBOR_DISABLE_UNCOMMON_TAGS) -target_compile_definitions(QCBOR PUBLIC USEFULBUF_CONFIG_LITTLE_ENDIAN) -set_target_properties(QCBOR PROPERTIES LINKER_LANGUAGE C) -target_compile_options(QCBOR PRIVATE - ${COMMON_FLAGS} - $<$<CONFIG:DEBUG>: ${DEBUG_FLAGS}> - $<$<CONFIG:RELEASE>: ${RELEASE_FLAGS}> - $<$<COMPILE_LANGUAGE:ASM>: ${ASM_FLAGS}> - -O3 - ) - # LITTLEFS_SRC add_library(littlefs STATIC ${LITTLEFS_SRC}) target_include_directories(littlefs SYSTEM PUBLIC . ../) @@ -924,7 +912,7 @@ set(EXECUTABLE_FILE_NAME ${EXECUTABLE_NAME}-${pinetime_VERSION_MAJOR}.${pinetime set(NRF5_LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/gcc_nrf52.ld") add_executable(${EXECUTABLE_NAME} ${SOURCE_FILES}) set_target_properties(${EXECUTABLE_NAME} PROPERTIES OUTPUT_NAME ${EXECUTABLE_FILE_NAME}) -target_link_libraries(${EXECUTABLE_NAME} nimble nrf-sdk lvgl littlefs QCBOR infinitime_fonts) +target_link_libraries(${EXECUTABLE_NAME} nimble nrf-sdk lvgl littlefs infinitime_fonts infinitime_apps) target_compile_options(${EXECUTABLE_NAME} PUBLIC ${COMMON_FLAGS} ${WARNING_FLAGS} @@ -958,7 +946,7 @@ set(IMAGE_MCUBOOT_FILE_NAME_BIN ${EXECUTABLE_MCUBOOT_NAME}-image-${pinetime_VERS set(DFU_MCUBOOT_FILE_NAME ${EXECUTABLE_MCUBOOT_NAME}-dfu-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}.zip) set(NRF5_LINKER_SCRIPT_MCUBOOT "${CMAKE_SOURCE_DIR}/gcc_nrf52-mcuboot.ld") add_executable(${EXECUTABLE_MCUBOOT_NAME} ${SOURCE_FILES}) -target_link_libraries(${EXECUTABLE_MCUBOOT_NAME} nimble nrf-sdk lvgl littlefs QCBOR infinitime_fonts) +target_link_libraries(${EXECUTABLE_MCUBOOT_NAME} nimble nrf-sdk lvgl littlefs infinitime_fonts infinitime_apps) set_target_properties(${EXECUTABLE_MCUBOOT_NAME} PROPERTIES OUTPUT_NAME ${EXECUTABLE_MCUBOOT_FILE_NAME}) target_compile_options(${EXECUTABLE_MCUBOOT_NAME} PUBLIC ${COMMON_FLAGS} @@ -1000,7 +988,7 @@ endif() set(EXECUTABLE_RECOVERY_NAME "pinetime-recovery") set(EXECUTABLE_RECOVERY_FILE_NAME ${EXECUTABLE_RECOVERY_NAME}-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}) add_executable(${EXECUTABLE_RECOVERY_NAME} ${RECOVERY_SOURCE_FILES}) -target_link_libraries(${EXECUTABLE_RECOVERY_NAME} nimble nrf-sdk littlefs QCBOR infinitime_fonts) +target_link_libraries(${EXECUTABLE_RECOVERY_NAME} nimble nrf-sdk littlefs infinitime_fonts infinitime_apps) set_target_properties(${EXECUTABLE_RECOVERY_NAME} PROPERTIES OUTPUT_NAME ${EXECUTABLE_RECOVERY_FILE_NAME}) target_compile_definitions(${EXECUTABLE_RECOVERY_NAME} PUBLIC "PINETIME_IS_RECOVERY") target_compile_options(${EXECUTABLE_RECOVERY_NAME} PUBLIC @@ -1032,7 +1020,7 @@ set(IMAGE_RECOVERY_MCUBOOT_FILE_NAME ${EXECUTABLE_RECOVERY_MCUBOOT_NAME}-image-$ set(IMAGE_RECOVERY_MCUBOOT_FILE_NAME_HEX ${IMAGE_RECOVERY_MCUBOOT_FILE_NAME}.hex) set(DFU_RECOVERY_MCUBOOT_FILE_NAME ${EXECUTABLE_RECOVERY_MCUBOOT_NAME}-dfu-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}.zip) add_executable(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} ${RECOVERY_SOURCE_FILES}) -target_link_libraries(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} nimble nrf-sdk littlefs QCBOR infinitime_fonts) +target_link_libraries(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} nimble nrf-sdk littlefs infinitime_fonts infinitime_apps) set_target_properties(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} PROPERTIES OUTPUT_NAME ${EXECUTABLE_RECOVERY_MCUBOOT_FILE_NAME}) target_compile_definitions(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} PUBLIC "PINETIME_IS_RECOVERY") target_compile_options(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} PUBLIC @@ -1072,7 +1060,7 @@ endif() set(EXECUTABLE_RECOVERYLOADER_NAME "pinetime-recovery-loader") set(EXECUTABLE_RECOVERYLOADER_FILE_NAME ${EXECUTABLE_RECOVERYLOADER_NAME}-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}) add_executable(${EXECUTABLE_RECOVERYLOADER_NAME} ${RECOVERYLOADER_SOURCE_FILES}) -target_link_libraries(${EXECUTABLE_RECOVERYLOADER_NAME} nrf-sdk QCBOR infinitime_fonts) +target_link_libraries(${EXECUTABLE_RECOVERYLOADER_NAME} nrf-sdk infinitime_fonts infinitime_apps) set_target_properties(${EXECUTABLE_RECOVERYLOADER_NAME} PROPERTIES OUTPUT_NAME ${EXECUTABLE_RECOVERYLOADER_FILE_NAME}) target_compile_options(${EXECUTABLE_RECOVERYLOADER_NAME} PUBLIC ${COMMON_FLAGS} @@ -1107,7 +1095,7 @@ set(IMAGE_MCUBOOT_RECOVERYLOADER_FILE_NAME ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_N set(IMAGE_MCUBOOT_RECOVERYLOADER_FILE_NAME_HEX ${IMAGE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.hex) set(DFU_MCUBOOT_RECOVERYLOADER_FILE_NAME ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME}-dfu-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}.zip) add_executable(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} ${RECOVERYLOADER_SOURCE_FILES}) -target_link_libraries(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} nrf-sdk QCBOR infinitime_fonts) +target_link_libraries(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} nrf-sdk infinitime_fonts infinitime_apps) set_target_properties(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} PROPERTIES OUTPUT_NAME ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}) target_compile_options(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} PUBLIC ${COMMON_FLAGS} diff --git a/src/FreeRTOS/heap_4_infinitime.c b/src/FreeRTOS/heap_4_infinitime.c new file mode 100644 index 00000000..bf649254 --- /dev/null +++ b/src/FreeRTOS/heap_4_infinitime.c @@ -0,0 +1,506 @@ +/* +* FreeRTOS Kernel V10.0.0 +* Copyright (C) 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy of +* this software and associated documentation files (the "Software"), to deal in +* the Software without restriction, including without limitation the rights to +* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +* the Software, and to permit persons to whom the Software is furnished to do so, +* subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. If you wish to use our Amazon +* FreeRTOS name, please do so in a fair use way that does not cause confusion. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +* +* http://www.FreeRTOS.org +* http://aws.amazon.com/freertos +* +* 1 tab == 4 spaces! +*/ + +/* +* A sample implementation of pvPortMalloc() and vPortFree() that combines +* (coalescences) adjacent memory blocks as they are freed, and in so doing +* limits memory fragmentation. +* +* This implementation is based on heap_4.c and add the function pvPortRealloc() +* to the original implementation. +* +* See heap_1.c, heap_2.c and heap_3.c for alternative implementations, and the +* memory management pages of http://www.FreeRTOS.org for more information. +*/ +#include <stdlib.h> +#include <string.h> + +/* Defining MPU_WRAPPERS_INCLUDED_FROM_API_FILE prevents task.h from redefining +all the API functions to use the MPU wrappers. That should only be done when +task.h is included from an application file. */ +#define MPU_WRAPPERS_INCLUDED_FROM_API_FILE + +#include "FreeRTOS.h" +#include "task.h" + +#undef MPU_WRAPPERS_INCLUDED_FROM_API_FILE + +#if( configSUPPORT_DYNAMIC_ALLOCATION == 0 ) + #error This file must not be used if configSUPPORT_DYNAMIC_ALLOCATION is 0 +#endif + +/* Block sizes must not get too small. */ +#define heapMINIMUM_BLOCK_SIZE ( ( size_t ) ( xHeapStructSize << 1 ) ) + +/* Assumes 8bit bytes! */ +#define heapBITS_PER_BYTE ( ( size_t ) 8 ) + +/* Define the linked list structure. This is used to link free blocks in order +of their memory address. */ +typedef struct A_BLOCK_LINK +{ + struct A_BLOCK_LINK *pxNextFreeBlock; /*<< The next free block in the list. */ + size_t xBlockSize; /*<< The size of the free block. */ +} BlockLink_t; + +/*-----------------------------------------------------------*/ + +/* +* Inserts a block of memory that is being freed into the correct position in +* the list of free memory blocks. The block being freed will be merged with +* the block in front it and/or the block behind it if the memory blocks are +* adjacent to each other. +*/ +static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert ); + +/* +* Called automatically to setup the required heap structures the first time +* pvPortMalloc() is called. +*/ +static void prvHeapInit( void ); + +/*-----------------------------------------------------------*/ + +/* The size of the structure placed at the beginning of each allocated memory +block must by correctly byte aligned. */ +static const size_t xHeapStructSize = ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK ); + +/* Create a couple of list links to mark the start and end of the list. */ +static BlockLink_t xStart, *pxEnd = NULL; + +/* Keeps track of the number of free bytes remaining, but says nothing about +fragmentation. */ +static size_t xFreeBytesRemaining = 0U; +static size_t xMinimumEverFreeBytesRemaining = 0U; + +/* Gets set to the top bit of an size_t type. When this bit in the xBlockSize +member of an BlockLink_t structure is set then the block belongs to the +application. When the bit is free the block is still part of the free heap +space. */ +static size_t xBlockAllocatedBit = 0; + +static size_t xHeapSize = 0; + +/*-----------------------------------------------------------*/ + +void *pvPortMalloc( size_t xWantedSize ) +{ + BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink; + void *pvReturn = NULL; + + vTaskSuspendAll(); + { + /* If this is the first call to malloc then the heap will require + initialisation to setup the list of free blocks. */ + if( pxEnd == NULL ) + { + prvHeapInit(); + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + + /* Check the requested block size is not so large that the top bit is + set. The top bit of the block size member of the BlockLink_t structure + is used to determine who owns the block - the application or the + kernel, so it must be free. */ + if( ( xWantedSize & xBlockAllocatedBit ) == 0 ) + { + /* The wanted size is increased so it can contain a BlockLink_t + structure in addition to the requested amount of bytes. */ + if( xWantedSize > 0 ) + { + xWantedSize += xHeapStructSize; + + /* Ensure that blocks are always aligned to the required number + of bytes. */ + if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ) + { + /* Byte alignment required. */ + xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ); + + if( (xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0) { + do { + xWantedSize++; + } + while(true); + } + configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 ); + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + + if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ) + { + /* Traverse the list from the start (lowest address) block until + one of adequate size is found. */ + pxPreviousBlock = &xStart; + pxBlock = xStart.pxNextFreeBlock; + while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) ) + { + pxPreviousBlock = pxBlock; + pxBlock = pxBlock->pxNextFreeBlock; + } + + /* If the end marker was reached then a block of adequate size + was not found. */ + if( pxBlock != pxEnd ) + { + /* Return the memory space pointed to - jumping over the + BlockLink_t structure at its start. */ + pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize ); + + /* This block is being returned for use so must be taken out + of the list of free blocks. */ + pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; + + /* If the block is larger than required it can be split into + two. */ + if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ) + { + /* This block is to be split into two. Create a new + block following the number of bytes requested. The void + cast is used to prevent byte alignment warnings from the + compiler. */ + pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize ); + configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 ); + + /* Calculate the sizes of two blocks split from the + single block. */ + pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize; + pxBlock->xBlockSize = xWantedSize; + + /* Insert the new block into the list of free blocks. */ + prvInsertBlockIntoFreeList( pxNewBlockLink ); + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + + xFreeBytesRemaining -= pxBlock->xBlockSize; + + if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ) + { + xMinimumEverFreeBytesRemaining = xFreeBytesRemaining; + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + + /* The block is being returned - it is allocated and owned + by the application and has no "next" block. */ + pxBlock->xBlockSize |= xBlockAllocatedBit; + pxBlock->pxNextFreeBlock = NULL; + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + + traceMALLOC( pvReturn, xWantedSize ); + } + ( void ) xTaskResumeAll(); + +#if( configUSE_MALLOC_FAILED_HOOK == 1 ) + { + if( pvReturn == NULL ) + { + extern void vApplicationMallocFailedHook( void ); + vApplicationMallocFailedHook(); + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + } +#endif + + configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 ); + return pvReturn; +} +/*-----------------------------------------------------------*/ + +void vPortFree( void *pv ) +{ + uint8_t *puc = ( uint8_t * ) pv; + BlockLink_t *pxLink; + + if( pv != NULL ) + { + /* The memory being freed will have an BlockLink_t structure immediately + before it. */ + puc -= xHeapStructSize; + + /* This casting is to keep the compiler from issuing warnings. */ + pxLink = ( void * ) puc; + + /* Check the block is actually allocated. */ + configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ); + configASSERT( pxLink->pxNextFreeBlock == NULL ); + + if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ) + { + if( pxLink->pxNextFreeBlock == NULL ) + { + /* The block is being returned to the heap - it is no longer + allocated. */ + pxLink->xBlockSize &= ~xBlockAllocatedBit; + + vTaskSuspendAll(); + { + /* Add this block to the list of free blocks. */ + xFreeBytesRemaining += pxLink->xBlockSize; + traceFREE( pv, pxLink->xBlockSize ); + prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); + } + ( void ) xTaskResumeAll(); + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + } +} +/*-----------------------------------------------------------*/ + +size_t xPortGetFreeHeapSize( void ) +{ + return xFreeBytesRemaining; +} +/*-----------------------------------------------------------*/ + +size_t xPortGetMinimumEverFreeHeapSize( void ) +{ + return xMinimumEverFreeBytesRemaining; +} +/*-----------------------------------------------------------*/ + +size_t xPortGetHeapSize( void ) +{ + return xHeapSize; +} +/*-----------------------------------------------------------*/ + +void vPortInitialiseBlocks( void ) +{ + /* This just exists to keep the linker quiet. */ +} +/*-----------------------------------------------------------*/ + +extern uint8_t *__HeapLimit; // Defined by nrf_common.ld + +static void prvHeapInit( void ) +{ + BlockLink_t *pxFirstFreeBlock; + uint8_t *pucAlignedHeap; + size_t uxAddress; + size_t xTotalHeapSize = ( size_t ) &__StackLimit - ( size_t ) &__HeapLimit; + uint8_t *pucHeap = ( uint8_t * ) &__HeapLimit; + + xHeapSize = xTotalHeapSize; + + /* Ensure the heap starts on a correctly aligned boundary. */ + uxAddress = ( size_t ) pucHeap; + + if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 ) + { + uxAddress += ( portBYTE_ALIGNMENT - 1 ); + uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK ); + xTotalHeapSize -= uxAddress - ( size_t ) pucHeap; + } + + pucAlignedHeap = ( uint8_t * ) uxAddress; + + /* xStart is used to hold a pointer to the first item in the list of free + blocks. The void cast is used to prevent compiler warnings. */ + xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; + xStart.xBlockSize = ( size_t ) 0; + + /* pxEnd is used to mark the end of the list of free blocks and is inserted + at the end of the heap space. */ + uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize; + uxAddress -= xHeapStructSize; + uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK ); + pxEnd = ( void * ) uxAddress; + pxEnd->xBlockSize = 0; + pxEnd->pxNextFreeBlock = NULL; + + /* To start with there is a single free block that is sized to take up the + entire heap space, minus the space taken by pxEnd. */ + pxFirstFreeBlock = ( void * ) pucAlignedHeap; + pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock; + pxFirstFreeBlock->pxNextFreeBlock = pxEnd; + + /* Only one block exists - and it covers the entire usable heap space. */ + xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; + xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; + + /* Work out the position of the top bit in a size_t variable. */ + xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 ); +} +/*-----------------------------------------------------------*/ + +static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert ) +{ + BlockLink_t *pxIterator; + uint8_t *puc; + + /* Iterate through the list until a block is found that has a higher address + than the block being inserted. */ + for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock ) + { + /* Nothing to do here, just iterate to the right position. */ + } + + /* Do the block being inserted, and the block it is being inserted after + make a contiguous block of memory? */ + puc = ( uint8_t * ) pxIterator; + if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ) + { + pxIterator->xBlockSize += pxBlockToInsert->xBlockSize; + pxBlockToInsert = pxIterator; + } + else + { + mtCOVERAGE_TEST_MARKER(); + } + + /* Do the block being inserted, and the block it is being inserted before + make a contiguous block of memory? */ + puc = ( uint8_t * ) pxBlockToInsert; + if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock ) + { + if( pxIterator->pxNextFreeBlock != pxEnd ) + { + /* Form one big block from the two blocks. */ + pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize; + pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock; + } + else + { + pxBlockToInsert->pxNextFreeBlock = pxEnd; + } + } + else + { + pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; + } + + /* If the block being inserted plugged a gab, so was merged with the block + before and the block after, then it's pxNextFreeBlock pointer will have + already been set, and should not be set here as that would make it point + to itself. */ + if( pxIterator != pxBlockToInsert ) + { + pxIterator->pxNextFreeBlock = pxBlockToInsert; + } + else + { + mtCOVERAGE_TEST_MARKER(); + } +} + +/*-----------------------------------------------------------*/ + +void* pvPortRealloc(void* pv, size_t xWantedSize) { + size_t move_size; + size_t block_size; + BlockLink_t* pxLink; + void* pvReturn = NULL; + uint8_t* puc = (uint8_t*) pv; + + if (xWantedSize == 0) { + // Zero bytes requested, do nothing (according to libc, this behavior implementation defined) + return NULL; + } + + if (pv == NULL) { + // pv points to NULL. Allocate a new buffer. + return pvPortMalloc(xWantedSize); + } + + // The memory being freed will have an BlockLink_t structure immediately before it. + puc -= xHeapStructSize; + + // This casting is to keep the compiler from issuing warnings. + pxLink = (void*) puc; + + // Check allocate block + if ((pxLink->xBlockSize & xBlockAllocatedBit) != 0) { + // The block is being returned to the heap - it is no longer allocated. + block_size = (pxLink->xBlockSize & ~xBlockAllocatedBit) - xHeapStructSize; + + // Allocate a new buffer + pvReturn = pvPortMalloc(xWantedSize); + + // Check creation and determine the data size to be copied to the new buffer + if (pvReturn != NULL) { + if (block_size < xWantedSize) { + move_size = block_size; + } else { + move_size = xWantedSize; + } + + // Copy the data from the old buffer to the new one + memcpy(pvReturn, pv, move_size); + + // Free the old buffer + vPortFree(pv); + } + } else { + // pv does not point to a valid memory buffer. Allocate a new one + pvReturn = pvPortMalloc(xWantedSize); + } + + return pvReturn; +}
\ No newline at end of file diff --git a/src/FreeRTOS/portmacro_cmsis.h b/src/FreeRTOS/portmacro_cmsis.h index e6e09158..d165d171 100644 --- a/src/FreeRTOS/portmacro_cmsis.h +++ b/src/FreeRTOS/portmacro_cmsis.h @@ -180,6 +180,7 @@ __STATIC_INLINE uint32_t ulPortRaiseBASEPRI( void ) /*-----------------------------------------------------------*/ +size_t xPortGetHeapSize(void); #ifdef __cplusplus } diff --git a/src/FreeRTOSConfig.h b/src/FreeRTOSConfig.h index 263d8031..d877705a 100644 --- a/src/FreeRTOSConfig.h +++ b/src/FreeRTOSConfig.h @@ -62,7 +62,6 @@ #define configTICK_RATE_HZ 1024 #define configMAX_PRIORITIES (3) #define configMINIMAL_STACK_SIZE (120) -#define configTOTAL_HEAP_SIZE (1024 * 17) #define configMAX_TASK_NAME_LEN (4) #define configUSE_16_BIT_TICKS 0 #define configIDLE_SHOULD_YIELD 1 @@ -75,12 +74,13 @@ #define configUSE_TIME_SLICING 0 #define configUSE_NEWLIB_REENTRANT 0 #define configENABLE_BACKWARD_COMPATIBILITY 1 +#define configUSE_TASK_NOTIFICATIONS 0 /* Hook function related definitions. */ #define configUSE_IDLE_HOOK 0 #define configUSE_TICK_HOOK 0 -#define configCHECK_FOR_STACK_OVERFLOW 0 -#define configUSE_MALLOC_FAILED_HOOK 0 +#define configCHECK_FOR_STACK_OVERFLOW 1 +#define configUSE_MALLOC_FAILED_HOOK 1 /* Run time and task stats gathering related definitions. */ #define configGENERATE_RUN_TIME_STATS 0 diff --git a/src/StaticStack.h b/src/StaticStack.h deleted file mode 100644 index 598febce..00000000 --- a/src/StaticStack.h +++ /dev/null @@ -1,43 +0,0 @@ -#include <array> -#include <cstddef> - -template <typename T, size_t N> -class StaticStack { -public: - T Pop(); - void Push(T element); - void Reset(); - T Top(); - -private: - std::array<T, N> elementArray; - // Number of elements in stack, points to the next empty slot - size_t stackPointer = 0; -}; - -// Returns random data when popping from empty array. -template <typename T, size_t N> -T StaticStack<T, N>::Pop() { - if (stackPointer > 0) { - stackPointer--; - } - return elementArray[stackPointer]; -} - -template <typename T, size_t N> -void StaticStack<T, N>::Push(T element) { - if (stackPointer < elementArray.size()) { - elementArray[stackPointer] = element; - stackPointer++; - } -} - -template <typename T, size_t N> -void StaticStack<T, N>::Reset() { - stackPointer = 0; -} - -template <typename T, size_t N> -T StaticStack<T, N>::Top() { - return elementArray[stackPointer - 1]; -} diff --git a/src/components/alarm/AlarmController.cpp b/src/components/alarm/AlarmController.cpp index c4eb8ed0..4ae42c08 100644 --- a/src/components/alarm/AlarmController.cpp +++ b/src/components/alarm/AlarmController.cpp @@ -19,11 +19,13 @@ #include "systemtask/SystemTask.h" #include "task.h" #include <chrono> +#include <libraries/log/nrf_log.h> using namespace Pinetime::Controllers; using namespace std::chrono_literals; -AlarmController::AlarmController(Controllers::DateTime& dateTimeController) : dateTimeController {dateTimeController} { +AlarmController::AlarmController(Controllers::DateTime& dateTimeController, Controllers::FS& fs) + : dateTimeController {dateTimeController}, fs {fs} { } namespace { @@ -36,11 +38,28 @@ namespace { void AlarmController::Init(System::SystemTask* systemTask) { this->systemTask = systemTask; alarmTimer = xTimerCreate("Alarm", 1, pdFALSE, this, SetOffAlarm); + LoadSettingsFromFile(); + if (alarm.isEnabled) { + NRF_LOG_INFO("[AlarmController] Loaded alarm was enabled, scheduling"); + ScheduleAlarm(); + } +} + +void AlarmController::SaveAlarm() { + // verify if it is necessary to save + if (alarmChanged) { + SaveSettingsToFile(); + } + alarmChanged = false; } void AlarmController::SetAlarmTime(uint8_t alarmHr, uint8_t alarmMin) { - hours = alarmHr; - minutes = alarmMin; + if (alarm.hours == alarmHr && alarm.minutes == alarmMin) { + return; + } + alarm.hours = alarmHr; + alarm.minutes = alarmMin; + alarmChanged = true; } void AlarmController::ScheduleAlarm() { @@ -53,18 +72,19 @@ void AlarmController::ScheduleAlarm() { tm* tmAlarmTime = std::localtime(&ttAlarmTime); // If the time being set has already passed today,the alarm should be set for tomorrow - if (hours < dateTimeController.Hours() || (hours == dateTimeController.Hours() && minutes <= dateTimeController.Minutes())) { + if (alarm.hours < dateTimeController.Hours() || + (alarm.hours == dateTimeController.Hours() && alarm.minutes <= dateTimeController.Minutes())) { tmAlarmTime->tm_mday += 1; // tm_wday doesn't update automatically tmAlarmTime->tm_wday = (tmAlarmTime->tm_wday + 1) % 7; } - tmAlarmTime->tm_hour = hours; - tmAlarmTime->tm_min = minutes; + tmAlarmTime->tm_hour = alarm.hours; + tmAlarmTime->tm_min = alarm.minutes; tmAlarmTime->tm_sec = 0; // if alarm is in weekday-only mode, make sure it shifts to the next weekday - if (recurrence == RecurType::Weekdays) { + if (alarm.recurrence == RecurType::Weekdays) { if (tmAlarmTime->tm_wday == 0) { // Sunday, shift 1 day tmAlarmTime->tm_mday += 1; } else if (tmAlarmTime->tm_wday == 6) { // Saturday, shift 2 days @@ -79,7 +99,10 @@ void AlarmController::ScheduleAlarm() { xTimerChangePeriod(alarmTimer, secondsToAlarm * configTICK_RATE_HZ, 0); xTimerStart(alarmTimer, 0); - state = AlarmState::Set; + if (!alarm.isEnabled) { + alarm.isEnabled = true; + alarmChanged = true; + } } uint32_t AlarmController::SecondsToAlarm() const { @@ -88,20 +111,71 @@ uint32_t AlarmController::SecondsToAlarm() const { void AlarmController::DisableAlarm() { xTimerStop(alarmTimer, 0); - state = AlarmState::Not_Set; + if (alarm.isEnabled) { + alarm.isEnabled = false; + alarmChanged = true; + } } void AlarmController::SetOffAlarmNow() { - state = AlarmState::Alerting; + isAlerting = true; systemTask->PushMessage(System::Messages::SetOffAlarm); } void AlarmController::StopAlerting() { - // Alarm state is off unless this is a recurring alarm - if (recurrence == RecurType::None) { - state = AlarmState::Not_Set; + isAlerting = false; + // Disable alarm unless it is recurring + if (alarm.recurrence == RecurType::None) { + alarm.isEnabled = false; + alarmChanged = true; } else { // set next instance ScheduleAlarm(); } } + +void AlarmController::SetRecurrence(RecurType recurrence) { + if (alarm.recurrence != recurrence) { + alarm.recurrence = recurrence; + alarmChanged = true; + } +} + +void AlarmController::LoadSettingsFromFile() { + lfs_file_t alarmFile; + AlarmSettings alarmBuffer; + + if (fs.FileOpen(&alarmFile, "/.system/alarm.dat", LFS_O_RDONLY) != LFS_ERR_OK) { + NRF_LOG_WARNING("[AlarmController] Failed to open alarm data file"); + return; + } + + fs.FileRead(&alarmFile, reinterpret_cast<uint8_t*>(&alarmBuffer), sizeof(alarmBuffer)); + fs.FileClose(&alarmFile); + if (alarmBuffer.version != alarmFormatVersion) { + NRF_LOG_WARNING("[AlarmController] Loaded alarm settings has version %u instead of %u, discarding", + alarmBuffer.version, + alarmFormatVersion); + return; + } + + alarm = alarmBuffer; + NRF_LOG_INFO("[AlarmController] Loaded alarm settings from file"); +} + +void AlarmController::SaveSettingsToFile() const { + lfs_dir systemDir; + if (fs.DirOpen("/.system", &systemDir) != LFS_ERR_OK) { + fs.DirCreate("/.system"); + } + fs.DirClose(&systemDir); + lfs_file_t alarmFile; + if (fs.FileOpen(&alarmFile, "/.system/alarm.dat", LFS_O_WRONLY | LFS_O_CREAT) != LFS_ERR_OK) { + NRF_LOG_WARNING("[AlarmController] Failed to open alarm data file for saving"); + return; + } + + fs.FileWrite(&alarmFile, reinterpret_cast<const uint8_t*>(&alarm), sizeof(alarm)); + fs.FileClose(&alarmFile); + NRF_LOG_INFO("[AlarmController] Saved alarm settings with format version %u to file", alarm.version); +} diff --git a/src/components/alarm/AlarmController.h b/src/components/alarm/AlarmController.h index 8ac0de9a..278e9cdb 100644 --- a/src/components/alarm/AlarmController.h +++ b/src/components/alarm/AlarmController.h @@ -30,47 +30,65 @@ namespace Pinetime { namespace Controllers { class AlarmController { public: - AlarmController(Controllers::DateTime& dateTimeController); + AlarmController(Controllers::DateTime& dateTimeController, Controllers::FS& fs); void Init(System::SystemTask* systemTask); + void SaveAlarm(); void SetAlarmTime(uint8_t alarmHr, uint8_t alarmMin); void ScheduleAlarm(); void DisableAlarm(); void SetOffAlarmNow(); uint32_t SecondsToAlarm() const; void StopAlerting(); - enum class AlarmState { Not_Set, Set, Alerting }; enum class RecurType { None, Daily, Weekdays }; uint8_t Hours() const { - return hours; + return alarm.hours; } uint8_t Minutes() const { - return minutes; + return alarm.minutes; } - AlarmState State() const { - return state; + bool IsAlerting() const { + return isAlerting; } - RecurType Recurrence() const { - return recurrence; + bool IsEnabled() const { + return alarm.isEnabled; } - void SetRecurrence(RecurType recurType) { - recurrence = recurType; + RecurType Recurrence() const { + return alarm.recurrence; } + void SetRecurrence(RecurType recurrence); + private: + // Versions 255 is reserved for now, so the version field can be made + // bigger, should it ever be needed. + static constexpr uint8_t alarmFormatVersion = 1; + + struct AlarmSettings { + uint8_t version = alarmFormatVersion; + uint8_t hours = 7; + uint8_t minutes = 0; + RecurType recurrence = RecurType::None; + bool isEnabled = false; + }; + + bool isAlerting = false; + bool alarmChanged = false; + Controllers::DateTime& dateTimeController; + Controllers::FS& fs; System::SystemTask* systemTask = nullptr; TimerHandle_t alarmTimer; - uint8_t hours = 7; - uint8_t minutes = 0; + AlarmSettings alarm; std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> alarmTime; - AlarmState state = AlarmState::Not_Set; - RecurType recurrence = RecurType::None; + + void LoadSettingsFromFile(); + void SaveSettingsToFile() const; }; } } diff --git a/src/components/battery/BatteryController.cpp b/src/components/battery/BatteryController.cpp index d9e198c5..fdb461f8 100644 --- a/src/components/battery/BatteryController.cpp +++ b/src/components/battery/BatteryController.cpp @@ -1,5 +1,5 @@ #include "components/battery/BatteryController.h" -#include "components/utility/LinearApproximation.h" +#include "utility/LinearApproximation.h" #include "drivers/PinMap.h" #include <hal/nrf_gpio.h> #include <nrfx_saadc.h> diff --git a/src/components/ble/DfuService.cpp b/src/components/ble/DfuService.cpp index 1f06b69e..2427513d 100644 --- a/src/components/ble/DfuService.cpp +++ b/src/components/ble/DfuService.cpp @@ -124,9 +124,11 @@ int DfuService::WritePacketHandler(uint16_t connectionHandle, os_mbuf* om) { bootloaderSize, applicationSize); - // wait until SystemTask has finished waking up all devices - while (systemTask.IsSleeping()) { - vTaskDelay(50); // 50ms + // Wait until SystemTask has disabled sleeping + // This isn't quite correct, as we don't actually know + // if BleFirmwareUpdateStarted has been received yet + while (!systemTask.IsSleepDisabled()) { + vTaskDelay(pdMS_TO_TICKS(5)); } dfuImage.Erase(); @@ -357,6 +359,8 @@ void DfuService::DfuImage::Init(size_t chunkSize, size_t totalSize, uint16_t exp this->totalSize = totalSize; this->expectedCrc = expectedCrc; this->ready = true; + totalWriteIndex = 0; + bufferWriteIndex = 0; } void DfuService::DfuImage::Append(uint8_t* data, size_t size) { diff --git a/src/components/ble/DfuService.h b/src/components/ble/DfuService.h index b56911b9..6652cdc1 100644 --- a/src/components/ble/DfuService.h +++ b/src/components/ble/DfuService.h @@ -77,6 +77,10 @@ namespace Pinetime { uint16_t ComputeCrc(uint8_t const* p_data, uint32_t size, uint16_t const* p_crc); }; + static constexpr ble_uuid128_t serviceUuid { + .u {.type = BLE_UUID_TYPE_128}, + .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00}}; + private: Pinetime::System::SystemTask& systemTask; Pinetime::Controllers::Ble& bleController; @@ -90,10 +94,6 @@ namespace Pinetime { uint16_t revision {0x0008}; - static constexpr ble_uuid128_t serviceUuid { - .u {.type = BLE_UUID_TYPE_128}, - .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00}}; - static constexpr ble_uuid128_t packetCharacteristicUuid { .u {.type = BLE_UUID_TYPE_128}, .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x32, 0x15, 0x00, 0x00}}; diff --git a/src/components/ble/HeartRateService.cpp b/src/components/ble/HeartRateService.cpp index c522e67e..d34dbf83 100644 --- a/src/components/ble/HeartRateService.cpp +++ b/src/components/ble/HeartRateService.cpp @@ -1,6 +1,6 @@ #include "components/ble/HeartRateService.h" #include "components/heartrate/HeartRateController.h" -#include "systemtask/SystemTask.h" +#include "components/ble/NimbleController.h" #include <nrf_log.h> using namespace Pinetime::Controllers; @@ -16,8 +16,8 @@ namespace { } // TODO Refactoring - remove dependency to SystemTask -HeartRateService::HeartRateService(Pinetime::System::SystemTask& system, Controllers::HeartRateController& heartRateController) - : system {system}, +HeartRateService::HeartRateService(NimbleController& nimble, Controllers::HeartRateController& heartRateController) + : nimble {nimble}, heartRateController {heartRateController}, characteristicDefinition {{.uuid = &heartRateMeasurementUuid.u, .access_cb = HeartRateServiceCallback, @@ -63,7 +63,7 @@ void HeartRateService::OnNewHeartRateValue(uint8_t heartRateValue) { uint8_t buffer[2] = {0, heartRateValue}; // [0] = flags, [1] = hr value auto* om = ble_hs_mbuf_from_flat(buffer, 2); - uint16_t connectionHandle = system.nimble().connHandle(); + uint16_t connectionHandle = nimble.connHandle(); if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) { return; diff --git a/src/components/ble/HeartRateService.h b/src/components/ble/HeartRateService.h index 003bdbd1..ca8f10fb 100644 --- a/src/components/ble/HeartRateService.h +++ b/src/components/ble/HeartRateService.h @@ -2,21 +2,18 @@ #define min // workaround: nimble's min/max macros conflict with libstdc++ #define max #include <host/ble_gap.h> -#include <atomic> #undef max #undef min +#include <atomic> namespace Pinetime { - namespace System { - class SystemTask; - } - namespace Controllers { class HeartRateController; + class NimbleController; class HeartRateService { public: - HeartRateService(Pinetime::System::SystemTask& system, Controllers::HeartRateController& heartRateController); + HeartRateService(NimbleController& nimble, Controllers::HeartRateController& heartRateController); void Init(); int OnHeartRateRequested(uint16_t attributeHandle, ble_gatt_access_ctxt* context); void OnNewHeartRateValue(uint8_t hearRateValue); @@ -24,14 +21,14 @@ namespace Pinetime { void SubscribeNotification(uint16_t attributeHandle); void UnsubscribeNotification(uint16_t attributeHandle); + static constexpr uint16_t heartRateServiceId {0x180D}; + static constexpr ble_uuid16_t heartRateServiceUuid {.u {.type = BLE_UUID_TYPE_16}, .value = heartRateServiceId}; + private: - Pinetime::System::SystemTask& system; + NimbleController& nimble; Controllers::HeartRateController& heartRateController; - static constexpr uint16_t heartRateServiceId {0x180D}; static constexpr uint16_t heartRateMeasurementId {0x2A37}; - static constexpr ble_uuid16_t heartRateServiceUuid {.u {.type = BLE_UUID_TYPE_16}, .value = heartRateServiceId}; - static constexpr ble_uuid16_t heartRateMeasurementUuid {.u {.type = BLE_UUID_TYPE_16}, .value = heartRateMeasurementId}; struct ble_gatt_chr_def characteristicDefinition[2]; diff --git a/src/components/ble/MotionService.cpp b/src/components/ble/MotionService.cpp index 604f22d5..1626a5bf 100644 --- a/src/components/ble/MotionService.cpp +++ b/src/components/ble/MotionService.cpp @@ -1,6 +1,6 @@ #include "components/ble/MotionService.h" #include "components/motion/MotionController.h" -#include "systemtask/SystemTask.h" +#include "components/ble/NimbleController.h" #include <nrf_log.h> using namespace Pinetime::Controllers; @@ -28,8 +28,8 @@ namespace { } // TODO Refactoring - remove dependency to SystemTask -MotionService::MotionService(Pinetime::System::SystemTask& system, Controllers::MotionController& motionController) - : system {system}, +MotionService::MotionService(NimbleController& nimble, Controllers::MotionController& motionController) + : nimble {nimble}, motionController {motionController}, characteristicDefinition {{.uuid = &stepCountCharUuid.u, .access_cb = MotionServiceCallback, @@ -82,7 +82,7 @@ void MotionService::OnNewStepCountValue(uint32_t stepCount) { uint32_t buffer = stepCount; auto* om = ble_hs_mbuf_from_flat(&buffer, 4); - uint16_t connectionHandle = system.nimble().connHandle(); + uint16_t connectionHandle = nimble.connHandle(); if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) { return; @@ -98,7 +98,7 @@ void MotionService::OnNewMotionValues(int16_t x, int16_t y, int16_t z) { int16_t buffer[3] = {x, y, z}; auto* om = ble_hs_mbuf_from_flat(buffer, 3 * sizeof(int16_t)); - uint16_t connectionHandle = system.nimble().connHandle(); + uint16_t connectionHandle = nimble.connHandle(); if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) { return; @@ -120,3 +120,7 @@ void MotionService::UnsubscribeNotification(uint16_t attributeHandle) { else if (attributeHandle == motionValuesHandle) motionValuesNoficationEnabled = false; } + +bool MotionService::IsMotionNotificationSubscribed() const { + return motionValuesNoficationEnabled; +} diff --git a/src/components/ble/MotionService.h b/src/components/ble/MotionService.h index 1b172528..acc91e8d 100644 --- a/src/components/ble/MotionService.h +++ b/src/components/ble/MotionService.h @@ -7,16 +7,13 @@ #undef min namespace Pinetime { - namespace System { - class SystemTask; - } - namespace Controllers { + class NimbleController; class MotionController; class MotionService { public: - MotionService(Pinetime::System::SystemTask& system, Controllers::MotionController& motionController); + MotionService(NimbleController& nimble, Controllers::MotionController& motionController); void Init(); int OnStepCountRequested(uint16_t attributeHandle, ble_gatt_access_ctxt* context); void OnNewStepCountValue(uint32_t stepCount); @@ -24,9 +21,10 @@ namespace Pinetime { void SubscribeNotification(uint16_t attributeHandle); void UnsubscribeNotification(uint16_t attributeHandle); + bool IsMotionNotificationSubscribed() const; private: - Pinetime::System::SystemTask& system; + NimbleController& nimble; Controllers::MotionController& motionController; struct ble_gatt_chr_def characteristicDefinition[3]; diff --git a/src/components/ble/MusicService.cpp b/src/components/ble/MusicService.cpp index 403c957b..43cbec70 100644 --- a/src/components/ble/MusicService.cpp +++ b/src/components/ble/MusicService.cpp @@ -16,8 +16,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ #include "components/ble/MusicService.h" -#include "systemtask/SystemTask.h" +#include "components/ble/NimbleController.h" #include <cstring> +#include <FreeRTOS.h> +#include <task.h> namespace { // 0000yyxx-78fc-48fe-8e23-433b3a1942d0 @@ -53,7 +55,7 @@ namespace { } } -Pinetime::Controllers::MusicService::MusicService(Pinetime::System::SystemTask& system) : m_system(system) { +Pinetime::Controllers::MusicService::MusicService(Pinetime::Controllers::NimbleController& nimble) : nimble(nimble) { characteristicDefinition[0] = {.uuid = &msEventCharUuid.u, .access_cb = MusicCallback, .arg = this, @@ -212,7 +214,7 @@ int Pinetime::Controllers::MusicService::getTrackLength() const { void Pinetime::Controllers::MusicService::event(char event) { auto* om = ble_hs_mbuf_from_flat(&event, 1); - uint16_t connectionHandle = m_system.nimble().connHandle(); + uint16_t connectionHandle = nimble.connHandle(); if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) { return; diff --git a/src/components/ble/MusicService.h b/src/components/ble/MusicService.h index 6aebc3c5..93d94a34 100644 --- a/src/components/ble/MusicService.h +++ b/src/components/ble/MusicService.h @@ -25,16 +25,15 @@ #include <host/ble_uuid.h> #undef max #undef min +#include <FreeRTOS.h> namespace Pinetime { - namespace System { - class SystemTask; - } - namespace Controllers { + class NimbleController; + class MusicService { public: - explicit MusicService(Pinetime::System::SystemTask& system); + explicit MusicService(NimbleController& nimble); void Init(); @@ -89,7 +88,7 @@ namespace Pinetime { bool repeat {false}; bool shuffle {false}; - Pinetime::System::SystemTask& m_system; + NimbleController& nimble; }; } } diff --git a/src/components/ble/NavigationService.cpp b/src/components/ble/NavigationService.cpp index 5508d08c..4922237c 100644 --- a/src/components/ble/NavigationService.cpp +++ b/src/components/ble/NavigationService.cpp @@ -18,8 +18,6 @@ #include "components/ble/NavigationService.h" -#include "systemtask/SystemTask.h" - namespace { // 0001yyxx-78fc-48fe-8e23-433b3a1942d0 constexpr ble_uuid128_t CharUuid(uint8_t x, uint8_t y) { @@ -45,7 +43,7 @@ namespace { } } // namespace -Pinetime::Controllers::NavigationService::NavigationService(Pinetime::System::SystemTask& system) : m_system(system) { +Pinetime::Controllers::NavigationService::NavigationService() { characteristicDefinition[0] = {.uuid = &navFlagCharUuid.u, .access_cb = NAVCallback, .arg = this, diff --git a/src/components/ble/NavigationService.h b/src/components/ble/NavigationService.h index 1c1739ba..03e79ac5 100644 --- a/src/components/ble/NavigationService.h +++ b/src/components/ble/NavigationService.h @@ -27,15 +27,11 @@ #undef min namespace Pinetime { - namespace System { - class SystemTask; - } - namespace Controllers { class NavigationService { public: - explicit NavigationService(Pinetime::System::SystemTask& system); + NavigationService(); void Init(); @@ -57,8 +53,6 @@ namespace Pinetime { std::string m_narrative; std::string m_manDist; int m_progress; - - Pinetime::System::SystemTask& m_system; }; } } diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp index 74c8f926..5059007a 100644 --- a/src/components/ble/NimbleController.cpp +++ b/src/components/ble/NimbleController.cpp @@ -42,13 +42,12 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask, anService {systemTask, notificationManager}, alertNotificationClient {systemTask, notificationManager}, currentTimeService {dateTimeController}, - musicService {systemTask}, - weatherService {systemTask, dateTimeController}, - navService {systemTask}, + musicService {*this}, + weatherService {dateTimeController}, batteryInformationService {batteryController}, immediateAlertService {systemTask, notificationManager}, - heartRateService {systemTask, heartRateController}, - motionService {systemTask, motionController}, + heartRateService {*this, heartRateController}, + motionService {*this, motionController}, fsService {systemTask, fs}, serviceDiscovery({¤tTimeClient, &alertNotificationClient}) { } @@ -159,7 +158,10 @@ void NimbleController::StartAdvertising() { } fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; - fields.uuids128 = &dfuServiceUuid; + fields.uuids16 = &HeartRateService::heartRateServiceUuid; + fields.num_uuids16 = 1; + fields.uuids16_is_complete = 1; + fields.uuids128 = &DfuService::serviceUuid; fields.num_uuids128 = 1; fields.uuids128_is_complete = 1; fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; @@ -452,9 +454,15 @@ void NimbleController::PersistBond(struct ble_gap_conn_desc& desc) { /* Wakeup Spi and SpiNorFlash before accessing the file system * This should be fixed in the FS driver */ - systemTask.PushMessage(Pinetime::System::Messages::GoToRunning); systemTask.PushMessage(Pinetime::System::Messages::DisableSleeping); - vTaskDelay(10); + + // This isn't quite correct + // SystemTask could receive EnableSleeping right after passing this check + // We need some guarantee that the SystemTask has processed the above message + // before we can continue + while (!systemTask.IsSleepDisabled()) { + vTaskDelay(pdMS_TO_TICKS(5)); + } lfs_file_t file_p; diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h index 8f1dfed7..597ef0cc 100644 --- a/src/components/ble/NimbleController.h +++ b/src/components/ble/NimbleController.h @@ -21,7 +21,7 @@ #include "components/ble/NavigationService.h" #include "components/ble/ServiceDiscovery.h" #include "components/ble/MotionService.h" -#include "components/ble/weather/WeatherService.h" +#include "components/ble/SimpleWeatherService.h" #include "components/fs/FS.h" namespace Pinetime { @@ -67,7 +67,7 @@ namespace Pinetime { return anService; }; - Pinetime::Controllers::WeatherService& weather() { + Pinetime::Controllers::SimpleWeatherService& weather() { return weatherService; }; @@ -99,7 +99,7 @@ namespace Pinetime { AlertNotificationClient alertNotificationClient; CurrentTimeService currentTimeService; MusicService musicService; - WeatherService weatherService; + SimpleWeatherService weatherService; NavigationService navService; BatteryInformationService batteryInformationService; ImmediateAlertService immediateAlertService; @@ -112,10 +112,6 @@ namespace Pinetime { uint16_t connectionHandle = BLE_HS_CONN_HANDLE_NONE; uint8_t fastAdvCount = 0; uint8_t bondId[16] = {0}; - - ble_uuid128_t dfuServiceUuid { - .u {.type = BLE_UUID_TYPE_128}, - .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00}}; }; static NimbleController* nptr; diff --git a/src/components/ble/NotificationManager.h b/src/components/ble/NotificationManager.h index f68a68af..57a9c715 100644 --- a/src/components/ble/NotificationManager.h +++ b/src/components/ble/NotificationManager.h @@ -27,11 +27,12 @@ namespace Pinetime { struct Notification { using Id = uint8_t; using Idx = uint8_t; - Id id = 0; - bool valid = false; + + std::array<char, MessageSize + 1> message{}; uint8_t size; - std::array<char, MessageSize + 1> message {}; Categories category = Categories::Unknown; + Id id = 0; + bool valid = false; const char* Message() const; const char* Title() const; diff --git a/src/components/ble/SimpleWeatherService.cpp b/src/components/ble/SimpleWeatherService.cpp new file mode 100644 index 00000000..51baf543 --- /dev/null +++ b/src/components/ble/SimpleWeatherService.cpp @@ -0,0 +1,173 @@ +/* Copyright (C) 2023 Jean-François Milants + + This file is part of InfiniTime. + + InfiniTime is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + InfiniTime is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "components/ble/SimpleWeatherService.h" + +#include <algorithm> +#include <array> +#include <cstring> +#include <nrf_log.h> + +using namespace Pinetime::Controllers; + +namespace { + enum class MessageType : uint8_t { CurrentWeather, Forecast, Unknown }; + + uint64_t ToUInt64(const uint8_t* data) { + return data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24) + (static_cast<uint64_t>(data[4]) << 32) + + (static_cast<uint64_t>(data[5]) << 40) + (static_cast<uint64_t>(data[6]) << 48) + (static_cast<uint64_t>(data[7]) << 56); + } + + int16_t ToInt16(const uint8_t* data) { + return data[0] + (data[1] << 8); + } + + SimpleWeatherService::CurrentWeather CreateCurrentWeather(const uint8_t* dataBuffer) { + SimpleWeatherService::Location cityName; + std::memcpy(cityName.data(), &dataBuffer[16], 32); + cityName[32] = '\0'; + return SimpleWeatherService::CurrentWeather(ToUInt64(&dataBuffer[2]), + SimpleWeatherService::Temperature(ToInt16(&dataBuffer[10])), + SimpleWeatherService::Temperature(ToInt16(&dataBuffer[12])), + SimpleWeatherService::Temperature(ToInt16(&dataBuffer[14])), + SimpleWeatherService::Icons {dataBuffer[16 + 32]}, + std::move(cityName)); + } + + SimpleWeatherService::Forecast CreateForecast(const uint8_t* dataBuffer) { + auto timestamp = static_cast<uint64_t>(ToUInt64(&dataBuffer[2])); + + std::array<std::optional<SimpleWeatherService::Forecast::Day>, SimpleWeatherService::MaxNbForecastDays> days; + const uint8_t nbDaysInBuffer = dataBuffer[10]; + const uint8_t nbDays = std::min(SimpleWeatherService::MaxNbForecastDays, nbDaysInBuffer); + for (int i = 0; i < nbDays; i++) { + days[i] = SimpleWeatherService::Forecast::Day {SimpleWeatherService::Temperature(ToInt16(&dataBuffer[11 + (i * 5)])), + SimpleWeatherService::Temperature(ToInt16(&dataBuffer[13 + (i * 5)])), + SimpleWeatherService::Icons {dataBuffer[15 + (i * 5)]}}; + } + return SimpleWeatherService::Forecast {timestamp, nbDays, days}; + } + + MessageType GetMessageType(const uint8_t* data) { + auto messageType = static_cast<MessageType>(*data); + if (messageType > MessageType::Unknown) { + return MessageType::Unknown; + } + return messageType; + } + + uint8_t GetVersion(const uint8_t* dataBuffer) { + return dataBuffer[1]; + } +} + +int WeatherCallback(uint16_t /*connHandle*/, uint16_t /*attrHandle*/, struct ble_gatt_access_ctxt* ctxt, void* arg) { + return static_cast<Pinetime::Controllers::SimpleWeatherService*>(arg)->OnCommand(ctxt); +} + +SimpleWeatherService::SimpleWeatherService(DateTime& dateTimeController) : dateTimeController(dateTimeController) { +} + +void SimpleWeatherService::Init() { + ble_gatts_count_cfg(serviceDefinition); + ble_gatts_add_svcs(serviceDefinition); +} + +int SimpleWeatherService::OnCommand(struct ble_gatt_access_ctxt* ctxt) { + const auto* buffer = ctxt->om; + const auto* dataBuffer = buffer->om_data; + + switch (GetMessageType(dataBuffer)) { + case MessageType::CurrentWeather: + if (GetVersion(dataBuffer) == 0) { + currentWeather = CreateCurrentWeather(dataBuffer); + NRF_LOG_INFO("Current weather :\n\tTimestamp : %d\n\tTemperature:%d\n\tMin:%d\n\tMax:%d\n\tIcon:%d\n\tLocation:%s", + currentWeather->timestamp, + currentWeather->temperature.PreciseCelsius(), + currentWeather->minTemperature.PreciseCelsius(), + currentWeather->maxTemperature.PreciseCelsius(), + currentWeather->iconId, + currentWeather->location.data()); + } + break; + case MessageType::Forecast: + if (GetVersion(dataBuffer) == 0) { + forecast = CreateForecast(dataBuffer); + NRF_LOG_INFO("Forecast : Timestamp : %d", forecast->timestamp); + for (int i = 0; i < 5; i++) { + NRF_LOG_INFO("\t[%d] Min: %d - Max : %d - Icon : %d", + i, + forecast->days[i]->minTemperature.PreciseCelsius(), + forecast->days[i]->maxTemperature.PreciseCelsius(), + forecast->days[i]->iconId); + } + } + break; + default: + break; + } + + return 0; +} + +std::optional<SimpleWeatherService::CurrentWeather> SimpleWeatherService::Current() const { + if (currentWeather) { + auto currentTime = dateTimeController.CurrentDateTime().time_since_epoch(); + auto weatherTpSecond = std::chrono::seconds {currentWeather->timestamp}; + auto weatherTp = std::chrono::duration_cast<std::chrono::seconds>(weatherTpSecond); + auto delta = currentTime - weatherTp; + + if (delta < std::chrono::hours {24}) { + return currentWeather; + } + } + return {}; +} + +std::optional<SimpleWeatherService::Forecast> SimpleWeatherService::GetForecast() const { + if (forecast) { + auto currentTime = dateTimeController.CurrentDateTime().time_since_epoch(); + auto weatherTpSecond = std::chrono::seconds {forecast->timestamp}; + auto weatherTp = std::chrono::duration_cast<std::chrono::seconds>(weatherTpSecond); + auto delta = currentTime - weatherTp; + + if (delta < std::chrono::hours {24}) { + return this->forecast; + } + } + return {}; +} + +bool SimpleWeatherService::CurrentWeather::operator==(const SimpleWeatherService::CurrentWeather& other) const { + return this->iconId == other.iconId && this->temperature == other.temperature && this->timestamp == other.timestamp && + this->maxTemperature == other.maxTemperature && this->minTemperature == other.maxTemperature && + std::strcmp(this->location.data(), other.location.data()) == 0; +} + +bool SimpleWeatherService::Forecast::Day::operator==(const SimpleWeatherService::Forecast::Day& other) const { + return this->iconId == other.iconId && this->maxTemperature == other.maxTemperature && this->minTemperature == other.maxTemperature; +} + +bool SimpleWeatherService::Forecast::operator==(const SimpleWeatherService::Forecast& other) const { + for (int i = 0; i < this->nbDays; i++) { + if (this->days[i] != other.days[i]) { + return false; + } + } + return this->timestamp == other.timestamp && this->nbDays == other.nbDays; +} diff --git a/src/components/ble/SimpleWeatherService.h b/src/components/ble/SimpleWeatherService.h new file mode 100644 index 00000000..0f8c181b --- /dev/null +++ b/src/components/ble/SimpleWeatherService.h @@ -0,0 +1,174 @@ +/* Copyright (C) 2023 Jean-François Milants + + This file is part of InfiniTime. + + InfiniTime is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + InfiniTime is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <cstdint> +#include <string> +#include <array> +#include <memory> + +#define min // workaround: nimble's min/max macros conflict with libstdc++ +#define max +#include <host/ble_gap.h> +#include <host/ble_uuid.h> +#include <optional> +#include <cstring> +#undef max +#undef min + +#include "components/datetime/DateTimeController.h" + +int WeatherCallback(uint16_t connHandle, uint16_t attrHandle, struct ble_gatt_access_ctxt* ctxt, void* arg); + +namespace Pinetime { + namespace Controllers { + + class SimpleWeatherService { + public: + explicit SimpleWeatherService(DateTime& dateTimeController); + + void Init(); + + int OnCommand(struct ble_gatt_access_ctxt* ctxt); + + static constexpr uint8_t MaxNbForecastDays = 5; + + enum class Icons : uint8_t { + Sun = 0, // ClearSky + CloudsSun = 1, // FewClouds + Clouds = 2, // Scattered clouds + BrokenClouds = 3, + CloudShowerHeavy = 4, // shower rain + CloudSunRain = 5, // rain + Thunderstorm = 6, + Snow = 7, + Smog = 8, // Mist + Unknown = 255 + }; + + class Temperature { + public: + explicit Temperature(int16_t raw) : raw {raw} { + } + + [[nodiscard]] int16_t PreciseCelsius() const { + return raw; + } + + [[nodiscard]] int16_t PreciseFahrenheit() const { + return raw * 9 / 5 + 3200; + } + + [[nodiscard]] int16_t Celsius() const { + return (PreciseCelsius() + 50) / 100; + } + + [[nodiscard]] int16_t Fahrenheit() const { + return (PreciseFahrenheit() + 50) / 100; + } + + bool operator==(const Temperature& other) const { + return raw == other.raw; + } + + private: + int16_t raw; + }; + + using Location = std::array<char, 33>; // 32 char + \0 (end of string) + + struct CurrentWeather { + CurrentWeather(uint64_t timestamp, + Temperature temperature, + Temperature minTemperature, + Temperature maxTemperature, + Icons iconId, + Location&& location) + : timestamp {timestamp}, + temperature {temperature}, + minTemperature {minTemperature}, + maxTemperature {maxTemperature}, + iconId {iconId}, + location {std::move(location)} { + } + + uint64_t timestamp; + Temperature temperature; + Temperature minTemperature; + Temperature maxTemperature; + Icons iconId; + Location location; + + bool operator==(const CurrentWeather& other) const; + }; + + struct Forecast { + uint64_t timestamp; + uint8_t nbDays; + + struct Day { + Temperature minTemperature; + Temperature maxTemperature; + Icons iconId; + + bool operator==(const Day& other) const; + }; + + std::array<std::optional<Day>, MaxNbForecastDays> days; + + bool operator==(const Forecast& other) const; + }; + + std::optional<CurrentWeather> Current() const; + std::optional<Forecast> GetForecast() const; + + private: + // 00050000-78fc-48fe-8e23-433b3a1942d0 + static constexpr ble_uuid128_t BaseUuid() { + return CharUuid(0x00, 0x00); + } + + // 0005yyxx-78fc-48fe-8e23-433b3a1942d0 + static constexpr ble_uuid128_t CharUuid(uint8_t x, uint8_t y) { + return ble_uuid128_t {.u = {.type = BLE_UUID_TYPE_128}, + .value = {0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, y, x, 0x05, 0x00}}; + } + + ble_uuid128_t weatherUuid {BaseUuid()}; + + ble_uuid128_t weatherDataCharUuid {CharUuid(0x00, 0x01)}; + + const struct ble_gatt_chr_def characteristicDefinition[2] = {{.uuid = &weatherDataCharUuid.u, + .access_cb = WeatherCallback, + .arg = this, + .flags = BLE_GATT_CHR_F_WRITE, + .val_handle = &eventHandle}, + {0}}; + const struct ble_gatt_svc_def serviceDefinition[2] = { + {.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &weatherUuid.u, .characteristics = characteristicDefinition}, + {0}}; + + uint16_t eventHandle {}; + + Pinetime::Controllers::DateTime& dateTimeController; + + std::optional<CurrentWeather> currentWeather; + std::optional<Forecast> forecast; + }; + } +} diff --git a/src/components/ble/weather/WeatherData.h b/src/components/ble/weather/WeatherData.h deleted file mode 100644 index 1a995eb9..00000000 --- a/src/components/ble/weather/WeatherData.h +++ /dev/null @@ -1,385 +0,0 @@ -/* Copyright (C) 2021 Avamander - - This file is part of InfiniTime. - - InfiniTime is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - InfiniTime is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ -#pragma once - -/** - * Different weather events, weather data structures used by {@link WeatherService.h} - * - * How to upload events to the timeline? - * - * All timeline write payloads are simply CBOR-encoded payloads of the structs described below. - * - * All payloads have a mandatory header part and the dynamic part that - * depends on the event type specified in the header. If you don't, - * you'll get an error returned. Data is relatively well-validated, - * so keep in the bounds of the data types given. - * - * Write all struct members (CamelCase keys) into a single finite-sized map, and write it to the characteristic. - * Mind the MTU. - * - * How to debug? - * - * There's a Screen that you can compile into your firmware that shows currently valid events. - * You can adapt that to display something else. That part right now is very much work in progress - * because the exact requirements are not yet known. - * - * - * Implemented based on and other material: - * https://en.wikipedia.org/wiki/METAR - * https://www.weather.gov/jetstream/obscurationtypes - * http://www.faraim.org/aim/aim-4-03-14-493.html - */ - -namespace Pinetime { - namespace Controllers { - class WeatherData { - public: - /** - * Visibility obscuration types - */ - enum class obscurationtype { - /** No obscuration */ - None = 0, - /** Water particles suspended in the air; low visibility; does not fall */ - Fog = 1, - /** Tiny, dry particles in the air; invisible to the eye; opalescent */ - Haze = 2, - /** Small fire-created particles suspended in the air */ - Smoke = 3, - /** Fine rock powder, from for example volcanoes */ - Ash = 4, - /** Fine particles of earth suspended in the air by the wind */ - Dust = 5, - /** Fine particles of sand suspended in the air by the wind */ - Sand = 6, - /** Water particles suspended in the air; low-ish visibility; temperature is near dewpoint */ - Mist = 7, - /** This is SPECIAL in the sense that the thing raining down is doing the obscuration */ - Precipitation = 8, - Length - }; - - /** - * Types of precipitation - */ - enum class precipitationtype { - /** - * No precipitation - * - * Theoretically we could just _not_ send the event, but then - * how do we differentiate between no precipitation and - * no information about precipitation - */ - None = 0, - /** Drops larger than a drizzle; also widely separated drizzle */ - Rain = 1, - /** Fairly uniform rain consisting of fine drops */ - Drizzle = 2, - /** Rain that freezes upon contact with objects and ground */ - FreezingRain = 3, - /** Rain + hail; ice pellets; small translucent frozen raindrops */ - Sleet = 4, - /** Larger ice pellets; falling separately or in irregular clumps */ - Hail = 5, - /** Hail with smaller grains of ice; mini-snowballs */ - SmallHail = 6, - /** Snow... */ - Snow = 7, - /** Frozen drizzle; very small snow crystals */ - SnowGrains = 8, - /** Needles; columns or plates of ice. Sometimes described as "diamond dust". In very cold regions */ - IceCrystals = 9, - /** It's raining down ash, e.g. from a volcano */ - Ash = 10, - Length - }; - - /** - * These are special events that can "enhance" the "experience" of existing weather events - */ - enum class specialtype { - /** Strong wind with a sudden onset that lasts at least a minute */ - Squall = 0, - /** Series of waves in a water body caused by the displacement of a large volume of water */ - Tsunami = 1, - /** Violent; rotating column of air */ - Tornado = 2, - /** Unplanned; unwanted; uncontrolled fire in an area */ - Fire = 3, - /** Thunder and/or lightning */ - Thunder = 4, - Length - }; - - /** - * These are used for weather timeline manipulation - * that isn't just adding to the stack of weather events - */ - enum class controlcodes { - /** How much is stored already */ - GetLength = 0, - /** This wipes the entire timeline */ - DelTimeline = 1, - /** There's a currently valid timeline event with the given type */ - HasValidEvent = 3, - Length - }; - - /** - * Events have types - * then they're easier to parse after sending them over the air - */ - enum class eventtype : uint8_t { - /** @see obscuration */ - Obscuration = 0, - /** @see precipitation */ - Precipitation = 1, - /** @see wind */ - Wind = 2, - /** @see temperature */ - Temperature = 3, - /** @see airquality */ - AirQuality = 4, - /** @see special */ - Special = 5, - /** @see pressure */ - Pressure = 6, - /** @see location */ - Location = 7, - /** @see cloud */ - Clouds = 8, - /** @see humidity */ - Humidity = 9, - Length - }; - - /** - * Valid event query - * - * NOTE: Not currently available, until needs are better known - */ - class ValidEventQuery { - public: - static constexpr controlcodes code = controlcodes::HasValidEvent; - eventtype eventType; - }; - - /** The header used for further parsing */ - class TimelineHeader { - public: - /** - * UNIX timestamp - * TODO: This is currently WITH A TIMEZONE OFFSET! - * Please send events with the timestamp offset by the timezone. - **/ - uint64_t timestamp; - /** - * Time in seconds until the event expires - * - * 32 bits ought to be enough for everyone - * - * If there's a newer event of the same type then it overrides this one, even if it hasn't expired - */ - uint32_t expires; - /** - * What type of weather-related event - */ - eventtype eventType; - }; - - /** Specifies how cloudiness is stored */ - class Clouds : public TimelineHeader { - public: - /** Cloud coverage in percentage, 0-100% */ - uint8_t amount; - }; - - /** Specifies how obscuration is stored */ - class Obscuration : public TimelineHeader { - public: - /** Type of precipitation */ - obscurationtype type; - /** - * Visibility distance in meters - * 65535 is reserved for unspecified - */ - uint16_t amount; - }; - - /** Specifies how precipitation is stored */ - class Precipitation : public TimelineHeader { - public: - /** Type of precipitation */ - precipitationtype type; - /** - * How much is it going to rain? In millimeters - * 255 is reserved for unspecified - **/ - uint8_t amount; - }; - - /** - * How wind speed is stored - * - * In order to represent bursts of wind instead of constant wind, - * you have minimum and maximum speeds. - * - * As direction can fluctuate wildly and some watch faces might wish to display it nicely, - * we're following the aerospace industry weather report option of specifying a range. - */ - class Wind : public TimelineHeader { - public: - /** Meters per second */ - uint8_t speedMin; - /** Meters per second */ - uint8_t speedMax; - /** Unitless direction between 0-255; approximately 1 unit per 0.71 degrees */ - uint8_t directionMin; - /** Unitless direction between 0-255; approximately 1 unit per 0.71 degrees */ - uint8_t directionMax; - }; - - /** - * How temperature is stored - * - * As it's annoying to figure out the dewpoint on the watch, - * please send it from the companion - * - * We don't do floats, picodegrees are not useful. Make sure to multiply. - */ - class Temperature : public TimelineHeader { - public: - /** - * Temperature °C but multiplied by 100 (e.g. -12.50°C becomes -1250) - * -32768 is reserved for "no data" - */ - int16_t temperature; - /** - * Dewpoint °C but multiplied by 100 (e.g. -12.50°C becomes -1250) - * -32768 is reserved for "no data" - */ - int16_t dewPoint; - }; - - /** - * How location info is stored - * - * This can be mostly static with long expiration, - * as it usually is, but it could change during a trip for ex. - * so we allow changing it dynamically. - * - * Location info can be for some kind of map watch face - * or daylight calculations, should those be required. - * - */ - class Location : public TimelineHeader { - public: - /** Location name */ - std::string location; - /** Altitude relative to sea level in meters */ - int16_t altitude; - /** Latitude, EPSG:3857 (Google Maps, Openstreetmaps datum) */ - int32_t latitude; - /** Longitude, EPSG:3857 (Google Maps, Openstreetmaps datum) */ - int32_t longitude; - }; - - /** - * How humidity is stored - */ - class Humidity : public TimelineHeader { - public: - /** Relative humidity, 0-100% */ - uint8_t humidity; - }; - - /** - * How air pressure is stored - */ - class Pressure : public TimelineHeader { - public: - /** Air pressure in hectopascals (hPa) */ - int16_t pressure; - }; - - /** - * How special events are stored - */ - class Special : public TimelineHeader { - public: - /** Special event's type */ - specialtype type; - }; - - /** - * How air quality is stored - * - * These events are a bit more complex because the topic is not simple, - * the intention is to heavy-lift the annoying preprocessing from the watch - * this allows watch face or watchapp makers to generate accurate alerts and graphics - * - * If this needs further enforced standardization, pull requests are welcome - */ - class AirQuality : public TimelineHeader { - public: - /** - * The name of the pollution - * - * for the sake of better compatibility with watchapps - * that might want to use this data for say visuals - * don't localize the name. - * - * Ideally watchapp itself localizes the name, if it's at all needed. - * - * E.g. - * For generic ones use "PM0.1", "PM5", "PM10" - * For chemical compounds use the molecular formula e.g. "NO2", "CO2", "O3" - * For pollen use the genus, e.g. "Betula" for birch or "Alternaria" for that mold's spores - */ - std::string polluter; - /** - * Amount of the pollution in SI units, - * otherwise it's going to be difficult to create UI, alerts - * and so on and for. - * - * See more: - * https://ec.europa.eu/environment/air/quality/standards.htm - * http://www.ourair.org/wp-content/uploads/2012-aaqs2.pdf - * - * Example units: - * count/m³ for pollen - * µgC/m³ for micrograms of organic carbon - * µg/m³ sulfates, PM0.1, PM1, PM2, PM10 and so on, dust - * mg/m³ CO2, CO - * ng/m³ for heavy metals - * - * List is not comprehensive, should be improved. - * The current ones are what watchapps assume! - * - * Note: ppb and ppm to concentration should be calculated on the companion, using - * the correct formula (taking into account temperature and air pressure) - * - * Note2: The amount is off by times 100, for two decimal places of precision. - * E.g. 54.32µg/m³ is 5432 - * - */ - uint32_t amount; - }; - }; - } -} diff --git a/src/components/ble/weather/WeatherService.cpp b/src/components/ble/weather/WeatherService.cpp deleted file mode 100644 index fd13f819..00000000 --- a/src/components/ble/weather/WeatherService.cpp +++ /dev/null @@ -1,607 +0,0 @@ -/* Copyright (C) 2021 Avamander - - This file is part of InfiniTime. - - InfiniTime is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - InfiniTime is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ -#include <algorithm> -#include <qcbor/qcbor_spiffy_decode.h> -#include "WeatherService.h" -#include "libs/QCBOR/inc/qcbor/qcbor.h" -#include "systemtask/SystemTask.h" - -int WeatherCallback(uint16_t /*connHandle*/, uint16_t /*attrHandle*/, struct ble_gatt_access_ctxt* ctxt, void* arg) { - return static_cast<Pinetime::Controllers::WeatherService*>(arg)->OnCommand(ctxt); -} - -namespace Pinetime { - namespace Controllers { - WeatherService::WeatherService(System::SystemTask& system, DateTime& dateTimeController) - : system(system), dateTimeController(dateTimeController) { - nullHeader = &nullTimelineheader; - nullTimelineheader->timestamp = 0; - } - - void WeatherService::Init() { - uint8_t res = 0; - res = ble_gatts_count_cfg(serviceDefinition); - ASSERT(res == 0); - - res = ble_gatts_add_svcs(serviceDefinition); - ASSERT(res == 0); - } - - int WeatherService::OnCommand(struct ble_gatt_access_ctxt* ctxt) { - if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { - const uint8_t packetLen = OS_MBUF_PKTLEN(ctxt->om); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) - if (packetLen <= 0) { - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - // Decode - QCBORDecodeContext decodeContext; - UsefulBufC encodedCbor = {ctxt->om->om_data, OS_MBUF_PKTLEN(ctxt->om)}; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) - - QCBORDecode_Init(&decodeContext, encodedCbor, QCBOR_DECODE_MODE_NORMAL); - // KINDLY provide us a fixed-length map - QCBORDecode_EnterMap(&decodeContext, nullptr); - // Always encodes to the smallest number of bytes based on the value - int64_t tmpTimestamp = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Timestamp", &tmpTimestamp); - if (QCBORDecode_GetError(&decodeContext) != QCBOR_SUCCESS) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - int64_t tmpExpires = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Expires", &tmpExpires); - if (QCBORDecode_GetError(&decodeContext) != QCBOR_SUCCESS || tmpExpires < 0 || tmpExpires > 4294967295) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - int64_t tmpEventType = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "EventType", &tmpEventType); - if (QCBORDecode_GetError(&decodeContext) != QCBOR_SUCCESS || tmpEventType < 0 || - tmpEventType >= static_cast<int64_t>(WeatherData::eventtype::Length)) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - - switch (static_cast<WeatherData::eventtype>(tmpEventType)) { - case WeatherData::eventtype::AirQuality: { - std::unique_ptr<WeatherData::AirQuality> airquality = std::make_unique<WeatherData::AirQuality>(); - airquality->timestamp = tmpTimestamp; - airquality->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - airquality->expires = tmpExpires; - - UsefulBufC stringBuf; // TODO: Everything ok with lifecycle here? - QCBORDecode_GetTextStringInMapSZ(&decodeContext, "Polluter", &stringBuf); - if (UsefulBuf_IsNULLOrEmptyC(stringBuf) != 0) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - airquality->polluter = std::string(static_cast<const char*>(stringBuf.ptr), stringBuf.len); - - int64_t tmpAmount = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); - if (tmpAmount < 0 || tmpAmount > 4294967295) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - airquality->amount = tmpAmount; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(airquality))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Obscuration: { - std::unique_ptr<WeatherData::Obscuration> obscuration = std::make_unique<WeatherData::Obscuration>(); - obscuration->timestamp = tmpTimestamp; - obscuration->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - obscuration->expires = tmpExpires; - - int64_t tmpType = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Type", &tmpType); - if (tmpType < 0 || tmpType >= static_cast<int64_t>(WeatherData::obscurationtype::Length)) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - obscuration->type = static_cast<WeatherData::obscurationtype>(tmpType); - - int64_t tmpAmount = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); - if (tmpAmount < 0 || tmpAmount > 65535) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - obscuration->amount = tmpAmount; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(obscuration))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Precipitation: { - std::unique_ptr<WeatherData::Precipitation> precipitation = std::make_unique<WeatherData::Precipitation>(); - precipitation->timestamp = tmpTimestamp; - precipitation->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - precipitation->expires = tmpExpires; - - int64_t tmpType = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Type", &tmpType); - if (tmpType < 0 || tmpType >= static_cast<int64_t>(WeatherData::precipitationtype::Length)) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - precipitation->type = static_cast<WeatherData::precipitationtype>(tmpType); - - int64_t tmpAmount = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); - if (tmpAmount < 0 || tmpAmount > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - precipitation->amount = tmpAmount; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(precipitation))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Wind: { - std::unique_ptr<WeatherData::Wind> wind = std::make_unique<WeatherData::Wind>(); - wind->timestamp = tmpTimestamp; - wind->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - wind->expires = tmpExpires; - - int64_t tmpMin = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "SpeedMin", &tmpMin); - if (tmpMin < 0 || tmpMin > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - wind->speedMin = tmpMin; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - int64_t tmpMax = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "SpeedMin", &tmpMax); - if (tmpMax < 0 || tmpMax > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - wind->speedMax = tmpMax; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - int64_t tmpDMin = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "DirectionMin", &tmpDMin); - if (tmpDMin < 0 || tmpDMin > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - wind->directionMin = tmpDMin; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - int64_t tmpDMax = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "DirectionMax", &tmpDMax); - if (tmpDMax < 0 || tmpDMax > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - wind->directionMax = tmpDMax; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(wind))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Temperature: { - std::unique_ptr<WeatherData::Temperature> temperature = std::make_unique<WeatherData::Temperature>(); - temperature->timestamp = tmpTimestamp; - temperature->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - temperature->expires = tmpExpires; - - int64_t tmpTemperature = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Temperature", &tmpTemperature); - if (tmpTemperature < -32768 || tmpTemperature > 32767) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - temperature->temperature = - static_cast<int16_t>(tmpTemperature); // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - int64_t tmpDewPoint = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "DewPoint", &tmpDewPoint); - if (tmpDewPoint < -32768 || tmpDewPoint > 32767) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - temperature->dewPoint = - static_cast<int16_t>(tmpDewPoint); // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(temperature))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Special: { - std::unique_ptr<WeatherData::Special> special = std::make_unique<WeatherData::Special>(); - special->timestamp = tmpTimestamp; - special->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - special->expires = tmpExpires; - - int64_t tmpType = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Type", &tmpType); - if (tmpType < 0 || tmpType >= static_cast<int64_t>(WeatherData::specialtype::Length)) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - special->type = static_cast<WeatherData::specialtype>(tmpType); - - if (!AddEventToTimeline(std::move(special))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Pressure: { - std::unique_ptr<WeatherData::Pressure> pressure = std::make_unique<WeatherData::Pressure>(); - pressure->timestamp = tmpTimestamp; - pressure->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - pressure->expires = tmpExpires; - - int64_t tmpPressure = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Pressure", &tmpPressure); - if (tmpPressure < 0 || tmpPressure >= 65535) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - pressure->pressure = tmpPressure; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(pressure))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Location: { - std::unique_ptr<WeatherData::Location> location = std::make_unique<WeatherData::Location>(); - location->timestamp = tmpTimestamp; - location->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - location->expires = tmpExpires; - - UsefulBufC stringBuf; // TODO: Everything ok with lifecycle here? - QCBORDecode_GetTextStringInMapSZ(&decodeContext, "Location", &stringBuf); - if (UsefulBuf_IsNULLOrEmptyC(stringBuf) != 0) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - location->location = std::string(static_cast<const char*>(stringBuf.ptr), stringBuf.len); - - int64_t tmpAltitude = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Altitude", &tmpAltitude); - if (tmpAltitude < -32768 || tmpAltitude >= 32767) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - location->altitude = static_cast<int16_t>(tmpAltitude); - - int64_t tmpLatitude = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Latitude", &tmpLatitude); - if (tmpLatitude < -2147483648 || tmpLatitude >= 2147483647) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - location->latitude = static_cast<int32_t>(tmpLatitude); - - int64_t tmpLongitude = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Longitude", &tmpLongitude); - if (tmpLongitude < -2147483648 || tmpLongitude >= 2147483647) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - location->latitude = static_cast<int32_t>(tmpLongitude); - - if (!AddEventToTimeline(std::move(location))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Clouds: { - std::unique_ptr<WeatherData::Clouds> clouds = std::make_unique<WeatherData::Clouds>(); - clouds->timestamp = tmpTimestamp; - clouds->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - clouds->expires = tmpExpires; - - int64_t tmpAmount = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); - if (tmpAmount < 0 || tmpAmount > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - clouds->amount = static_cast<uint8_t>(tmpAmount); - - if (!AddEventToTimeline(std::move(clouds))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Humidity: { - std::unique_ptr<WeatherData::Humidity> humidity = std::make_unique<WeatherData::Humidity>(); - humidity->timestamp = tmpTimestamp; - humidity->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - humidity->expires = tmpExpires; - - int64_t tmpType = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Humidity", &tmpType); - if (tmpType < 0 || tmpType >= 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - humidity->humidity = static_cast<uint8_t>(tmpType); - - if (!AddEventToTimeline(std::move(humidity))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - default: { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - } - - QCBORDecode_ExitMap(&decodeContext); - GetTimelineLength(); - TidyTimeline(); - - if (QCBORDecode_Finish(&decodeContext) != QCBOR_SUCCESS) { - return BLE_ATT_ERR_INSUFFICIENT_RES; - } - } else if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { - // Encode - uint8_t buffer[64]; - QCBOREncodeContext encodeContext; - /* TODO: This is very much still a test endpoint - * it needs a characteristic UUID check - * and actual implementations that show - * what actually has to be read. - * WARN: Consider commands not part of the API for now! - */ - QCBOREncode_Init(&encodeContext, UsefulBuf_FROM_BYTE_ARRAY(buffer)); - QCBOREncode_OpenMap(&encodeContext); - QCBOREncode_AddTextToMap(&encodeContext, "test", UsefulBuf_FROM_SZ_LITERAL("test")); - QCBOREncode_AddInt64ToMap(&encodeContext, "test", 1ul); - QCBOREncode_CloseMap(&encodeContext); - - UsefulBufC encodedEvent; - auto uErr = QCBOREncode_Finish(&encodeContext, &encodedEvent); - if (uErr != 0) { - return BLE_ATT_ERR_INSUFFICIENT_RES; - } - auto res = os_mbuf_append(ctxt->om, &buffer, sizeof(buffer)); - if (res == 0) { - return BLE_ATT_ERR_INSUFFICIENT_RES; - } - - return 0; - } - return 0; - } - - std::unique_ptr<WeatherData::Clouds>& WeatherService::GetCurrentClouds() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Clouds && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Clouds>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Clouds>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Obscuration>& WeatherService::GetCurrentObscuration() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Obscuration && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Obscuration>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Obscuration>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Precipitation>& WeatherService::GetCurrentPrecipitation() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Precipitation && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Precipitation>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Precipitation>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Wind>& WeatherService::GetCurrentWind() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Wind && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Wind>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Wind>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Temperature>& WeatherService::GetCurrentTemperature() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Temperature && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Temperature>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Temperature>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Humidity>& WeatherService::GetCurrentHumidity() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Humidity && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Humidity>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Humidity>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Pressure>& WeatherService::GetCurrentPressure() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Pressure && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Pressure>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Pressure>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Location>& WeatherService::GetCurrentLocation() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Location && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Location>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Location>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::AirQuality>& WeatherService::GetCurrentQuality() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::AirQuality && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::AirQuality>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::AirQuality>&>(*this->nullHeader); - } - - size_t WeatherService::GetTimelineLength() const { - return timeline.size(); - } - - bool WeatherService::AddEventToTimeline(std::unique_ptr<WeatherData::TimelineHeader> event) { - if (timeline.size() == timeline.max_size()) { - return false; - } - - timeline.push_back(std::move(event)); - return true; - } - - bool WeatherService::HasTimelineEventOfType(const WeatherData::eventtype type) const { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : timeline) { - if (header->eventType == type && IsEventStillValid(header, currentTimestamp)) { - return true; - } - } - return false; - } - - void WeatherService::TidyTimeline() { - uint64_t timeCurrent = GetCurrentUnixTimestamp(); - timeline.erase(std::remove_if(std::begin(timeline), - std::end(timeline), - [&](std::unique_ptr<WeatherData::TimelineHeader> const& header) { - return !IsEventStillValid(header, timeCurrent); - }), - std::end(timeline)); - - std::sort(std::begin(timeline), std::end(timeline), CompareTimelineEvents); - } - - bool WeatherService::CompareTimelineEvents(const std::unique_ptr<WeatherData::TimelineHeader>& first, - const std::unique_ptr<WeatherData::TimelineHeader>& second) { - return first->timestamp > second->timestamp; - } - - bool WeatherService::IsEventStillValid(const std::unique_ptr<WeatherData::TimelineHeader>& uniquePtr, const uint64_t timestamp) { - // Not getting timestamp in isEventStillValid for more speed - return uniquePtr->timestamp + uniquePtr->expires >= timestamp; - } - - uint64_t WeatherService::GetCurrentUnixTimestamp() const { - return std::chrono::duration_cast<std::chrono::seconds>(dateTimeController.CurrentDateTime().time_since_epoch()).count(); - } - - int16_t WeatherService::GetTodayMinTemp() const { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - uint64_t currentDayEnd = currentTimestamp + ((24 - dateTimeController.Hours()) * 60 * 60) + - ((60 - dateTimeController.Minutes()) * 60) + (60 - dateTimeController.Seconds()); - uint64_t currentDayStart = currentDayEnd - 86400; - int16_t result = -32768; - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Temperature && header->timestamp >= currentDayStart && - header->timestamp < currentDayEnd && - reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature != -32768) { - int16_t temperature = reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature; - if (result == -32768) { - result = temperature; - } else if (result > temperature) { - result = temperature; - } else { - // The temperature in this item is higher than the lowest we've found - } - } - } - - return result; - } - - int16_t WeatherService::GetTodayMaxTemp() const { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - uint64_t currentDayEnd = currentTimestamp + ((24 - dateTimeController.Hours()) * 60 * 60) + - ((60 - dateTimeController.Minutes()) * 60) + (60 - dateTimeController.Seconds()); - uint64_t currentDayStart = currentDayEnd - 86400; - int16_t result = -32768; - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Temperature && header->timestamp >= currentDayStart && - header->timestamp < currentDayEnd && - reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature != -32768) { - int16_t temperature = reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature; - if (result == -32768) { - result = temperature; - } else if (result < temperature) { - result = temperature; - } else { - // The temperature in this item is lower than the highest we've found - } - } - } - - return result; - } - - void WeatherService::CleanUpQcbor(QCBORDecodeContext* decodeContext) { - QCBORDecode_ExitMap(decodeContext); - QCBORDecode_Finish(decodeContext); - } - } -} diff --git a/src/components/ble/weather/WeatherService.h b/src/components/ble/weather/WeatherService.h deleted file mode 100644 index 00650e90..00000000 --- a/src/components/ble/weather/WeatherService.h +++ /dev/null @@ -1,174 +0,0 @@ -/* Copyright (C) 2021 Avamander - - This file is part of InfiniTime. - - InfiniTime is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - InfiniTime is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ -#pragma once - -#include <cstdint> -#include <string> -#include <vector> -#include <memory> - -#define min // workaround: nimble's min/max macros conflict with libstdc++ -#define max -#include <host/ble_gap.h> -#include <host/ble_uuid.h> -#undef max -#undef min - -#include "WeatherData.h" -#include "libs/QCBOR/inc/qcbor/qcbor.h" -#include "components/datetime/DateTimeController.h" - -int WeatherCallback(uint16_t connHandle, uint16_t attrHandle, struct ble_gatt_access_ctxt* ctxt, void* arg); - -namespace Pinetime { - namespace System { - class SystemTask; - } - - namespace Controllers { - - class WeatherService { - public: - explicit WeatherService(System::SystemTask& system, DateTime& dateTimeController); - - void Init(); - - int OnCommand(struct ble_gatt_access_ctxt* ctxt); - - /* - * Helper functions for quick access to currently valid data - */ - std::unique_ptr<WeatherData::Location>& GetCurrentLocation(); - std::unique_ptr<WeatherData::Clouds>& GetCurrentClouds(); - std::unique_ptr<WeatherData::Obscuration>& GetCurrentObscuration(); - std::unique_ptr<WeatherData::Precipitation>& GetCurrentPrecipitation(); - std::unique_ptr<WeatherData::Wind>& GetCurrentWind(); - std::unique_ptr<WeatherData::Temperature>& GetCurrentTemperature(); - std::unique_ptr<WeatherData::Humidity>& GetCurrentHumidity(); - std::unique_ptr<WeatherData::Pressure>& GetCurrentPressure(); - std::unique_ptr<WeatherData::AirQuality>& GetCurrentQuality(); - - /** - * Searches for the current day's maximum temperature - * @return -32768 if there's no data, degrees Celsius times 100 otherwise - */ - int16_t GetTodayMaxTemp() const; - /** - * Searches for the current day's minimum temperature - * @return -32768 if there's no data, degrees Celsius times 100 otherwise - */ - int16_t GetTodayMinTemp() const; - - /* - * Management functions - */ - /** - * Adds an event to the timeline - * @return - */ - bool AddEventToTimeline(std::unique_ptr<WeatherData::TimelineHeader> event); - /** - * Gets the current timeline length - */ - size_t GetTimelineLength() const; - /** - * Checks if an event of a certain type exists in the timeline - */ - bool HasTimelineEventOfType(WeatherData::eventtype type) const; - - private: - // 00040000-78fc-48fe-8e23-433b3a1942d0 - static constexpr ble_uuid128_t BaseUuid() { - return CharUuid(0x00, 0x00); - } - - // 0004yyxx-78fc-48fe-8e23-433b3a1942d0 - static constexpr ble_uuid128_t CharUuid(uint8_t x, uint8_t y) { - return ble_uuid128_t {.u = {.type = BLE_UUID_TYPE_128}, - .value = {0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, y, x, 0x04, 0x00}}; - } - - ble_uuid128_t weatherUuid {BaseUuid()}; - - /** - * Just write timeline data here. - * - * See {@link WeatherData.h} for more information. - */ - ble_uuid128_t weatherDataCharUuid {CharUuid(0x00, 0x01)}; - /** - * This doesn't take timeline data, provides some control over it. - * - * NOTE: Currently not supported. Companion app implementer feedback required. - * There's very little point in solidifying an API before we know the needs. - */ - ble_uuid128_t weatherControlCharUuid {CharUuid(0x00, 0x02)}; - - const struct ble_gatt_chr_def characteristicDefinition[3] = { - {.uuid = &weatherDataCharUuid.u, - .access_cb = WeatherCallback, - .arg = this, - .flags = BLE_GATT_CHR_F_WRITE, - .val_handle = &eventHandle}, - {.uuid = &weatherControlCharUuid.u, .access_cb = WeatherCallback, .arg = this, .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_READ}, - {nullptr}}; - const struct ble_gatt_svc_def serviceDefinition[2] = { - {.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &weatherUuid.u, .characteristics = characteristicDefinition}, - {0}}; - - uint16_t eventHandle {}; - - Pinetime::System::SystemTask& system; - Pinetime::Controllers::DateTime& dateTimeController; - - std::vector<std::unique_ptr<WeatherData::TimelineHeader>> timeline; - std::unique_ptr<WeatherData::TimelineHeader> nullTimelineheader = std::make_unique<WeatherData::TimelineHeader>(); - std::unique_ptr<WeatherData::TimelineHeader>* nullHeader; - - /** - * Cleans up the timeline of expired events - */ - void TidyTimeline(); - - /** - * Compares two timeline events - */ - static bool CompareTimelineEvents(const std::unique_ptr<WeatherData::TimelineHeader>& first, - const std::unique_ptr<WeatherData::TimelineHeader>& second); - - /** - * Returns current UNIX timestamp - */ - uint64_t GetCurrentUnixTimestamp() const; - - /** - * Checks if the event hasn't gone past and expired - * - * @param header timeline event to check - * @param currentTimestamp what's the time right now - * @return if the event is valid - */ - static bool IsEventStillValid(const std::unique_ptr<WeatherData::TimelineHeader>& uniquePtr, const uint64_t timestamp); - - /** - * This is a helper function that closes a QCBOR map and decoding context cleanly - */ - void CleanUpQcbor(QCBORDecodeContext* decodeContext); - }; - } -} diff --git a/src/components/brightness/BrightnessController.cpp b/src/components/brightness/BrightnessController.cpp index 0392158c..4d1eba6a 100644 --- a/src/components/brightness/BrightnessController.cpp +++ b/src/components/brightness/BrightnessController.cpp @@ -2,38 +2,138 @@ #include <hal/nrf_gpio.h> #include "displayapp/screens/Symbols.h" #include "drivers/PinMap.h" +#include <libraries/delay/nrf_delay.h> using namespace Pinetime::Controllers; +namespace { + // reinterpret_cast is not constexpr so this is the best we can do + static NRF_RTC_Type* const RTC = reinterpret_cast<NRF_RTC_Type*>(NRF_RTC2_BASE); +} + void BrightnessController::Init() { nrf_gpio_cfg_output(PinMap::LcdBacklightLow); nrf_gpio_cfg_output(PinMap::LcdBacklightMedium); nrf_gpio_cfg_output(PinMap::LcdBacklightHigh); + + nrf_gpio_pin_clear(PinMap::LcdBacklightLow); + nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); + nrf_gpio_pin_clear(PinMap::LcdBacklightHigh); + + static_assert(timerFrequency == 32768, "Change the prescaler below"); + RTC->PRESCALER = 0; + // CC1 switches the backlight on (pin transitions from high to low) and resets the counter to 0 + RTC->CC[1] = timerPeriod; + // Enable compare events for CC0,CC1 + RTC->EVTEN = 0b0000'0000'0000'0011'0000'0000'0000'0000; + // Disable all interrupts + RTC->INTENCLR = 0b0000'0000'0000'1111'0000'0000'0000'0011; Set(level); } +void BrightnessController::ApplyBrightness(uint16_t rawBrightness) { + // The classic off, low, medium, high brightnesses are at {0, timerPeriod, timerPeriod*2, timerPeriod*3} + // These brightness levels do not use PWM: they only set/clear the corresponding pins + // Any brightness level between the above levels is achieved with efficient RTC based PWM on the next pin up + // E.g 2.5*timerPeriod corresponds to medium brightness with 50% PWM on the high pin + // Note: Raw brightness does not necessarily correspond to a linear perceived brightness + + uint8_t pin; + if (rawBrightness > 2 * timerPeriod) { + rawBrightness -= 2 * timerPeriod; + pin = PinMap::LcdBacklightHigh; + } else if (rawBrightness > timerPeriod) { + rawBrightness -= timerPeriod; + pin = PinMap::LcdBacklightMedium; + } else { + pin = PinMap::LcdBacklightLow; + } + if (rawBrightness == timerPeriod || rawBrightness == 0) { + if (lastPin != UNSET) { + RTC->TASKS_STOP = 1; + nrf_delay_us(rtcStopTime); + nrf_ppi_channel_disable(ppiBacklightOff); + nrf_ppi_channel_disable(ppiBacklightOn); + nrfx_gpiote_out_uninit(lastPin); + nrf_gpio_cfg_output(lastPin); + } + lastPin = UNSET; + if (rawBrightness == 0) { + nrf_gpio_pin_set(pin); + } else { + nrf_gpio_pin_clear(pin); + } + } else { + // If the pin on which we are doing PWM is changing + // Disable old PWM channel (if exists) and set up new one + if (lastPin != pin) { + if (lastPin != UNSET) { + RTC->TASKS_STOP = 1; + nrf_delay_us(rtcStopTime); + nrf_ppi_channel_disable(ppiBacklightOff); + nrf_ppi_channel_disable(ppiBacklightOn); + nrfx_gpiote_out_uninit(lastPin); + nrf_gpio_cfg_output(lastPin); + } + nrfx_gpiote_out_config_t gpioteCfg = {.action = NRF_GPIOTE_POLARITY_TOGGLE, + .init_state = NRF_GPIOTE_INITIAL_VALUE_LOW, + .task_pin = true}; + APP_ERROR_CHECK(nrfx_gpiote_out_init(pin, &gpioteCfg)); + nrfx_gpiote_out_task_enable(pin); + nrf_ppi_channel_endpoint_setup(ppiBacklightOff, + reinterpret_cast<uint32_t>(&RTC->EVENTS_COMPARE[0]), + nrfx_gpiote_out_task_addr_get(pin)); + nrf_ppi_channel_endpoint_setup(ppiBacklightOn, + reinterpret_cast<uint32_t>(&RTC->EVENTS_COMPARE[1]), + nrfx_gpiote_out_task_addr_get(pin)); + nrf_ppi_fork_endpoint_setup(ppiBacklightOn, reinterpret_cast<uint32_t>(&RTC->TASKS_CLEAR)); + nrf_ppi_channel_enable(ppiBacklightOff); + nrf_ppi_channel_enable(ppiBacklightOn); + } else { + // If the pin used for PWM isn't changing, we only need to set the pin state to the initial value (low) + RTC->TASKS_STOP = 1; + nrf_delay_us(rtcStopTime); + // Due to errata 20,179 and the intricacies of RTC timing, keep it simple: override the pin state + nrfx_gpiote_out_task_force(pin, false); + } + // CC0 switches the backlight off (pin transitions from low to high) + RTC->CC[0] = rawBrightness; + RTC->TASKS_CLEAR = 1; + RTC->TASKS_START = 1; + lastPin = pin; + } + switch (pin) { + case PinMap::LcdBacklightHigh: + nrf_gpio_pin_clear(PinMap::LcdBacklightLow); + nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); + break; + case PinMap::LcdBacklightMedium: + nrf_gpio_pin_clear(PinMap::LcdBacklightLow); + nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + break; + case PinMap::LcdBacklightLow: + nrf_gpio_pin_set(PinMap::LcdBacklightMedium); + nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + } +} + void BrightnessController::Set(BrightnessController::Levels level) { this->level = level; switch (level) { default: case Levels::High: - nrf_gpio_pin_clear(PinMap::LcdBacklightLow); - nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); - nrf_gpio_pin_clear(PinMap::LcdBacklightHigh); + ApplyBrightness(3 * timerPeriod); break; case Levels::Medium: - nrf_gpio_pin_clear(PinMap::LcdBacklightLow); - nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); - nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + ApplyBrightness(2 * timerPeriod); break; case Levels::Low: - nrf_gpio_pin_clear(PinMap::LcdBacklightLow); - nrf_gpio_pin_set(PinMap::LcdBacklightMedium); - nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + ApplyBrightness(timerPeriod); + break; + case Levels::AlwaysOn: + ApplyBrightness(timerPeriod / 10); break; case Levels::Off: - nrf_gpio_pin_set(PinMap::LcdBacklightLow); - nrf_gpio_pin_set(PinMap::LcdBacklightMedium); - nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + ApplyBrightness(0); break; } } diff --git a/src/components/brightness/BrightnessController.h b/src/components/brightness/BrightnessController.h index 7f86759a..650749a8 100644 --- a/src/components/brightness/BrightnessController.h +++ b/src/components/brightness/BrightnessController.h @@ -2,11 +2,14 @@ #include <cstdint> +#include "nrf_ppi.h" +#include "nrfx_gpiote.h" + namespace Pinetime { namespace Controllers { class BrightnessController { public: - enum class Levels { Off, Low, Medium, High }; + enum class Levels { Off, AlwaysOn, Low, Medium, High }; void Init(); void Set(Levels level); @@ -20,6 +23,25 @@ namespace Pinetime { private: Levels level = Levels::High; + static constexpr uint8_t UNSET = UINT8_MAX; + uint8_t lastPin = UNSET; + // Maximum time (μs) it takes for the RTC to fully stop + static constexpr uint8_t rtcStopTime = 46; + // Frequency of timer used for PWM (Hz) + static constexpr uint16_t timerFrequency = 32768; + // Backlight PWM frequency (Hz) + static constexpr uint16_t pwmFreq = 1000; + // Wraparound point in timer ticks + // Defines the number of brightness levels between each pin + static constexpr uint16_t timerPeriod = timerFrequency / pwmFreq; + // Warning: nimble reserves some PPIs + // https://github.com/InfiniTimeOrg/InfiniTime/blob/034d83fe6baf1ab3875a34f8cee387e24410a824/src/libs/mynewt-nimble/nimble/drivers/nrf52/src/ble_phy.c#L53 + // SpiMaster uses PPI 0 for an erratum workaround + // Channel 1, 2 should be free to use + static constexpr nrf_ppi_channel_t ppiBacklightOn = NRF_PPI_CHANNEL1; + static constexpr nrf_ppi_channel_t ppiBacklightOff = NRF_PPI_CHANNEL2; + + void ApplyBrightness(uint16_t val); }; } } diff --git a/src/components/datetime/DateTimeController.cpp b/src/components/datetime/DateTimeController.cpp index 9e9fb6e4..d439821b 100644 --- a/src/components/datetime/DateTimeController.cpp +++ b/src/components/datetime/DateTimeController.cpp @@ -1,22 +1,42 @@ #include "components/datetime/DateTimeController.h" #include <libraries/log/nrf_log.h> #include <systemtask/SystemTask.h> +#include <hal/nrf_rtc.h> +#include "nrf_assert.h" using namespace Pinetime::Controllers; namespace { - char const* DaysStringShort[] = {"--", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; - char const* DaysStringShortLow[] = {"--", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; - char const* MonthsString[] = {"--", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; - char const* MonthsStringLow[] = {"--", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + constexpr const char* const DaysStringShort[] = {"--", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; + constexpr const char* const DaysStringShortLow[] = {"--", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; + constexpr const char* const MonthsString[] = {"--", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; + constexpr const char* const MonthsStringLow[] = + {"--", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + + constexpr int compileTimeAtoi(const char* str) { + int result = 0; + while (*str >= '0' && *str <= '9') { + result = result * 10 + *str - '0'; + str++; + } + return result; + } } DateTime::DateTime(Controllers::Settings& settingsController) : settingsController {settingsController} { + mutex = xSemaphoreCreateMutex(); + ASSERT(mutex != nullptr); + xSemaphoreGive(mutex); + + // __DATE__ is a string of the format "MMM DD YYYY", so an offset of 7 gives the start of the year + SetTime(compileTimeAtoi(&__DATE__[7]), 1, 1, 0, 0, 0); } void DateTime::SetCurrentTime(std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> t) { + xSemaphoreTake(mutex, portMAX_DELAY); this->currentDateTime = t; - UpdateTime(previousSystickCounter); // Update internal state without updating the time + UpdateTime(previousSystickCounter, true); // Update internal state without updating the time + xSemaphoreGive(mutex); } void DateTime::SetTime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second) { @@ -29,15 +49,19 @@ void DateTime::SetTime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, /* .tm_year = */ year - 1900, }; - tm.tm_isdst = -1; // Use DST value from local time zone - currentDateTime = std::chrono::system_clock::from_time_t(std::mktime(&tm)); - NRF_LOG_INFO("%d %d %d ", day, month, year); NRF_LOG_INFO("%d %d %d ", hour, minute, second); - UpdateTime(previousSystickCounter); + tm.tm_isdst = -1; // Use DST value from local time zone + + xSemaphoreTake(mutex, portMAX_DELAY); + currentDateTime = std::chrono::system_clock::from_time_t(std::mktime(&tm)); + UpdateTime(previousSystickCounter, true); + xSemaphoreGive(mutex); - systemTask->PushMessage(System::Messages::OnNewTime); + if (systemTask != nullptr) { + systemTask->PushMessage(System::Messages::OnNewTime); + } } void DateTime::SetTimeZone(int8_t timezone, int8_t dst) { @@ -45,25 +69,34 @@ void DateTime::SetTimeZone(int8_t timezone, int8_t dst) { dstOffset = dst; } -void DateTime::UpdateTime(uint32_t systickCounter) { +std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> DateTime::CurrentDateTime() { + xSemaphoreTake(mutex, portMAX_DELAY); + UpdateTime(nrf_rtc_counter_get(portNRF_RTC_REG), false); + xSemaphoreGive(mutex); + return currentDateTime; +} + +void DateTime::UpdateTime(uint32_t systickCounter, bool forceUpdate) { // Handle systick counter overflow uint32_t systickDelta = 0; if (systickCounter < previousSystickCounter) { - systickDelta = 0xffffff - previousSystickCounter; + systickDelta = static_cast<uint32_t>(portNRF_RTC_MAXTICKS) - previousSystickCounter; systickDelta += systickCounter + 1; } else { systickDelta = systickCounter - previousSystickCounter; } - /* - * 1000 ms = 1024 ticks - */ - auto correctedDelta = systickDelta / 1024; - auto rest = systickDelta % 1024; + auto correctedDelta = systickDelta / configTICK_RATE_HZ; + // If a second hasn't passed, there is nothing to do + // If the time has been changed, set forceUpdate to trigger internal state updates + if (correctedDelta == 0 && !forceUpdate) { + return; + } + auto rest = systickDelta % configTICK_RATE_HZ; if (systickCounter >= rest) { previousSystickCounter = systickCounter - rest; } else { - previousSystickCounter = 0xffffff - (rest - systickCounter); + previousSystickCounter = static_cast<uint32_t>(portNRF_RTC_MAXTICKS) - (rest - systickCounter - 1); } currentDateTime += std::chrono::seconds(correctedDelta); @@ -115,8 +148,8 @@ const char* DateTime::MonthShortToStringLow(Months month) { return MonthsStringLow[static_cast<uint8_t>(month)]; } -const char* DateTime::DayOfWeekShortToStringLow() const { - return DaysStringShortLow[static_cast<uint8_t>(DayOfWeek())]; +const char* DateTime::DayOfWeekShortToStringLow(Days day) { + return DaysStringShortLow[static_cast<uint8_t>(day)]; } void DateTime::Register(Pinetime::System::SystemTask* systemTask) { @@ -140,9 +173,9 @@ std::string DateTime::FormattedTime() { hour12 = (hour == 12) ? 12 : hour - 12; amPmStr = "PM"; } - sprintf(buff, "%i:%02i %s", hour12, minute, amPmStr); + snprintf(buff, sizeof(buff), "%i:%02i %s", hour12, minute, amPmStr); } else { - sprintf(buff, "%02i:%02i", hour, minute); + snprintf(buff, sizeof(buff), "%02i:%02i", hour, minute); } return std::string(buff); } diff --git a/src/components/datetime/DateTimeController.h b/src/components/datetime/DateTimeController.h index 0bf6ac2a..a005f9ac 100644 --- a/src/components/datetime/DateTimeController.h +++ b/src/components/datetime/DateTimeController.h @@ -5,6 +5,8 @@ #include <ctime> #include <string> #include "components/settings/Settings.h" +#include <FreeRTOS.h> +#include <semphr.h> namespace Pinetime { namespace System { @@ -39,14 +41,12 @@ namespace Pinetime { * * used to update difference between utc and local time (see UtcOffset()) * - * parameters are in quarters of an our. Following the BLE CTS specification, + * parameters are in quarters of an hour. Following the BLE CTS specification, * timezone is expected to be constant over DST which will be reported in * dst field. */ void SetTimeZone(int8_t timezone, int8_t dst); - void UpdateTime(uint32_t systickCounter); - uint16_t Year() const { return 1900 + localTime.tm_year; } @@ -122,14 +122,12 @@ namespace Pinetime { const char* MonthShortToString() const; const char* DayOfWeekShortToString() const; static const char* MonthShortToStringLow(Months month); - const char* DayOfWeekShortToStringLow() const; + static const char* DayOfWeekShortToStringLow(Days day); - std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> CurrentDateTime() const { - return currentDateTime; - } + std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> CurrentDateTime(); - std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> UTCDateTime() const { - return currentDateTime - std::chrono::seconds((tzOffset + dstOffset) * 15 * 60); + std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> UTCDateTime() { + return CurrentDateTime() - std::chrono::seconds((tzOffset + dstOffset) * 15 * 60); } std::chrono::seconds Uptime() const { @@ -141,10 +139,14 @@ namespace Pinetime { std::string FormattedTime(); private: + void UpdateTime(uint32_t systickCounter, bool forceUpdate); + std::tm localTime; int8_t tzOffset = 0; int8_t dstOffset = 0; + SemaphoreHandle_t mutex = nullptr; + uint32_t previousSystickCounter = 0; std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> currentDateTime; std::chrono::seconds uptime {0}; diff --git a/src/components/datetime/TODO.md b/src/components/datetime/TODO.md new file mode 100644 index 00000000..e9590898 --- /dev/null +++ b/src/components/datetime/TODO.md @@ -0,0 +1,41 @@ +# Refactoring needed + +## Context + +The [PR #2041 - Continuous time updates](https://github.com/InfiniTimeOrg/InfiniTime/pull/2041) highlighted some +limitations in the design of DateTimeController: the granularity of the time returned by `DateTime::CurrentDateTime()` +is limited by the frequency at which SystemTask calls `DateTime::UpdateTime()`, which is currently set to 100ms. + +@mark9064 provided more details +in [this comment](https://github.com/InfiniTimeOrg/InfiniTime/pull/2041#issuecomment-2048528967). + +The [PR #2041 - Continuous time updates](https://github.com/InfiniTimeOrg/InfiniTime/pull/2041) provided some changes +to `DateTime` controller that improves the granularity of the time returned by `DateTime::CurrentDateTime()`. + +However, the review showed that `DateTime` cannot be `const` anymore, even when it's only used to get the current time, +since `DateTime::CurrentDateTime()` changes the internal state of the instance. + +We tried to identify alternative implementation that would have maintained the "const correctness" but we eventually +figured that this would lead to a re-design of `DateTime` which was out of scope of the initial PR (Continuous time +updates and always on display). + +So we decided to merge this PR #2041 and agree to fix/improve `DateTime` later on. + +## What needs to be done? + +Improve/redesign `DateTime` so that it + +* provides a very granular (ideally down to the millisecond) date and time via `CurrentDateTime()`. +* can be declared/passed as `const` when it's only used to **get** the time. +* limits the use of mutex as much as possible (an ideal implementation would not use any mutex, but this might not be + possible). +* improves the design of `DateTime::Seconds()`, `DateTime::Minutes()`, `DateTime::Hours()`, etc as + explained [in this comment](https://github.com/InfiniTimeOrg/InfiniTime/pull/2054#pullrequestreview-2037033105). + +Once this redesign is implemented, all instances/references to `DateTime` should be reviewed and updated to use `const` +where appropriate. + +Please check the following PR to get more context about this redesign: + +* [#2041 - Continuous time updates by @mark9064](https://github.com/InfiniTimeOrg/InfiniTime/pull/2041) +* [#2054 - Continuous time update - Alternative implementation to #2041 by @JF002](https://github.com/InfiniTimeOrg/InfiniTime/pull/2054)
\ No newline at end of file diff --git a/src/components/gfx/Gfx.cpp b/src/components/gfx/Gfx.cpp deleted file mode 100644 index 3eaaa3fe..00000000 --- a/src/components/gfx/Gfx.cpp +++ /dev/null @@ -1,196 +0,0 @@ -#include "components/gfx/Gfx.h" -#include "drivers/St7789.h" -using namespace Pinetime::Components; - -Gfx::Gfx(Pinetime::Drivers::St7789& lcd) : lcd {lcd} { -} - -void Gfx::Init() { -} - -void Gfx::ClearScreen() { - SetBackgroundColor(0x0000); - - state.remainingIterations = 240 + 1; - state.currentIteration = 0; - state.busy = true; - state.action = Action::FillRectangle; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(0, 0, width, height, reinterpret_cast<const uint8_t*>(buffer), width * 2); - WaitTransferFinished(); -} - -void Gfx::FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t color) { - SetBackgroundColor(color); - - state.remainingIterations = h; - state.currentIteration = 0; - state.busy = true; - state.action = Action::FillRectangle; - state.color = color; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(x, y, w, h, reinterpret_cast<const uint8_t*>(buffer), width * 2); - - WaitTransferFinished(); -} - -void Gfx::FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t* b) { - state.remainingIterations = h; - state.currentIteration = 0; - state.busy = true; - state.action = Action::FillRectangle; - state.color = 0x00; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(x, y, w, h, reinterpret_cast<const uint8_t*>(b), width * 2); - - WaitTransferFinished(); -} - -void Gfx::DrawString(uint8_t x, uint8_t y, uint16_t color, const char* text, const FONT_INFO* p_font, bool wrap) { - if (y > (height - p_font->height)) { - // Not enough space to write even single char. - return; - } - - uint8_t current_x = x; - uint8_t current_y = y; - - for (size_t i = 0; text[i] != '\0'; i++) { - if (text[i] == '\n') { - current_x = x; - current_y += p_font->height + p_font->height / 10; - } else { - DrawChar(p_font, (uint8_t) text[i], ¤t_x, current_y, color); - } - - uint8_t char_idx = text[i] - p_font->startChar; - uint16_t char_width = text[i] == ' ' ? (p_font->height / 2) : p_font->charInfo[char_idx].widthBits; - - if (current_x > (width - char_width)) { - if (wrap) { - current_x = x; - current_y += p_font->height + p_font->height / 10; - } else { - break; - } - - if (y > (height - p_font->height)) { - break; - } - } - } -} - -void Gfx::DrawChar(const FONT_INFO* font, uint8_t c, uint8_t* x, uint8_t y, uint16_t color) { - uint8_t char_idx = c - font->startChar; - uint16_t bytes_in_line = CEIL_DIV(font->charInfo[char_idx].widthBits, 8); - uint16_t bg = 0x0000; - - if (c == ' ') { - *x += font->height / 2; - return; - } - - // Build first line - for (uint16_t j = 0; j < bytes_in_line; j++) { - for (uint8_t k = 0; k < 8; k++) { - if ((1 << (7 - k)) & font->data[font->charInfo[char_idx].offset + j]) { - buffer[(j * 8) + k] = color; - } else { - buffer[(j * 8) + k] = bg; - } - } - } - - state.remainingIterations = font->height + 0; - state.currentIteration = 0; - state.busy = true; - state.action = Action::DrawChar; - state.font = const_cast<FONT_INFO*>(font); - state.character = c; - state.color = color; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(*x, y, bytes_in_line * 8, font->height, reinterpret_cast<const uint8_t*>(&buffer), bytes_in_line * 8 * 2); - WaitTransferFinished(); - - *x += font->charInfo[char_idx].widthBits + font->spacePixels; -} - -void Gfx::pixel_draw(uint8_t x, uint8_t y, uint16_t color) { - lcd.DrawPixel(x, y, color); -} - -void Gfx::Sleep() { - lcd.Sleep(); -} - -void Gfx::Wakeup() { - lcd.Wakeup(); -} - -void Gfx::SetBackgroundColor(uint16_t color) { - for (int i = 0; i < width; i++) { - buffer[i] = color; - } -} - -bool Gfx::GetNextBuffer(uint8_t** data, size_t& size) { - if (!state.busy) - return false; - state.remainingIterations--; - if (state.remainingIterations == 0) { - state.busy = false; - NotifyEndOfTransfer(state.taskToNotify); - return false; - } - - if (state.action == Action::FillRectangle) { - *data = reinterpret_cast<uint8_t*>(buffer); - size = width * 2; - } else if (state.action == Action::DrawChar) { - uint16_t bg = 0x0000; - uint8_t char_idx = state.character - state.font->startChar; - uint16_t bytes_in_line = CEIL_DIV(state.font->charInfo[char_idx].widthBits, 8); - - for (uint16_t j = 0; j < bytes_in_line; j++) { - for (uint8_t k = 0; k < 8; k++) { - if ((1 << (7 - k)) & state.font->data[state.font->charInfo[char_idx].offset + ((state.currentIteration + 1) * bytes_in_line) + j]) { - buffer[(j * 8) + k] = state.color; - } else { - buffer[(j * 8) + k] = bg; - } - } - } - - *data = reinterpret_cast<uint8_t*>(buffer); - size = bytes_in_line * 8 * 2; - } - - state.currentIteration++; - - return true; -} - -void Gfx::NotifyEndOfTransfer(TaskHandle_t task) { - if (task != nullptr) { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - vTaskNotifyGiveFromISR(task, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } -} - -void Gfx::WaitTransferFinished() const { - ulTaskNotifyTake(pdTRUE, 500); -} - -void Gfx::SetScrollArea(uint16_t topFixedLines, uint16_t scrollLines, uint16_t bottomFixedLines) { - lcd.VerticalScrollDefinition(topFixedLines, scrollLines, bottomFixedLines); -} - -void Gfx::SetScrollStartLine(uint16_t line) { - lcd.VerticalScrollStartAddress(line); -} diff --git a/src/components/gfx/Gfx.h b/src/components/gfx/Gfx.h deleted file mode 100644 index 17c248f7..00000000 --- a/src/components/gfx/Gfx.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once -#include <FreeRTOS.h> -#include <nrf_font.h> -#include <task.h> -#include <cstddef> -#include <cstdint> -#include "drivers/BufferProvider.h" - -namespace Pinetime { - namespace Drivers { - class St7789; - } - - namespace Components { - class Gfx : public Pinetime::Drivers::BufferProvider { - public: - explicit Gfx(Drivers::St7789& lcd); - void Init(); - void ClearScreen(); - void DrawString(uint8_t x, uint8_t y, uint16_t color, const char* text, const FONT_INFO* p_font, bool wrap); - void DrawChar(const FONT_INFO* font, uint8_t c, uint8_t* x, uint8_t y, uint16_t color); - void FillRectangle(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint16_t color); - void FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t* b); - void SetScrollArea(uint16_t topFixedLines, uint16_t scrollLines, uint16_t bottomFixedLines); - void SetScrollStartLine(uint16_t line); - - void Sleep(); - void Wakeup(); - bool GetNextBuffer(uint8_t** buffer, size_t& size) override; - void pixel_draw(uint8_t x, uint8_t y, uint16_t color); - - private: - static constexpr uint8_t width = 240; - static constexpr uint8_t height = 240; - - enum class Action { None, FillRectangle, DrawChar }; - - struct State { - State() : busy {false}, action {Action::None}, remainingIterations {0}, currentIteration {0} { - } - - volatile bool busy; - volatile Action action; - volatile uint16_t remainingIterations; - volatile uint16_t currentIteration; - volatile FONT_INFO* font; - volatile uint16_t color; - volatile uint8_t character; - volatile TaskHandle_t taskToNotify = nullptr; - }; - - volatile State state; - - uint16_t buffer[width]; // 1 line buffer - Drivers::St7789& lcd; - - void SetBackgroundColor(uint16_t color); - void WaitTransferFinished() const; - void NotifyEndOfTransfer(TaskHandle_t task); - }; - } -} diff --git a/src/components/heartrate/Biquad.cpp b/src/components/heartrate/Biquad.cpp deleted file mode 100644 index b7edd403..00000000 --- a/src/components/heartrate/Biquad.cpp +++ /dev/null @@ -1,26 +0,0 @@ -/* - SPDX-License-Identifier: LGPL-3.0-or-later - Original work Copyright (C) 2020 Daniel Thompson - C++ port Copyright (C) 2021 Jean-François Milants -*/ - -#include "components/heartrate/Biquad.h" - -using namespace Pinetime::Controllers; - -/** Original implementation from wasp-os : https://github.com/daniel-thompson/wasp-os/blob/master/wasp/ppg.py */ -Biquad::Biquad(float b0, float b1, float b2, float a1, float a2) : b0 {b0}, b1 {b1}, b2 {b2}, a1 {a1}, a2 {a2} { -} - -float Biquad::Step(float x) { - auto v1 = this->v1; - auto v2 = this->v2; - - auto v = x - (a1 * v1) - (a2 * v2); - auto y = (b0 * v) + (b1 * v1) + (b2 * v2); - - this->v2 = v1; - this->v1 = v; - - return y; -} diff --git a/src/components/heartrate/Biquad.h b/src/components/heartrate/Biquad.h deleted file mode 100644 index 7c8ca58f..00000000 --- a/src/components/heartrate/Biquad.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -namespace Pinetime { - namespace Controllers { - /// Direct Form II Biquad Filter - class Biquad { - public: - Biquad(float b0, float b1, float b2, float a1, float a2); - float Step(float x); - - private: - float b0; - float b1; - float b2; - float a1; - float a2; - - float v1 = 0.0f; - float v2 = 0.0f; - }; - } -} diff --git a/src/components/heartrate/Ppg.cpp b/src/components/heartrate/Ppg.cpp index 900b1c22..efbed852 100644 --- a/src/components/heartrate/Ppg.cpp +++ b/src/components/heartrate/Ppg.cpp @@ -1,107 +1,292 @@ -/* - SPDX-License-Identifier: LGPL-3.0-or-later - Original work Copyright (C) 2020 Daniel Thompson - C++ port Copyright (C) 2021 Jean-François Milants -*/ - #include "components/heartrate/Ppg.h" -#include <vector> #include <nrf_log.h> +#include <vector> + using namespace Pinetime::Controllers; -/** Original implementation from wasp-os : https://github.com/daniel-thompson/wasp-os/blob/master/wasp/ppg.py */ namespace { - int Compare(int8_t* d1, int8_t* d2, size_t count) { - int e = 0; - for (size_t i = 0; i < count; i++) { - auto d = d1[i] - d2[i]; - e += d * d; + float LinearInterpolation(const float* xValues, const float* yValues, int length, float pointX) { + if (pointX > xValues[length - 1]) { + return yValues[length - 1]; + } else if (pointX <= xValues[0]) { + return yValues[0]; + } + int index = 0; + while (pointX > xValues[index] && index < length - 1) { + index++; + } + float pointX0 = xValues[index - 1]; + float pointX1 = xValues[index]; + float pointY0 = yValues[index - 1]; + float pointY1 = yValues[index]; + float mu = (pointX - pointX0) / (pointX1 - pointX0); + + return (pointY0 * (1 - mu) + pointY1 * mu); + } + + float PeakSearch(float* xVals, float* yVals, float threshold, float& width, float start, float end, int length) { + int peaks = 0; + bool enabled = false; + float minBin = 0.0f; + float maxBin = 0.0f; + float peakCenter = 0.0f; + float prevValue = LinearInterpolation(xVals, yVals, length, start - 0.01f); + float currValue = LinearInterpolation(xVals, yVals, length, start); + float idx = start; + while (idx < end) { + float nextValue = LinearInterpolation(xVals, yVals, length, idx + 0.01f); + if (currValue < threshold) { + enabled = true; + } + if (currValue >= threshold and enabled) { + if (prevValue < threshold) { + minBin = idx; + } else if (nextValue <= threshold) { + maxBin = idx; + peaks++; + width = maxBin - minBin; + peakCenter = width / 2.0f + minBin; + } + } + prevValue = currValue; + currValue = nextValue; + idx += 0.01f; } - return e; + if (peaks != 1) { + width = 0.0f; + peakCenter = 0.0f; + } + return peakCenter; } - int CompareShift(int8_t* d, int shift, size_t count) { - return Compare(d + shift, d, count - shift); + float SpectrumMean(const std::array<float, Ppg::spectrumLength>& signal, int start, int end) { + int total = 0; + float mean = 0.0f; + for (int idx = start; idx < end; idx++) { + mean += signal.at(idx); + total++; + } + if (total > 0) { + mean /= static_cast<float>(total); + } + return mean; } - int Trough(int8_t* d, size_t size, uint8_t mn, uint8_t mx) { - auto z2 = CompareShift(d, mn - 2, size); - auto z1 = CompareShift(d, mn - 1, size); - for (int i = mn; i < mx + 1; i++) { - auto z = CompareShift(d, i, size); - if (z2 > z1 && z1 < z) { - return i; + float SignalToNoise(const std::array<float, Ppg::spectrumLength>& signal, int start, int end, float max) { + float mean = SpectrumMean(signal, start, end); + return max / mean; + } + + // Simple bandpass filter using exponential moving average + void Filter30to240(std::array<float, Ppg::dataLength>& signal) { + // From: + // https://www.norwegiancreations.com/2016/03/arduino-tutorial-simple-high-pass-band-pass-and-band-stop-filtering/ + + int length = signal.size(); + // 0.268 is ~0.5Hz and 0.816 is ~4Hz cutoff at 10Hz sampling + float expAlpha = 0.816f; + float expAvg = 0.0f; + for (int loop = 0; loop < 4; loop++) { + expAvg = signal.front(); + for (int idx = 0; idx < length; idx++) { + expAvg = (expAlpha * signal.at(idx)) + ((1 - expAlpha) * expAvg); + signal[idx] = expAvg; + } + } + expAlpha = 0.268f; + for (int loop = 0; loop < 4; loop++) { + expAvg = signal.front(); + for (int idx = 0; idx < length; idx++) { + expAvg = (expAlpha * signal.at(idx)) + ((1 - expAlpha) * expAvg); + signal[idx] -= expAvg; } - z2 = z1; - z1 = z; } - return -1; } -} -Ppg::Ppg() - : hpf {0.87033078, -1.74066156, 0.87033078, -1.72377617, 0.75754694}, - agc {20, 0.971, 2}, - lpf {0.11595249, 0.23190498, 0.11595249, -0.72168143, 0.18549138} { -} + float SpectrumMax(const std::array<float, Ppg::spectrumLength>& data, int start, int end) { + float max = 0.0f; + for (int idx = start; idx < end; idx++) { + if (data.at(idx) > max) { + max = data.at(idx); + } + } + return max; + } -int8_t Ppg::Preprocess(float spl) { - spl -= offset; - spl = hpf.Step(spl); - spl = agc.Step(spl); - spl = lpf.Step(spl); + void Detrend(std::array<float, Ppg::dataLength>& signal) { + int size = signal.size(); + float offset = signal.front(); + float slope = (signal.at(size - 1) - offset) / static_cast<float>(size - 1); - auto spl_int = static_cast<int8_t>(spl); + for (int idx = 0; idx < size; idx++) { + signal[idx] -= (slope * static_cast<float>(idx) + offset); + } + for (int idx = 0; idx < size - 1; idx++) { + signal[idx] = signal[idx + 1] - signal[idx]; + } + } - if (dataIndex < 200) { - data[dataIndex++] = spl_int; + // Hanning Coefficients from numpy: python -c 'import numpy;print(numpy.hanning(64))' + // Note: Harcoded and must be updated if constexpr dataLength is changed. Prevents the need to + // use cosf() which results in an extra ~5KB in storage. + // This data is symetrical so just using the first half (saves 128B when dataLength is 64). + static constexpr float hanning[Ppg::dataLength >> 1] { + 0.0f, 0.00248461f, 0.00991376f, 0.0222136f, 0.03926189f, 0.06088921f, 0.08688061f, 0.11697778f, + 0.15088159f, 0.1882551f, 0.22872687f, 0.27189467f, 0.31732949f, 0.36457977f, 0.41317591f, 0.46263495f, + 0.51246535f, 0.56217185f, 0.61126047f, 0.65924333f, 0.70564355f, 0.75f, 0.79187184f, 0.83084292f, + 0.86652594f, 0.89856625f, 0.92664544f, 0.95048443f, 0.96984631f, 0.98453864f, 0.99441541f, 0.99937846f}; +} + +Ppg::Ppg() { + dataAverage.fill(0.0f); + spectrum.fill(0.0f); +} + +int8_t Ppg::Preprocess(uint16_t hrs, uint16_t als) { + if (dataIndex < dataLength) { + dataHRS[dataIndex++] = hrs; } - return spl_int; + alsValue = als; + if (alsValue > alsThreshold) { + return 1; + } + return 0; } int Ppg::HeartRate() { - if (dataIndex < 200) { + if (dataIndex < dataLength) { return 0; } - - NRF_LOG_INFO("PREPROCESS, offset = %d", offset); - auto hr = ProcessHeartRate(); - dataIndex = 0; + int hr = 0; + hr = ProcessHeartRate(resetSpectralAvg); + resetSpectralAvg = false; + // Make room for overlapWindow number of new samples + for (int idx = 0; idx < dataLength - overlapWindow; idx++) { + dataHRS[idx] = dataHRS[idx + overlapWindow]; + } + dataIndex = dataLength - overlapWindow; return hr; } -int Ppg::ProcessHeartRate() { - int t0 = Trough(data.data(), dataIndex, 7, 48); - if (t0 < 0) { - return 0; +void Ppg::Reset(bool resetDaqBuffer) { + if (resetDaqBuffer) { + dataIndex = 0; } + avgIndex = 0; + dataAverage.fill(0.0f); + lastPeakLocation = 0.0f; + alsThreshold = UINT16_MAX; + alsValue = 0; + resetSpectralAvg = true; + spectrum.fill(0.0f); +} - int t1 = t0 * 2; - t1 = Trough(data.data(), dataIndex, t1 - 5, t1 + 5); - if (t1 < 0) { - return 0; +// Pass init == true to reset spectral averaging. +// Returns -1 (Reset Acquisition), 0 (Unable to obtain HR) or HR (BPM). +int Ppg::ProcessHeartRate(bool init) { + std::copy(dataHRS.begin(), dataHRS.end(), vReal.begin()); + Detrend(vReal); + Filter30to240(vReal); + vImag.fill(0.0f); + // Apply Hanning Window + int hannIdx = 0; + for (int idx = 0; idx < dataLength; idx++) { + if (idx >= dataLength >> 1) { + hannIdx--; + } + vReal[idx] *= hanning[hannIdx]; + if (idx < dataLength >> 1) { + hannIdx++; + } } - - int t2 = (t1 * 3) / 2; - t2 = Trough(data.data(), dataIndex, t2 - 5, t2 + 5); - if (t2 < 0) { - return 0; + // Compute in place power spectrum + ArduinoFFT<float> FFT = ArduinoFFT<float>(vReal.data(), vImag.data(), dataLength, sampleFreq); + FFT.compute(FFTDirection::Forward); + FFT.complexToMagnitude(); + FFT.~ArduinoFFT(); + SpectrumAverage(vReal.data(), spectrum.data(), spectrum.size(), init); + peakLocation = 0.0f; + float threshold = peakDetectionThreshold; + float peakWidth = 0.0f; + int specLen = spectrum.size(); + float max = SpectrumMax(spectrum, hrROIbegin, hrROIend); + float signalToNoiseRatio = SignalToNoise(spectrum, hrROIbegin, hrROIend, max); + if (signalToNoiseRatio > signalToNoiseThreshold && spectrum.at(0) < dcThreshold) { + threshold *= max; + // Reuse VImag for interpolation x values passed to PeakSearch + for (int idx = 0; idx < dataLength; idx++) { + vImag[idx] = idx; + } + peakLocation = PeakSearch(vImag.data(), + spectrum.data(), + threshold, + peakWidth, + static_cast<float>(hrROIbegin), + static_cast<float>(hrROIend), + specLen); + peakLocation *= freqResolution; } - - int t3 = (t2 * 4) / 3; - t3 = Trough(data.data(), dataIndex, t3 - 4, t3 + 4); - if (t3 < 0) { - return (60 * 24 * 3) / t2; + // Peak too wide? (broad spectrum noise or large, rapid HR change) + if (peakWidth > maxPeakWidth) { + peakLocation = 0.0f; } - - return (60 * 24 * 4) / t3; + // Check HR limits + if (peakLocation < minHR || peakLocation > maxHR) { + peakLocation = 0.0f; + } + // Reset spectral averaging if bad reading + if (peakLocation == 0.0f) { + resetSpectralAvg = true; + } + // Set the ambient light threshold and return HR in BPM + alsThreshold = static_cast<uint16_t>(alsValue * alsFactor); + // Get current average HR. If HR reduced to zero, return -1 (reset) else HR + peakLocation = HeartRateAverage(peakLocation); + int rtn = -1; + if (peakLocation == 0.0f && lastPeakLocation > 0.0f) { + lastPeakLocation = 0.0f; + } else { + lastPeakLocation = peakLocation; + rtn = static_cast<int>((peakLocation * 60.0f) + 0.5f); + } + return rtn; } -void Ppg::SetOffset(uint16_t offset) { - this->offset = offset; - dataIndex = 0; +void Ppg::SpectrumAverage(const float* data, float* spectrum, int length, bool reset) { + if (reset) { + spectralAvgCount = 0; + } + float count = static_cast<float>(spectralAvgCount); + for (int idx = 0; idx < length; idx++) { + spectrum[idx] = (spectrum[idx] * count + data[idx]) / (count + 1); + } + if (spectralAvgCount < spectralAvgMax) { + spectralAvgCount++; + } } -void Ppg::Reset() { - dataIndex = 0; +float Ppg::HeartRateAverage(float hr) { + avgIndex++; + avgIndex %= dataAverage.size(); + dataAverage[avgIndex] = hr; + float avg = 0.0f; + float total = 0.0f; + float min = 300.0f; + float max = 0.0f; + for (const float& value : dataAverage) { + if (value > 0.0f) { + avg += value; + if (value < min) + min = value; + if (value > max) + max = value; + total++; + } + } + if (total > 0) { + avg /= total; + } else { + avg = 0.0f; + } + return avg; } diff --git a/src/components/heartrate/Ppg.h b/src/components/heartrate/Ppg.h index 1f709bab..373e7985 100644 --- a/src/components/heartrate/Ppg.h +++ b/src/components/heartrate/Ppg.h @@ -3,29 +3,78 @@ #include <array> #include <cstddef> #include <cstdint> -#include "components/heartrate/Biquad.h" -#include "components/heartrate/Ptagc.h" +// Note: Change internal define 'sqrt_internal sqrt' to +// 'sqrt_internal sqrtf' to save ~3KB of flash. +#define sqrt_internal sqrtf +#define FFT_SPEED_OVER_PRECISION +#include "libs/arduinoFFT/src/arduinoFFT.h" namespace Pinetime { namespace Controllers { class Ppg { public: Ppg(); - int8_t Preprocess(float spl); + int8_t Preprocess(uint16_t hrs, uint16_t als); int HeartRate(); - - void SetOffset(uint16_t offset); - void Reset(); + void Reset(bool resetDaqBuffer); + static constexpr int deltaTms = 100; + // Daq dataLength: Must be power of 2 + static constexpr uint16_t dataLength = 64; + static constexpr uint16_t spectrumLength = dataLength >> 1; private: - std::array<int8_t, 200> data; - size_t dataIndex = 0; - float offset; - Biquad hpf; - Ptagc agc; - Biquad lpf; + // The sampling frequency (Hz) based on sampling time in milliseconds (DeltaTms) + static constexpr float sampleFreq = 1000.0f / static_cast<float>(deltaTms); + // The frequency resolution (Hz) + static constexpr float freqResolution = sampleFreq / dataLength; + // Number of samples before each analysis + // 0.5 second update rate at 10Hz + static constexpr uint16_t overlapWindow = 5; + // Maximum number of spectrum running averages + // Note: actual number of spectra averaged = spectralAvgMax + 1 + static constexpr uint16_t spectralAvgMax = 2; + // Multiple Peaks above this threshold (% of max) are rejected + static constexpr float peakDetectionThreshold = 0.6f; + // Maximum peak width (bins) at threshold for valid peak. + static constexpr float maxPeakWidth = 2.5f; + // Metric for spectrum noise level. + static constexpr float signalToNoiseThreshold = 3.0f; + // Heart rate Region Of Interest begin (bins) + static constexpr uint16_t hrROIbegin = static_cast<uint16_t>((30.0f / 60.0f) / freqResolution + 0.5f); + // Heart rate Region Of Interest end (bins) + static constexpr uint16_t hrROIend = static_cast<uint16_t>((240.0f / 60.0f) / freqResolution + 0.5f); + // Minimum HR (Hz) + static constexpr float minHR = 40.0f / 60.0f; + // Maximum HR (Hz) + static constexpr float maxHR = 230.0f / 60.0f; + // Threshold for high DC level after filtering + static constexpr float dcThreshold = 0.5f; + // ALS detection factor + static constexpr float alsFactor = 2.0f; + + // Raw ADC data + std::array<uint16_t, dataLength> dataHRS; + // Stores Real numbers from FFT + std::array<float, dataLength> vReal; + // Stores Imaginary numbers from FFT + std::array<float, dataLength> vImag; + // Stores power spectrum calculated from FFT real and imag values + std::array<float, (spectrumLength)> spectrum; + // Stores each new HR value (Hz). Non zero values are averaged for HR output + std::array<float, 20> dataAverage; + + uint16_t avgIndex = 0; + uint16_t spectralAvgCount = 0; + float lastPeakLocation = 0.0f; + uint16_t alsThreshold = UINT16_MAX; + uint16_t alsValue = 0; + uint16_t dataIndex = 0; + float peakLocation; + bool resetSpectralAvg = true; - int ProcessHeartRate(); + int ProcessHeartRate(bool init); + float HeartRateAverage(float hr); + void SpectrumAverage(const float* data, float* spectrum, int length, bool reset); }; } } diff --git a/src/components/heartrate/Ptagc.cpp b/src/components/heartrate/Ptagc.cpp deleted file mode 100644 index 221be460..00000000 --- a/src/components/heartrate/Ptagc.cpp +++ /dev/null @@ -1,29 +0,0 @@ -/* - SPDX-License-Identifier: LGPL-3.0-or-later - Original work Copyright (C) 2020 Daniel Thompson - C++ port Copyright (C) 2021 Jean-François Milants -*/ - -#include "components/heartrate/Ptagc.h" -#include <cmath> - -using namespace Pinetime::Controllers; - -/** Original implementation from wasp-os : https://github.com/daniel-thompson/wasp-os/blob/master/wasp/ppg.py */ -Ptagc::Ptagc(float start, float decay, float threshold) : peak {start}, decay {decay}, boost {1.0f / decay}, threshold {threshold} { -} - -float Ptagc::Step(float spl) { - if (std::abs(spl) > peak) { - peak *= boost; - } else { - peak *= decay; - } - - if ((spl > (peak * threshold)) || (spl < (peak * -threshold))) { - return 0.0f; - } - - spl = 100.0f * spl / (2.0f * peak); - return spl; -} diff --git a/src/components/heartrate/Ptagc.h b/src/components/heartrate/Ptagc.h deleted file mode 100644 index 3476636b..00000000 --- a/src/components/heartrate/Ptagc.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -namespace Pinetime { - namespace Controllers { - class Ptagc { - public: - Ptagc(float start, float decay, float threshold); - float Step(float spl); - - private: - float peak; - float decay; - float boost; - float threshold; - }; - } -} diff --git a/src/components/motion/MotionController.cpp b/src/components/motion/MotionController.cpp index 9d16e00d..72507ac5 100644 --- a/src/components/motion/MotionController.cpp +++ b/src/components/motion/MotionController.cpp @@ -2,25 +2,59 @@ #include <task.h> +#include "utility/Math.h" + using namespace Pinetime::Controllers; +namespace { + constexpr inline int32_t Clamp(int32_t val, int32_t min, int32_t max) { + return val < min ? min : (val > max ? max : val); + } + + // only returns meaningful values if inputs are acceleration due to gravity + int16_t DegreesRolled(int16_t y, int16_t z, int16_t prevY, int16_t prevZ) { + int16_t prevYAngle = Pinetime::Utility::Asin(Clamp(prevY * 32, -32767, 32767)); + int16_t yAngle = Pinetime::Utility::Asin(Clamp(y * 32, -32767, 32767)); + + if (z < 0 && prevZ < 0) { + return yAngle - prevYAngle; + } + if (prevZ < 0) { + if (y < 0) { + return -prevYAngle - yAngle - 180; + } + return -prevYAngle - yAngle + 180; + } + if (z < 0) { + if (y < 0) { + return prevYAngle + yAngle + 180; + } + return prevYAngle + yAngle - 180; + } + return prevYAngle - yAngle; + } +} + void MotionController::Update(int16_t x, int16_t y, int16_t z, uint32_t nbSteps) { if (this->nbSteps != nbSteps && service != nullptr) { service->OnNewStepCountValue(nbSteps); } - if (service != nullptr && (this->x != x || this->y != y || this->z != z)) { + if (service != nullptr && (xHistory[0] != x || yHistory[0] != y || zHistory[0] != z)) { service->OnNewMotionValues(x, y, z); } lastTime = time; time = xTaskGetTickCount(); - this->x = x; - lastY = this->y; - this->y = y; - lastZ = this->z; - this->z = z; + xHistory++; + xHistory[0] = x; + yHistory++; + yHistory[0] = y; + zHistory++; + zHistory[0] = z; + + stats = GetAccelStats(); int32_t deltaSteps = nbSteps - this->nbSteps; if (deltaSteps > 0) { @@ -29,38 +63,84 @@ void MotionController::Update(int16_t x, int16_t y, int16_t z, uint32_t nbSteps) this->nbSteps = nbSteps; } -bool MotionController::ShouldRaiseWake(bool isSleeping) { - if ((x + 335) <= 670 && z < 0) { - if (!isSleeping) { - if (y <= 0) { - return false; - } - lastYForRaiseWake = 0; - return false; - } +MotionController::AccelStats MotionController::GetAccelStats() const { + AccelStats stats; - if (y >= 0) { - lastYForRaiseWake = 0; - return false; - } - if (y + 230 < lastYForRaiseWake) { - lastYForRaiseWake = y; - return true; - } + for (uint8_t i = 0; i < AccelStats::numHistory; i++) { + stats.xMean += xHistory[histSize - i]; + stats.yMean += yHistory[histSize - i]; + stats.zMean += zHistory[histSize - i]; + stats.prevXMean += xHistory[1 + i]; + stats.prevYMean += yHistory[1 + i]; + stats.prevZMean += zHistory[1 + i]; } - return false; + stats.xMean /= AccelStats::numHistory; + stats.yMean /= AccelStats::numHistory; + stats.zMean /= AccelStats::numHistory; + stats.prevXMean /= AccelStats::numHistory; + stats.prevYMean /= AccelStats::numHistory; + stats.prevZMean /= AccelStats::numHistory; + + for (uint8_t i = 0; i < AccelStats::numHistory; i++) { + stats.xVariance += (xHistory[histSize - i] - stats.xMean) * (xHistory[histSize - i] - stats.xMean); + stats.yVariance += (yHistory[histSize - i] - stats.yMean) * (yHistory[histSize - i] - stats.yMean); + stats.zVariance += (zHistory[histSize - i] - stats.zMean) * (zHistory[histSize - i] - stats.zMean); + } + stats.xVariance /= AccelStats::numHistory; + stats.yVariance /= AccelStats::numHistory; + stats.zVariance /= AccelStats::numHistory; + + return stats; +} + +bool MotionController::ShouldRaiseWake() const { + constexpr uint32_t varianceThresh = 56 * 56; + constexpr int16_t xThresh = 384; + constexpr int16_t yThresh = -64; + constexpr int16_t rollDegreesThresh = -45; + + if (std::abs(stats.xMean) > xThresh) { + return false; + } + + // if the variance is below the threshold, the accelerometer values can be considered to be from acceleration due to gravity + if (stats.yVariance > varianceThresh || (stats.yMean < -724 && stats.zVariance > varianceThresh) || stats.yMean > yThresh) { + return false; + } + + return DegreesRolled(stats.yMean, stats.zMean, stats.prevYMean, stats.prevZMean) < rollDegreesThresh; } bool MotionController::ShouldShakeWake(uint16_t thresh) { /* Currently Polling at 10hz, If this ever goes faster scalar and EMA might need adjusting */ - int32_t speed = std::abs(z + (y / 2) + (x / 4) - lastY / 2 - lastZ) / (time - lastTime) * 100; - //(.2 * speed) + ((1 - .2) * accumulatedSpeed); - // implemented without floats as .25Alpha - accumulatedSpeed = (speed / 5) + ((accumulatedSpeed / 5) * 4); + int32_t speed = std::abs(zHistory[0] - zHistory[histSize - 1] + (yHistory[0] - yHistory[histSize - 1]) / 2 + + (xHistory[0] - xHistory[histSize - 1]) / 4) * + 100 / (time - lastTime); + // (.2 * speed) + ((1 - .2) * accumulatedSpeed); + accumulatedSpeed = speed / 5 + accumulatedSpeed * 4 / 5; return accumulatedSpeed > thresh; } +bool MotionController::ShouldLowerSleep() const { + if ((stats.xMean > 887 && DegreesRolled(stats.xMean, stats.zMean, stats.prevXMean, stats.prevZMean) > 30) || + (stats.xMean < -887 && DegreesRolled(stats.xMean, stats.zMean, stats.prevXMean, stats.prevZMean) < -30)) { + return true; + } + + if (stats.yMean < 724 || DegreesRolled(stats.yMean, stats.zMean, stats.prevYMean, stats.prevZMean) < 30) { + return false; + } + + for (uint8_t i = AccelStats::numHistory + 1; i < yHistory.Size(); i++) { + if (yHistory[i] < 265) { + return false; + } + } + + return true; +} + void MotionController::Init(Pinetime::Drivers::Bma421::DeviceTypes types) { switch (types) { case Drivers::Bma421::DeviceTypes::BMA421: diff --git a/src/components/motion/MotionController.h b/src/components/motion/MotionController.h index eb8d04aa..be0241d3 100644 --- a/src/components/motion/MotionController.h +++ b/src/components/motion/MotionController.h @@ -6,6 +6,7 @@ #include "drivers/Bma421.h" #include "components/ble/MotionService.h" +#include "utility/CircularBuffer.h" namespace Pinetime { namespace Controllers { @@ -20,15 +21,15 @@ namespace Pinetime { void Update(int16_t x, int16_t y, int16_t z, uint32_t nbSteps); int16_t X() const { - return x; + return xHistory[0]; } int16_t Y() const { - return y; + return yHistory[0]; } int16_t Z() const { - return z; + return zHistory[0]; } uint32_t NbSteps() const { @@ -44,20 +45,13 @@ namespace Pinetime { } bool ShouldShakeWake(uint16_t thresh); - bool ShouldRaiseWake(bool isSleeping); + bool ShouldRaiseWake() const; + bool ShouldLowerSleep() const; int32_t CurrentShakeSpeed() const { return accumulatedSpeed; } - void IsSensorOk(bool isOk) { - isSensorOk = isOk; - } - - bool IsSensorOk() const { - return isSensorOk; - } - DeviceTypes DeviceType() const { return deviceType; } @@ -68,6 +62,10 @@ namespace Pinetime { this->service = service; } + Pinetime::Controllers::MotionService* GetService() const { + return service; + } + private: uint32_t nbSteps = 0; uint32_t currentTripSteps = 0; @@ -75,15 +73,31 @@ namespace Pinetime { TickType_t lastTime = 0; TickType_t time = 0; - int16_t x = 0; - int16_t lastYForRaiseWake = 0; - int16_t lastY = 0; - int16_t y = 0; - int16_t lastZ = 0; - int16_t z = 0; + struct AccelStats { + static constexpr uint8_t numHistory = 2; + + int16_t xMean = 0; + int16_t yMean = 0; + int16_t zMean = 0; + int16_t prevXMean = 0; + int16_t prevYMean = 0; + int16_t prevZMean = 0; + + uint32_t xVariance = 0; + uint32_t yVariance = 0; + uint32_t zVariance = 0; + }; + + AccelStats GetAccelStats() const; + + AccelStats stats = {}; + + static constexpr uint8_t histSize = 8; + Utility::CircularBuffer<int16_t, histSize> xHistory = {}; + Utility::CircularBuffer<int16_t, histSize> yHistory = {}; + Utility::CircularBuffer<int16_t, histSize> zHistory = {}; int32_t accumulatedSpeed = 0; - bool isSensorOk = false; DeviceTypes deviceType = DeviceTypes::Unknown; Pinetime::Controllers::MotionService* service = nullptr; }; diff --git a/src/components/settings/Settings.h b/src/components/settings/Settings.h index d1e71656..602de3a5 100644 --- a/src/components/settings/Settings.h +++ b/src/components/settings/Settings.h @@ -3,20 +3,17 @@ #include <bitset> #include "components/brightness/BrightnessController.h" #include "components/fs/FS.h" +#include "displayapp/apps/Apps.h" namespace Pinetime { namespace Controllers { class Settings { public: enum class ClockType : uint8_t { H24, H12 }; + enum class WeatherFormat : uint8_t { Metric, Imperial }; enum class Notification : uint8_t { On, Off, Sleep }; enum class ChimesOption : uint8_t { None, Hours, HalfHours }; - enum class WakeUpMode : uint8_t { - SingleTap = 0, - DoubleTap = 1, - RaiseWrist = 2, - Shake = 3, - }; + enum class WakeUpMode : uint8_t { SingleTap = 0, DoubleTap = 1, RaiseWrist = 2, Shake = 3, LowerWrist = 4 }; enum class Colors : uint8_t { White, Silver, @@ -38,12 +35,14 @@ namespace Pinetime { Pink }; enum class PTSGaugeStyle : uint8_t { Full, Half, Numeric }; + enum class PTSWeather : uint8_t { On, Off }; struct PineTimeStyle { Colors ColorTime = Colors::Teal; Colors ColorBar = Colors::Teal; Colors ColorBG = Colors::Black; PTSGaugeStyle gaugeStyle = PTSGaugeStyle::Full; + PTSWeather weatherEnable = PTSWeather::Off; }; struct WatchFaceInfineat { @@ -61,15 +60,15 @@ namespace Pinetime { void Init(); void SaveSettings(); - void SetClockFace(uint8_t face) { - if (face != settings.clockFace) { + void SetWatchFace(Pinetime::Applications::WatchFace face) { + if (face != settings.watchFace) { settingsChanged = true; } - settings.clockFace = face; + settings.watchFace = face; }; - uint8_t GetClockFace() const { - return settings.clockFace; + Pinetime::Applications::WatchFace GetWatchFace() const { + return settings.watchFace; }; void SetChimeOption(ChimesOption chimeOption) { @@ -145,6 +144,16 @@ namespace Pinetime { return settings.PTS.gaugeStyle; }; + void SetPTSWeather(PTSWeather weatherEnable) { + if (weatherEnable != settings.PTS.weatherEnable) + settingsChanged = true; + settings.PTS.weatherEnable = weatherEnable; + }; + + PTSWeather GetPTSWeather() const { + return settings.PTS.weatherEnable; + }; + void SetAppMenu(uint8_t menu) { appMenu = menu; }; @@ -172,6 +181,17 @@ namespace Pinetime { return settings.clockType; }; + void SetWeatherFormat(WeatherFormat weatherFormat) { + if (weatherFormat != settings.weatherFormat) { + settingsChanged = true; + } + settings.weatherFormat = weatherFormat; + }; + + WeatherFormat GetWeatherFormat() const { + return settings.weatherFormat; + }; + void SetNotificationStatus(Notification status) { if (status != settings.notificationStatus) { settingsChanged = true; @@ -194,6 +214,21 @@ namespace Pinetime { return settings.screenTimeOut; }; + bool GetAlwaysOnDisplay() const { + return settings.alwaysOnDisplay && GetNotificationStatus() != Notification::Sleep; + }; + + void SetAlwaysOnDisplaySetting(bool state) { + if (state != settings.alwaysOnDisplay) { + settingsChanged = true; + } + settings.alwaysOnDisplay = state; + } + + bool GetAlwaysOnDisplaySetting() const { + return settings.alwaysOnDisplay; + } + void SetShakeThreshold(uint16_t thresh) { if (settings.shakeWakeThreshold != thresh) { settings.shakeWakeThreshold = thresh; @@ -225,7 +260,7 @@ namespace Pinetime { } }; - std::bitset<4> getWakeUpModes() const { + std::bitset<5> getWakeUpModes() const { return settings.wakeUpMode; } @@ -266,25 +301,29 @@ namespace Pinetime { private: Pinetime::Controllers::FS& fs; - static constexpr uint32_t settingsVersion = 0x0004; + static constexpr uint32_t settingsVersion = 0x0008; struct SettingsData { uint32_t version = settingsVersion; uint32_t stepsGoal = 10000; uint32_t screenTimeOut = 15000; + bool alwaysOnDisplay = false; + ClockType clockType = ClockType::H24; + WeatherFormat weatherFormat = WeatherFormat::Metric; Notification notificationStatus = Notification::On; - uint8_t clockFace = 0; + Pinetime::Applications::WatchFace watchFace = Pinetime::Applications::WatchFace::Digital; ChimesOption chimesOption = ChimesOption::None; PineTimeStyle PTS; WatchFaceInfineat watchFaceInfineat; - std::bitset<4> wakeUpMode {0}; + std::bitset<5> wakeUpMode {0}; uint16_t shakeWakeThreshold = 150; + Controllers::BrightnessController::Levels brightLevel = Controllers::BrightnessController::Levels::Medium; }; diff --git a/src/components/timer/Timer.cpp b/src/components/timer/Timer.cpp new file mode 100644 index 00000000..279178cd --- /dev/null +++ b/src/components/timer/Timer.cpp @@ -0,0 +1,28 @@ +#include "components/timer/Timer.h" + +using namespace Pinetime::Controllers; + +Timer::Timer(void* const timerData, TimerCallbackFunction_t timerCallbackFunction) { + timer = xTimerCreate("Timer", 1, pdFALSE, timerData, timerCallbackFunction); +} + +void Timer::StartTimer(std::chrono::milliseconds duration) { + xTimerChangePeriod(timer, pdMS_TO_TICKS(duration.count()), 0); + xTimerStart(timer, 0); +} + +std::chrono::milliseconds Timer::GetTimeRemaining() { + if (IsRunning()) { + TickType_t remainingTime = xTimerGetExpiryTime(timer) - xTaskGetTickCount(); + return std::chrono::milliseconds(remainingTime * 1000 / configTICK_RATE_HZ); + } + return std::chrono::milliseconds(0); +} + +void Timer::StopTimer() { + xTimerStop(timer, 0); +} + +bool Timer::IsRunning() { + return (xTimerIsTimerActive(timer) == pdTRUE); +} diff --git a/src/components/timer/TimerController.h b/src/components/timer/Timer.h index 1c2e44b6..2469666f 100644 --- a/src/components/timer/TimerController.h +++ b/src/components/timer/Timer.h @@ -6,17 +6,10 @@ #include <chrono> namespace Pinetime { - namespace System { - class SystemTask; - } - namespace Controllers { - - class TimerController { + class Timer { public: - TimerController() = default; - - void Init(System::SystemTask* systemTask); + Timer(void* timerData, TimerCallbackFunction_t timerCallbackFunction); void StartTimer(std::chrono::milliseconds duration); @@ -26,10 +19,7 @@ namespace Pinetime { bool IsRunning(); - void OnTimerEnd(); - private: - System::SystemTask* systemTask = nullptr; TimerHandle_t timer; }; } diff --git a/src/components/timer/TimerController.cpp b/src/components/timer/TimerController.cpp deleted file mode 100644 index 5e7f1eed..00000000 --- a/src/components/timer/TimerController.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "components/timer/TimerController.h" -#include "systemtask/SystemTask.h" - -using namespace Pinetime::Controllers; - -void TimerCallback(TimerHandle_t xTimer) { - auto* controller = static_cast<TimerController*>(pvTimerGetTimerID(xTimer)); - controller->OnTimerEnd(); -} - -void TimerController::Init(Pinetime::System::SystemTask* systemTask) { - this->systemTask = systemTask; - timer = xTimerCreate("Timer", 1, pdFALSE, this, TimerCallback); -} - -void TimerController::StartTimer(std::chrono::milliseconds duration) { - xTimerChangePeriod(timer, pdMS_TO_TICKS(duration.count()), 0); - xTimerStart(timer, 0); -} - -std::chrono::milliseconds TimerController::GetTimeRemaining() { - if (IsRunning()) { - TickType_t remainingTime = xTimerGetExpiryTime(timer) - xTaskGetTickCount(); - return std::chrono::milliseconds(remainingTime * 1000 / configTICK_RATE_HZ); - } - return std::chrono::milliseconds(0); -} - -void TimerController::StopTimer() { - xTimerStop(timer, 0); -} - -bool TimerController::IsRunning() { - return (xTimerIsTimerActive(timer) == pdTRUE); -} - -void TimerController::OnTimerEnd() { - systemTask->PushMessage(System::Messages::OnTimerDone); -} diff --git a/src/displayapp/Apps.h b/src/displayapp/Apps.h deleted file mode 100644 index f253bc03..00000000 --- a/src/displayapp/Apps.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -namespace Pinetime { - namespace Applications { - enum class Apps { - None, - Launcher, - Clock, - SysInfo, - FirmwareUpdate, - FirmwareValidation, - NotificationsPreview, - Notifications, - Timer, - Alarm, - FlashLight, - BatteryInfo, - Music, - Paint, - Paddle, - Twos, - HeartRate, - Navigation, - StopWatch, - Metronome, - Motion, - Steps, - PassKey, - QuickSettings, - Settings, - SettingWatchFace, - SettingTimeFormat, - SettingDisplay, - SettingWakeUp, - SettingSteps, - SettingSetDateTime, - SettingChimes, - SettingShakeThreshold, - SettingBluetooth, - Error - }; - } -} diff --git a/src/displayapp/Controllers.h b/src/displayapp/Controllers.h new file mode 100644 index 00000000..9992426c --- /dev/null +++ b/src/displayapp/Controllers.h @@ -0,0 +1,56 @@ +#pragma once + +namespace Pinetime { + namespace Applications { + class DisplayApp; + } + + namespace Components { + class LittleVgl; + } + + namespace Controllers { + class Battery; + class Ble; + class DateTime; + class NotificationManager; + class HeartRateController; + class Settings; + class MotorController; + class MotionController; + class AlarmController; + class BrightnessController; + class SimpleWeatherService; + class FS; + class Timer; + class MusicService; + class NavigationService; + } + + namespace System { + class SystemTask; + } + + namespace Applications { + struct AppControllers { + const Pinetime::Controllers::Battery& batteryController; + const Pinetime::Controllers::Ble& bleController; + Pinetime::Controllers::DateTime& dateTimeController; + Pinetime::Controllers::NotificationManager& notificationManager; + Pinetime::Controllers::HeartRateController& heartRateController; + Pinetime::Controllers::Settings& settingsController; + Pinetime::Controllers::MotorController& motorController; + Pinetime::Controllers::MotionController& motionController; + Pinetime::Controllers::AlarmController& alarmController; + Pinetime::Controllers::BrightnessController& brightnessController; + Pinetime::Controllers::SimpleWeatherService* weatherController; + Pinetime::Controllers::FS& filesystem; + Pinetime::Controllers::Timer& timer; + Pinetime::System::SystemTask* systemTask; + Pinetime::Applications::DisplayApp* displayApp; + Pinetime::Components::LittleVgl& lvgl; + Pinetime::Controllers::MusicService* musicService; + Pinetime::Controllers::NavigationService* navigationService; + }; + } +} diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index ccba7ee6..add00650 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -11,7 +11,6 @@ #include "components/motion/MotionController.h" #include "components/motor/MotorController.h" #include "displayapp/screens/ApplicationList.h" -#include "displayapp/screens/Clock.h" #include "displayapp/screens/FirmwareUpdate.h" #include "displayapp/screens/FirmwareValidation.h" #include "displayapp/screens/InfiniPaint.h" @@ -27,8 +26,11 @@ #include "displayapp/screens/FlashLight.h" #include "displayapp/screens/BatteryInfo.h" #include "displayapp/screens/Steps.h" +#include "displayapp/screens/Dice.h" +#include "displayapp/screens/Weather.h" #include "displayapp/screens/PassKey.h" #include "displayapp/screens/Error.h" +#include "displayapp/screens/Calculator.h" #include "drivers/Cst816s.h" #include "drivers/St7789.h" @@ -40,6 +42,7 @@ #include "displayapp/screens/settings/Settings.h" #include "displayapp/screens/settings/SettingWatchFace.h" #include "displayapp/screens/settings/SettingTimeFormat.h" +#include "displayapp/screens/settings/SettingWeatherFormat.h" #include "displayapp/screens/settings/SettingWakeUp.h" #include "displayapp/screens/settings/SettingDisplay.h" #include "displayapp/screens/settings/SettingSteps.h" @@ -49,6 +52,7 @@ #include "displayapp/screens/settings/SettingBluetooth.h" #include "libs/lv_conf.h" +#include "UserApps.h" using namespace Pinetime::Applications; using namespace Pinetime::Applications::Display; @@ -57,6 +61,11 @@ namespace { inline bool in_isr() { return (SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk) != 0; } + + void TimerCallback(TimerHandle_t xTimer) { + auto* dispApp = static_cast<DisplayApp*>(pvTimerGetTimerID(xTimer)); + dispApp->PushMessage(Display::Messages::TimerDone); + } } DisplayApp::DisplayApp(Drivers::St7789& lcd, @@ -70,11 +79,11 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, Controllers::Settings& settingsController, Pinetime::Controllers::MotorController& motorController, Pinetime::Controllers::MotionController& motionController, - Pinetime::Controllers::TimerController& timerController, Pinetime::Controllers::AlarmController& alarmController, Pinetime::Controllers::BrightnessController& brightnessController, Pinetime::Controllers::TouchHandler& touchHandler, - Pinetime::Controllers::FS& filesystem) + Pinetime::Controllers::FS& filesystem, + Pinetime::Drivers::SpiNorFlash& spiNorFlash) : lcd {lcd}, touchPanel {touchPanel}, batteryController {batteryController}, @@ -86,12 +95,31 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, settingsController {settingsController}, motorController {motorController}, motionController {motionController}, - timerController {timerController}, alarmController {alarmController}, brightnessController {brightnessController}, touchHandler {touchHandler}, filesystem {filesystem}, - lvgl {lcd, filesystem} { + spiNorFlash {spiNorFlash}, + lvgl {lcd, filesystem}, + timer(this, TimerCallback), + controllers {batteryController, + bleController, + dateTimeController, + notificationManager, + heartRateController, + settingsController, + motorController, + motionController, + alarmController, + brightnessController, + nullptr, + filesystem, + timer, + nullptr, + this, + lvgl, + nullptr, + nullptr} { } void DisplayApp::Start(System::BootErrors error) { @@ -100,6 +128,7 @@ void DisplayApp::Start(System::BootErrors error) { bootError = error; lvgl.Init(); + motorController.Init(); if (error == System::BootErrors::TouchController) { LoadNewScreen(Apps::Error, DisplayApp::FullRefreshDirections::None); @@ -117,9 +146,6 @@ void DisplayApp::Process(void* instance) { NRF_LOG_INFO("displayapp task started!"); app->InitHw(); - // Send a dummy notification to unlock the lvgl display driver for the first iteration - xTaskNotifyGive(xTaskGetCurrentTaskHandle()); - while (true) { app->Refresh(); } @@ -128,10 +154,47 @@ void DisplayApp::Process(void* instance) { void DisplayApp::InitHw() { brightnessController.Init(); ApplyBrightness(); - motorController.Init(); lcd.Init(); } +TickType_t DisplayApp::CalculateSleepTime() { + // Calculates how many system ticks DisplayApp should sleep before rendering the next AOD frame + // Next frame time is frame count * refresh period (ms) * tick rate + + auto RoundedDiv = [](uint32_t a, uint32_t b) { + return ((a + (b / 2)) / b); + }; + // RoundedDiv overflows when numerator + (denominator floordiv 2) > uint32 max + // in this case around 9 hours (=overflow frame count / always on refresh period) + constexpr TickType_t overflowFrameCount = (UINT32_MAX - (1000 / 16)) / ((configTICK_RATE_HZ / 8) * alwaysOnRefreshPeriod); + + TickType_t ticksElapsed = xTaskGetTickCount() - alwaysOnStartTime; + // Divide both the numerator and denominator by 8 (=GCD(1000,1024)) + // to increase the number of ticks (frames) before the overflow tick is reached + TickType_t targetRenderTick = RoundedDiv((configTICK_RATE_HZ / 8) * alwaysOnFrameCount * alwaysOnRefreshPeriod, 1000 / 8); + + // Assumptions + + // Tick rate is multiple of 8 + // Needed for division trick above + static_assert(configTICK_RATE_HZ % 8 == 0); + + // Frame count must always wraparound more often than the system tick count does + // Always on overflow time (ms) < system tick overflow time (ms) + // Using 64bit ints here to avoid overflow + static_assert((uint64_t) overflowFrameCount * (uint64_t) alwaysOnRefreshPeriod < (uint64_t) UINT32_MAX * 1000ULL / configTICK_RATE_HZ); + + if (alwaysOnFrameCount == overflowFrameCount) { + alwaysOnFrameCount = 0; + alwaysOnStartTime = xTaskGetTickCount(); + } + if (targetRenderTick > ticksElapsed) { + return targetRenderTick - ticksElapsed; + } else { + return 0; + } +} + void DisplayApp::Refresh() { auto LoadPreviousScreen = [this]() { FullRefreshDirections returnDirection; @@ -155,16 +218,70 @@ void DisplayApp::Refresh() { LoadScreen(returnAppStack.Pop(), returnDirection); }; + auto IsPastDimTime = [this]() -> bool { + return lv_disp_get_inactive_time(nullptr) >= pdMS_TO_TICKS(settingsController.GetScreenTimeOut() - 2000); + }; + + auto IsPastSleepTime = [this]() -> bool { + return lv_disp_get_inactive_time(nullptr) >= pdMS_TO_TICKS(settingsController.GetScreenTimeOut()); + }; + TickType_t queueTimeout; switch (state) { case States::Idle: queueTimeout = portMAX_DELAY; break; + case States::AOD: + if (!currentScreen->IsRunning()) { + LoadPreviousScreen(); + } + // Check we've slept long enough + // Might not be true if the loop received an event + // If not true, then wait that amount of time + queueTimeout = CalculateSleepTime(); + if (queueTimeout == 0) { + // Only advance the tick count when LVGL is done + // Otherwise keep running the task handler while it still has things to draw + // Note: under high graphics load, LVGL will always have more work to do + if (lv_task_handler() > 0) { + // Drop frames that we've missed if drawing/event handling took way longer than expected + while (queueTimeout == 0) { + alwaysOnFrameCount += 1; + queueTimeout = CalculateSleepTime(); + } + } + } + break; case States::Running: if (!currentScreen->IsRunning()) { LoadPreviousScreen(); } queueTimeout = lv_task_handler(); + + if (!systemTask->IsSleepDisabled() && IsPastDimTime()) { + if (!isDimmed) { + isDimmed = true; + brightnessController.Set(Controllers::BrightnessController::Levels::Low); + } + if (IsPastSleepTime() && uxQueueMessagesWaiting(msgQueue) == 0) { + PushMessageToSystemTask(System::Messages::GoToSleep); + // Can't set state to Idle here, something may send + // DisableSleeping before this GoToSleep arrives + // Instead we check we have no messages queued before sending GoToSleep + // This works as the SystemTask is higher priority than DisplayApp + // As soon as we send GoToSleep, SystemTask pre-empts DisplayApp + // Whenever DisplayApp is running again, it is guaranteed that + // SystemTask has handled the message + // If it responded, we will have a GoToSleep waiting in the queue + // By checking that there are no messages in the queue, we avoid + // resending GoToSleep when we already have a response + // SystemTask is resilient to duplicate messages, this is an + // optimisation to reduce pressure on the message queues + } + } else if (isDimmed) { + isDimmed = false; + ApplyBrightness(); + } break; default: queueTimeout = portMAX_DELAY; @@ -174,38 +291,83 @@ void DisplayApp::Refresh() { Messages msg; if (xQueueReceive(msgQueue, &msg, queueTimeout) == pdTRUE) { switch (msg) { - case Messages::DimScreen: - brightnessController.Set(Controllers::BrightnessController::Levels::Low); - break; - case Messages::RestoreBrightness: - ApplyBrightness(); - break; case Messages::GoToSleep: - while (brightnessController.Level() != Controllers::BrightnessController::Levels::Off) { + case Messages::GoToAOD: + // Checking if SystemTask is sleeping is purely an optimisation. + // If it's no longer sleeping since it sent GoToSleep, it has + // cancelled the sleep and transitioned directly from + // GoingToSleep->Running, so we are about to receive GoToRunning + // and can ignore this message. If it wasn't ignored, DisplayApp + // would go to sleep and then immediately re-wake + if (state != States::Running || !systemTask->IsSleeping()) { + break; + } + while (brightnessController.Level() != Controllers::BrightnessController::Levels::Low) { brightnessController.Lower(); vTaskDelay(100); } - lcd.Sleep(); - PushMessageToSystemTask(Pinetime::System::Messages::OnDisplayTaskSleeping); - state = States::Idle; + // Turn brightness down (or set to AlwaysOn mode) + if (msg == Messages::GoToAOD) { + brightnessController.Set(Controllers::BrightnessController::Levels::AlwaysOn); + } else { + brightnessController.Set(Controllers::BrightnessController::Levels::Off); + } + // Since the active screen is not really an app, go back to Clock. + if (currentApp == Apps::Launcher || currentApp == Apps::Notifications || currentApp == Apps::QuickSettings || + currentApp == Apps::Settings) { + LoadScreen(Apps::Clock, DisplayApp::FullRefreshDirections::None); + // Wait for the clock app to load before moving on. + while (!lv_task_handler()) { + }; + } + // Clear any ongoing touch pressed events + // Without this LVGL gets stuck in the pressed state and will keep refreshing the + // display activity timer causing the screen to never sleep after timeout + lvgl.ClearTouchState(); + if (msg == Messages::GoToAOD) { + lcd.LowPowerOn(); + // Record idle entry time + alwaysOnFrameCount = 0; + alwaysOnStartTime = xTaskGetTickCount(); + PushMessageToSystemTask(Pinetime::System::Messages::OnDisplayTaskAOD); + state = States::AOD; + } else { + lcd.Sleep(); + PushMessageToSystemTask(Pinetime::System::Messages::OnDisplayTaskSleeping); + state = States::Idle; + } + break; + case Messages::NotifyDeviceActivity: + lv_disp_trig_activity(nullptr); break; case Messages::GoToRunning: - lcd.Wakeup(); + // If SystemTask is sleeping, the GoToRunning message is old + // and must be ignored. Otherwise DisplayApp will use SPI + // that is powered down and cause bad behaviour + if (state == States::Running || systemTask->IsSleeping()) { + break; + } + if (state == States::AOD) { + lcd.LowPowerOff(); + } else { + lcd.Wakeup(); + } + lv_disp_trig_activity(nullptr); ApplyBrightness(); state = States::Running; break; - case Messages::UpdateTimeOut: - PushMessageToSystemTask(System::Messages::UpdateTimeOut); - break; case Messages::UpdateBleConnection: - // clockScreen.SetBleConnectionState(bleController.IsConnected() ? Screens::Clock::BleConnectionStates::Connected : - // Screens::Clock::BleConnectionStates::NotConnected); + // Only used for recovery firmware break; case Messages::NewNotification: LoadNewScreen(Apps::NotificationsPreview, DisplayApp::FullRefreshDirections::Down); break; case Messages::TimerDone: + if (state != States::Running) { + PushMessageToSystemTask(System::Messages::GoToRunning); + } if (currentApp == Apps::Timer) { + lv_disp_trig_activity(nullptr); auto* timer = static_cast<Screens::Timer*>(currentScreen.get()); timer->Reset(); } else { @@ -310,21 +472,14 @@ void DisplayApp::Refresh() { case Messages::BleRadioEnableToggle: PushMessageToSystemTask(System::Messages::BleRadioEnableToggle); break; - case Messages::UpdateDateTime: - // Added to remove warning - // What should happen here? - break; case Messages::Chime: LoadNewScreen(Apps::Clock, DisplayApp::FullRefreshDirections::None); motorController.RunForDuration(35); break; - case Messages::OnChargingEvent: - motorController.RunForDuration(15); - break; } } - if (touchHandler.IsTouching()) { + if (state == States::Running && touchHandler.IsTouching()) { currentScreen->OnTouchEvent(touchHandler.GetX(), touchHandler.GetY()); } @@ -352,32 +507,40 @@ void DisplayApp::LoadNewScreen(Apps app, DisplayApp::FullRefreshDirections direc void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections direction) { lvgl.CancelTap(); - ApplyBrightness(); + lv_disp_trig_activity(nullptr); motorController.StopRinging(); currentScreen.reset(nullptr); SetFullRefresh(direction); switch (app) { - case Apps::Launcher: - currentScreen = - std::make_unique<Screens::ApplicationList>(this, settingsController, batteryController, bleController, dateTimeController); - break; - case Apps::Motion: - // currentScreen = std::make_unique<Screens::Motion>(motionController); - // break; - case Apps::None: - case Apps::Clock: - currentScreen = std::make_unique<Screens::Clock>(dateTimeController, - batteryController, - bleController, - notificationManager, - settingsController, - heartRateController, - motionController, - filesystem); - break; - + case Apps::Launcher: { + std::array<Screens::Tile::Applications, UserAppTypes::Count> apps; + int i = 0; + for (const auto& userApp : userApps) { + apps[i++] = Screens::Tile::Applications {userApp.icon, userApp.app, true}; + } + currentScreen = std::make_unique<Screens::ApplicationList>(this, + settingsController, + batteryController, + bleController, + alarmController, + dateTimeController, + filesystem, + std::move(apps)); + } break; + case Apps::Clock: { + const auto* watchFace = + std::find_if(userWatchFaces.begin(), userWatchFaces.end(), [this](const WatchFaceDescription& watchfaceDescription) { + return watchfaceDescription.watchFace == settingsController.GetWatchFace(); + }); + if (watchFace != userWatchFaces.end()) + currentScreen.reset(watchFace->create(controllers)); + else { + currentScreen.reset(userWatchFaces[0].create(controllers)); + } + settingsController.SetAppMenu(0); + } break; case Apps::Error: currentScreen = std::make_unique<Screens::Error>(bootError); break; @@ -409,14 +572,6 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio *systemTask, Screens::Notifications::Modes::Preview); break; - case Apps::Timer: - currentScreen = std::make_unique<Screens::Timer>(timerController); - break; - case Apps::Alarm: - currentScreen = std::make_unique<Screens::Alarm>(alarmController, settingsController.GetClockType(), *systemTask, motorController); - break; - - // Settings case Apps::QuickSettings: currentScreen = std::make_unique<Screens::QuickSettings>(this, batteryController, @@ -424,22 +579,32 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio brightnessController, motorController, settingsController, - bleController); + bleController, + alarmController); break; case Apps::Settings: currentScreen = std::make_unique<Screens::Settings>(this, settingsController); break; - case Apps::SettingWatchFace: - currentScreen = std::make_unique<Screens::SettingWatchFace>(this, settingsController, filesystem); - break; + case Apps::SettingWatchFace: { + std::array<Screens::SettingWatchFace::Item, UserWatchFaceTypes::Count> items; + int i = 0; + for (const auto& userWatchFace : userWatchFaces) { + items[i++] = + Screens::SettingWatchFace::Item {userWatchFace.name, userWatchFace.watchFace, userWatchFace.isAvailable(controllers.filesystem)}; + } + currentScreen = std::make_unique<Screens::SettingWatchFace>(this, std::move(items), settingsController, filesystem); + } break; case Apps::SettingTimeFormat: currentScreen = std::make_unique<Screens::SettingTimeFormat>(settingsController); break; + case Apps::SettingWeatherFormat: + currentScreen = std::make_unique<Screens::SettingWeatherFormat>(settingsController); + break; case Apps::SettingWakeUp: currentScreen = std::make_unique<Screens::SettingWakeUp>(settingsController); break; case Apps::SettingDisplay: - currentScreen = std::make_unique<Screens::SettingDisplay>(this, settingsController); + currentScreen = std::make_unique<Screens::SettingDisplay>(settingsController); break; case Apps::SettingSteps: currentScreen = std::make_unique<Screens::SettingSteps>(settingsController); @@ -467,38 +632,23 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio bleController, watchdog, motionController, - touchPanel); + touchPanel, + spiNorFlash); break; case Apps::FlashLight: currentScreen = std::make_unique<Screens::FlashLight>(*systemTask, brightnessController); break; - case Apps::StopWatch: - currentScreen = std::make_unique<Screens::StopWatch>(*systemTask); - break; - case Apps::Twos: - currentScreen = std::make_unique<Screens::Twos>(); - break; - case Apps::Paint: - currentScreen = std::make_unique<Screens::InfiniPaint>(lvgl, motorController); - break; - case Apps::Paddle: - currentScreen = std::make_unique<Screens::Paddle>(lvgl); - break; - case Apps::Music: - currentScreen = std::make_unique<Screens::Music>(systemTask->nimble().music()); - break; - case Apps::Navigation: - currentScreen = std::make_unique<Screens::Navigation>(systemTask->nimble().navigation()); - break; - case Apps::HeartRate: - currentScreen = std::make_unique<Screens::HeartRate>(heartRateController, *systemTask); - break; - case Apps::Metronome: - currentScreen = std::make_unique<Screens::Metronome>(motorController, *systemTask); - break; - case Apps::Steps: - currentScreen = std::make_unique<Screens::Steps>(motionController, settingsController); + default: { + const auto* d = std::find_if(userApps.begin(), userApps.end(), [app](const AppDescription& appDescription) { + return appDescription.app == app; + }); + if (d != userApps.end()) { + currentScreen.reset(d->create(controllers)); + } else { + currentScreen.reset(userWatchFaces[0].create(controllers)); + } break; + } } currentApp = app; } @@ -507,11 +657,17 @@ void DisplayApp::PushMessage(Messages msg) { if (in_isr()) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xQueueSendFromISR(msgQueue, &msg, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken == pdTRUE) { - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } else { - xQueueSend(msgQueue, &msg, portMAX_DELAY); + TickType_t timeout = portMAX_DELAY; + // Make xQueueSend() non-blocking if the message is a Notification message. We do this to avoid + // deadlock between SystemTask and DisplayApp when their respective message queues are getting full + // when a lot of notifications are received on a very short time span. + if (msg == Messages::NewNotification) { + timeout = static_cast<TickType_t>(0); + } + + xQueueSend(msgQueue, &msg, timeout); } } @@ -548,6 +704,19 @@ void DisplayApp::PushMessageToSystemTask(Pinetime::System::Messages message) { void DisplayApp::Register(Pinetime::System::SystemTask* systemTask) { this->systemTask = systemTask; + this->controllers.systemTask = systemTask; +} + +void DisplayApp::Register(Pinetime::Controllers::SimpleWeatherService* weatherService) { + this->controllers.weatherController = weatherService; +} + +void DisplayApp::Register(Pinetime::Controllers::MusicService* musicService) { + this->controllers.musicService = musicService; +} + +void DisplayApp::Register(Pinetime::Controllers::NavigationService* NavigationService) { + this->controllers.navigationService = NavigationService; } void DisplayApp::ApplyBrightness() { diff --git a/src/displayapp/DisplayApp.h b/src/displayapp/DisplayApp.h index 09111865..2f276eaf 100644 --- a/src/displayapp/DisplayApp.h +++ b/src/displayapp/DisplayApp.h @@ -4,7 +4,7 @@ #include <task.h> #include <memory> #include <systemtask/Messages.h> -#include "displayapp/Apps.h" +#include "displayapp/apps/Apps.h" #include "displayapp/LittleVgl.h" #include "displayapp/TouchEvents.h" #include "components/brightness/BrightnessController.h" @@ -12,14 +12,15 @@ #include "components/firmwarevalidator/FirmwareValidator.h" #include "components/settings/Settings.h" #include "displayapp/screens/Screen.h" -#include "components/timer/TimerController.h" +#include "components/timer/Timer.h" #include "components/alarm/AlarmController.h" #include "touchhandler/TouchHandler.h" #include "displayapp/Messages.h" #include "BootErrors.h" -#include "StaticStack.h" +#include "utility/StaticStack.h" +#include "displayapp/Controllers.h" namespace Pinetime { @@ -38,6 +39,7 @@ namespace Pinetime { class HeartRateController; class MotionController; class TouchHandler; + class SimpleWeatherService; } namespace System { @@ -47,7 +49,7 @@ namespace Pinetime { namespace Applications { class DisplayApp { public: - enum class States { Idle, Running }; + enum class States { Idle, Running, AOD }; enum class FullRefreshDirections { None, Up, Down, Left, Right, LeftAnim, RightAnim }; DisplayApp(Drivers::St7789& lcd, @@ -61,11 +63,11 @@ namespace Pinetime { Controllers::Settings& settingsController, Pinetime::Controllers::MotorController& motorController, Pinetime::Controllers::MotionController& motionController, - Pinetime::Controllers::TimerController& timerController, Pinetime::Controllers::AlarmController& alarmController, Pinetime::Controllers::BrightnessController& brightnessController, Pinetime::Controllers::TouchHandler& touchHandler, - Pinetime::Controllers::FS& filesystem); + Pinetime::Controllers::FS& filesystem, + Pinetime::Drivers::SpiNorFlash& spiNorFlash); void Start(System::BootErrors error); void PushMessage(Display::Messages msg); @@ -74,6 +76,9 @@ namespace Pinetime { void SetFullRefresh(FullRefreshDirections direction); void Register(Pinetime::System::SystemTask* systemTask); + void Register(Pinetime::Controllers::SimpleWeatherService* weatherService); + void Register(Pinetime::Controllers::MusicService* musicService); + void Register(Pinetime::Controllers::NavigationService* NavigationService); private: Pinetime::Drivers::St7789& lcd; @@ -88,15 +93,17 @@ namespace Pinetime { Pinetime::Controllers::Settings& settingsController; Pinetime::Controllers::MotorController& motorController; Pinetime::Controllers::MotionController& motionController; - Pinetime::Controllers::TimerController& timerController; Pinetime::Controllers::AlarmController& alarmController; Pinetime::Controllers::BrightnessController& brightnessController; Pinetime::Controllers::TouchHandler& touchHandler; Pinetime::Controllers::FS& filesystem; + Pinetime::Drivers::SpiNorFlash& spiNorFlash; Pinetime::Controllers::FirmwareValidator validator; Pinetime::Components::LittleVgl lvgl; + Pinetime::Controllers::Timer timer; + AppControllers controllers; TaskHandle_t taskHandle; States state = States::Running; @@ -126,8 +133,17 @@ namespace Pinetime { void ApplyBrightness(); static constexpr size_t returnAppStackSize = 10; - StaticStack<Apps, returnAppStackSize> returnAppStack; - StaticStack<FullRefreshDirections, returnAppStackSize> appStackDirections; + Utility::StaticStack<Apps, returnAppStackSize> returnAppStack; + Utility::StaticStack<FullRefreshDirections, returnAppStackSize> appStackDirections; + + bool isDimmed = false; + + TickType_t CalculateSleepTime(); + TickType_t alwaysOnFrameCount; + TickType_t alwaysOnStartTime; + // If this is to be changed, make sure the actual always on refresh rate is changed + // by configuring the LCD refresh timings + static constexpr uint32_t alwaysOnRefreshPeriod = 500; }; } } diff --git a/src/displayapp/DisplayAppRecovery.cpp b/src/displayapp/DisplayAppRecovery.cpp index 94e83791..bcb8db0e 100644 --- a/src/displayapp/DisplayAppRecovery.cpp +++ b/src/displayapp/DisplayAppRecovery.cpp @@ -21,11 +21,11 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, Controllers::Settings& /*settingsController*/, Pinetime::Controllers::MotorController& /*motorController*/, Pinetime::Controllers::MotionController& /*motionController*/, - Pinetime::Controllers::TimerController& /*timerController*/, Pinetime::Controllers::AlarmController& /*alarmController*/, Pinetime::Controllers::BrightnessController& /*brightnessController*/, Pinetime::Controllers::TouchHandler& /*touchHandler*/, - Pinetime::Controllers::FS& /*filesystem*/) + Pinetime::Controllers::FS& /*filesystem*/, + Pinetime::Drivers::SpiNorFlash& /*spiNorFlash*/) : lcd {lcd}, bleController {bleController} { } @@ -39,9 +39,6 @@ void DisplayApp::Process(void* instance) { auto* app = static_cast<DisplayApp*>(instance); NRF_LOG_INFO("displayapp task started!"); - // Send a dummy notification to unlock the lvgl display driver for the first iteration - xTaskNotifyGive(xTaskGetCurrentTaskHandle()); - app->InitHw(); while (true) { app->Refresh(); @@ -95,7 +92,6 @@ void DisplayApp::DisplayLogo(uint16_t color) { Pinetime::Tools::RleDecoder rleDecoder(infinitime_nb, sizeof(infinitime_nb), color, colorBlack); for (int i = 0; i < displayWidth; i++) { rleDecoder.DecodeNext(displayBuffer, displayWidth * bytesPerPixel); - ulTaskNotifyTake(pdTRUE, 500); lcd.DrawBuffer(0, i, displayWidth, 1, reinterpret_cast<const uint8_t*>(displayBuffer), displayWidth * bytesPerPixel); } } @@ -104,21 +100,25 @@ void DisplayApp::DisplayOtaProgress(uint8_t percent, uint16_t color) { const uint8_t barHeight = 20; std::fill(displayBuffer, displayBuffer + (displayWidth * bytesPerPixel), color); for (int i = 0; i < barHeight; i++) { - ulTaskNotifyTake(pdTRUE, 500); uint16_t barWidth = std::min(static_cast<float>(percent) * 2.4f, static_cast<float>(displayWidth)); lcd.DrawBuffer(0, displayWidth - barHeight + i, barWidth, 1, reinterpret_cast<const uint8_t*>(displayBuffer), barWidth * bytesPerPixel); } } void DisplayApp::PushMessage(Display::Messages msg) { - BaseType_t xHigherPriorityTaskWoken; - xHigherPriorityTaskWoken = pdFALSE; + BaseType_t xHigherPriorityTaskWoken = pdFALSE; xQueueSendFromISR(msgQueue, &msg, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken) { - /* Actual macro used here is port specific. */ - // TODO : should I do something here? - } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void DisplayApp::Register(Pinetime::System::SystemTask* /*systemTask*/) { } + +void DisplayApp::Register(Pinetime::Controllers::SimpleWeatherService* /*weatherService*/) { +} + +void DisplayApp::Register(Pinetime::Controllers::MusicService* /*musicService*/) { +} + +void DisplayApp::Register(Pinetime::Controllers::NavigationService* /*NavigationService*/) { +} diff --git a/src/displayapp/DisplayAppRecovery.h b/src/displayapp/DisplayAppRecovery.h index 7be0c6d8..162ff257 100644 --- a/src/displayapp/DisplayAppRecovery.h +++ b/src/displayapp/DisplayAppRecovery.h @@ -5,13 +5,12 @@ #include <drivers/SpiMaster.h> #include <bits/unique_ptr.h> #include <queue.h> -#include "components/gfx/Gfx.h" #include "drivers/Cst816s.h" #include <drivers/Watchdog.h> #include <components/motor/MotorController.h> #include "BootErrors.h" #include "displayapp/TouchEvents.h" -#include "displayapp/Apps.h" +#include "displayapp/apps/Apps.h" #include "displayapp/Messages.h" namespace Pinetime { @@ -19,6 +18,7 @@ namespace Pinetime { class St7789; class Cst816S; class Watchdog; + class SpiNorFlash; } namespace Controllers { @@ -31,10 +31,12 @@ namespace Pinetime { class MotionController; class TouchHandler; class MotorController; - class TimerController; class AlarmController; class BrightnessController; class FS; + class SimpleWeatherService; + class MusicService; + class NavigationService; } namespace System { @@ -55,11 +57,11 @@ namespace Pinetime { Controllers::Settings& settingsController, Pinetime::Controllers::MotorController& motorController, Pinetime::Controllers::MotionController& motionController, - Pinetime::Controllers::TimerController& timerController, Pinetime::Controllers::AlarmController& alarmController, Pinetime::Controllers::BrightnessController& brightnessController, Pinetime::Controllers::TouchHandler& touchHandler, - Pinetime::Controllers::FS& filesystem); + Pinetime::Controllers::FS& filesystem, + Pinetime::Drivers::SpiNorFlash& spiNorFlash); void Start(); void Start(Pinetime::System::BootErrors) { @@ -68,6 +70,9 @@ namespace Pinetime { void PushMessage(Pinetime::Applications::Display::Messages msg); void Register(Pinetime::System::SystemTask* systemTask); + void Register(Pinetime::Controllers::SimpleWeatherService* weatherService); + void Register(Pinetime::Controllers::MusicService* musicService); + void Register(Pinetime::Controllers::NavigationService* NavigationService); private: TaskHandle_t taskHandle; diff --git a/src/displayapp/LittleVgl.cpp b/src/displayapp/LittleVgl.cpp index 89893cf7..c6f6f784 100644 --- a/src/displayapp/LittleVgl.cpp +++ b/src/displayapp/LittleVgl.cpp @@ -152,10 +152,6 @@ void LittleVgl::SetFullRefresh(FullRefreshDirections direction) { void LittleVgl::FlushDisplay(const lv_area_t* area, lv_color_t* color_p) { uint16_t y1, y2, width, height = 0; - ulTaskNotifyTake(pdTRUE, 200); - // Notification is still needed (even if there is a mutex on SPI) because of the DataCommand pin - // which cannot be set/clear during a transfer. - if ((scrollDirection == LittleVgl::FullRefreshDirections::Down) && (area->y2 == visibleNbLines - 1)) { writeOffset = ((writeOffset + totalNbLines) - visibleNbLines) % totalNbLines; } else if ((scrollDirection == FullRefreshDirections::Up) && (area->y1 == 0)) { @@ -219,7 +215,6 @@ void LittleVgl::FlushDisplay(const lv_area_t* area, lv_color_t* color_p) { if (height > 0) { lcd.DrawBuffer(area->x1, y1, width, height, reinterpret_cast<const uint8_t*>(color_p), width * height * 2); - ulTaskNotifyTake(pdTRUE, 100); } uint16_t pixOffset = width * height; @@ -253,6 +248,8 @@ void LittleVgl::SetNewTouchPoint(int16_t x, int16_t y, bool contact) { } } +// Cancel an ongoing tap +// Signifies that LVGL should not handle the current tap void LittleVgl::CancelTap() { if (tapped) { isCancelled = true; @@ -260,6 +257,13 @@ void LittleVgl::CancelTap() { } } +// Clear the current tapped state +// Signifies that touch input processing is suspended +void LittleVgl::ClearTouchState() { + touchPoint = {-1, -1}; + tapped = false; +} + bool LittleVgl::GetTouchPadInfo(lv_indev_data_t* ptr) { ptr->point.x = touchPoint.x; ptr->point.y = touchPoint.y; diff --git a/src/displayapp/LittleVgl.h b/src/displayapp/LittleVgl.h index 9a15ae15..54505b36 100644 --- a/src/displayapp/LittleVgl.h +++ b/src/displayapp/LittleVgl.h @@ -26,6 +26,7 @@ namespace Pinetime { void SetFullRefresh(FullRefreshDirections direction); void SetNewTouchPoint(int16_t x, int16_t y, bool contact); void CancelTap(); + void ClearTouchState(); bool GetFullRefresh() { bool returnValue = fullRefresh; diff --git a/src/displayapp/Messages.h b/src/displayapp/Messages.h index b670b1aa..1fcd72d2 100644 --- a/src/displayapp/Messages.h +++ b/src/displayapp/Messages.h @@ -6,8 +6,8 @@ namespace Pinetime { namespace Display { enum class Messages : uint8_t { GoToSleep, + GoToAOD, GoToRunning, - UpdateDateTime, UpdateBleConnection, TouchEvent, ButtonPushed, @@ -17,14 +17,13 @@ namespace Pinetime { NewNotification, TimerDone, BleFirmwareUpdateStarted, - UpdateTimeOut, - DimScreen, - RestoreBrightness, + // Resets the screen timeout timer when awake + // Does nothing when asleep + NotifyDeviceActivity, ShowPairingKey, AlarmTriggered, Chime, BleRadioEnableToggle, - OnChargingEvent, }; } } diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h new file mode 100644 index 00000000..67bbfa7d --- /dev/null +++ b/src/displayapp/UserApps.h @@ -0,0 +1,60 @@ +#pragma once +#include "displayapp/apps/Apps.h" +#include "Controllers.h" + +#include "displayapp/screens/Alarm.h" +#include "displayapp/screens/Dice.h" +#include "displayapp/screens/Timer.h" +#include "displayapp/screens/Twos.h" +#include "displayapp/screens/Tile.h" +#include "displayapp/screens/ApplicationList.h" +#include "displayapp/screens/WatchFaceDigital.h" +#include "displayapp/screens/WatchFaceAnalog.h" +#include "displayapp/screens/WatchFaceCasioStyleG7710.h" +#include "displayapp/screens/WatchFaceInfineat.h" +#include "displayapp/screens/WatchFacePineTimeStyle.h" +#include "displayapp/screens/WatchFaceTerminal.h" + +namespace Pinetime { + namespace Applications { + namespace Screens { + class Screen; + } + + struct AppDescription { + Apps app; + const char* icon; + Screens::Screen* (*create)(AppControllers& controllers); + }; + + struct WatchFaceDescription { + WatchFace watchFace; + const char* name; + Screens::Screen* (*create)(AppControllers& controllers); + bool (*isAvailable)(Controllers::FS& fileSystem); + }; + + template <Apps t> + consteval AppDescription CreateAppDescription() { + return {AppTraits<t>::app, AppTraits<t>::icon, &AppTraits<t>::Create}; + } + + template <WatchFace t> + consteval WatchFaceDescription CreateWatchFaceDescription() { + return {WatchFaceTraits<t>::watchFace, WatchFaceTraits<t>::name, &WatchFaceTraits<t>::Create, &WatchFaceTraits<t>::IsAvailable}; + } + + template <template <Apps...> typename T, Apps... ts> + consteval std::array<AppDescription, sizeof...(ts)> CreateAppDescriptions(T<ts...>) { + return {CreateAppDescription<ts>()...}; + } + + template <template <WatchFace...> typename T, WatchFace... ts> + consteval std::array<WatchFaceDescription, sizeof...(ts)> CreateWatchFaceDescriptions(T<ts...>) { + return {CreateWatchFaceDescription<ts>()...}; + } + + constexpr auto userApps = CreateAppDescriptions(UserAppTypes {}); + constexpr auto userWatchFaces = CreateWatchFaceDescriptions(UserWatchFaceTypes {}); + } +} diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in new file mode 100644 index 00000000..e697096a --- /dev/null +++ b/src/displayapp/apps/Apps.h.in @@ -0,0 +1,80 @@ +#pragma once +#include <cstddef> +#include <cstdint> + +namespace Pinetime { + namespace Applications { + enum class Apps : uint8_t { + None, + Launcher, + Clock, + SysInfo, + FirmwareUpdate, + FirmwareValidation, + NotificationsPreview, + Notifications, + Timer, + Alarm, + FlashLight, + BatteryInfo, + Music, + Paint, + Paddle, + Twos, + HeartRate, + Navigation, + StopWatch, + Metronome, + Motion, + Calculator, + Steps, + Dice, + Weather, + PassKey, + QuickSettings, + Settings, + SettingWatchFace, + SettingTimeFormat, + SettingWeatherFormat, + SettingDisplay, + SettingWakeUp, + SettingSteps, + SettingSetDateTime, + SettingChimes, + SettingShakeThreshold, + SettingBluetooth, + Error + }; + + enum class WatchFace : uint8_t { + Digital, + Analog, + PineTimeStyle, + Terminal, + Infineat, + CasioStyleG7710, + }; + + template <Apps> + struct AppTraits {}; + + template <WatchFace> + struct WatchFaceTraits {}; + + template <Apps... As> + struct TypeList { + static constexpr size_t Count = sizeof...(As); + }; + + using UserAppTypes = TypeList<@USERAPP_TYPES@>; + + template <WatchFace... Ws> + struct WatchFaceTypeList { + static constexpr size_t Count = sizeof...(Ws); + }; + + using UserWatchFaceTypes = WatchFaceTypeList<@WATCHFACE_TYPES@>; + + static_assert(UserWatchFaceTypes::Count >= 1); + } +} diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt new file mode 100644 index 00000000..33e54323 --- /dev/null +++ b/src/displayapp/apps/CMakeLists.txt @@ -0,0 +1,39 @@ +if(DEFINED ENABLE_USERAPPS) + set(USERAPP_TYPES ${ENABLE_USERAPPS} CACHE STRING "List of user apps to build into the firmware") +else () + set(DEFAULT_USER_APP_TYPES "Apps::StopWatch") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Alarm") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Timer") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Steps") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::HeartRate") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Music") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paint") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paddle") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Twos") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Dice") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Metronome") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Navigation") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Calculator") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Weather") + #set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Motion") + set(USERAPP_TYPES "${DEFAULT_USER_APP_TYPES}" CACHE STRING "List of user apps to build into the firmware") +endif () + +if(DEFINED ENABLE_WATCHFACES) + set(WATCHFACE_TYPES ${ENABLE_WATCHFACES} CACHE STRING "List of watch faces to build into the firmware") +else() + set(DEFAULT_WATCHFACE_TYPES "WatchFace::Digital") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Analog") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::PineTimeStyle") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Terminal") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Infineat") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710") + set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") +endif() + +add_library(infinitime_apps INTERFACE) +target_sources(infinitime_apps INTERFACE "${CMAKE_CURRENT_BINARY_DIR}/Apps.h") +target_include_directories(infinitime_apps INTERFACE "${CMAKE_CURRENT_BINARY_DIR}/") + +# Generate the list of user apps to be compiled into the firmware +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Apps.h.in ${CMAKE_CURRENT_BINARY_DIR}/Apps.h) diff --git a/src/displayapp/fonts/CMakeLists.txt b/src/displayapp/fonts/CMakeLists.txt index 84830cc0..562f0801 100644 --- a/src/displayapp/fonts/CMakeLists.txt +++ b/src/displayapp/fonts/CMakeLists.txt @@ -1,6 +1,6 @@ set(FONTS jetbrains_mono_42 jetbrains_mono_76 jetbrains_mono_bold_20 - jetbrains_mono_extrabold_compressed lv_font_navi_80 lv_font_sys_48 - open_sans_light) + jetbrains_mono_extrabold_compressed lv_font_sys_48 + open_sans_light fontawesome_weathericons) find_program(LV_FONT_CONV "lv_font_conv" NO_CACHE REQUIRED HINTS "${CMAKE_SOURCE_DIR}/node_modules/.bin") message(STATUS "Using ${LV_FONT_CONV} to generate font files") @@ -11,6 +11,7 @@ configure_file(${CMAKE_CURRENT_LIST_DIR}/jetbrains_mono_bold_20.c_M.patch if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12) # FindPython3 module introduces with CMake 3.12 # https://cmake.org/cmake/help/latest/module/FindPython3.html + set(Python3_FIND_STRATEGY LOCATION) # https://discourse.cmake.org/t/find-package-python3-is-not-finding-the-correct-python/10563 find_package(Python3 REQUIRED) else() set(Python3_EXECUTABLE "python") diff --git a/src/displayapp/fonts/README.md b/src/displayapp/fonts/README.md index b2669a78..20fb4a43 100644 --- a/src/displayapp/fonts/README.md +++ b/src/displayapp/fonts/README.md @@ -16,7 +16,7 @@ - Define the new symbols in `src/displayapp/screens/Symbols.h`: ``` -static constexpr const char* newSymbol = "\xEF\x86\x85"; +static constexpr const char* newSymbol = "\xEF\x99\x81"; ``` ### the config file format: @@ -33,3 +33,17 @@ and for each font there is: ### Navigation font `navigtion.ttf` is created with the web app [icomoon](https://icomoon.io/app) by importing the svg files from `src/displayapp/icons/navigation/unique` and generating the font. `lv_font_navi_80.json` is a project file for the site, which you can import to add or remove icons. + +To save space in the internal flash memory, the navigation icons are now moved into the external flash memory. To do this, the TTF font is converted into pictures (1 for each symbol). Those pictures are then concatenated into 2 big pictures (we need two files since LVGL supports maximum 2048px width/height). At runtime, a map is used to locate the desired icon in the corresponding file at a specific offset. + +Here is the command to convert the TTF font in PNG picture: + +```shell +convert -background none -fill white -font navigation.ttf -pointsize 80 -gravity center label:"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" navigation0.png + +convert -background none -fill white -font navigation.ttf -pointsize 80 -gravity center label:"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" navigation1.png +``` + +*Please note that the characters after `label:` are UTF-8 characters and might not be displayed correctly in this document.* + +The characters in the TTF font range from `0xEEA480` to `0xEEA4A9`. Characters from `0xEEA480` to `0xEEA498` are stored in `navigation0.png` and the others in `navigation1.png`. Each character is 80px height so displaying a specific character consists in multiplying its index in the file by -80 and use this value as the offset when calling `lv_img_set_offset_y()`. diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index 914ba163..fea31605 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -3,11 +3,11 @@ "sources": [ { "file": "JetBrainsMono-Bold.ttf", - "range": "0x20-0x7e, 0x410-0x44f" + "range": "0x20-0x7e, 0x410-0x44f, 0xB0" }, { "file": "FontAwesome5-Solid+Brands+Regular.woff", - "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c" + "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a" } ], "bpp": 1, @@ -18,7 +18,7 @@ "sources": [ { "file": "JetBrainsMono-Regular.ttf", - "range": "0x25, 0x2b, 0x2d, 0x30-0x3a" + "range": "0x25, 0x2b, 0x2d, 0x2e, 0x30-0x3a, 0x43, 0x46, 0x4b-0x4d, 0x66, 0x69, 0x6b, 0x6d, 0x74, 0xb0" } ], "bpp": 1, @@ -28,7 +28,7 @@ "sources": [ { "file": "JetBrainsMono-Light.ttf", - "range": "0x25, 0x2D, 0x2F, 0x30-0x3a" + "range": "0x25, 0x2D, 0x2F, 0x30-0x3a, 0x43, 0x46, 0xb0" } ], "bpp": 1, @@ -64,15 +64,14 @@ "bpp": 1, "size": 48 }, - "lv_font_navi_80": { + "fontawesome_weathericons": { "sources": [ { - "file": "navigation.ttf", - "range": "0xe900-0xe929" + "file": "FontAwesome5-Solid+Brands+Regular.woff", + "range": "0xf185, 0xf6c4, 0xf743, 0xf740, 0xf75f, 0xf0c2, 0xf05e, 0xf73b, 0xf0e7, 0xf2dc" } ], - "bpp": 2, - "size": 80, - "compress": true + "bpp": 1, + "size": 25 } } diff --git a/src/displayapp/fonts/generate.py b/src/displayapp/fonts/generate.py index 5f940bf5..88bdacd6 100755 --- a/src/displayapp/fonts/generate.py +++ b/src/displayapp/fonts/generate.py @@ -67,7 +67,7 @@ def main(): subprocess.check_call(line) if patches: for patch in patches: - subprocess.check_call(['/usr/bin/env', 'patch', name+'.c', patch]) + subprocess.check_call(['/usr/bin/env', 'patch', '--silent', name+'.c', patch]) diff --git a/src/displayapp/icons/bg_clock.c b/src/displayapp/icons/bg_clock.c deleted file mode 100644 index a9de4146..00000000 --- a/src/displayapp/icons/bg_clock.c +++ /dev/null @@ -1,272 +0,0 @@ -#if defined(LV_LVGL_H_INCLUDE_SIMPLE) -#include "lvgl.h" -#else -#include "lvgl/lvgl.h" -#endif - - -#ifndef LV_ATTRIBUTE_MEM_ALIGN -#define LV_ATTRIBUTE_MEM_ALIGN -#endif - -#ifndef LV_ATTRIBUTE_IMG_BG_CLOCK -#define LV_ATTRIBUTE_IMG_BG_CLOCK -#endif - -const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMG_BG_CLOCK uint8_t bg_clock_map[] = { - 0x00, 0x00, 0x00, 0xff, /*Color of index 0*/ - 0x68, 0x5b, 0x44, 0xff, /*Color of index 1*/ - 0xde, 0xa5, 0x33, 0xff, /*Color of index 2*/ - 0xff, 0xff, 0xff, 0xff, /*Color of index 3*/ - - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xa4, 0x02, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa8, 0x0a, 0xaa, 0xaa, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6a, 0xa8, 0x0a, 0x40, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0xa8, 0x04, 0x00, 0x02, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0xa8, 0x00, 0x00, 0x01, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x01, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x01, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x02, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x06, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x0a, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x2a, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x01, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x06, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x1a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x2a, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x02, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x0a, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x1a, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x1a, 0xaa, 0xaa, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x1a, 0xaa, 0xaa, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x05, 0x55, 0x55, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x07, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xd0, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0f, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xf0, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xf4, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xf4, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x05, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x50, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, - 0x00, 0x00, 0x15, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x54, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x15, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x54, 0x00, - 0x00, 0x01, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x40, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x55, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x55, 0x40, - 0x00, 0x15, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, - 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, - 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x55, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x55, 0x40, - 0x01, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x40, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x00, 0x15, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x54, 0x00, - 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x05, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x50, 0x00, 0x00, - 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, - 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x55, 0x52, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xaa, 0xa8, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0xaa, 0xa1, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x15, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x06, 0x45, 0x01, 0x45, 0x40, 0x00, 0x54, 0x00, 0x60, 0x01, 0x40, 0x45, 0x50, 0x15, 0x00, 0x05, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x54, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x02, 0x46, 0x02, 0xaa, 0xa4, 0x06, 0x9a, 0x40, 0x60, 0x01, 0x80, 0xaa, 0xa9, 0xaa, 0x80, 0x1a, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x02, 0x46, 0x02, 0x90, 0x28, 0x19, 0x01, 0x80, 0x60, 0x01, 0x80, 0xa0, 0x1a, 0x40, 0x90, 0x64, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x0a, 0x06, 0x02, 0x80, 0x18, 0x18, 0x00, 0x90, 0x60, 0x01, 0x80, 0x90, 0x0a, 0x00, 0xa0, 0xa0, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xaa, 0xa9, 0x06, 0x02, 0x40, 0x18, 0x2a, 0xaa, 0x90, 0x60, 0x01, 0x80, 0x90, 0x09, 0x00, 0x60, 0xaa, 0xaa, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x95, 0x50, 0x06, 0x02, 0x40, 0x18, 0x28, 0x00, 0x00, 0x60, 0x01, 0x80, 0x90, 0x09, 0x00, 0x60, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x06, 0x02, 0x40, 0x18, 0x19, 0x00, 0x00, 0x60, 0x01, 0x80, 0x90, 0x09, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x06, 0x02, 0x40, 0x18, 0x0a, 0x56, 0x80, 0x60, 0x01, 0x80, 0x90, 0x09, 0x00, 0x60, 0x29, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x06, 0x02, 0x40, 0x18, 0x01, 0xa9, 0x00, 0x60, 0x01, 0x80, 0x90, 0x09, 0x00, 0x60, 0x06, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xd0, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xf4, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0f, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xf0, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x0a, 0xa0, 0x00, 0x01, 0x00, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x0a, 0xa0, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x0a, 0xa0, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x40, 0x00, 0x0a, 0xa0, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x0a, 0xa0, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - -const lv_img_dsc_t bg_clock = { - .header.always_zero = 0, - .header.w = 240, - .header.h = 240, - .data_size = 14416, - .header.cf = LV_IMG_CF_INDEXED_2BIT, - .data = bg_clock_map, -}; - diff --git a/src/displayapp/screens/Alarm.cpp b/src/displayapp/screens/Alarm.cpp index 4e6ce797..4cf43921 100644 --- a/src/displayapp/screens/Alarm.cpp +++ b/src/displayapp/screens/Alarm.cpp @@ -19,6 +19,10 @@ #include "displayapp/screens/Screen.h" #include "displayapp/screens/Symbols.h" #include "displayapp/InfiniTimeTheme.h" +#include "components/settings/Settings.h" +#include "components/alarm/AlarmController.h" +#include "components/motor/MotorController.h" +#include "systemtask/SystemTask.h" using namespace Pinetime::Applications::Screens; using Pinetime::Controllers::AlarmController; @@ -44,7 +48,7 @@ Alarm::Alarm(Controllers::AlarmController& alarmController, Controllers::Settings::ClockType clockType, System::SystemTask& systemTask, Controllers::MotorController& motorController) - : alarmController {alarmController}, systemTask {systemTask}, motorController {motorController} { + : alarmController {alarmController}, wakeLock(systemTask), motorController {motorController} { hourCounter.Create(); lv_obj_align(hourCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0); @@ -73,7 +77,7 @@ Alarm::Alarm(Controllers::AlarmController& alarmController, btnStop = lv_btn_create(lv_scr_act(), nullptr); btnStop->user_data = this; lv_obj_set_event_cb(btnStop, btnEventHandler); - lv_obj_set_size(btnStop, 115, 50); + lv_obj_set_size(btnStop, 240, 70); lv_obj_align(btnStop, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); lv_obj_set_style_local_bg_color(btnStop, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED); txtStop = lv_label_create(btnStop, nullptr); @@ -113,7 +117,7 @@ Alarm::Alarm(Controllers::AlarmController& alarmController, UpdateAlarmTime(); - if (alarmController.State() == Controllers::AlarmController::AlarmState::Alerting) { + if (alarmController.IsAlerting()) { SetAlerting(); } else { SetSwitchState(LV_ANIM_OFF); @@ -121,14 +125,15 @@ Alarm::Alarm(Controllers::AlarmController& alarmController, } Alarm::~Alarm() { - if (alarmController.State() == AlarmController::AlarmState::Alerting) { + if (alarmController.IsAlerting()) { StopAlerting(); } lv_obj_clean(lv_scr_act()); + alarmController.SaveAlarm(); } void Alarm::DisableAlarm() { - if (alarmController.State() == AlarmController::AlarmState::Set) { + if (alarmController.IsEnabled()) { alarmController.DisableAlarm(); lv_switch_off(enableSwitch, LV_ANIM_ON); } @@ -168,7 +173,7 @@ bool Alarm::OnButtonPushed() { HideInfo(); return true; } - if (alarmController.State() == AlarmController::AlarmState::Alerting) { + if (alarmController.IsAlerting()) { StopAlerting(); return true; } @@ -177,7 +182,7 @@ bool Alarm::OnButtonPushed() { bool Alarm::OnTouchEvent(Pinetime::Applications::TouchEvents event) { // Don't allow closing the screen by swiping while the alarm is alerting - return alarmController.State() == AlarmController::AlarmState::Alerting && event == TouchEvents::SwipeDown; + return alarmController.IsAlerting() && event == TouchEvents::SwipeDown; } void Alarm::OnValueChanged() { @@ -198,10 +203,14 @@ void Alarm::UpdateAlarmTime() { void Alarm::SetAlerting() { lv_obj_set_hidden(enableSwitch, true); + lv_obj_set_hidden(btnRecur, true); + lv_obj_set_hidden(btnInfo, true); + hourCounter.HideControls(); + minuteCounter.HideControls(); lv_obj_set_hidden(btnStop, false); taskStopAlarm = lv_task_create(StopAlarmTaskCallback, pdMS_TO_TICKS(60 * 1000), LV_TASK_PRIO_MID, this); motorController.StartRinging(); - systemTask.PushMessage(System::Messages::DisableSleeping); + wakeLock.Lock(); } void Alarm::StopAlerting() { @@ -212,21 +221,20 @@ void Alarm::StopAlerting() { lv_task_del(taskStopAlarm); taskStopAlarm = nullptr; } - systemTask.PushMessage(System::Messages::EnableSleeping); - lv_obj_set_hidden(enableSwitch, false); + wakeLock.Release(); lv_obj_set_hidden(btnStop, true); + hourCounter.ShowControls(); + minuteCounter.ShowControls(); + lv_obj_set_hidden(btnInfo, false); + lv_obj_set_hidden(btnRecur, false); + lv_obj_set_hidden(enableSwitch, false); } void Alarm::SetSwitchState(lv_anim_enable_t anim) { - switch (alarmController.State()) { - case AlarmController::AlarmState::Set: - lv_switch_on(enableSwitch, anim); - break; - case AlarmController::AlarmState::Not_Set: - lv_switch_off(enableSwitch, anim); - break; - default: - break; + if (alarmController.IsEnabled()) { + lv_switch_on(enableSwitch, anim); + } else { + lv_switch_off(enableSwitch, anim); } } @@ -243,7 +251,7 @@ void Alarm::ShowInfo() { txtMessage = lv_label_create(btnMessage, nullptr); lv_obj_set_style_local_bg_color(btnMessage, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_NAVY); - if (alarmController.State() == AlarmController::AlarmState::Set) { + if (alarmController.IsEnabled()) { auto timeToAlarm = alarmController.SecondsToAlarm(); auto daysToAlarm = timeToAlarm / 86400; diff --git a/src/displayapp/screens/Alarm.h b/src/displayapp/screens/Alarm.h index 91177366..a875b275 100644 --- a/src/displayapp/screens/Alarm.h +++ b/src/displayapp/screens/Alarm.h @@ -17,21 +17,23 @@ */ #pragma once +#include "displayapp/apps/Apps.h" +#include "components/settings/Settings.h" #include "displayapp/screens/Screen.h" -#include "systemtask/SystemTask.h" -#include "displayapp/LittleVgl.h" -#include "components/alarm/AlarmController.h" #include "displayapp/widgets/Counter.h" +#include "displayapp/Controllers.h" +#include "systemtask/WakeLock.h" +#include "Symbols.h" namespace Pinetime { namespace Applications { namespace Screens { class Alarm : public Screen { public: - Alarm(Controllers::AlarmController& alarmController, - Controllers::Settings::ClockType clockType, - System::SystemTask& systemTask, - Controllers::MotorController& motorController); + explicit Alarm(Controllers::AlarmController& alarmController, + Controllers::Settings::ClockType clockType, + System::SystemTask& systemTask, + Controllers::MotorController& motorController); ~Alarm() override; void SetAlerting(); void OnButtonEvent(lv_obj_t* obj, lv_event_t event); @@ -42,7 +44,7 @@ namespace Pinetime { private: Controllers::AlarmController& alarmController; - System::SystemTask& systemTask; + System::WakeLock wakeLock; Controllers::MotorController& motorController; lv_obj_t *btnStop, *txtStop, *btnRecur, *txtRecur, *btnInfo, *enableSwitch; @@ -63,6 +65,19 @@ namespace Pinetime { Widgets::Counter hourCounter = Widgets::Counter(0, 23, jetbrains_mono_76); Widgets::Counter minuteCounter = Widgets::Counter(0, 59, jetbrains_mono_76); }; + } + + template <> + struct AppTraits<Apps::Alarm> { + static constexpr Apps app = Apps::Alarm; + static constexpr const char* icon = Screens::Symbols::bell; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Alarm(controllers.alarmController, + controllers.settingsController.GetClockType(), + *controllers.systemTask, + controllers.motorController); + }; }; - }; + } } diff --git a/src/displayapp/screens/ApplicationList.cpp b/src/displayapp/screens/ApplicationList.cpp index 0a65a5d4..fb46b413 100644 --- a/src/displayapp/screens/ApplicationList.cpp +++ b/src/displayapp/screens/ApplicationList.cpp @@ -1,13 +1,12 @@ #include "displayapp/screens/ApplicationList.h" +#include "displayapp/screens/Tile.h" #include <lvgl/lvgl.h> #include <functional> -#include "displayapp/Apps.h" -#include "displayapp/DisplayApp.h" +#include <algorithm> +#include "components/settings/Settings.h" using namespace Pinetime::Applications::Screens; -constexpr std::array<Tile::Applications, ApplicationList::applications.size()> ApplicationList::applications; - auto ApplicationList::CreateScreenList() const { std::array<std::function<std::unique_ptr<Screen>()>, nScreens> screens; for (size_t i = 0; i < screens.size(); i++) { @@ -18,16 +17,22 @@ auto ApplicationList::CreateScreenList() const { return screens; } -ApplicationList::ApplicationList(Pinetime::Applications::DisplayApp* app, +ApplicationList::ApplicationList(DisplayApp* app, Pinetime::Controllers::Settings& settingsController, const Pinetime::Controllers::Battery& batteryController, const Pinetime::Controllers::Ble& bleController, - Controllers::DateTime& dateTimeController) + const Pinetime::Controllers::AlarmController& alarmController, + Controllers::DateTime& dateTimeController, + Pinetime::Controllers::FS& filesystem, + std::array<Tile::Applications, UserAppTypes::Count>&& apps) : app {app}, settingsController {settingsController}, batteryController {batteryController}, bleController {bleController}, + alarmController {alarmController}, dateTimeController {dateTimeController}, + filesystem {filesystem}, + apps {std::move(apps)}, screens {app, settingsController.GetAppMenu(), CreateScreenList(), Screens::ScreenListModes::UpDown} { } @@ -40,9 +45,14 @@ bool ApplicationList::OnTouchEvent(Pinetime::Applications::TouchEvents event) { } std::unique_ptr<Screen> ApplicationList::CreateScreen(unsigned int screenNum) const { - std::array<Tile::Applications, appsPerScreen> apps; + std::array<Tile::Applications, appsPerScreen> pageApps; + for (int i = 0; i < appsPerScreen; i++) { - apps[i] = applications[screenNum * appsPerScreen + i]; + if (i + (screenNum * appsPerScreen) >= apps.size()) { + pageApps[i] = {"", Pinetime::Applications::Apps::None, false}; + } else { + pageApps[i] = apps[i + (screenNum * appsPerScreen)]; + } } return std::make_unique<Screens::Tile>(screenNum, @@ -51,6 +61,7 @@ std::unique_ptr<Screen> ApplicationList::CreateScreen(unsigned int screenNum) co settingsController, batteryController, bleController, + alarmController, dateTimeController, - apps); + pageApps); } diff --git a/src/displayapp/screens/ApplicationList.h b/src/displayapp/screens/ApplicationList.h index 7bdd1154..4a57d7c0 100644 --- a/src/displayapp/screens/ApplicationList.h +++ b/src/displayapp/screens/ApplicationList.h @@ -2,14 +2,12 @@ #include <array> #include <memory> - -#include "displayapp/screens/Screen.h" -#include "displayapp/screens/ScreenList.h" -#include "components/datetime/DateTimeController.h" -#include "components/settings/Settings.h" -#include "components/battery/BatteryController.h" -#include "displayapp/screens/Symbols.h" -#include "displayapp/screens/Tile.h" +#include "displayapp/apps/Apps.h" +#include "Screen.h" +#include "ScreenList.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" +#include "Tile.h" namespace Pinetime { namespace Applications { @@ -20,7 +18,10 @@ namespace Pinetime { Pinetime::Controllers::Settings& settingsController, const Pinetime::Controllers::Battery& batteryController, const Pinetime::Controllers::Ble& bleController, - Controllers::DateTime& dateTimeController); + const Pinetime::Controllers::AlarmController& alarmController, + Controllers::DateTime& dateTimeController, + Pinetime::Controllers::FS& filesystem, + std::array<Tile::Applications, UserAppTypes::Count>&& apps); ~ApplicationList() override; bool OnTouchEvent(TouchEvents event) override; @@ -32,30 +33,15 @@ namespace Pinetime { Controllers::Settings& settingsController; const Pinetime::Controllers::Battery& batteryController; const Pinetime::Controllers::Ble& bleController; + const Pinetime::Controllers::AlarmController& alarmController; Controllers::DateTime& dateTimeController; + Pinetime::Controllers::FS& filesystem; + std::array<Tile::Applications, UserAppTypes::Count> apps; static constexpr int appsPerScreen = 6; - // Increment this when more space is needed - static constexpr int nScreens = 2; - - static constexpr std::array<Tile::Applications, appsPerScreen * nScreens> applications {{ - {Symbols::stopWatch, Apps::StopWatch}, - {Symbols::clock, Apps::Alarm}, - {Symbols::hourGlass, Apps::Timer}, - {Symbols::shoe, Apps::Steps}, - {Symbols::heartBeat, Apps::HeartRate}, - {Symbols::music, Apps::Music}, - - {Symbols::paintbrush, Apps::Paint}, - {Symbols::paddle, Apps::Paddle}, - {"2", Apps::Twos}, - {Symbols::drum, Apps::Metronome}, - {Symbols::map, Apps::Navigation}, - {Symbols::none, Apps::None}, + static constexpr int nScreens = UserAppTypes::Count > 0 ? (UserAppTypes::Count - 1) / appsPerScreen + 1 : 1; - // {"M", Apps::Motion}, - }}; ScreenList<nScreens> screens; }; } diff --git a/src/displayapp/screens/BatteryInfo.cpp b/src/displayapp/screens/BatteryInfo.cpp index ab0a2bd4..20401988 100644 --- a/src/displayapp/screens/BatteryInfo.cpp +++ b/src/displayapp/screens/BatteryInfo.cpp @@ -10,33 +10,35 @@ BatteryInfo::BatteryInfo(const Pinetime::Controllers::Battery& batteryController batteryPercent = batteryController.PercentRemaining(); batteryVoltage = batteryController.Voltage(); - charging_bar = lv_bar_create(lv_scr_act(), nullptr); - lv_obj_set_size(charging_bar, 200, 15); - lv_bar_set_range(charging_bar, 0, 100); - lv_obj_align(charging_bar, nullptr, LV_ALIGN_CENTER, 0, 10); - lv_bar_set_anim_time(charging_bar, 1000); - lv_obj_set_style_local_radius(charging_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); - lv_obj_set_style_local_bg_color(charging_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, Colors::bgAlt); - lv_obj_set_style_local_bg_opa(charging_bar, LV_BAR_PART_BG, LV_STATE_DEFAULT, LV_OPA_100); - lv_obj_set_style_local_bg_color(charging_bar, LV_BAR_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_RED); - lv_bar_set_value(charging_bar, batteryPercent, LV_ANIM_ON); + chargingArc = lv_arc_create(lv_scr_act(), nullptr); + lv_arc_set_rotation(chargingArc, 270); + lv_arc_set_bg_angles(chargingArc, 0, 360); + lv_arc_set_adjustable(chargingArc, false); + lv_obj_set_size(chargingArc, 180, 180); + lv_obj_align(chargingArc, nullptr, LV_ALIGN_CENTER, 0, -30); + lv_arc_set_value(chargingArc, batteryPercent); + lv_obj_set_style_local_bg_opa(chargingArc, LV_ARC_PART_BG, LV_STATE_DEFAULT, LV_OPA_0); + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_BG, LV_STATE_DEFAULT, Colors::bgAlt); + lv_obj_set_style_local_border_width(chargingArc, LV_ARC_PART_BG, LV_STATE_DEFAULT, 2); + lv_obj_set_style_local_radius(chargingArc, LV_ARC_PART_BG, LV_STATE_DEFAULT, 0); + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_LIME); status = lv_label_create(lv_scr_act(), nullptr); lv_label_set_text_static(status, "Reading Battery status"); lv_label_set_align(status, LV_LABEL_ALIGN_CENTER); - lv_obj_align(status, charging_bar, LV_ALIGN_OUT_BOTTOM_MID, 0, 20); + lv_obj_align(status, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, -17); percent = lv_label_create(lv_scr_act(), nullptr); - lv_obj_set_style_local_text_font(percent, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); + lv_obj_set_style_local_text_font(percent, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_42); lv_label_set_text_fmt(percent, "%02i%%", batteryPercent); lv_label_set_align(percent, LV_LABEL_ALIGN_LEFT); - lv_obj_align(percent, nullptr, LV_ALIGN_CENTER, 0, -60); + lv_obj_align(percent, chargingArc, LV_ALIGN_CENTER, 0, 0); voltage = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(voltage, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::orange); lv_label_set_text_fmt(voltage, "%1i.%02i volts", batteryVoltage / 1000, batteryVoltage % 1000 / 10); lv_label_set_align(voltage, LV_LABEL_ALIGN_CENTER); - lv_obj_align(voltage, nullptr, LV_ALIGN_CENTER, 0, 95); + lv_obj_align(voltage, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, -7); taskRefresh = lv_task_create(RefreshTaskCallback, 5000, LV_TASK_PRIO_MID, this); Refresh(); @@ -53,22 +55,23 @@ void BatteryInfo::Refresh() { batteryVoltage = batteryController.Voltage(); if (batteryController.IsCharging()) { - lv_obj_set_style_local_bg_color(charging_bar, LV_BAR_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_RED); + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_LIME); lv_label_set_text_static(status, "Charging"); } else if (batteryPercent == 100) { - lv_obj_set_style_local_bg_color(charging_bar, LV_BAR_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_BLUE); + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_BLUE); lv_label_set_text_static(status, "Fully charged"); } else if (batteryPercent < 10) { - lv_obj_set_style_local_bg_color(charging_bar, LV_BAR_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_YELLOW); + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_RED); lv_label_set_text_static(status, "Battery low"); } else { - lv_obj_set_style_local_bg_color(charging_bar, LV_BAR_PART_INDIC, LV_STATE_DEFAULT, Colors::highlight); + lv_obj_set_style_local_line_color(chargingArc, LV_ARC_PART_INDIC, LV_STATE_DEFAULT, LV_COLOR_GREEN); lv_label_set_text_static(status, "Discharging"); } lv_label_set_text_fmt(percent, "%02i%%", batteryPercent); + lv_obj_align(percent, chargingArc, LV_ALIGN_CENTER, 0, 0); - lv_obj_align(status, charging_bar, LV_ALIGN_OUT_BOTTOM_MID, 0, 20); + lv_obj_align(status, voltage, LV_ALIGN_IN_BOTTOM_MID, 0, -27); lv_label_set_text_fmt(voltage, "%1i.%02i volts", batteryVoltage / 1000, batteryVoltage % 1000 / 10); - lv_bar_set_value(charging_bar, batteryPercent, LV_ANIM_ON); + lv_arc_set_value(chargingArc, batteryPercent); } diff --git a/src/displayapp/screens/BatteryInfo.h b/src/displayapp/screens/BatteryInfo.h index aa01d464..27bbaa00 100644 --- a/src/displayapp/screens/BatteryInfo.h +++ b/src/displayapp/screens/BatteryInfo.h @@ -24,7 +24,7 @@ namespace Pinetime { lv_obj_t* voltage; lv_obj_t* percent; - lv_obj_t* charging_bar; + lv_obj_t* chargingArc; lv_obj_t* status; lv_task_t* taskRefresh; diff --git a/src/displayapp/screens/Calculator.cpp b/src/displayapp/screens/Calculator.cpp new file mode 100644 index 00000000..a1f09383 --- /dev/null +++ b/src/displayapp/screens/Calculator.cpp @@ -0,0 +1,375 @@ +#include <cmath> +#include <cinttypes> +#include "Calculator.h" +#include "displayapp/InfiniTimeTheme.h" +#include "Symbols.h" + +using namespace Pinetime::Applications::Screens; + +static void eventHandler(lv_obj_t* obj, lv_event_t event) { + auto app = static_cast<Calculator*>(obj->user_data); + app->OnButtonEvent(obj, event); +} + +Calculator::~Calculator() { + lv_obj_clean(lv_scr_act()); +} + +constexpr const char* const buttonMap[] = { + "7", "8", "9", Symbols::backspace, "\n", "4", "5", "6", "+ -", "\n", "1", "2", "3", "* /", "\n", "0", ".", "(-)", "=", ""}; + +Calculator::Calculator() { + resultLabel = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_long_mode(resultLabel, LV_LABEL_LONG_CROP); + lv_label_set_align(resultLabel, LV_LABEL_ALIGN_RIGHT); + lv_label_set_text_fmt(resultLabel, "%" PRId64, result); + lv_obj_set_size(resultLabel, 200, 20); + lv_obj_set_pos(resultLabel, 10, 5); + + valueLabel = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_long_mode(valueLabel, LV_LABEL_LONG_CROP); + lv_label_set_align(valueLabel, LV_LABEL_ALIGN_RIGHT); + lv_label_set_text_fmt(valueLabel, "%" PRId64, value); + lv_obj_set_size(valueLabel, 200, 20); + lv_obj_set_pos(valueLabel, 10, 35); + + buttonMatrix = lv_btnmatrix_create(lv_scr_act(), nullptr); + buttonMatrix->user_data = this; + lv_obj_set_event_cb(buttonMatrix, eventHandler); + lv_btnmatrix_set_map(buttonMatrix, const_cast<const char**>(buttonMap)); + lv_btnmatrix_set_one_check(buttonMatrix, true); + lv_obj_set_size(buttonMatrix, 238, 180); + lv_obj_set_style_local_bg_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_DEFAULT, Colors::bgAlt); + lv_obj_set_style_local_pad_inner(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 1); + lv_obj_set_style_local_pad_top(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 1); + lv_obj_set_style_local_pad_bottom(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 1); + lv_obj_set_style_local_pad_left(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 1); + lv_obj_set_style_local_pad_right(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 1); + lv_obj_align(buttonMatrix, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, 0); + + lv_obj_set_style_local_bg_opa(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, LV_OPA_COVER); + lv_obj_set_style_local_bg_grad_stop(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, 128); + lv_obj_set_style_local_bg_main_stop(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, 128); +} + +void Calculator::OnButtonEvent(lv_obj_t* obj, lv_event_t event) { + if ((obj == buttonMatrix) && (event == LV_EVENT_PRESSED)) { + HandleInput(); + } +} + +void Calculator::HandleInput() { + const char* buttonText = lv_btnmatrix_get_active_btn_text(buttonMatrix); + + if (buttonText == nullptr) { + return; + } + + if ((equalSignPressedBefore && (*buttonText != '=')) || (error != Error::None)) { + ResetInput(); + UpdateOperation(); + } + + // we only compare the first char because it is enough + switch (*buttonText) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + // *buttonText is the first char in buttonText + // "- '0'" results in the int value of the char + uint8_t digit = (*buttonText) - '0'; + int8_t sign = (value < 0) ? -1 : 1; + + // if this is true, we already pressed the . button + if (offset < FIXED_POINT_OFFSET) { + value += sign * offset * digit; + offset /= 10; + } else if (value <= MAX_VALUE / 10) { + value *= 10; + value += sign * offset * digit; + } + } break; + + // unary minus + case '(': + value = -value; + break; + + case '.': + if (offset == FIXED_POINT_OFFSET) { + offset /= 10; + } + break; + + // for every operator we: + // - eval the current operator if value > FIXED_POINT_OFFSET + // - then set the new operator + // - + and - as well as * and / cycle on the same button + case '+': + if (value != 0) { + Eval(); + ResetInput(); + } + + switch (operation) { + case '+': + operation = '-'; + break; + case '-': + operation = ' '; + break; + default: + operation = '+'; + break; + } + UpdateOperation(); + break; + + case '*': + if (value != 0) { + Eval(); + ResetInput(); + } + + switch (operation) { + case '*': + operation = '/'; + break; + case '/': + operation = ' '; + break; + default: + operation = '*'; + break; + } + UpdateOperation(); + break; + + // this is a little hacky because it matches only the first char + case Symbols::backspace[0]: + if (value != 0) { + // delete one value digit + if (offset < FIXED_POINT_OFFSET) { + if (offset == 0) { + offset = 1; + } else { + offset *= 10; + } + } else { + value /= 10; + } + if (offset < FIXED_POINT_OFFSET) { + value -= value % (10 * offset); + } else { + value -= value % offset; + } + } else if (offset < FIXED_POINT_OFFSET) { + if (offset == 0) { + offset = 1; + } else { + offset *= 10; + } + } else { + // reset the result + result = 0; + } + + if (value == 0) { + operation = ' '; + UpdateOperation(); + } + break; + + case '=': + equalSignPressedBefore = true; + Eval(); + // If the operation is ' ' then we move the value to the result. + // We reset the input after this. + // This seems more convenient. + if (operation == ' ') { + ResetInput(); + } + break; + } + + UpdateValueLabel(); + UpdateResultLabel(); +} + +void Calculator::UpdateOperation() const { + switch (operation) { + case '+': + lv_obj_set_style_local_bg_grad_dir(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, LV_GRAD_DIR_HOR); + lv_obj_set_style_local_bg_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::deepOrange); + lv_obj_set_style_local_bg_grad_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::bgAlt); + lv_btnmatrix_set_btn_ctrl(buttonMatrix, 7, LV_BTNMATRIX_CTRL_CHECK_STATE); + break; + case '-': + lv_obj_set_style_local_bg_grad_dir(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, LV_GRAD_DIR_HOR); + lv_obj_set_style_local_bg_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::bgAlt); + lv_obj_set_style_local_bg_grad_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::deepOrange); + lv_btnmatrix_set_btn_ctrl(buttonMatrix, 7, LV_BTNMATRIX_CTRL_CHECK_STATE); + break; + case '*': + lv_obj_set_style_local_bg_grad_dir(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, LV_GRAD_DIR_HOR); + lv_obj_set_style_local_bg_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::deepOrange); + lv_obj_set_style_local_bg_grad_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::bgAlt); + lv_btnmatrix_set_btn_ctrl(buttonMatrix, 11, LV_BTNMATRIX_CTRL_CHECK_STATE); + break; + case '/': + lv_obj_set_style_local_bg_grad_dir(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, LV_GRAD_DIR_HOR); + lv_obj_set_style_local_bg_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::bgAlt); + lv_obj_set_style_local_bg_grad_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::deepOrange); + lv_btnmatrix_set_btn_ctrl(buttonMatrix, 11, LV_BTNMATRIX_CTRL_CHECK_STATE); + break; + default: + lv_btnmatrix_clear_btn_ctrl_all(buttonMatrix, LV_BTNMATRIX_CTRL_CHECK_STATE); + break; + } +} + +void Calculator::ResetInput() { + value = 0; + offset = FIXED_POINT_OFFSET; + operation = ' '; + equalSignPressedBefore = false; + error = Error::None; +} + +void Calculator::UpdateResultLabel() const { + int64_t integer = result / FIXED_POINT_OFFSET; + int64_t remainder = result % FIXED_POINT_OFFSET; + bool negative = (remainder < 0); + + if (remainder == 0) { + lv_label_set_text_fmt(resultLabel, "%" PRId64, integer); + return; + } + + if (remainder < 0) { + remainder = -remainder; + } + + uint8_t minWidth = N_DECIMALS; + + // cut "0"-digits on the right + while ((remainder > 0) && (remainder % 10 == 0)) { + remainder /= 10; + minWidth--; + } + + if ((integer == 0) && negative) { + lv_label_set_text_fmt(resultLabel, "-0.%0*" PRId64, minWidth, remainder); + } else { + lv_label_set_text_fmt(resultLabel, "%" PRId64 ".%0*" PRId64, integer, minWidth, remainder); + } +} + +void Calculator::UpdateValueLabel() { + switch (error) { + case Error::TooLarge: + lv_label_set_text_static(valueLabel, "too large"); + break; + case Error::ZeroDivision: + lv_label_set_text_static(valueLabel, "zero division"); + break; + case Error::None: + default: { + int64_t integer = value / FIXED_POINT_OFFSET; + int64_t remainder = value % FIXED_POINT_OFFSET; + bool negative = (remainder < 0); + + int64_t printRemainder = remainder < 0 ? -remainder : remainder; + + uint8_t minWidth = 0; + int64_t tmpOffset = offset; + + if (tmpOffset == 0) { + tmpOffset = 1; + minWidth = 1; + } + while (tmpOffset < FIXED_POINT_OFFSET) { + tmpOffset *= 10; + minWidth++; + } + minWidth--; + + for (uint8_t i = minWidth; i < N_DECIMALS; i++) { + printRemainder /= 10; + } + + if ((integer == 0) && negative) { + lv_label_set_text_fmt(valueLabel, "-0.%0*" PRId64, minWidth, printRemainder); + } else if (offset == FIXED_POINT_OFFSET) { + lv_label_set_text_fmt(valueLabel, "%" PRId64, integer); + } else if ((offset == (FIXED_POINT_OFFSET / 10)) && (remainder == 0)) { + lv_label_set_text_fmt(valueLabel, "%" PRId64 ".", integer); + } else { + lv_label_set_text_fmt(valueLabel, "%" PRId64 ".%0*" PRId64, integer, minWidth, printRemainder); + } + } break; + } +} + +// update the result based on value and operation +void Calculator::Eval() { + switch (operation) { + case ' ': + result = value; + break; + + case '+': + // check for overflow + if (((result > 0) && (value > (MAX_VALUE - result))) || ((result < 0) && (value < (MIN_VALUE - result)))) { + error = Error::TooLarge; + break; + } + + result += value; + break; + case '-': + // check for overflow + if (((result < 0) && (value > (MAX_VALUE + result))) || ((result > 0) && (value < (MIN_VALUE + result)))) { + error = Error::TooLarge; + break; + } + + result -= value; + break; + case '*': + // check for overflow + // while dividing we eliminate the fixed point offset + // therefore we have to multiply it again for the comparison with value + // we also assume here that MAX_VALUE == -MIN_VALUE + if ((result != 0) && (std::abs(value) > (FIXED_POINT_OFFSET * (MAX_VALUE / std::abs(result))))) { + error = Error::TooLarge; + break; + } + + result *= value; + // fixed point offset was multiplied too + result /= FIXED_POINT_OFFSET; + break; + case '/': + // check for zero division + if (value == 0) { + error = Error::ZeroDivision; + break; + } + + // fixed point offset will be divided too + result *= FIXED_POINT_OFFSET; + result /= value; + break; + + default: + break; + } +} diff --git a/src/displayapp/screens/Calculator.h b/src/displayapp/screens/Calculator.h new file mode 100644 index 00000000..9971f275 --- /dev/null +++ b/src/displayapp/screens/Calculator.h @@ -0,0 +1,83 @@ +#pragma once + +#include "displayapp/screens/Screen.h" +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" + +namespace { + constexpr int64_t powi(int64_t base, uint8_t exponent) { + int64_t value = 1; + while (exponent) { + value *= base; + exponent--; + } + return value; + } +} + +namespace Pinetime { + namespace Applications { + namespace Screens { + class Calculator : public Screen { + public: + ~Calculator() override; + + Calculator(); + + void OnButtonEvent(lv_obj_t* obj, lv_event_t event); + + private: + lv_obj_t* buttonMatrix {}; + lv_obj_t* valueLabel {}; + lv_obj_t* resultLabel {}; + + void Eval(); + void ResetInput(); + void HandleInput(); + void UpdateValueLabel(); + void UpdateResultLabel() const; + void UpdateOperation() const; + + // change this if you want to change the number of decimals + // keep in mind, that we only have 12 digits in total (see MAX_DIGITS) + // due to the fixed point implementation + static constexpr uint8_t N_DECIMALS = 3; + // this is the constant default offset + static constexpr int64_t FIXED_POINT_OFFSET = powi(10, N_DECIMALS); + // this is the current offset, may vary after pressing '.' + int64_t offset = FIXED_POINT_OFFSET; + + // the screen can show 12 chars + // but two are needed for '.' and '-' + static constexpr uint8_t MAX_DIGITS = 12; + static constexpr int64_t MAX_VALUE = powi(10, MAX_DIGITS) - 1; + // this is assumed in the multiplication overflow! + static constexpr int64_t MIN_VALUE = -MAX_VALUE; + + int64_t value = 0; + int64_t result = 0; + char operation = ' '; + bool equalSignPressedBefore = false; + + enum Error { + TooLarge, + ZeroDivision, + None, + }; + + Error error = Error::None; + }; + } + + template <> + struct AppTraits<Apps::Calculator> { + static constexpr Apps app = Apps::Calculator; + static constexpr const char* icon = Screens::Symbols::calculator; + + static Screens::Screen* Create(AppControllers& /* controllers */) { + return new Screens::Calculator(); + }; + }; + } +} diff --git a/src/displayapp/screens/CheckboxList.h b/src/displayapp/screens/CheckboxList.h index c208bc48..c6119970 100644 --- a/src/displayapp/screens/CheckboxList.h +++ b/src/displayapp/screens/CheckboxList.h @@ -1,6 +1,6 @@ #pragma once -#include "displayapp/Apps.h" +#include "displayapp/apps/Apps.h" #include "displayapp/screens/Screen.h" #include <array> #include <cstdint> diff --git a/src/displayapp/screens/Clock.cpp b/src/displayapp/screens/Clock.cpp deleted file mode 100644 index a03dc68b..00000000 --- a/src/displayapp/screens/Clock.cpp +++ /dev/null @@ -1,129 +0,0 @@ -#include "displayapp/screens/Clock.h" - -#include <lvgl/lvgl.h> -#include "components/battery/BatteryController.h" -#include "components/motion/MotionController.h" -#include "components/ble/BleController.h" -#include "components/ble/NotificationManager.h" -#include "components/settings/Settings.h" -#include "displayapp/DisplayApp.h" -#include "displayapp/screens/WatchFaceDigital.h" -#include "displayapp/screens/WatchFaceTerminal.h" -#include "displayapp/screens/WatchFaceInfineat.h" -#include "displayapp/screens/WatchFaceAnalog.h" -#include "displayapp/screens/WatchFacePineTimeStyle.h" -#include "displayapp/screens/WatchFaceCasioStyleG7710.h" - -using namespace Pinetime::Applications::Screens; - -Clock::Clock(Controllers::DateTime& dateTimeController, - const Controllers::Battery& batteryController, - const Controllers::Ble& bleController, - Controllers::NotificationManager& notificationManager, - Controllers::Settings& settingsController, - Controllers::HeartRateController& heartRateController, - Controllers::MotionController& motionController, - Controllers::FS& filesystem) - : dateTimeController {dateTimeController}, - batteryController {batteryController}, - bleController {bleController}, - notificationManager {notificationManager}, - settingsController {settingsController}, - heartRateController {heartRateController}, - motionController {motionController}, - filesystem {filesystem}, - screen {[this, &settingsController]() { - switch (settingsController.GetClockFace()) { - case 0: - return WatchFaceDigitalScreen(); - break; - case 1: - return WatchFaceAnalogScreen(); - break; - case 2: - return WatchFacePineTimeStyleScreen(); - break; - case 3: - return WatchFaceTerminalScreen(); - break; - case 4: - return WatchFaceInfineatScreen(); - break; - case 5: - return WatchFaceCasioStyleG7710(); - break; - } - return WatchFaceDigitalScreen(); - }()} { - settingsController.SetAppMenu(0); -} - -Clock::~Clock() { - lv_obj_clean(lv_scr_act()); -} - -bool Clock::OnTouchEvent(Pinetime::Applications::TouchEvents event) { - return screen->OnTouchEvent(event); -} - -bool Clock::OnButtonPushed() { - return screen->OnButtonPushed(); -} - -std::unique_ptr<Screen> Clock::WatchFaceDigitalScreen() { - return std::make_unique<Screens::WatchFaceDigital>(dateTimeController, - batteryController, - bleController, - notificationManager, - settingsController, - heartRateController, - motionController); -} - -std::unique_ptr<Screen> Clock::WatchFaceAnalogScreen() { - return std::make_unique<Screens::WatchFaceAnalog>(dateTimeController, - batteryController, - bleController, - notificationManager, - settingsController); -} - -std::unique_ptr<Screen> Clock::WatchFacePineTimeStyleScreen() { - return std::make_unique<Screens::WatchFacePineTimeStyle>(dateTimeController, - batteryController, - bleController, - notificationManager, - settingsController, - motionController); -} - -std::unique_ptr<Screen> Clock::WatchFaceTerminalScreen() { - return std::make_unique<Screens::WatchFaceTerminal>(dateTimeController, - batteryController, - bleController, - notificationManager, - settingsController, - heartRateController, - motionController); -} - -std::unique_ptr<Screen> Clock::WatchFaceInfineatScreen() { - return std::make_unique<Screens::WatchFaceInfineat>(dateTimeController, - batteryController, - bleController, - notificationManager, - settingsController, - motionController, - filesystem); -} - -std::unique_ptr<Screen> Clock::WatchFaceCasioStyleG7710() { - return std::make_unique<Screens::WatchFaceCasioStyleG7710>(dateTimeController, - batteryController, - bleController, - notificationManager, - settingsController, - heartRateController, - motionController, - filesystem); -} diff --git a/src/displayapp/screens/Clock.h b/src/displayapp/screens/Clock.h deleted file mode 100644 index 8c987fbb..00000000 --- a/src/displayapp/screens/Clock.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include <lvgl/src/lv_core/lv_obj.h> -#include <chrono> -#include <cstdint> -#include <memory> -#include <components/heartrate/HeartRateController.h> -#include "displayapp/screens/Screen.h" -#include "components/datetime/DateTimeController.h" - -namespace Pinetime { - namespace Controllers { - class Settings; - class Battery; - class Ble; - class NotificationManager; - class MotionController; - } - - namespace Applications { - namespace Screens { - class Clock : public Screen { - public: - Clock(Controllers::DateTime& dateTimeController, - const Controllers::Battery& batteryController, - const Controllers::Ble& bleController, - Controllers::NotificationManager& notificationManager, - Controllers::Settings& settingsController, - Controllers::HeartRateController& heartRateController, - Controllers::MotionController& motionController, - Controllers::FS& filesystem); - ~Clock() override; - - bool OnTouchEvent(TouchEvents event) override; - bool OnButtonPushed() override; - - private: - Controllers::DateTime& dateTimeController; - const Controllers::Battery& batteryController; - const Controllers::Ble& bleController; - Controllers::NotificationManager& notificationManager; - Controllers::Settings& settingsController; - Controllers::HeartRateController& heartRateController; - Controllers::MotionController& motionController; - Controllers::FS& filesystem; - - std::unique_ptr<Screen> screen; - std::unique_ptr<Screen> WatchFaceDigitalScreen(); - std::unique_ptr<Screen> WatchFaceAnalogScreen(); - std::unique_ptr<Screen> WatchFacePineTimeStyleScreen(); - std::unique_ptr<Screen> WatchFaceTerminalScreen(); - std::unique_ptr<Screen> WatchFaceInfineatScreen(); - std::unique_ptr<Screen> WatchFaceCasioStyleG7710(); - }; - } - } -} diff --git a/src/displayapp/screens/Dice.cpp b/src/displayapp/screens/Dice.cpp new file mode 100644 index 00000000..302c5f3f --- /dev/null +++ b/src/displayapp/screens/Dice.cpp @@ -0,0 +1,199 @@ +#include "displayapp/screens/Dice.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/screens/Symbols.h" +#include "components/settings/Settings.h" +#include "components/motor/MotorController.h" +#include "components/motion/MotionController.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + lv_obj_t* MakeLabel(lv_font_t* font, + lv_color_t color, + lv_label_long_mode_t longMode, + uint8_t width, + lv_label_align_t labelAlignment, + const char* text, + lv_obj_t* reference, + lv_align_t alignment, + int8_t x, + int8_t y) { + lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, font); + lv_obj_set_style_local_text_color(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color); + lv_label_set_long_mode(label, longMode); + if (width != 0) { + lv_obj_set_width(label, width); + } + lv_label_set_align(label, labelAlignment); + lv_label_set_text(label, text); + lv_obj_align(label, reference, alignment, x, y); + return label; + } + + void btnRollEventHandler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast<Dice*>(obj->user_data); + if (event == LV_EVENT_CLICKED) { + screen->Roll(); + } + } +} + +Dice::Dice(Controllers::MotionController& motionController, + Controllers::MotorController& motorController, + Controllers::Settings& settingsController) + : motorController {motorController}, motionController {motionController}, settingsController {settingsController} { + std::seed_seq sseq {static_cast<uint32_t>(xTaskGetTickCount()), + static_cast<uint32_t>(motionController.X()), + static_cast<uint32_t>(motionController.Y()), + static_cast<uint32_t>(motionController.Z())}; + gen.seed(sseq); + + lv_obj_t* nCounterLabel = MakeLabel(&jetbrains_mono_bold_20, + LV_COLOR_WHITE, + LV_LABEL_LONG_EXPAND, + 0, + LV_LABEL_ALIGN_CENTER, + "count", + lv_scr_act(), + LV_ALIGN_IN_TOP_LEFT, + 0, + 0); + + lv_obj_t* dCounterLabel = MakeLabel(&jetbrains_mono_bold_20, + LV_COLOR_WHITE, + LV_LABEL_LONG_EXPAND, + 0, + LV_LABEL_ALIGN_CENTER, + "sides", + nCounterLabel, + LV_ALIGN_OUT_RIGHT_MID, + 20, + 0); + + nCounter.Create(); + lv_obj_align(nCounter.GetObject(), nCounterLabel, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); + nCounter.SetValue(1); + + dCounter.Create(); + lv_obj_align(dCounter.GetObject(), dCounterLabel, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); + dCounter.SetValue(6); + + std::uniform_int_distribution<> distrib(0, resultColors.size() - 1); + currentColorIndex = distrib(gen); + + resultTotalLabel = MakeLabel(&jetbrains_mono_42, + resultColors[currentColorIndex], + LV_LABEL_LONG_BREAK, + 120, + LV_LABEL_ALIGN_CENTER, + "", + lv_scr_act(), + LV_ALIGN_IN_TOP_RIGHT, + 11, + 38); + resultIndividualLabel = MakeLabel(&jetbrains_mono_bold_20, + resultColors[currentColorIndex], + LV_LABEL_LONG_BREAK, + 90, + LV_LABEL_ALIGN_CENTER, + "", + resultTotalLabel, + LV_ALIGN_OUT_BOTTOM_MID, + 0, + 10); + + Roll(); + openingRoll = false; + + btnRoll = lv_btn_create(lv_scr_act(), nullptr); + btnRoll->user_data = this; + lv_obj_set_event_cb(btnRoll, btnRollEventHandler); + lv_obj_set_size(btnRoll, 240, 50); + lv_obj_align(btnRoll, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0); + + btnRollLabel = MakeLabel(&jetbrains_mono_bold_20, + LV_COLOR_WHITE, + LV_LABEL_LONG_EXPAND, + 0, + LV_LABEL_ALIGN_CENTER, + Symbols::dice, + btnRoll, + LV_ALIGN_CENTER, + 0, + 0); + + // Spagetti code in motion controller: it only updates the shake speed when shake to wake is on... + enableShakeForDice = !settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::Shake); + if (enableShakeForDice) { + settingsController.setWakeUpMode(Pinetime::Controllers::Settings::WakeUpMode::Shake, true); + } + refreshTask = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); +} + +Dice::~Dice() { + // reset the shake to wake mode. + if (enableShakeForDice) { + settingsController.setWakeUpMode(Pinetime::Controllers::Settings::WakeUpMode::Shake, false); + enableShakeForDice = false; + } + lv_task_del(refreshTask); + lv_obj_clean(lv_scr_act()); +} + +void Dice::Refresh() { + // we only reset the hysteresis when at rest + if (motionController.CurrentShakeSpeed() >= settingsController.GetShakeThreshold()) { + if (currentRollHysteresis <= 0) { + // this timestamp is used for the screen timeout + lv_disp_get_next(NULL)->last_activity_time = lv_tick_get(); + + Roll(); + } + } else if (currentRollHysteresis > 0) + --currentRollHysteresis; +} + +void Dice::Roll() { + uint8_t resultIndividual; + uint16_t resultTotal = 0; + std::uniform_int_distribution<> distrib(1, dCounter.GetValue()); + + lv_label_set_text(resultIndividualLabel, ""); + + if (nCounter.GetValue() == 1) { + resultTotal = distrib(gen); + if (dCounter.GetValue() == 2) { + switch (resultTotal) { + case 1: + lv_label_set_text(resultIndividualLabel, "HEADS"); + break; + case 2: + lv_label_set_text(resultIndividualLabel, "TAILS"); + break; + } + } + } else { + for (uint8_t i = 0; i < nCounter.GetValue(); i++) { + resultIndividual = distrib(gen); + resultTotal += resultIndividual; + lv_label_ins_text(resultIndividualLabel, LV_LABEL_POS_LAST, std::to_string(resultIndividual).c_str()); + if (i < (nCounter.GetValue() - 1)) { + lv_label_ins_text(resultIndividualLabel, LV_LABEL_POS_LAST, "+"); + } + } + } + + lv_label_set_text_fmt(resultTotalLabel, "%d", resultTotal); + if (openingRoll == false) { + motorController.RunForDuration(30); + NextColor(); + currentRollHysteresis = rollHysteresis; + } +} + +void Dice::NextColor() { + currentColorIndex = (currentColorIndex + 1) % resultColors.size(); + lv_obj_set_style_local_text_color(resultTotalLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, resultColors[currentColorIndex]); + lv_obj_set_style_local_text_color(resultIndividualLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, resultColors[currentColorIndex]); +} diff --git a/src/displayapp/screens/Dice.h b/src/displayapp/screens/Dice.h new file mode 100644 index 00000000..da91657d --- /dev/null +++ b/src/displayapp/screens/Dice.h @@ -0,0 +1,61 @@ +#pragma once + +#include "displayapp/apps/Apps.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/widgets/Counter.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" + +#include <array> +#include <random> + +namespace Pinetime { + namespace Applications { + namespace Screens { + class Dice : public Screen { + public: + Dice(Controllers::MotionController& motionController, + Controllers::MotorController& motorController, + Controllers::Settings& settingsController); + ~Dice() override; + void Roll(); + void Refresh() override; + + private: + lv_obj_t* btnRoll; + lv_obj_t* btnRollLabel; + lv_obj_t* resultTotalLabel; + lv_obj_t* resultIndividualLabel; + lv_task_t* refreshTask; + bool enableShakeForDice = false; + + std::mt19937 gen; + + std::array<lv_color_t, 3> resultColors = {LV_COLOR_YELLOW, LV_COLOR_MAGENTA, LV_COLOR_AQUA}; + uint8_t currentColorIndex; + void NextColor(); + + Widgets::Counter nCounter = Widgets::Counter(1, 9, jetbrains_mono_42); + Widgets::Counter dCounter = Widgets::Counter(2, 99, jetbrains_mono_42); + + bool openingRoll = true; + uint8_t currentRollHysteresis = 0; + static constexpr uint8_t rollHysteresis = 10; + + Controllers::MotorController& motorController; + Controllers::MotionController& motionController; + Controllers::Settings& settingsController; + }; + } + + template <> + struct AppTraits<Apps::Dice> { + static constexpr Apps app = Apps::Dice; + static constexpr const char* icon = Screens::Symbols::dice; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Dice(controllers.motionController, controllers.motorController, controllers.settingsController); + }; + }; + } +} diff --git a/src/displayapp/screens/FirmwareUpdate.cpp b/src/displayapp/screens/FirmwareUpdate.cpp index c40240c9..7d00ef39 100644 --- a/src/displayapp/screens/FirmwareUpdate.cpp +++ b/src/displayapp/screens/FirmwareUpdate.cpp @@ -2,6 +2,7 @@ #include <lvgl/lvgl.h> #include "components/ble/BleController.h" #include "displayapp/DisplayApp.h" +#include "displayapp/InfiniTimeTheme.h" using namespace Pinetime::Applications::Screens; @@ -12,6 +13,9 @@ FirmwareUpdate::FirmwareUpdate(const Pinetime::Controllers::Ble& bleController) lv_obj_align(titleLabel, nullptr, LV_ALIGN_IN_TOP_MID, 0, 50); bar1 = lv_bar_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_bg_color(bar1, LV_BAR_PART_BG, LV_STATE_DEFAULT, Colors::bgAlt); + lv_obj_set_style_local_bg_opa(bar1, LV_BAR_PART_BG, LV_STATE_DEFAULT, LV_OPA_100); + lv_obj_set_style_local_radius(bar1, LV_BAR_PART_BG, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE); lv_obj_set_size(bar1, 200, 30); lv_obj_align(bar1, nullptr, LV_ALIGN_CENTER, 0, 0); lv_bar_set_range(bar1, 0, 1000); @@ -75,7 +79,7 @@ void FirmwareUpdate::DisplayProgression() const { const uint32_t total = bleController.FirmwareUpdateTotalBytes(); const int16_t permille = current / (total / 1000); - lv_label_set_text_fmt(percentLabel, "%d %%", permille / 10); + lv_label_set_text_fmt(percentLabel, "%d%%", permille / 10); lv_bar_set_value(bar1, permille, LV_ANIM_OFF); } diff --git a/src/displayapp/screens/FirmwareValidation.cpp b/src/displayapp/screens/FirmwareValidation.cpp index 4a1b3d9f..2a9919d5 100644 --- a/src/displayapp/screens/FirmwareValidation.cpp +++ b/src/displayapp/screens/FirmwareValidation.cpp @@ -17,8 +17,8 @@ namespace { FirmwareValidation::FirmwareValidation(Pinetime::Controllers::FirmwareValidator& validator) : validator {validator} { labelVersion = lv_label_create(lv_scr_act(), nullptr); lv_label_set_text_fmt(labelVersion, - "Version : %lu.%lu.%lu\n" - "ShortRef : %s", + "Version: %lu.%lu.%lu\n" + "ShortRef: %s", Version::Major(), Version::Minor(), Version::Patch(), diff --git a/src/displayapp/screens/FlashLight.cpp b/src/displayapp/screens/FlashLight.cpp index 1b7cf39c..7e0caff1 100644 --- a/src/displayapp/screens/FlashLight.cpp +++ b/src/displayapp/screens/FlashLight.cpp @@ -15,8 +15,9 @@ namespace { } FlashLight::FlashLight(System::SystemTask& systemTask, Controllers::BrightnessController& brightnessController) - : systemTask {systemTask}, brightnessController {brightnessController} { + : wakeLock(systemTask), brightnessController {brightnessController} { + previousBrightnessLevel = brightnessController.Level(); brightnessController.Set(Controllers::BrightnessController::Levels::Low); flashLight = lv_label_create(lv_scr_act(), nullptr); @@ -46,13 +47,13 @@ FlashLight::FlashLight(System::SystemTask& systemTask, Controllers::BrightnessCo backgroundAction->user_data = this; lv_obj_set_event_cb(backgroundAction, EventHandler); - systemTask.PushMessage(Pinetime::System::Messages::DisableSleeping); + wakeLock.Lock(); } FlashLight::~FlashLight() { lv_obj_clean(lv_scr_act()); lv_obj_set_style_local_bg_color(lv_scr_act(), LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); - systemTask.PushMessage(Pinetime::System::Messages::EnableSleeping); + brightnessController.Set(previousBrightnessLevel); } void FlashLight::SetColors() { diff --git a/src/displayapp/screens/FlashLight.h b/src/displayapp/screens/FlashLight.h index 2b710ed5..00ef4a7e 100644 --- a/src/displayapp/screens/FlashLight.h +++ b/src/displayapp/screens/FlashLight.h @@ -3,6 +3,7 @@ #include "displayapp/screens/Screen.h" #include "components/brightness/BrightnessController.h" #include "systemtask/SystemTask.h" +#include "systemtask/WakeLock.h" #include <cstdint> #include <lvgl/lvgl.h> @@ -23,10 +24,11 @@ namespace Pinetime { void SetIndicators(); void SetColors(); - Pinetime::System::SystemTask& systemTask; + Pinetime::System::WakeLock wakeLock; Controllers::BrightnessController& brightnessController; Controllers::BrightnessController::Levels brightnessLevel = Controllers::BrightnessController::Levels::High; + Controllers::BrightnessController::Levels previousBrightnessLevel; lv_obj_t* flashLight; lv_obj_t* backgroundAction; diff --git a/src/displayapp/screens/HeartRate.cpp b/src/displayapp/screens/HeartRate.cpp index f611fa26..1a84d349 100644 --- a/src/displayapp/screens/HeartRate.cpp +++ b/src/displayapp/screens/HeartRate.cpp @@ -29,7 +29,7 @@ namespace { } HeartRate::HeartRate(Controllers::HeartRateController& heartRateController, System::SystemTask& systemTask) - : heartRateController {heartRateController}, systemTask {systemTask} { + : heartRateController {heartRateController}, wakeLock(systemTask) { bool isHrRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; label_hr = lv_label_create(lv_scr_act(), nullptr); @@ -41,7 +41,7 @@ HeartRate::HeartRate(Controllers::HeartRateController& heartRateController, Syst lv_obj_set_style_local_text_color(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); } - lv_label_set_text_static(label_hr, "000"); + lv_label_set_text_static(label_hr, "---"); lv_obj_align(label_hr, nullptr, LV_ALIGN_CENTER, 0, -40); label_bpm = lv_label_create(lv_scr_act(), nullptr); @@ -63,7 +63,7 @@ HeartRate::HeartRate(Controllers::HeartRateController& heartRateController, Syst label_startStop = lv_label_create(btn_startStop, nullptr); UpdateStartStopButton(isHrRunning); if (isHrRunning) { - systemTask.PushMessage(Pinetime::System::Messages::DisableSleeping); + wakeLock.Lock(); } taskRefresh = lv_task_create(RefreshTaskCallback, 100, LV_TASK_PRIO_MID, this); @@ -72,7 +72,6 @@ HeartRate::HeartRate(Controllers::HeartRateController& heartRateController, Syst HeartRate::~HeartRate() { lv_task_del(taskRefresh); lv_obj_clean(lv_scr_act()); - systemTask.PushMessage(Pinetime::System::Messages::EnableSleeping); } void HeartRate::Refresh() { @@ -82,10 +81,14 @@ void HeartRate::Refresh() { case Controllers::HeartRateController::States::NoTouch: case Controllers::HeartRateController::States::NotEnoughData: // case Controllers::HeartRateController::States::Stopped: - lv_label_set_text_static(label_hr, "000"); + lv_label_set_text_static(label_hr, "---"); break; default: - lv_label_set_text_fmt(label_hr, "%03d", heartRateController.HeartRate()); + if (heartRateController.HeartRate() == 0) { + lv_label_set_text_static(label_hr, "---"); + } else { + lv_label_set_text_fmt(label_hr, "%03d", heartRateController.HeartRate()); + } } lv_label_set_text_static(label_status, ToString(state)); @@ -97,12 +100,12 @@ void HeartRate::OnStartStopEvent(lv_event_t event) { if (heartRateController.State() == Controllers::HeartRateController::States::Stopped) { heartRateController.Start(); UpdateStartStopButton(heartRateController.State() != Controllers::HeartRateController::States::Stopped); - systemTask.PushMessage(Pinetime::System::Messages::DisableSleeping); + wakeLock.Lock(); lv_obj_set_style_local_text_color(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::highlight); } else { heartRateController.Stop(); UpdateStartStopButton(heartRateController.State() != Controllers::HeartRateController::States::Stopped); - systemTask.PushMessage(Pinetime::System::Messages::EnableSleeping); + wakeLock.Release(); lv_obj_set_style_local_text_color(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); } } diff --git a/src/displayapp/screens/HeartRate.h b/src/displayapp/screens/HeartRate.h index 78ae63db..88b4918c 100644 --- a/src/displayapp/screens/HeartRate.h +++ b/src/displayapp/screens/HeartRate.h @@ -4,6 +4,8 @@ #include <chrono> #include "displayapp/screens/Screen.h" #include "systemtask/SystemTask.h" +#include "systemtask/WakeLock.h" +#include "Symbols.h" #include <lvgl/src/lv_core/lv_style.h> #include <lvgl/src/lv_core/lv_obj.h> @@ -26,7 +28,7 @@ namespace Pinetime { private: Controllers::HeartRateController& heartRateController; - Pinetime::System::SystemTask& systemTask; + Pinetime::System::WakeLock wakeLock; void UpdateStartStopButton(bool isRunning); lv_obj_t* label_hr; lv_obj_t* label_bpm; @@ -37,5 +39,15 @@ namespace Pinetime { lv_task_t* taskRefresh; }; } + + template <> + struct AppTraits<Apps::HeartRate> { + static constexpr Apps app = Apps::HeartRate; + static constexpr const char* icon = Screens::Symbols::heartBeat; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::HeartRate(controllers.heartRateController, *controllers.systemTask); + }; + }; } } diff --git a/src/displayapp/screens/InfiniPaint.h b/src/displayapp/screens/InfiniPaint.h index ec184c44..b1f9741a 100644 --- a/src/displayapp/screens/InfiniPaint.h +++ b/src/displayapp/screens/InfiniPaint.h @@ -5,6 +5,9 @@ #include <algorithm> // std::fill #include "displayapp/screens/Screen.h" #include "components/motor/MotorController.h" +#include "Symbols.h" +#include "displayapp/apps/Apps.h" +#include <displayapp/Controllers.h> namespace Pinetime { namespace Components { @@ -35,5 +38,15 @@ namespace Pinetime { uint8_t color = 2; }; } + + template <> + struct AppTraits<Apps::Paint> { + static constexpr Apps app = Apps::Paint; + static constexpr const char* icon = Screens::Symbols::paintbrush; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::InfiniPaint(controllers.lvgl, controllers.motorController); + }; + }; } } diff --git a/src/displayapp/screens/List.h b/src/displayapp/screens/List.h index 564229e6..17a25f82 100644 --- a/src/displayapp/screens/List.h +++ b/src/displayapp/screens/List.h @@ -5,7 +5,7 @@ #include <array> #include "displayapp/screens/Screen.h" #include "displayapp/widgets/PageIndicator.h" -#include "displayapp/Apps.h" +#include "displayapp/apps/Apps.h" #include "components/settings/Settings.h" #define MAXLISTITEMS 4 diff --git a/src/displayapp/screens/Metronome.cpp b/src/displayapp/screens/Metronome.cpp index 314fde73..6b758470 100644 --- a/src/displayapp/screens/Metronome.cpp +++ b/src/displayapp/screens/Metronome.cpp @@ -22,7 +22,7 @@ namespace { } Metronome::Metronome(Controllers::MotorController& motorController, System::SystemTask& systemTask) - : motorController {motorController}, systemTask {systemTask} { + : motorController {motorController}, wakeLock(systemTask) { bpmArc = lv_arc_create(lv_scr_act(), nullptr); bpmArc->user_data = this; @@ -72,7 +72,6 @@ Metronome::Metronome(Controllers::MotorController& motorController, System::Syst Metronome::~Metronome() { lv_task_del(taskRefresh); - systemTask.PushMessage(System::Messages::EnableSleeping); lv_obj_clean(lv_scr_act()); } @@ -128,12 +127,12 @@ void Metronome::OnEvent(lv_obj_t* obj, lv_event_t event) { metronomeStarted = !metronomeStarted; if (metronomeStarted) { lv_label_set_text_static(lblPlayPause, Symbols::pause); - systemTask.PushMessage(System::Messages::DisableSleeping); + wakeLock.Lock(); startTime = xTaskGetTickCount(); counter = 1; } else { lv_label_set_text_static(lblPlayPause, Symbols::play); - systemTask.PushMessage(System::Messages::EnableSleeping); + wakeLock.Release(); } } break; diff --git a/src/displayapp/screens/Metronome.h b/src/displayapp/screens/Metronome.h index 13b0d664..fab7ff87 100644 --- a/src/displayapp/screens/Metronome.h +++ b/src/displayapp/screens/Metronome.h @@ -1,8 +1,10 @@ #pragma once #include "systemtask/SystemTask.h" +#include "systemtask/WakeLock.h" #include "components/motor/MotorController.h" #include "displayapp/screens/Screen.h" +#include "Symbols.h" namespace Pinetime { namespace Applications { @@ -20,7 +22,7 @@ namespace Pinetime { TickType_t startTime = 0; TickType_t tappedTime = 0; Controllers::MotorController& motorController; - System::SystemTask& systemTask; + System::WakeLock wakeLock; int16_t bpm = 120; uint8_t bpb = 4; uint8_t counter = 1; @@ -36,5 +38,15 @@ namespace Pinetime { lv_task_t* taskRefresh; }; } + + template <> + struct AppTraits<Apps::Metronome> { + static constexpr Apps app = Apps::Metronome; + static constexpr const char* icon = Screens::Symbols::drum; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Metronome(controllers.motorController, *controllers.systemTask); + }; + }; } } diff --git a/src/displayapp/screens/Motion.cpp b/src/displayapp/screens/Motion.cpp index 87c55eea..ecbed317 100644 --- a/src/displayapp/screens/Motion.cpp +++ b/src/displayapp/screens/Motion.cpp @@ -53,9 +53,9 @@ void Motion::Refresh() { lv_label_set_text_fmt(labelStep, "Steps %lu", motionController.NbSteps()); lv_label_set_text_fmt(label, - "X #FF0000 %d# Y #00B000 %d# Z #FFFF00 %d#", - motionController.X() / 0x10, - motionController.Y() / 0x10, - motionController.Z() / 0x10); + "X #FF0000 %d# Y #00B000 %d# Z #FFFF00 %d# mg", + motionController.X(), + motionController.Y(), + motionController.Z()); lv_obj_align(label, nullptr, LV_ALIGN_IN_TOP_MID, 0, 10); } diff --git a/src/displayapp/screens/Motion.h b/src/displayapp/screens/Motion.h index e4cbe483..e13e068c 100644 --- a/src/displayapp/screens/Motion.h +++ b/src/displayapp/screens/Motion.h @@ -6,6 +6,8 @@ #include <lvgl/src/lv_core/lv_style.h> #include <lvgl/src/lv_core/lv_obj.h> #include <components/motion/MotionController.h> +#include "displayapp/Controllers.h" +#include "displayapp/apps/Apps.h" namespace Pinetime { namespace Applications { @@ -30,5 +32,15 @@ namespace Pinetime { lv_task_t* taskRefresh; }; } + + template <> + struct AppTraits<Apps::Motion> { + static constexpr Apps app = Apps::Motion; + static constexpr const char* icon = "M"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Motion(controllers.motionController); + }; + }; } } diff --git a/src/displayapp/screens/Music.h b/src/displayapp/screens/Music.h index 847c6e74..52253321 100644 --- a/src/displayapp/screens/Music.h +++ b/src/displayapp/screens/Music.h @@ -21,6 +21,9 @@ #include <lvgl/src/lv_core/lv_obj.h> #include <string> #include "displayapp/screens/Screen.h" +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" namespace Pinetime { namespace Controllers { @@ -82,5 +85,15 @@ namespace Pinetime { /** Watchapp */ }; } + + template <> + struct AppTraits<Apps::Music> { + static constexpr Apps app = Apps::Music; + static constexpr const char* icon = Screens::Symbols::music; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Music(*controllers.musicService); + }; + }; } } diff --git a/src/displayapp/screens/Navigation.cpp b/src/displayapp/screens/Navigation.cpp index 7baea09d..ee9f2a00 100644 --- a/src/displayapp/screens/Navigation.cpp +++ b/src/displayapp/screens/Navigation.cpp @@ -23,105 +23,166 @@ using namespace Pinetime::Applications::Screens; -LV_FONT_DECLARE(lv_font_navi_80) +/* Notes about the navigation icons : + * - Icons are generated from a TTF font converted in PNG images. Those images are all appended + * vertically into a single PNG images. Since LVGL support images width and height up to + * 2048 px, the icons needs to be split into 2 separate PNG pictures. More info in + * src/displayapp/fonts/README.md + * - To make the handling of those icons easier, they must all have the same width and height + * - Those PNG are then converted into BINARY format using the classical image generator + * (in src/resources/generate-img.py) + * - The array `iconMap` maps each icon with an index. This index corresponds to the position of + * the icon in the file. All index lower than 25 (`maxIconsPerFile`) represent icons located + * in the first file (navigation0.bin). All the other icons are located in the second file + * (navigation1.bin). Since all icons have the same height, this index must be multiplied by + * 80px (`iconHeight`) to get the actual position (in pixels) of the icon in the image. + * - This is how the images are laid out in the PNG files : + * *---------------* + * | ICON 0 | + * | FILE 0 | + * | INDEX = 0 | + * | PIXEL# = 0 | + * *---------------* + * | ICON 1 | + * | FILE 0 | + * | INDEX = 1 | + * | PIXEL# = -80 | + * *---------------* + * | ICON 2 | + * | FILE 0 | + * | INDEX = 2 | + * | PIXEL# = -160 | + * *---------------* + * | ... | + * *---------------* + * | ICON 25 | + * | FILE 1 | + * | INDEX = 25 | + * | PIXEL# = 0 | + * *---------------* + * | ICON 26 | + * | FILE 1 | + * | INDEX = 26 | + * | PIXEL# = -80 | + * *---------------* + * - The source images are located in `src/resources/navigation0.png` and `src/resources/navigation1.png` + */ namespace { - constexpr std::array<std::pair<const char*, const char*>, 86> m_iconMap = {{ - {"arrive-left", "\xEE\xA4\x81"}, - {"arrive-right", "\xEE\xA4\x82"}, - {"arrive-straight", "\xEE\xA4\x80"}, - {"arrive", "\xEE\xA4\x80"}, - {"close", "\xEE\xA4\x83"}, - {"continue-left", "\xEE\xA4\x85"}, - {"continue-right", "\xEE\xA4\x86"}, - {"continue-slight-left", "\xEE\xA4\x87"}, - {"continue-slight-right", "\xEE\xA4\x88"}, - {"continue-straight", "\xEE\xA4\x84"}, - {"continue-uturn", "\xEE\xA4\x89"}, - {"continue", "\xEE\xA4\x84"}, - {"depart-left", "\xEE\xA4\x8B"}, - {"depart-right", "\xEE\xA4\x8C"}, - {"depart-straight", "\xEE\xA4\x8A"}, - {"end-of-road-left", "\xEE\xA4\x8D"}, - {"end-of-road-right", "\xEE\xA4\x8E"}, - {"ferry", "\xEE\xA4\x8F"}, - {"flag", "\xEE\xA4\x90"}, - {"fork-left", "\xEE\xA4\x92"}, - {"fork-right", "\xEE\xA4\x93"}, - {"fork-slight-left", "\xEE\xA4\x94"}, - {"fork-slight-right", "\xEE\xA4\x95"}, - {"fork-straight", "\xEE\xA4\x96"}, - {"invalid", "\xEE\xA4\x84"}, - {"invalid-left", "\xEE\xA4\x85"}, - {"invalid-right", "\xEE\xA4\x86"}, - {"invalid-slight-left", "\xEE\xA4\x87"}, - {"invalid-slight-right", "\xEE\xA4\x88"}, - {"invalid-straight", "\xEE\xA4\x84"}, - {"invalid-uturn", "\xEE\xA4\x89"}, - {"merge-left", "\xEE\xA4\x97"}, - {"merge-right", "\xEE\xA4\x98"}, - {"merge-slight-left", "\xEE\xA4\x99"}, - {"merge-slight-right", "\xEE\xA4\x9A"}, - {"merge-straight", "\xEE\xA4\x84"}, - {"new-name-left", "\xEE\xA4\x85"}, - {"new-name-right", "\xEE\xA4\x86"}, - {"new-name-sharp-left", "\xEE\xA4\x9B"}, - {"new-name-sharp-right", "\xEE\xA4\x9C"}, - {"new-name-slight-left", "\xEE\xA4\x87"}, - {"new-name-slight-right", "\xEE\xA4\x88"}, - {"new-name-straight", "\xEE\xA4\x84"}, - {"notification-left", "\xEE\xA4\x85"}, - {"notification-right", "\xEE\xA4\x86"}, - {"notification-sharp-left", "\xEE\xA4\x9B"}, - {"notification-sharp-right", "\xEE\xA4\xA5"}, - {"notification-slight-left", "\xEE\xA4\x87"}, - {"notification-slight-right", "\xEE\xA4\x88"}, - {"notification-straight", "\xEE\xA4\x84"}, - {"off-ramp-left", "\xEE\xA4\x9D"}, - {"off-ramp-right", "\xEE\xA4\x9E"}, - {"off-ramp-slight-left", "\xEE\xA4\x9F"}, - {"off-ramp-slight-right", "\xEE\xA4\xA0"}, - {"on-ramp-left", "\xEE\xA4\x85"}, - {"on-ramp-right", "\xEE\xA4\x86"}, - {"on-ramp-sharp-left", "\xEE\xA4\x9B"}, - {"on-ramp-sharp-right", "\xEE\xA4\xA5"}, - {"on-ramp-slight-left", "\xEE\xA4\x87"}, - {"on-ramp-slight-right", "\xEE\xA4\x88"}, - {"on-ramp-straight", "\xEE\xA4\x84"}, - {"rotary", "\xEE\xA4\xA1"}, - {"rotary-left", "\xEE\xA4\xA2"}, - {"rotary-right", "\xEE\xA4\xA3"}, - {"rotary-sharp-left", "\xEE\xA4\xA4"}, - {"rotary-sharp-right", "\xEE\xA4\xA5"}, - {"rotary-slight-left", "\xEE\xA4\xA6"}, - {"rotary-slight-right", "\xEE\xA4\xA7"}, - {"rotary-straight", "\xEE\xA4\xA8"}, - {"roundabout", "\xEE\xA4\xA1"}, - {"roundabout-left", "\xEE\xA4\xA2"}, - {"roundabout-right", "\xEE\xA4\xA3"}, - {"roundabout-sharp-left", "\xEE\xA4\xA4"}, - {"roundabout-sharp-right", "\xEE\xA4\xA5"}, - {"roundabout-slight-left", "\xEE\xA4\xA6"}, - {"roundabout-slight-right", "\xEE\xA4\xA7"}, - {"roundabout-straight", "\xEE\xA4\xA8"}, - {"turn-left", "\xEE\xA4\x85"}, - {"turn-right", "\xEE\xA4\x86"}, - {"turn-sharp-left", "\xEE\xA4\x9B"}, - {"turn-sharp-right", "\xEE\xA4\xA5"}, - {"turn-slight-left", "\xEE\xA4\x87"}, - {"turn-slight-right", "\xEE\xA4\x88"}, - {"turn-straight", "\xEE\xA4\x84"}, - {"updown", "\xEE\xA4\xA9"}, - {"uturn", "\xEE\xA4\x89"}, + struct Icon { + const char* fileName; + int16_t offset; + }; + + constexpr uint16_t iconHeight = -80; + constexpr uint8_t flagIndex = 18; + constexpr uint8_t maxIconsPerFile = 25; + const char* iconsFile0 = "F:/images/navigation0.bin"; + const char* iconsFile1 = "F:/images/navigation1.bin"; + + constexpr std::array<std::pair<const char*, uint8_t>, 86> iconMap = {{ + {"arrive-left", 1}, + {"arrive-right", 2}, + {"arrive-straight", 0}, + {"arrive", 0}, + {"close", 3}, + {"continue-left", 5}, + {"continue-right", 6}, + {"continue-slight-left", 7}, + {"continue-slight-right", 8}, + {"continue-straight", 4}, + {"continue-uturn", 9}, + {"continue", 4}, + {"depart-left", 11}, + {"depart-right", 12}, + {"depart-straight", 10}, + {"end-of-road-left", 13}, + {"end-of-road-right", 14}, + {"ferry", 15}, + {"flag", 16}, + {"fork-left", 18}, + {"fork-right", 19}, + {"fork-slight-left", 20}, + {"fork-slight-right", 21}, + {"fork-straight", 22}, + {"invalid", 4}, + {"invalid-left", 5}, + {"invalid-right", 6}, + {"invalid-slight-left", 7}, + {"invalid-slight-right", 8}, + {"invalid-straight", 4}, + {"invalid-uturn", 9}, + {"merge-left", 23}, + {"merge-right", 24}, + {"merge-slight-left", 25}, + {"merge-slight-right", 26}, + {"merge-straight", 4}, + {"new-name-left", 5}, + {"new-name-right", 6}, + {"new-name-sharp-left", 27}, + {"new-name-sharp-right", 28}, + {"new-name-slight-left", 7}, + {"new-name-slight-right", 8}, + {"new-name-straight", 4}, + {"notification-left", 5}, + {"notification-right", 6}, + {"notification-sharp-left", 27}, + {"notification-sharp-right", 37}, + {"notification-slight-left", 7}, + {"notification-slight-right", 8}, + {"notification-straight", 4}, + {"off-ramp-left", 29}, + {"off-ramp-right", 30}, + {"off-ramp-slight-left", 31}, + {"off-ramp-slight-right", 32}, + {"on-ramp-left", 5}, + {"on-ramp-right", 6}, + {"on-ramp-sharp-left", 27}, + {"on-ramp-sharp-right", 37}, + {"on-ramp-slight-left", 7}, + {"on-ramp-slight-right", 8}, + {"on-ramp-straight", 4}, + {"rotary", 33}, + {"rotary-left", 34}, + {"rotary-right", 35}, + {"rotary-sharp-left", 36}, + {"rotary-sharp-right", 37}, + {"rotary-slight-left", 38}, + {"rotary-slight-right", 39}, + {"rotary-straight", 40}, + {"roundabout", 33}, + {"roundabout-left", 34}, + {"roundabout-right", 35}, + {"roundabout-sharp-left", 36}, + {"roundabout-sharp-right", 37}, + {"roundabout-slight-left", 38}, + {"roundabout-slight-right", 39}, + {"roundabout-straight", 40}, + {"turn-left", 5}, + {"turn-right", 6}, + {"turn-sharp-left", 27}, + {"turn-sharp-right", 37}, + {"turn-slight-left", 7}, + {"turn-slight-right", 8}, + {"turn-straight", 4}, + {"updown", 41}, + {"uturn", 9}, }}; - const char* iconForName(const std::string& icon) { - for (auto iter : m_iconMap) { + Icon GetIcon(uint8_t index) { + if (index < maxIconsPerFile) { + return {iconsFile0, static_cast<int16_t>(iconHeight * index)}; + } + return {iconsFile1, static_cast<int16_t>(iconHeight * (index - maxIconsPerFile))}; + } + + Icon GetIcon(const std::string& icon) { + for (const auto& iter : iconMap) { if (iter.first == icon) { - return iter.second; + return GetIcon(iter.second); } } - return "\xEE\xA4\x90"; + return GetIcon(flagIndex); } } @@ -130,27 +191,33 @@ namespace { * */ Navigation::Navigation(Pinetime::Controllers::NavigationService& nav) : navService(nav) { - - imgFlag = lv_label_create(lv_scr_act(), nullptr); - lv_obj_set_style_local_text_font(imgFlag, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_navi_80); - lv_obj_set_style_local_text_color(imgFlag, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_CYAN); - lv_label_set_text_static(imgFlag, iconForName("flag")); + const auto& image = GetIcon("flag"); + imgFlag = lv_img_create(lv_scr_act(), nullptr); + lv_img_set_auto_size(imgFlag, false); + lv_obj_set_size(imgFlag, 80, 80); + lv_img_set_src(imgFlag, image.fileName); + lv_img_set_offset_x(imgFlag, 0); + lv_img_set_offset_y(imgFlag, image.offset); + lv_obj_set_style_local_image_recolor_opa(imgFlag, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_COVER); + lv_obj_set_style_local_image_recolor(imgFlag, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_CYAN); lv_obj_align(imgFlag, nullptr, LV_ALIGN_CENTER, 0, -60); txtNarrative = lv_label_create(lv_scr_act(), nullptr); - lv_label_set_long_mode(txtNarrative, LV_LABEL_LONG_BREAK); + lv_label_set_long_mode(txtNarrative, LV_LABEL_LONG_DOT); lv_obj_set_width(txtNarrative, LV_HOR_RES); + lv_obj_set_height(txtNarrative, 80); lv_label_set_text_static(txtNarrative, "Navigation"); lv_label_set_align(txtNarrative, LV_LABEL_ALIGN_CENTER); - lv_obj_align(txtNarrative, nullptr, LV_ALIGN_CENTER, 0, 10); + lv_obj_align(txtNarrative, nullptr, LV_ALIGN_CENTER, 0, 30); txtManDist = lv_label_create(lv_scr_act(), nullptr); lv_label_set_long_mode(txtManDist, LV_LABEL_LONG_BREAK); lv_obj_set_style_local_text_color(txtManDist, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN); + lv_obj_set_style_local_text_font(txtManDist, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_42); lv_obj_set_width(txtManDist, LV_HOR_RES); lv_label_set_text_static(txtManDist, "--M"); lv_label_set_align(txtManDist, LV_LABEL_ALIGN_CENTER); - lv_obj_align(txtManDist, nullptr, LV_ALIGN_CENTER, 0, 60); + lv_obj_align(txtManDist, nullptr, LV_ALIGN_CENTER, 0, 90); // Route Progress barProgress = lv_bar_create(lv_scr_act(), nullptr); @@ -173,7 +240,11 @@ Navigation::~Navigation() { void Navigation::Refresh() { if (flag != navService.getFlag()) { flag = navService.getFlag(); - lv_label_set_text_static(imgFlag, iconForName(flag)); + const auto& image = GetIcon(flag); + lv_img_set_src(imgFlag, image.fileName); + lv_obj_set_style_local_image_recolor_opa(imgFlag, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_COVER); + lv_obj_set_style_local_image_recolor(imgFlag, LV_IMG_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_CYAN); + lv_img_set_offset_y(imgFlag, image.offset); } if (narrative != navService.getNarrative()) { @@ -196,3 +267,19 @@ void Navigation::Refresh() { } } } + +bool Navigation::IsAvailable(Pinetime::Controllers::FS& filesystem) { + lfs_file file = {}; + + if (filesystem.FileOpen(&file, "/images/navigation0.bin", LFS_O_RDONLY) < 0) { + return false; + } + filesystem.FileClose(&file); + + if (filesystem.FileOpen(&file, "/images/navigation1.bin", LFS_O_RDONLY) < 0) { + return false; + } + filesystem.FileClose(&file); + + return true; +} diff --git a/src/displayapp/screens/Navigation.h b/src/displayapp/screens/Navigation.h index 6495edb2..5c7a0429 100644 --- a/src/displayapp/screens/Navigation.h +++ b/src/displayapp/screens/Navigation.h @@ -22,20 +22,25 @@ #include <string> #include "displayapp/screens/Screen.h" #include <array> +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" namespace Pinetime { namespace Controllers { class NavigationService; + class FS; } namespace Applications { namespace Screens { class Navigation : public Screen { public: - Navigation(Pinetime::Controllers::NavigationService& nav); + explicit Navigation(Pinetime::Controllers::NavigationService& nav); ~Navigation() override; void Refresh() override; + static bool IsAvailable(Pinetime::Controllers::FS& filesystem); private: lv_obj_t* imgFlag; @@ -48,10 +53,20 @@ namespace Pinetime { std::string flag; std::string narrative; std::string manDist; - int progress; + int progress = 0; lv_task_t* taskRefresh; }; } + + template <> + struct AppTraits<Apps::Navigation> { + static constexpr Apps app = Apps::Navigation; + static constexpr const char* icon = Screens::Symbols::map; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Navigation(*controllers.navigationService); + }; + }; } } diff --git a/src/displayapp/screens/Notifications.cpp b/src/displayapp/screens/Notifications.cpp index 037c43a7..837c4683 100644 --- a/src/displayapp/screens/Notifications.cpp +++ b/src/displayapp/screens/Notifications.cpp @@ -20,7 +20,7 @@ Notifications::Notifications(DisplayApp* app, notificationManager {notificationManager}, alertNotificationService {alertNotificationService}, motorController {motorController}, - systemTask {systemTask}, + wakeLock(systemTask), mode {mode} { notificationManager.ClearNewNotificationFlag(); @@ -40,7 +40,7 @@ Notifications::Notifications(DisplayApp* app, validDisplay = false; } if (mode == Modes::Preview) { - systemTask.PushMessage(System::Messages::DisableSleeping); + wakeLock.Lock(); if (notification.category == Controllers::NotificationManager::Categories::IncomingCall) { motorController.StartRinging(); } else { @@ -65,7 +65,6 @@ Notifications::~Notifications() { lv_task_del(taskRefresh); // make sure we stop any vibrations before exiting motorController.StopRinging(); - systemTask.PushMessage(System::Messages::EnableSleeping); lv_obj_clean(lv_scr_act()); } @@ -82,7 +81,6 @@ void Notifications::Refresh() { } else if (mode == Modes::Preview && dismissingNotification) { running = false; - currentItem = std::make_unique<NotificationItem>(alertNotificationService, motorController); } else if (dismissingNotification) { dismissingNotification = false; @@ -113,15 +111,15 @@ void Notifications::Refresh() { alertNotificationService, motorController); } else { - currentItem = std::make_unique<NotificationItem>(alertNotificationService, motorController); + running = false; } } - running = currentItem->IsRunning() && running; + running = running && currentItem->IsRunning(); } void Notifications::OnPreviewInteraction() { - systemTask.PushMessage(System::Messages::EnableSleeping); + wakeLock.Release(); motorController.StopRinging(); if (timeoutLine != nullptr) { lv_obj_del(timeoutLine); @@ -173,7 +171,9 @@ bool Notifications::OnTouchEvent(Pinetime::Applications::TouchEvents event) { } else if (nextMessage.valid) { currentId = nextMessage.id; } else { - // don't update id, won't be found be refresh and try to load latest message or no message box + // don't update id, notification manager will try to fetch + // but not find it. Refresh will try to load latest message + // or dismiss to watchface } DismissToBlack(); return true; @@ -246,8 +246,8 @@ namespace { Notifications::NotificationItem::NotificationItem(Pinetime::Controllers::AlertNotificationService& alertNotificationService, Pinetime::Controllers::MotorController& motorController) - : NotificationItem("Notification", - "No notification to display", + : NotificationItem("Notifications", + "No notifications to display", 0, Controllers::NotificationManager::Categories::Unknown, 0, diff --git a/src/displayapp/screens/Notifications.h b/src/displayapp/screens/Notifications.h index 114316b3..8488dc5b 100644 --- a/src/displayapp/screens/Notifications.h +++ b/src/displayapp/screens/Notifications.h @@ -8,6 +8,7 @@ #include "components/ble/NotificationManager.h" #include "components/motor/MotorController.h" #include "systemtask/SystemTask.h" +#include "systemtask/WakeLock.h" namespace Pinetime { namespace Controllers { @@ -73,7 +74,7 @@ namespace Pinetime { Pinetime::Controllers::NotificationManager& notificationManager; Pinetime::Controllers::AlertNotificationService& alertNotificationService; Pinetime::Controllers::MotorController& motorController; - System::SystemTask& systemTask; + System::WakeLock wakeLock; Modes mode = Modes::Normal; std::unique_ptr<NotificationItem> currentItem; Pinetime::Controllers::NotificationManager::Notification::Id currentId; diff --git a/src/displayapp/screens/Paddle.h b/src/displayapp/screens/Paddle.h index 33dac191..586cccf4 100644 --- a/src/displayapp/screens/Paddle.h +++ b/src/displayapp/screens/Paddle.h @@ -3,6 +3,9 @@ #include <lvgl/lvgl.h> #include <cstdint> #include "displayapp/screens/Screen.h" +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" namespace Pinetime { namespace Components { @@ -45,5 +48,15 @@ namespace Pinetime { lv_task_t* taskRefresh; }; } + + template <> + struct AppTraits<Apps::Paddle> { + static constexpr Apps app = Apps::Paddle; + static constexpr const char* icon = Screens::Symbols::paddle; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Paddle(controllers.lvgl); + }; + }; } } diff --git a/src/displayapp/screens/Screen.h b/src/displayapp/screens/Screen.h index 09bd6131..9f6e0ede 100644 --- a/src/displayapp/screens/Screen.h +++ b/src/displayapp/screens/Screen.h @@ -9,41 +9,6 @@ namespace Pinetime { class DisplayApp; namespace Screens { - - template <class T> - class DirtyValue { - public: - DirtyValue() = default; // Use NSDMI - - explicit DirtyValue(T const& v) : value {v} { - } // Use MIL and const-lvalue-ref - - bool IsUpdated() { - if (this->isUpdated) { - this->isUpdated = false; - return true; - } - return false; - } - - T const& Get() { - this->isUpdated = false; - return value; - } // never expose a non-const lvalue-ref - - DirtyValue& operator=(const T& other) { - if (this->value != other) { - this->value = other; - this->isUpdated = true; - } - return *this; - } - - private: - T value {}; // NSDMI - default initialise type - bool isUpdated {true}; // NSDMI - use brace initialisation - }; - class Screen { private: virtual void Refresh() { diff --git a/src/displayapp/screens/Steps.h b/src/displayapp/screens/Steps.h index 5dc07eff..6443582f 100644 --- a/src/displayapp/screens/Steps.h +++ b/src/displayapp/screens/Steps.h @@ -4,6 +4,9 @@ #include <lvgl/lvgl.h> #include "displayapp/screens/Screen.h" #include <components/motion/MotionController.h> +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" namespace Pinetime { @@ -39,5 +42,15 @@ namespace Pinetime { lv_task_t* taskRefresh; }; } + + template <> + struct AppTraits<Apps::Steps> { + static constexpr Apps app = Apps::Steps; + static constexpr const char* icon = Screens::Symbols::shoe; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Steps(controllers.motionController, controllers.settingsController); + }; + }; } } diff --git a/src/displayapp/screens/StopWatch.cpp b/src/displayapp/screens/StopWatch.cpp index 72904c88..ff852beb 100644 --- a/src/displayapp/screens/StopWatch.cpp +++ b/src/displayapp/screens/StopWatch.cpp @@ -12,8 +12,9 @@ namespace { const int hundredths = (timeElapsedCentis % 100); const int secs = (timeElapsedCentis / 100) % 60; - const int mins = (timeElapsedCentis / 100) / 60; - return TimeSeparated_t {mins, secs, hundredths}; + const int mins = ((timeElapsedCentis / 100) / 60) % 60; + const int hours = ((timeElapsedCentis / 100) / 60) / 60; + return TimeSeparated_t {hours, mins, secs, hundredths}; } void play_pause_event_handler(lv_obj_t* obj, lv_event_t event) { @@ -33,7 +34,7 @@ namespace { constexpr TickType_t blinkInterval = pdMS_TO_TICKS(1000); } -StopWatch::StopWatch(System::SystemTask& systemTask) : systemTask {systemTask} { +StopWatch::StopWatch(System::SystemTask& systemTask) : wakeLock(systemTask) { static constexpr uint8_t btnWidth = 115; static constexpr uint8_t btnHeight = 80; btnPlayPause = lv_btn_create(lv_scr_act(), nullptr); @@ -78,7 +79,6 @@ StopWatch::StopWatch(System::SystemTask& systemTask) : systemTask {systemTask} { StopWatch::~StopWatch() { lv_task_del(taskRefresh); - systemTask.PushMessage(Pinetime::System::Messages::EnableSleeping); lv_obj_clean(lv_scr_act()); } @@ -110,6 +110,12 @@ void StopWatch::SetInterfaceStopped() { lv_label_set_text_static(time, "00:00"); lv_label_set_text_static(msecTime, "00"); + if (isHoursLabelUpdated) { + lv_obj_set_style_local_text_font(time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); + lv_obj_realign(time); + isHoursLabelUpdated = false; + } + lv_label_set_text_static(lapText, ""); lv_label_set_text_static(txtPlayPause, Symbols::play); lv_label_set_text_static(txtStopLap, Symbols::lapsFlag); @@ -128,7 +134,7 @@ void StopWatch::Start() { SetInterfaceRunning(); startTime = xTaskGetTickCount(); currentState = States::Running; - systemTask.PushMessage(Pinetime::System::Messages::DisableSleeping); + wakeLock.Lock(); } void StopWatch::Pause() { @@ -138,7 +144,7 @@ void StopWatch::Pause() { oldTimeElapsed = laps[lapsDone]; blinkTime = xTaskGetTickCount() + blinkInterval; currentState = States::Halted; - systemTask.PushMessage(Pinetime::System::Messages::EnableSleeping); + wakeLock.Release(); } void StopWatch::Refresh() { @@ -146,7 +152,16 @@ void StopWatch::Refresh() { laps[lapsDone] = oldTimeElapsed + xTaskGetTickCount() - startTime; TimeSeparated_t currentTimeSeparated = convertTicksToTimeSegments(laps[lapsDone]); - lv_label_set_text_fmt(time, "%02d:%02d", currentTimeSeparated.mins, currentTimeSeparated.secs); + if (currentTimeSeparated.hours == 0) { + lv_label_set_text_fmt(time, "%02d:%02d", currentTimeSeparated.mins, currentTimeSeparated.secs); + } else { + lv_label_set_text_fmt(time, "%02d:%02d:%02d", currentTimeSeparated.hours, currentTimeSeparated.mins, currentTimeSeparated.secs); + if (!isHoursLabelUpdated) { + lv_obj_set_style_local_text_font(time, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_42); + lv_obj_realign(time); + isHoursLabelUpdated = true; + } + } lv_label_set_text_fmt(msecTime, "%02d", currentTimeSeparated.hundredths); } else if (currentState == States::Halted) { const TickType_t currentTime = xTaskGetTickCount(); @@ -182,8 +197,12 @@ void StopWatch::stopLapBtnEventHandler() { continue; } TimeSeparated_t times = convertTicksToTimeSegments(laps[i]); - char buffer[16]; - sprintf(buffer, "#%2d %2d:%02d.%02d\n", i + 1, times.mins, times.secs, times.hundredths); + char buffer[17]; + if (times.hours == 0) { + snprintf(buffer, sizeof(buffer), "#%2d %2d:%02d.%02d\n", i + 1, times.mins, times.secs, times.hundredths); + } else { + snprintf(buffer, sizeof(buffer), "#%2d %2d:%02d:%02d.%02d\n", i + 1, times.hours, times.mins, times.secs, times.hundredths); + } lv_label_ins_text(lapText, LV_LABEL_POS_LAST, buffer); } } else if (currentState == States::Halted) { diff --git a/src/displayapp/screens/StopWatch.h b/src/displayapp/screens/StopWatch.h index 409e3a19..55a178dc 100644 --- a/src/displayapp/screens/StopWatch.h +++ b/src/displayapp/screens/StopWatch.h @@ -7,48 +7,68 @@ #include "portmacro_cmsis.h" #include "systemtask/SystemTask.h" +#include "systemtask/WakeLock.h" +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" -namespace Pinetime::Applications::Screens { +namespace Pinetime { + namespace Applications { + namespace Screens { - enum class States { Init, Running, Halted }; + enum class States { Init, Running, Halted }; - struct TimeSeparated_t { - int mins; - int secs; - int hundredths; - }; + struct TimeSeparated_t { + int hours; + int mins; + int secs; + int hundredths; + }; - class StopWatch : public Screen { - public: - explicit StopWatch(System::SystemTask& systemTask); - ~StopWatch() override; - void Refresh() override; + class StopWatch : public Screen { + public: + explicit StopWatch(System::SystemTask& systemTask); + ~StopWatch() override; + void Refresh() override; - void playPauseBtnEventHandler(); - void stopLapBtnEventHandler(); - bool OnButtonPushed() override; + void playPauseBtnEventHandler(); + void stopLapBtnEventHandler(); + bool OnButtonPushed() override; - private: - void SetInterfacePaused(); - void SetInterfaceRunning(); - void SetInterfaceStopped(); + private: + void SetInterfacePaused(); + void SetInterfaceRunning(); + void SetInterfaceStopped(); - void Reset(); - void Start(); - void Pause(); + void Reset(); + void Start(); + void Pause(); - Pinetime::System::SystemTask& systemTask; - States currentState = States::Init; - TickType_t startTime; - TickType_t oldTimeElapsed = 0; - TickType_t blinkTime = 0; - static constexpr int maxLapCount = 20; - TickType_t laps[maxLapCount + 1]; - static constexpr int displayedLaps = 2; - int lapsDone = 0; - lv_obj_t *time, *msecTime, *btnPlayPause, *btnStopLap, *txtPlayPause, *txtStopLap; - lv_obj_t* lapText; + Pinetime::System::WakeLock wakeLock; + States currentState = States::Init; + TickType_t startTime; + TickType_t oldTimeElapsed = 0; + TickType_t blinkTime = 0; + static constexpr int maxLapCount = 20; + TickType_t laps[maxLapCount + 1]; + static constexpr int displayedLaps = 2; + int lapsDone = 0; + lv_obj_t *time, *msecTime, *btnPlayPause, *btnStopLap, *txtPlayPause, *txtStopLap; + lv_obj_t* lapText; + bool isHoursLabelUpdated = false; - lv_task_t* taskRefresh; - }; + lv_task_t* taskRefresh; + }; + } + + template <> + struct AppTraits<Apps::StopWatch> { + static constexpr Apps app = Apps::StopWatch; + static constexpr const char* icon = Screens::Symbols::stopWatch; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::StopWatch(*controllers.systemTask); + }; + }; + } } diff --git a/src/displayapp/screens/Symbols.h b/src/displayapp/screens/Symbols.h index 934cdc3f..40699b3d 100644 --- a/src/displayapp/screens/Symbols.h +++ b/src/displayapp/screens/Symbols.h @@ -11,6 +11,7 @@ namespace Pinetime { static constexpr const char* plug = "\xEF\x87\xA6"; static constexpr const char* shoe = "\xEF\x95\x8B"; static constexpr const char* clock = "\xEF\x80\x97"; + static constexpr const char* bell = "\xEF\x83\xB3"; static constexpr const char* info = "\xEF\x84\xA9"; static constexpr const char* list = "\xEF\x80\xBA"; static constexpr const char* sun = "\xEF\x86\x85"; @@ -34,9 +35,24 @@ namespace Pinetime { static constexpr const char* hourGlass = "\xEF\x89\x92"; static constexpr const char* lapsFlag = "\xEF\x80\xA4"; static constexpr const char* drum = "\xEF\x95\xA9"; + static constexpr const char* dice = "\xEF\x94\xA2"; static constexpr const char* eye = "\xEF\x81\xAE"; static constexpr const char* home = "\xEF\x80\x95"; static constexpr const char* sleep = "\xEE\xBD\x84"; + static constexpr const char* calculator = "\xEF\x87\xAC"; + static constexpr const char* backspace = "\xEF\x95\x9A"; + + // fontawesome_weathericons.c + // static constexpr const char* sun = "\xEF\x86\x85"; + static constexpr const char* cloudSun = "\xEF\x9B\x84"; + static constexpr const char* cloudSunRain = "\xEF\x9D\x83"; + static constexpr const char* cloudShowersHeavy = "\xEF\x9D\x80"; + static constexpr const char* smog = "\xEF\x9D\x9F"; + static constexpr const char* cloud = "\xEF\x83\x82"; + static constexpr const char* cloudMeatball = "\xEF\x9C\xBB"; + static constexpr const char* bolt = "\xEF\x83\xA7"; + static constexpr const char* snowflake = "\xEF\x8B\x9C"; + static constexpr const char* ban = "\xEF\x81\x9E"; // lv_font_sys_48.c static constexpr const char* settings = "\xEE\xA2\xB8"; diff --git a/src/displayapp/screens/SystemInfo.cpp b/src/displayapp/screens/SystemInfo.cpp index 511ecf50..2392f3be 100644 --- a/src/displayapp/screens/SystemInfo.cpp +++ b/src/displayapp/screens/SystemInfo.cpp @@ -38,15 +38,16 @@ SystemInfo::SystemInfo(Pinetime::Applications::DisplayApp* app, const Pinetime::Controllers::Ble& bleController, const Pinetime::Drivers::Watchdog& watchdog, Pinetime::Controllers::MotionController& motionController, - const Pinetime::Drivers::Cst816S& touchPanel) - : app {app}, - dateTimeController {dateTimeController}, + const Pinetime::Drivers::Cst816S& touchPanel, + const Pinetime::Drivers::SpiNorFlash& spiNorFlash) + : dateTimeController {dateTimeController}, batteryController {batteryController}, brightnessController {brightnessController}, bleController {bleController}, watchdog {watchdog}, motionController {motionController}, touchPanel {touchPanel}, + spiNorFlash {spiNorFlash}, screens {app, 0, {[this]() -> std::unique_ptr<Screen> { @@ -101,24 +102,24 @@ std::unique_ptr<Screen> SystemInfo::CreateScreen1() { std::unique_ptr<Screen> SystemInfo::CreateScreen2() { auto batteryPercent = batteryController.PercentRemaining(); const auto* resetReason = [this]() { - switch (watchdog.ResetReason()) { - case Drivers::Watchdog::ResetReasons::Watchdog: + switch (watchdog.GetResetReason()) { + case Drivers::Watchdog::ResetReason::Watchdog: return "wtdg"; - case Drivers::Watchdog::ResetReasons::HardReset: + case Drivers::Watchdog::ResetReason::HardReset: return "hardr"; - case Drivers::Watchdog::ResetReasons::NFC: + case Drivers::Watchdog::ResetReason::NFC: return "nfc"; - case Drivers::Watchdog::ResetReasons::SoftReset: + case Drivers::Watchdog::ResetReason::SoftReset: return "softr"; - case Drivers::Watchdog::ResetReasons::CpuLockup: + case Drivers::Watchdog::ResetReason::CpuLockup: return "cpulock"; - case Drivers::Watchdog::ResetReasons::SystemOff: + case Drivers::Watchdog::ResetReason::SystemOff: return "off"; - case Drivers::Watchdog::ResetReasons::LpComp: + case Drivers::Watchdog::ResetReason::LpComp: return "lpcomp"; - case Drivers::Watchdog::ResetReasons::DebugInterface: + case Drivers::Watchdog::ResetReason::DebugInterface: return "dbg"; - case Drivers::Watchdog::ResetReasons::ResetPin: + case Drivers::Watchdog::ResetReason::ResetPin: return "rst"; default: return "?"; @@ -177,6 +178,8 @@ std::unique_ptr<Screen> SystemInfo::CreateScreen2() { return std::make_unique<Screens::Label>(1, 5, label); } +extern int mallocFailedCount; +extern int stackOverflowCount; std::unique_ptr<Screen> SystemInfo::CreateScreen3() { lv_mem_monitor_t mon; lv_mem_monitor(&mon); @@ -184,26 +187,32 @@ std::unique_ptr<Screen> SystemInfo::CreateScreen3() { lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr); lv_label_set_recolor(label, true); const auto& bleAddr = bleController.Address(); + auto spiFlashId = spiNorFlash.GetIdentification(); lv_label_set_text_fmt(label, "#808080 BLE MAC#\n" - " %02x:%02x:%02x:%02x:%02x:%02x" + " %02x:%02x:%02x:%02x:%02x:%02x\n" "\n" - "#808080 LVGL Memory#\n" - " #808080 used# %d (%d%%)\n" - " #808080 max used# %lu\n" - " #808080 frag# %d%%\n" - " #808080 free# %d", + "#808080 SPI Flash# %02x-%02x-%02x\n" + "\n" + "#808080 Memory heap#\n" + " #808080 Free# %d/%d\n" + " #808080 Min free# %d\n" + " #808080 Alloc err# %d\n" + " #808080 Ovrfl err# %d", bleAddr[5], bleAddr[4], bleAddr[3], bleAddr[2], bleAddr[1], bleAddr[0], - static_cast<int>(mon.total_size - mon.free_size), - mon.used_pct, - mon.max_used, - mon.frag_pct, - static_cast<int>(mon.free_biggest_size)); + spiFlashId.manufacturer, + spiFlashId.type, + spiFlashId.density, + xPortGetFreeHeapSize(), + xPortGetHeapSize(), + xPortGetMinimumEverFreeHeapSize(), + mallocFailedCount, + stackOverflowCount); lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); return std::make_unique<Screens::Label>(2, 5, label); } @@ -232,11 +241,16 @@ std::unique_ptr<Screen> SystemInfo::CreateScreen4() { lv_table_set_col_width(infoTask, 3, 90); auto nb = uxTaskGetSystemState(tasksStatus, maxTaskCount, nullptr); +// g++ emits a spurious warning (and thus error because we compile with -Werror) +// due to the way std::sort is implemented +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" std::sort(tasksStatus, tasksStatus + nb, sortById); +#pragma GCC diagnostic pop for (uint8_t i = 0; i < nb && i < maxTaskCount; i++) { - char buffer[7] = {0}; + char buffer[11] = {0}; - sprintf(buffer, "%lu", tasksStatus[i].xTaskNumber); + snprintf(buffer, sizeof(buffer), "%lu", tasksStatus[i].xTaskNumber); lv_table_set_cell_value(infoTask, i + 1, 0, buffer); switch (tasksStatus[i].eCurrentState) { case eReady: @@ -260,9 +274,9 @@ std::unique_ptr<Screen> SystemInfo::CreateScreen4() { lv_table_set_cell_value(infoTask, i + 1, 1, buffer); lv_table_set_cell_value(infoTask, i + 1, 2, tasksStatus[i].pcTaskName); if (tasksStatus[i].usStackHighWaterMark < 20) { - sprintf(buffer, "%d low", tasksStatus[i].usStackHighWaterMark); + snprintf(buffer, sizeof(buffer), "%" PRIu16 " low", tasksStatus[i].usStackHighWaterMark); } else { - sprintf(buffer, "%d", tasksStatus[i].usStackHighWaterMark); + snprintf(buffer, sizeof(buffer), "%" PRIu16, tasksStatus[i].usStackHighWaterMark); } lv_table_set_cell_value(infoTask, i + 1, 3, buffer); } diff --git a/src/displayapp/screens/SystemInfo.h b/src/displayapp/screens/SystemInfo.h index 199af51e..301662dc 100644 --- a/src/displayapp/screens/SystemInfo.h +++ b/src/displayapp/screens/SystemInfo.h @@ -29,12 +29,12 @@ namespace Pinetime { const Pinetime::Controllers::Ble& bleController, const Pinetime::Drivers::Watchdog& watchdog, Pinetime::Controllers::MotionController& motionController, - const Pinetime::Drivers::Cst816S& touchPanel); + const Pinetime::Drivers::Cst816S& touchPanel, + const Pinetime::Drivers::SpiNorFlash& spiNorFlash); ~SystemInfo() override; bool OnTouchEvent(TouchEvents event) override; private: - DisplayApp* app; Pinetime::Controllers::DateTime& dateTimeController; const Pinetime::Controllers::Battery& batteryController; Pinetime::Controllers::BrightnessController& brightnessController; @@ -42,6 +42,7 @@ namespace Pinetime { const Pinetime::Drivers::Watchdog& watchdog; Pinetime::Controllers::MotionController& motionController; const Pinetime::Drivers::Cst816S& touchPanel; + const Pinetime::Drivers::SpiNorFlash& spiNorFlash; ScreenList<5> screens; diff --git a/src/displayapp/screens/Tile.cpp b/src/displayapp/screens/Tile.cpp index 1266f379..7c585a4b 100644 --- a/src/displayapp/screens/Tile.cpp +++ b/src/displayapp/screens/Tile.cpp @@ -1,5 +1,4 @@ #include "displayapp/screens/Tile.h" -#include "displayapp/DisplayApp.h" #include "displayapp/screens/BatteryIcon.h" #include "components/ble/BleController.h" #include "displayapp/InfiniTimeTheme.h" @@ -30,9 +29,13 @@ Tile::Tile(uint8_t screenID, Controllers::Settings& settingsController, const Controllers::Battery& batteryController, const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController, Controllers::DateTime& dateTimeController, std::array<Applications, 6>& applications) - : app {app}, dateTimeController {dateTimeController}, pageIndicator(screenID, numScreens), statusIcons(batteryController, bleController) { + : app {app}, + dateTimeController {dateTimeController}, + pageIndicator(screenID, numScreens), + statusIcons(batteryController, bleController, alarmController) { settingsController.SetAppMenu(screenID); @@ -76,7 +79,7 @@ Tile::Tile(uint8_t screenID, for (uint8_t i = 0; i < 6; i++) { lv_btnmatrix_set_btn_ctrl(btnm1, i, LV_BTNMATRIX_CTRL_CLICK_TRIG); - if (applications[i].application == Apps::None) { + if (applications[i].application == Apps::None || !applications[i].enabled) { lv_btnmatrix_set_btn_ctrl(btnm1, i, LV_BTNMATRIX_CTRL_DISABLED); } } diff --git a/src/displayapp/screens/Tile.h b/src/displayapp/screens/Tile.h index 91acb26c..c16151d0 100644 --- a/src/displayapp/screens/Tile.h +++ b/src/displayapp/screens/Tile.h @@ -4,7 +4,7 @@ #include <cstdint> #include <memory> #include "displayapp/screens/Screen.h" -#include "displayapp/Apps.h" +#include "displayapp/apps/Apps.h" #include "components/datetime/DateTimeController.h" #include "components/settings/Settings.h" #include "components/battery/BatteryController.h" @@ -19,6 +19,7 @@ namespace Pinetime { struct Applications { const char* icon; Pinetime::Applications::Apps application; + bool enabled; }; explicit Tile(uint8_t screenID, @@ -27,6 +28,7 @@ namespace Pinetime { Controllers::Settings& settingsController, const Controllers::Battery& batteryController, const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController, Controllers::DateTime& dateTimeController, std::array<Applications, 6>& applications); diff --git a/src/displayapp/screens/Timer.cpp b/src/displayapp/screens/Timer.cpp index df78a5a0..31cde733 100644 --- a/src/displayapp/screens/Timer.cpp +++ b/src/displayapp/screens/Timer.cpp @@ -17,7 +17,7 @@ static void btnEventHandler(lv_obj_t* obj, lv_event_t event) { } } -Timer::Timer(Controllers::TimerController& timerController) : timerController {timerController} { +Timer::Timer(Controllers::Timer& timerController) : timer {timerController} { lv_obj_t* colonLabel = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_font(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); @@ -59,10 +59,10 @@ Timer::Timer(Controllers::TimerController& timerController) : timerController {t lv_obj_set_event_cb(btnPlayPause, btnEventHandler); lv_obj_set_size(btnPlayPause, LV_HOR_RES, 50); - txtPlayPause = lv_label_create(lv_scr_act(), nullptr); - lv_obj_align(txtPlayPause, btnPlayPause, LV_ALIGN_CENTER, 0, 0); + // Create the label as a child of the button so it stays centered by default + txtPlayPause = lv_label_create(btnPlayPause, nullptr); - if (timerController.IsRunning()) { + if (timer.IsRunning()) { SetTimerRunning(); } else { SetTimerStopped(); @@ -85,7 +85,7 @@ void Timer::MaskReset() { buttonPressing = false; // A click event is processed before a release event, // so the release event would override the "Pause" text without this check - if (!timerController.IsRunning()) { + if (!timer.IsRunning()) { lv_label_set_text_static(txtPlayPause, "Start"); } maskPosition = 0; @@ -103,10 +103,8 @@ void Timer::UpdateMask() { } void Timer::Refresh() { - if (timerController.IsRunning()) { - auto secondsRemaining = std::chrono::duration_cast<std::chrono::seconds>(timerController.GetTimeRemaining()); - minuteCounter.SetValue(secondsRemaining.count() / 60); - secondCounter.SetValue(secondsRemaining.count() % 60); + if (timer.IsRunning()) { + DisplayTime(); } else if (buttonPressing && xTaskGetTickCount() > pressTime + pdMS_TO_TICKS(150)) { lv_label_set_text_static(txtPlayPause, "Reset"); maskPosition += 15; @@ -119,6 +117,14 @@ void Timer::Refresh() { } } +void Timer::DisplayTime() { + displaySeconds = std::chrono::duration_cast<std::chrono::seconds>(timer.GetTimeRemaining()); + if (displaySeconds.IsUpdated()) { + minuteCounter.SetValue(displaySeconds.Get().count() / 60); + secondCounter.SetValue(displaySeconds.Get().count() % 60); + } +} + void Timer::SetTimerRunning() { minuteCounter.HideControls(); secondCounter.HideControls(); @@ -132,22 +138,19 @@ void Timer::SetTimerStopped() { } void Timer::ToggleRunning() { - if (timerController.IsRunning()) { - auto secondsRemaining = std::chrono::duration_cast<std::chrono::seconds>(timerController.GetTimeRemaining()); - minuteCounter.SetValue(secondsRemaining.count() / 60); - secondCounter.SetValue(secondsRemaining.count() % 60); - timerController.StopTimer(); + if (timer.IsRunning()) { + DisplayTime(); + timer.StopTimer(); SetTimerStopped(); } else if (secondCounter.GetValue() + minuteCounter.GetValue() > 0) { auto timerDuration = std::chrono::minutes(minuteCounter.GetValue()) + std::chrono::seconds(secondCounter.GetValue()); - timerController.StartTimer(timerDuration); + timer.StartTimer(timerDuration); Refresh(); SetTimerRunning(); } } void Timer::Reset() { - minuteCounter.SetValue(0); - secondCounter.SetValue(0); + DisplayTime(); SetTimerStopped(); } diff --git a/src/displayapp/screens/Timer.h b/src/displayapp/screens/Timer.h index a6e26063..a07c729b 100644 --- a/src/displayapp/screens/Timer.h +++ b/src/displayapp/screens/Timer.h @@ -1,45 +1,60 @@ #pragma once #include "displayapp/screens/Screen.h" -#include "components/datetime/DateTimeController.h" #include "systemtask/SystemTask.h" #include "displayapp/LittleVgl.h" #include "displayapp/widgets/Counter.h" +#include "utility/DirtyValue.h" #include <lvgl/lvgl.h> -#include "components/timer/TimerController.h" +#include "components/timer/Timer.h" +#include "Symbols.h" -namespace Pinetime::Applications::Screens { - class Timer : public Screen { - public: - Timer(Controllers::TimerController& timerController); - ~Timer() override; - void Refresh() override; - void Reset(); - void ToggleRunning(); - void ButtonPressed(); - void MaskReset(); +namespace Pinetime::Applications { + namespace Screens { + class Timer : public Screen { + public: + Timer(Controllers::Timer& timerController); + ~Timer() override; + void Refresh() override; + void Reset(); + void ToggleRunning(); + void ButtonPressed(); + void MaskReset(); - private: - void SetTimerRunning(); - void SetTimerStopped(); - void UpdateMask(); - Controllers::TimerController& timerController; + private: + void SetTimerRunning(); + void SetTimerStopped(); + void UpdateMask(); + void DisplayTime(); + Pinetime::Controllers::Timer& timer; - lv_obj_t* btnPlayPause; - lv_obj_t* txtPlayPause; + lv_obj_t* btnPlayPause; + lv_obj_t* txtPlayPause; - lv_obj_t* btnObjectMask; - lv_obj_t* highlightObjectMask; - lv_objmask_mask_t* btnMask; - lv_objmask_mask_t* highlightMask; + lv_obj_t* btnObjectMask; + lv_obj_t* highlightObjectMask; + lv_objmask_mask_t* btnMask; + lv_objmask_mask_t* highlightMask; - lv_task_t* taskRefresh; - Widgets::Counter minuteCounter = Widgets::Counter(0, 59, jetbrains_mono_76); - Widgets::Counter secondCounter = Widgets::Counter(0, 59, jetbrains_mono_76); + lv_task_t* taskRefresh; + Widgets::Counter minuteCounter = Widgets::Counter(0, 59, jetbrains_mono_76); + Widgets::Counter secondCounter = Widgets::Counter(0, 59, jetbrains_mono_76); - bool buttonPressing = false; - lv_coord_t maskPosition = 0; - TickType_t pressTime = 0; + bool buttonPressing = false; + lv_coord_t maskPosition = 0; + TickType_t pressTime = 0; + Utility::DirtyValue<std::chrono::seconds> displaySeconds; + }; + } + + template <> + struct AppTraits<Apps::Timer> { + static constexpr Apps app = Apps::Timer; + static constexpr const char* icon = Screens::Symbols::hourGlass; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Timer(controllers.timer); + }; }; } diff --git a/src/displayapp/screens/Twos.cpp b/src/displayapp/screens/Twos.cpp index 8157a160..6f2eff40 100644 --- a/src/displayapp/screens/Twos.cpp +++ b/src/displayapp/screens/Twos.cpp @@ -242,7 +242,7 @@ void Twos::updateGridDisplay() { const unsigned int col = i % nCols; if (grid[row][col].value > 0) { char buffer[7]; - sprintf(buffer, "%d", grid[row][col].value); + snprintf(buffer, sizeof(buffer), "%u", grid[row][col].value); lv_table_set_cell_value(gridDisplay, row, col, buffer); } else { lv_table_set_cell_value(gridDisplay, row, col, ""); diff --git a/src/displayapp/screens/Twos.h b/src/displayapp/screens/Twos.h index e731eae6..52449fd3 100644 --- a/src/displayapp/screens/Twos.h +++ b/src/displayapp/screens/Twos.h @@ -1,7 +1,8 @@ #pragma once -#include <lvgl/src/lv_core/lv_obj.h> +#include "displayapp/apps/Apps.h" #include "displayapp/screens/Screen.h" +#include "displayapp/Controllers.h" namespace Pinetime { namespace Applications { @@ -35,5 +36,15 @@ namespace Pinetime { bool placeNewTile(); }; } + + template <> + struct AppTraits<Apps::Twos> { + static constexpr Apps app = Apps::Twos; + static constexpr const char* icon = "2"; + + static Screens::Screen* Create(AppControllers& /*controllers*/) { + return new Screens::Twos(); + }; + }; } } diff --git a/src/displayapp/screens/WatchFaceAnalog.cpp b/src/displayapp/screens/WatchFaceAnalog.cpp index 76d01cf1..80a1c8b9 100644 --- a/src/displayapp/screens/WatchFaceAnalog.cpp +++ b/src/displayapp/screens/WatchFaceAnalog.cpp @@ -8,8 +8,6 @@ #include "components/settings/Settings.h" #include "displayapp/InfiniTimeTheme.h" -LV_IMG_DECLARE(bg_clock); - using namespace Pinetime::Applications::Screens; namespace { @@ -60,9 +58,41 @@ WatchFaceAnalog::WatchFaceAnalog(Controllers::DateTime& dateTimeController, sMinute = 99; sSecond = 99; - lv_obj_t* bg_clock_img = lv_img_create(lv_scr_act(), nullptr); - lv_img_set_src(bg_clock_img, &bg_clock); - lv_obj_align(bg_clock_img, nullptr, LV_ALIGN_CENTER, 0, 0); + minor_scales = lv_linemeter_create(lv_scr_act(), nullptr); + lv_linemeter_set_scale(minor_scales, 300, 51); + lv_linemeter_set_angle_offset(minor_scales, 180); + lv_obj_set_size(minor_scales, 240, 240); + lv_obj_align(minor_scales, nullptr, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_local_bg_opa(minor_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + lv_obj_set_style_local_scale_width(minor_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 4); + lv_obj_set_style_local_scale_end_line_width(minor_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 1); + lv_obj_set_style_local_scale_end_color(minor_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); + + major_scales = lv_linemeter_create(lv_scr_act(), nullptr); + lv_linemeter_set_scale(major_scales, 300, 11); + lv_linemeter_set_angle_offset(major_scales, 180); + lv_obj_set_size(major_scales, 240, 240); + lv_obj_align(major_scales, nullptr, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_local_bg_opa(major_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + lv_obj_set_style_local_scale_width(major_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 6); + lv_obj_set_style_local_scale_end_line_width(major_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 4); + lv_obj_set_style_local_scale_end_color(major_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + + large_scales = lv_linemeter_create(lv_scr_act(), nullptr); + lv_linemeter_set_scale(large_scales, 180, 3); + lv_linemeter_set_angle_offset(large_scales, 180); + lv_obj_set_size(large_scales, 240, 240); + lv_obj_align(large_scales, nullptr, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_local_bg_opa(large_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_TRANSP); + lv_obj_set_style_local_scale_width(large_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 20); + lv_obj_set_style_local_scale_end_line_width(large_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, 4); + lv_obj_set_style_local_scale_end_color(large_scales, LV_LINEMETER_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_AQUA); + + twelve = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_align(twelve, LV_LABEL_ALIGN_CENTER); + lv_label_set_text_static(twelve, "12"); + lv_obj_set_pos(twelve, 110, 10); + lv_obj_set_style_local_text_color(twelve, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_AQUA); batteryIcon.Create(lv_scr_act()); lv_obj_align(batteryIcon.GetObject(), nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0); @@ -226,7 +256,7 @@ void WatchFaceAnalog::Refresh() { if (currentDateTime.IsUpdated()) { UpdateClock(); - currentDate = std::chrono::time_point_cast<days>(currentDateTime.Get()); + currentDate = std::chrono::time_point_cast<std::chrono::days>(currentDateTime.Get()); if (currentDate.IsUpdated()) { lv_label_set_text_fmt(label_date_day, "%s\n%02i", dateTimeController.DayOfWeekShortToString(), dateTimeController.Day()); } diff --git a/src/displayapp/screens/WatchFaceAnalog.h b/src/displayapp/screens/WatchFaceAnalog.h index b32293da..958ff64d 100644 --- a/src/displayapp/screens/WatchFaceAnalog.h +++ b/src/displayapp/screens/WatchFaceAnalog.h @@ -9,7 +9,8 @@ #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" #include "components/ble/NotificationManager.h" -#include <displayapp/screens/BatteryIcon.h> +#include "displayapp/screens/BatteryIcon.h" +#include "utility/DirtyValue.h" namespace Pinetime { namespace Controllers { @@ -37,13 +38,17 @@ namespace Pinetime { private: uint8_t sHour, sMinute, sSecond; - DirtyValue<uint8_t> batteryPercentRemaining {0}; - DirtyValue<bool> isCharging {}; - DirtyValue<bool> bleState {}; - DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>> currentDateTime; - DirtyValue<bool> notificationState {false}; - using days = std::chrono::duration<int32_t, std::ratio<86400>>; // TODO: days is standard in c++20 - DirtyValue<std::chrono::time_point<std::chrono::system_clock, days>> currentDate; + Utility::DirtyValue<uint8_t> batteryPercentRemaining {0}; + Utility::DirtyValue<bool> isCharging {}; + Utility::DirtyValue<bool> bleState {}; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>> currentDateTime; + Utility::DirtyValue<bool> notificationState {false}; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::days>> currentDate; + + lv_obj_t* minor_scales; + lv_obj_t* major_scales; + lv_obj_t* large_scales; + lv_obj_t* twelve; lv_obj_t* hour_body; lv_obj_t* hour_body_trace; @@ -70,7 +75,7 @@ namespace Pinetime { BatteryIcon batteryIcon; - const Controllers::DateTime& dateTimeController; + Controllers::DateTime& dateTimeController; const Controllers::Battery& batteryController; const Controllers::Ble& bleController; Controllers::NotificationManager& notificationManager; @@ -82,5 +87,23 @@ namespace Pinetime { lv_task_t* taskRefresh; }; } + + template <> + struct WatchFaceTraits<WatchFace::Analog> { + static constexpr WatchFace watchFace = WatchFace::Analog; + static constexpr const char* name = "Analog face"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceAnalog(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; } } diff --git a/src/displayapp/screens/WatchFaceCasioStyleG7710.cpp b/src/displayapp/screens/WatchFaceCasioStyleG7710.cpp index ca37c8fc..c695f852 100644 --- a/src/displayapp/screens/WatchFaceCasioStyleG7710.cpp +++ b/src/displayapp/screens/WatchFaceCasioStyleG7710.cpp @@ -48,14 +48,14 @@ WatchFaceCasioStyleG7710::WatchFaceCasioStyleG7710(Controllers::DateTime& dateTi font_segment115 = lv_font_load("F:/fonts/7segments_115.bin"); } - label_battery_vallue = lv_label_create(lv_scr_act(), nullptr); - lv_obj_align(label_battery_vallue, lv_scr_act(), LV_ALIGN_IN_TOP_RIGHT, 0, 0); - lv_obj_set_style_local_text_color(label_battery_vallue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); - lv_label_set_text_static(label_battery_vallue, "00%"); + label_battery_value = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(label_battery_value, lv_scr_act(), LV_ALIGN_IN_TOP_RIGHT, 0, 0); + lv_obj_set_style_local_text_color(label_battery_value, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); + lv_label_set_text_static(label_battery_value, "00%"); batteryIcon.Create(lv_scr_act()); batteryIcon.SetColor(color_text); - lv_obj_align(batteryIcon.GetObject(), label_battery_vallue, LV_ALIGN_OUT_LEFT_MID, -5, 0); + lv_obj_align(batteryIcon.GetObject(), label_battery_value, LV_ALIGN_OUT_LEFT_MID, -5, 0); batteryPlug = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(batteryPlug, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, color_text); @@ -203,7 +203,7 @@ void WatchFaceCasioStyleG7710::Refresh() { if (batteryPercentRemaining.IsUpdated()) { auto batteryPercent = batteryPercentRemaining.Get(); batteryIcon.SetBatteryPercentage(batteryPercent); - lv_label_set_text_fmt(label_battery_vallue, "%d%%", batteryPercent); + lv_label_set_text_fmt(label_battery_value, "%d%%", batteryPercent); } bleState = bleController.IsConnected(); @@ -211,7 +211,7 @@ void WatchFaceCasioStyleG7710::Refresh() { if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) { lv_label_set_text_static(bleIcon, BleIcon::GetIcon(bleState.Get())); } - lv_obj_realign(label_battery_vallue); + lv_obj_realign(label_battery_value); lv_obj_realign(batteryIcon.GetObject()); lv_obj_realign(batteryPlug); lv_obj_realign(bleIcon); @@ -222,43 +222,36 @@ void WatchFaceCasioStyleG7710::Refresh() { lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(notificationState.Get())); } - currentDateTime = dateTimeController.CurrentDateTime(); - + currentDateTime = std::chrono::time_point_cast<std::chrono::minutes>(dateTimeController.CurrentDateTime()); if (currentDateTime.IsUpdated()) { - auto hour = dateTimeController.Hours(); - auto minute = dateTimeController.Minutes(); - auto year = dateTimeController.Year(); - auto month = dateTimeController.Month(); - auto dayOfWeek = dateTimeController.DayOfWeek(); - auto day = dateTimeController.Day(); - auto dayOfYear = dateTimeController.DayOfYear(); - - auto weekNumberFormat = "%V"; + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); - if (displayedHour != hour || displayedMinute != minute) { - displayedHour = hour; - displayedMinute = minute; - - if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { - char ampmChar[2] = "A"; - if (hour == 0) { - hour = 12; - } else if (hour == 12) { - ampmChar[0] = 'P'; - } else if (hour > 12) { - hour = hour - 12; - ampmChar[0] = 'P'; - } - lv_label_set_text(label_time_ampm, ampmChar); - lv_label_set_text_fmt(label_time, "%2d:%02d", hour, minute); - lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_CENTER, 0, 40); - } else { - lv_label_set_text_fmt(label_time, "%02d:%02d", hour, minute); - lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_CENTER, 0, 40); + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + char ampmChar[2] = "A"; + if (hour == 0) { + hour = 12; + } else if (hour == 12) { + ampmChar[0] = 'P'; + } else if (hour > 12) { + hour = hour - 12; + ampmChar[0] = 'P'; } + lv_label_set_text(label_time_ampm, ampmChar); + lv_label_set_text_fmt(label_time, "%2d:%02d", hour, minute); + } else { + lv_label_set_text_fmt(label_time, "%02d:%02d", hour, minute); } + lv_obj_realign(label_time); + + currentDate = std::chrono::time_point_cast<std::chrono::days>(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + const char* weekNumberFormat = "%V"; - if ((year != currentYear) || (month != currentMonth) || (dayOfWeek != currentDayOfWeek) || (day != currentDay)) { + uint16_t year = dateTimeController.Year(); + Controllers::DateTime::Months month = dateTimeController.Month(); + uint8_t day = dateTimeController.Day(); + int dayOfYear = dateTimeController.DayOfYear(); if (settingsController.GetClockType() == Controllers::Settings::ClockType::H24) { // 24h mode: ddmmyyyy, first DOW=Monday; lv_label_set_text_fmt(label_date, "%3d-%2d", day, month); @@ -293,11 +286,6 @@ void WatchFaceCasioStyleG7710::Refresh() { lv_obj_realign(label_day_of_year); lv_obj_realign(label_week_number); lv_obj_realign(label_date); - - currentYear = year; - currentMonth = month; - currentDayOfWeek = dayOfWeek; - currentDay = day; } } @@ -317,8 +305,7 @@ void WatchFaceCasioStyleG7710::Refresh() { } stepCount = motionController.NbSteps(); - motionSensorOk = motionController.IsSensorOk(); - if (stepCount.IsUpdated() || motionSensorOk.IsUpdated()) { + if (stepCount.IsUpdated()) { lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); lv_obj_realign(stepValue); lv_obj_realign(stepIcon); diff --git a/src/displayapp/screens/WatchFaceCasioStyleG7710.h b/src/displayapp/screens/WatchFaceCasioStyleG7710.h index 0445c9f2..0f46a692 100644 --- a/src/displayapp/screens/WatchFaceCasioStyleG7710.h +++ b/src/displayapp/screens/WatchFaceCasioStyleG7710.h @@ -5,9 +5,12 @@ #include <chrono> #include <cstdint> #include <memory> +#include <displayapp/Controllers.h> #include "displayapp/screens/Screen.h" #include "components/datetime/DateTimeController.h" #include "components/ble/BleController.h" +#include "utility/DirtyValue.h" +#include "displayapp/apps/Apps.h" namespace Pinetime { namespace Controllers { @@ -39,24 +42,16 @@ namespace Pinetime { static bool IsAvailable(Pinetime::Controllers::FS& filesystem); private: - uint8_t displayedHour = -1; - uint8_t displayedMinute = -1; - - uint16_t currentYear = 1970; - Controllers::DateTime::Months currentMonth = Pinetime::Controllers::DateTime::Months::Unknown; - Controllers::DateTime::Days currentDayOfWeek = Pinetime::Controllers::DateTime::Days::Unknown; - uint8_t currentDay = 0; - - DirtyValue<uint8_t> batteryPercentRemaining {}; - DirtyValue<bool> powerPresent {}; - DirtyValue<bool> bleState {}; - DirtyValue<bool> bleRadioEnabled {}; - DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>> currentDateTime {}; - DirtyValue<bool> motionSensorOk {}; - DirtyValue<uint32_t> stepCount {}; - DirtyValue<uint8_t> heartbeat {}; - DirtyValue<bool> heartbeatRunning {}; - DirtyValue<bool> notificationState {}; + Utility::DirtyValue<uint8_t> batteryPercentRemaining {}; + Utility::DirtyValue<bool> powerPresent {}; + Utility::DirtyValue<bool> bleState {}; + Utility::DirtyValue<bool> bleRadioEnabled {}; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes>> currentDateTime {}; + Utility::DirtyValue<uint32_t> stepCount {}; + Utility::DirtyValue<uint8_t> heartbeat {}; + Utility::DirtyValue<bool> heartbeatRunning {}; + Utility::DirtyValue<bool> notificationState {}; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::days>> currentDate; lv_point_t line_icons_points[3] {{0, 5}, {117, 5}, {122, 0}}; lv_point_t line_day_of_week_number_points[4] {{0, 0}, {100, 0}, {95, 95}, {0, 95}}; @@ -82,7 +77,7 @@ namespace Pinetime { lv_obj_t* backgroundLabel; lv_obj_t* bleIcon; lv_obj_t* batteryPlug; - lv_obj_t* label_battery_vallue; + lv_obj_t* label_battery_value; lv_obj_t* heartbeatIcon; lv_obj_t* heartbeatValue; lv_obj_t* stepIcon; @@ -106,5 +101,26 @@ namespace Pinetime { lv_font_t* font_segment115 = nullptr; }; } + + template <> + struct WatchFaceTraits<WatchFace::CasioStyleG7710> { + static constexpr WatchFace watchFace = WatchFace::CasioStyleG7710; + static constexpr const char* name = "Casio G7710"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceCasioStyleG7710(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController, + controllers.filesystem); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& filesystem) { + return Screens::WatchFaceCasioStyleG7710::IsAvailable(filesystem); + } + }; } } diff --git a/src/displayapp/screens/WatchFaceDigital.cpp b/src/displayapp/screens/WatchFaceDigital.cpp index ad35b5c9..3163c6e7 100644 --- a/src/displayapp/screens/WatchFaceDigital.cpp +++ b/src/displayapp/screens/WatchFaceDigital.cpp @@ -2,13 +2,16 @@ #include <lvgl/lvgl.h> #include <cstdio> + #include "displayapp/screens/NotificationIcon.h" #include "displayapp/screens/Symbols.h" +#include "displayapp/screens/WeatherSymbols.h" #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" #include "components/ble/NotificationManager.h" #include "components/heartrate/HeartRateController.h" #include "components/motion/MotionController.h" +#include "components/ble/SimpleWeatherService.h" #include "components/settings/Settings.h" using namespace Pinetime::Applications::Screens; @@ -16,17 +19,20 @@ using namespace Pinetime::Applications::Screens; WatchFaceDigital::WatchFaceDigital(Controllers::DateTime& dateTimeController, const Controllers::Battery& batteryController, const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController, Controllers::NotificationManager& notificationManager, Controllers::Settings& settingsController, Controllers::HeartRateController& heartRateController, - Controllers::MotionController& motionController) + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherService) : currentDateTime {{}}, dateTimeController {dateTimeController}, notificationManager {notificationManager}, settingsController {settingsController}, heartRateController {heartRateController}, motionController {motionController}, - statusIcons(batteryController, bleController) { + weatherService {weatherService}, + statusIcons(batteryController, bleController, alarmController) { statusIcons.Create(); @@ -35,6 +41,18 @@ WatchFaceDigital::WatchFaceDigital(Controllers::DateTime& dateTimeController, lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(false)); lv_obj_align(notificationIcon, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0); + weatherIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); + lv_obj_set_style_local_text_font(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons); + lv_label_set_text(weatherIcon, ""); + lv_obj_align(weatherIcon, nullptr, LV_ALIGN_IN_TOP_MID, -20, 50); + lv_obj_set_auto_realign(weatherIcon, true); + + temperature = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); + lv_label_set_text(temperature, ""); + lv_obj_align(temperature, nullptr, LV_ALIGN_IN_TOP_MID, 20, 50); + label_date = lv_label_create(lv_scr_act(), nullptr); lv_obj_align(label_date, lv_scr_act(), LV_ALIGN_CENTER, 0, 60); lv_obj_set_style_local_text_color(label_date, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x999999)); @@ -85,40 +103,34 @@ void WatchFaceDigital::Refresh() { lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(notificationState.Get())); } - currentDateTime = dateTimeController.CurrentDateTime(); + currentDateTime = std::chrono::time_point_cast<std::chrono::minutes>(dateTimeController.CurrentDateTime()); if (currentDateTime.IsUpdated()) { - auto hour = dateTimeController.Hours(); - auto minute = dateTimeController.Minutes(); - auto year = dateTimeController.Year(); - auto month = dateTimeController.Month(); - auto dayOfWeek = dateTimeController.DayOfWeek(); - auto day = dateTimeController.Day(); + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); - if (displayedHour != hour || displayedMinute != minute) { - displayedHour = hour; - displayedMinute = minute; - - if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { - char ampmChar[3] = "AM"; - if (hour == 0) { - hour = 12; - } else if (hour == 12) { - ampmChar[0] = 'P'; - } else if (hour > 12) { - hour = hour - 12; - ampmChar[0] = 'P'; - } - lv_label_set_text(label_time_ampm, ampmChar); - lv_label_set_text_fmt(label_time, "%2d:%02d", hour, minute); - lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, 0, 0); - } else { - lv_label_set_text_fmt(label_time, "%02d:%02d", hour, minute); - lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + char ampmChar[3] = "AM"; + if (hour == 0) { + hour = 12; + } else if (hour == 12) { + ampmChar[0] = 'P'; + } else if (hour > 12) { + hour = hour - 12; + ampmChar[0] = 'P'; } + lv_label_set_text(label_time_ampm, ampmChar); + lv_label_set_text_fmt(label_time, "%2d:%02d", hour, minute); + lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, 0, 0); + } else { + lv_label_set_text_fmt(label_time, "%02d:%02d", hour, minute); + lv_obj_align(label_time, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); } - if ((year != currentYear) || (month != currentMonth) || (dayOfWeek != currentDayOfWeek) || (day != currentDay)) { + currentDate = std::chrono::time_point_cast<std::chrono::days>(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + uint16_t year = dateTimeController.Year(); + uint8_t day = dateTimeController.Day(); if (settingsController.GetClockType() == Controllers::Settings::ClockType::H24) { lv_label_set_text_fmt(label_date, "%s %d %s %d", @@ -135,11 +147,6 @@ void WatchFaceDigital::Refresh() { year); } lv_obj_realign(label_date); - - currentYear = year; - currentMonth = month; - currentDayOfWeek = dayOfWeek; - currentDay = day; } } @@ -159,10 +166,29 @@ void WatchFaceDigital::Refresh() { } stepCount = motionController.NbSteps(); - motionSensorOk = motionController.IsSensorOk(); - if (stepCount.IsUpdated() || motionSensorOk.IsUpdated()) { + if (stepCount.IsUpdated()) { lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); lv_obj_realign(stepValue); lv_obj_realign(stepIcon); } + + currentWeather = weatherService.Current(); + if (currentWeather.IsUpdated()) { + auto optCurrentWeather = currentWeather.Get(); + if (optCurrentWeather) { + int16_t temp = optCurrentWeather->temperature.Celsius(); + char tempUnit = 'C'; + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = optCurrentWeather->temperature.Fahrenheit(); + tempUnit = 'F'; + } + lv_label_set_text_fmt(temperature, "%d°%c", temp, tempUnit); + lv_label_set_text(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId)); + } else { + lv_label_set_text_static(temperature, ""); + lv_label_set_text(weatherIcon, ""); + } + lv_obj_realign(temperature); + lv_obj_realign(weatherIcon); + } } diff --git a/src/displayapp/screens/WatchFaceDigital.h b/src/displayapp/screens/WatchFaceDigital.h index 0931f007..3005cea5 100644 --- a/src/displayapp/screens/WatchFaceDigital.h +++ b/src/displayapp/screens/WatchFaceDigital.h @@ -6,14 +6,18 @@ #include <memory> #include "displayapp/screens/Screen.h" #include "components/datetime/DateTimeController.h" +#include "components/ble/SimpleWeatherService.h" #include "components/ble/BleController.h" #include "displayapp/widgets/StatusIcons.h" +#include "utility/DirtyValue.h" +#include "displayapp/apps/Apps.h" namespace Pinetime { namespace Controllers { class Settings; class Battery; class Ble; + class AlarmController; class NotificationManager; class HeartRateController; class MotionController; @@ -27,10 +31,12 @@ namespace Pinetime { WatchFaceDigital(Controllers::DateTime& dateTimeController, const Controllers::Battery& batteryController, const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController, Controllers::NotificationManager& notificationManager, Controllers::Settings& settingsController, Controllers::HeartRateController& heartRateController, - Controllers::MotionController& motionController); + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weather); ~WatchFaceDigital() override; void Refresh() override; @@ -39,21 +45,14 @@ namespace Pinetime { uint8_t displayedHour = -1; uint8_t displayedMinute = -1; - uint16_t currentYear = 1970; - Controllers::DateTime::Months currentMonth = Pinetime::Controllers::DateTime::Months::Unknown; - Controllers::DateTime::Days currentDayOfWeek = Pinetime::Controllers::DateTime::Days::Unknown; - uint8_t currentDay = 0; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes>> currentDateTime {}; + Utility::DirtyValue<uint32_t> stepCount {}; + Utility::DirtyValue<uint8_t> heartbeat {}; + Utility::DirtyValue<bool> heartbeatRunning {}; + Utility::DirtyValue<bool> notificationState {}; + Utility::DirtyValue<std::optional<Pinetime::Controllers::SimpleWeatherService::CurrentWeather>> currentWeather {}; - DirtyValue<uint8_t> batteryPercentRemaining {}; - DirtyValue<bool> powerPresent {}; - DirtyValue<bool> bleState {}; - DirtyValue<bool> bleRadioEnabled {}; - DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>> currentDateTime {}; - DirtyValue<bool> motionSensorOk {}; - DirtyValue<uint32_t> stepCount {}; - DirtyValue<uint8_t> heartbeat {}; - DirtyValue<bool> heartbeatRunning {}; - DirtyValue<bool> notificationState {}; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::days>> currentDate; lv_obj_t* label_time; lv_obj_t* label_time_ampm; @@ -63,16 +62,41 @@ namespace Pinetime { lv_obj_t* stepIcon; lv_obj_t* stepValue; lv_obj_t* notificationIcon; + lv_obj_t* weatherIcon; + lv_obj_t* temperature; Controllers::DateTime& dateTimeController; Controllers::NotificationManager& notificationManager; Controllers::Settings& settingsController; Controllers::HeartRateController& heartRateController; Controllers::MotionController& motionController; + Controllers::SimpleWeatherService& weatherService; lv_task_t* taskRefresh; Widgets::StatusIcons statusIcons; }; } + + template <> + struct WatchFaceTraits<WatchFace::Digital> { + static constexpr WatchFace watchFace = WatchFace::Digital; + static constexpr const char* name = "Digital face"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceDigital(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.alarmController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController, + *controllers.weatherController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; } } diff --git a/src/displayapp/screens/WatchFaceInfineat.cpp b/src/displayapp/screens/WatchFaceInfineat.cpp index ab0898e0..40f2abbb 100644 --- a/src/displayapp/screens/WatchFaceInfineat.cpp +++ b/src/displayapp/screens/WatchFaceInfineat.cpp @@ -397,74 +397,52 @@ void WatchFaceInfineat::Refresh() { lv_obj_align(notificationIcon, lv_scr_act(), LV_ALIGN_IN_TOP_RIGHT, 0, 0); } - currentDateTime = dateTimeController.CurrentDateTime(); - + currentDateTime = std::chrono::time_point_cast<std::chrono::minutes>(dateTimeController.CurrentDateTime()); if (currentDateTime.IsUpdated()) { - auto hour = dateTimeController.Hours(); - auto minute = dateTimeController.Minutes(); - auto year = dateTimeController.Year(); - auto month = dateTimeController.Month(); - auto dayOfWeek = dateTimeController.DayOfWeek(); - auto day = dateTimeController.Day(); - - char minutesChar[3]; - sprintf(minutesChar, "%02d", static_cast<int>(minute)); - - char hoursChar[3]; - char ampmChar[3]; + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { - if (hour < 12) { - if (hour == 0) { - hour = 12; - } - sprintf(ampmChar, "AM"); - } else { // hour >= 12 - if (hour != 12) { - hour = hour - 12; - } - sprintf(ampmChar, "PM"); + char ampmChar[3] = "AM"; + if (hour == 0) { + hour = 12; + } else if (hour == 12) { + ampmChar[0] = 'P'; + } else if (hour > 12) { + hour = hour - 12; + ampmChar[0] = 'P'; } + lv_label_set_text(labelTimeAmPm, ampmChar); } - sprintf(hoursChar, "%02d", hour); - - if ((hoursChar[0] != displayedChar[0]) || (hoursChar[1] != displayedChar[1]) || (minutesChar[0] != displayedChar[2]) || - (minutesChar[1] != displayedChar[3])) { - displayedChar[0] = hoursChar[0]; - displayedChar[1] = hoursChar[1]; - displayedChar[2] = minutesChar[0]; - displayedChar[3] = minutesChar[1]; - - lv_label_set_text_fmt(labelHour, "%s", hoursChar); - lv_label_set_text_fmt(labelMinutes, "%s", minutesChar); - } + lv_label_set_text_fmt(labelHour, "%02d", hour); + lv_label_set_text_fmt(labelMinutes, "%02d", minute); if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { - lv_label_set_text(labelTimeAmPm, ampmChar); lv_obj_align(labelTimeAmPm, timeContainer, LV_ALIGN_OUT_RIGHT_TOP, 0, 10); lv_obj_align(labelHour, timeContainer, LV_ALIGN_IN_TOP_MID, 0, 5); lv_obj_align(labelMinutes, timeContainer, LV_ALIGN_IN_BOTTOM_MID, 0, 0); } - if ((year != currentYear) || (month != currentMonth) || (dayOfWeek != currentDayOfWeek) || (day != currentDay)) { - lv_label_set_text_fmt(labelDate, "%s %02d", dateTimeController.DayOfWeekShortToStringLow(), day); + currentDate = std::chrono::time_point_cast<std::chrono::days>(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + uint8_t day = dateTimeController.Day(); + Controllers::DateTime::Days dayOfWeek = dateTimeController.DayOfWeek(); + lv_label_set_text_fmt(labelDate, "%s %02d", dateTimeController.DayOfWeekShortToStringLow(dayOfWeek), day); lv_obj_realign(labelDate); - - currentYear = year; - currentMonth = month; - currentDayOfWeek = dayOfWeek; - currentDay = day; } } batteryPercentRemaining = batteryController.PercentRemaining(); isCharging = batteryController.IsCharging(); - if (batteryController.IsCharging()) { // Charging battery animation - chargingBatteryPercent += 1; + // Charging battery animation + if (batteryController.IsCharging() && (xTaskGetTickCount() - chargingAnimationTick > pdMS_TO_TICKS(150))) { + // Dividing 100 by the height gives the battery percentage required to shift the animation by 1 pixel + chargingBatteryPercent += 100 / lv_obj_get_height(logoPine); if (chargingBatteryPercent > 100) { chargingBatteryPercent = batteryPercentRemaining.Get(); } SetBatteryLevel(chargingBatteryPercent); + chargingAnimationTick = xTaskGetTickCount(); } else if (isCharging.IsUpdated() || batteryPercentRemaining.IsUpdated()) { chargingBatteryPercent = batteryPercentRemaining.Get(); SetBatteryLevel(chargingBatteryPercent); @@ -478,8 +456,7 @@ void WatchFaceInfineat::Refresh() { } stepCount = motionController.NbSteps(); - motionSensorOk = motionController.IsSensorOk(); - if (stepCount.IsUpdated() || motionSensorOk.IsUpdated()) { + if (stepCount.IsUpdated()) { lv_label_set_text_fmt(stepValue, "%lu", stepCount.Get()); lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 10, 0); lv_obj_align(stepIcon, stepValue, LV_ALIGN_OUT_LEFT_MID, -5, 0); diff --git a/src/displayapp/screens/WatchFaceInfineat.h b/src/displayapp/screens/WatchFaceInfineat.h index 26973efe..78d020f1 100644 --- a/src/displayapp/screens/WatchFaceInfineat.h +++ b/src/displayapp/screens/WatchFaceInfineat.h @@ -4,8 +4,11 @@ #include <chrono> #include <cstdint> #include <memory> +#include <displayapp/Controllers.h> #include "displayapp/screens/Screen.h" #include "components/datetime/DateTimeController.h" +#include "utility/DirtyValue.h" +#include "displayapp/apps/Apps.h" namespace Pinetime { namespace Controllers { @@ -42,23 +45,18 @@ namespace Pinetime { static bool IsAvailable(Pinetime::Controllers::FS& filesystem); private: - char displayedChar[5] {}; - - uint16_t currentYear = 1970; - Pinetime::Controllers::DateTime::Months currentMonth = Pinetime::Controllers::DateTime::Months::Unknown; - Pinetime::Controllers::DateTime::Days currentDayOfWeek = Pinetime::Controllers::DateTime::Days::Unknown; - uint8_t currentDay = 0; uint32_t savedTick = 0; uint8_t chargingBatteryPercent = 101; // not a mistake ;) + TickType_t chargingAnimationTick = 0; - DirtyValue<uint8_t> batteryPercentRemaining {}; - DirtyValue<bool> isCharging {}; - DirtyValue<bool> bleState {}; - DirtyValue<bool> bleRadioEnabled {}; - DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>> currentDateTime {}; - DirtyValue<bool> motionSensorOk {}; - DirtyValue<uint32_t> stepCount {}; - DirtyValue<bool> notificationState {}; + Utility::DirtyValue<uint8_t> batteryPercentRemaining {}; + Utility::DirtyValue<bool> isCharging {}; + Utility::DirtyValue<bool> bleState {}; + Utility::DirtyValue<bool> bleRadioEnabled {}; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes>> currentDateTime {}; + Utility::DirtyValue<uint32_t> stepCount {}; + Utility::DirtyValue<bool> notificationState {}; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::days>> currentDate; // Lines making up the side cover lv_obj_t* lineBattery; @@ -102,5 +100,25 @@ namespace Pinetime { lv_font_t* font_bebas = nullptr; }; } + + template <> + struct WatchFaceTraits<WatchFace::Infineat> { + static constexpr WatchFace watchFace = WatchFace::Infineat; + static constexpr const char* name = "Infineat face"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceInfineat(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController, + controllers.motionController, + controllers.filesystem); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& filesystem) { + return Screens::WatchFaceInfineat::IsAvailable(filesystem); + } + }; } } diff --git a/src/displayapp/screens/WatchFacePineTimeStyle.cpp b/src/displayapp/screens/WatchFacePineTimeStyle.cpp index 85505a63..22ccefc7 100644 --- a/src/displayapp/screens/WatchFacePineTimeStyle.cpp +++ b/src/displayapp/screens/WatchFacePineTimeStyle.cpp @@ -22,17 +22,19 @@ #include "displayapp/screens/WatchFacePineTimeStyle.h" #include <lvgl/lvgl.h> #include <cstdio> -#include <displayapp/Colors.h> +#include "displayapp/Colors.h" #include "displayapp/screens/BatteryIcon.h" #include "displayapp/screens/BleIcon.h" #include "displayapp/screens/NotificationIcon.h" #include "displayapp/screens/Symbols.h" +#include "displayapp/screens/WeatherSymbols.h" #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" #include "components/ble/NotificationManager.h" #include "components/motion/MotionController.h" #include "components/settings/Settings.h" #include "displayapp/DisplayApp.h" +#include "components/ble/SimpleWeatherService.h" using namespace Pinetime::Applications::Screens; @@ -48,7 +50,8 @@ WatchFacePineTimeStyle::WatchFacePineTimeStyle(Controllers::DateTime& dateTimeCo const Controllers::Ble& bleController, Controllers::NotificationManager& notificationManager, Controllers::Settings& settingsController, - Controllers::MotionController& motionController) + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherService) : currentDateTime {{}}, batteryIcon(false), dateTimeController {dateTimeController}, @@ -56,7 +59,8 @@ WatchFacePineTimeStyle::WatchFacePineTimeStyle(Controllers::DateTime& dateTimeCo bleController {bleController}, notificationManager {notificationManager}, settingsController {settingsController}, - motionController {motionController} { + motionController {motionController}, + weatherService {weatherService} { // Create a 200px wide background rectangle timebar = lv_obj_create(lv_scr_act(), nullptr); @@ -94,27 +98,53 @@ WatchFacePineTimeStyle::WatchFacePineTimeStyle(Controllers::DateTime& dateTimeCo // Display icons batteryIcon.Create(sidebar); batteryIcon.SetColor(LV_COLOR_BLACK); - lv_obj_align(batteryIcon.GetObject(), nullptr, LV_ALIGN_IN_TOP_MID, 0, 2); + lv_obj_align(batteryIcon.GetObject(), nullptr, LV_ALIGN_IN_TOP_MID, 10, 2); plugIcon = lv_label_create(lv_scr_act(), nullptr); lv_label_set_text_static(plugIcon, Symbols::plug); lv_obj_set_style_local_text_color(plugIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); - lv_obj_align(plugIcon, sidebar, LV_ALIGN_IN_TOP_MID, 0, 2); + lv_obj_align(plugIcon, sidebar, LV_ALIGN_IN_TOP_MID, 10, 2); bleIcon = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(bleIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); - lv_label_set_text_static(bleIcon, ""); + lv_obj_align(bleIcon, sidebar, LV_ALIGN_IN_TOP_MID, -10, 2); notificationIcon = lv_label_create(lv_scr_act(), nullptr); - lv_obj_set_style_local_text_color(notificationIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); - lv_label_set_text_static(notificationIcon, ""); + lv_obj_set_style_local_text_color(notificationIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Convert(settingsController.GetPTSColorTime())); + lv_obj_align(notificationIcon, timebar, LV_ALIGN_IN_TOP_LEFT, 5, 5); + + weatherIcon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_font(weatherIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons); + lv_label_set_text(weatherIcon, Symbols::ban); + lv_obj_align(weatherIcon, sidebar, LV_ALIGN_IN_TOP_MID, 0, 35); + lv_obj_set_auto_realign(weatherIcon, true); + if (settingsController.GetPTSWeather() == Pinetime::Controllers::Settings::PTSWeather::On) { + lv_obj_set_hidden(weatherIcon, false); + } else { + lv_obj_set_hidden(weatherIcon, true); + } + + temperature = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_label_set_text(temperature, "--"); + lv_obj_align(temperature, sidebar, LV_ALIGN_IN_TOP_MID, 0, 65); + if (settingsController.GetPTSWeather() == Pinetime::Controllers::Settings::PTSWeather::On) { + lv_obj_set_hidden(temperature, false); + } else { + lv_obj_set_hidden(temperature, true); + } // Calendar icon calendarOuter = lv_obj_create(lv_scr_act(), nullptr); lv_obj_set_style_local_bg_color(calendarOuter, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); lv_obj_set_style_local_radius(calendarOuter, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, 0); lv_obj_set_size(calendarOuter, 34, 34); - lv_obj_align(calendarOuter, sidebar, LV_ALIGN_CENTER, 0, 0); + if (settingsController.GetPTSWeather() == Pinetime::Controllers::Settings::PTSWeather::On) { + lv_obj_align(calendarOuter, sidebar, LV_ALIGN_CENTER, 0, 20); + } else { + lv_obj_align(calendarOuter, sidebar, LV_ALIGN_CENTER, 0, 0); + } calendarInner = lv_obj_create(lv_scr_act(), nullptr); lv_obj_set_style_local_bg_color(calendarInner, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); @@ -150,17 +180,17 @@ WatchFacePineTimeStyle::WatchFacePineTimeStyle(Controllers::DateTime& dateTimeCo dateDayOfWeek = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(dateDayOfWeek, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); lv_label_set_text_static(dateDayOfWeek, "THU"); - lv_obj_align(dateDayOfWeek, sidebar, LV_ALIGN_CENTER, 0, -34); + lv_obj_align(dateDayOfWeek, calendarOuter, LV_ALIGN_CENTER, 0, -32); dateDay = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(dateDay, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); lv_label_set_text_static(dateDay, "25"); - lv_obj_align(dateDay, sidebar, LV_ALIGN_CENTER, 0, 3); + lv_obj_align(dateDay, calendarOuter, LV_ALIGN_CENTER, 0, 3); dateMonth = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(dateMonth, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); lv_label_set_text_static(dateMonth, "MAR"); - lv_obj_align(dateMonth, sidebar, LV_ALIGN_CENTER, 0, 32); + lv_obj_align(dateMonth, calendarOuter, LV_ALIGN_CENTER, 0, 32); // Step count gauge if (settingsController.GetPTSColorBar() == Pinetime::Controllers::Settings::Colors::White) { @@ -323,13 +353,23 @@ WatchFacePineTimeStyle::WatchFacePineTimeStyle(Controllers::DateTime& dateTimeCo btnSteps = lv_btn_create(lv_scr_act(), nullptr); btnSteps->user_data = this; lv_obj_set_size(btnSteps, 160, 60); - lv_obj_align(btnSteps, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); + lv_obj_align(btnSteps, lv_scr_act(), LV_ALIGN_CENTER, 0, -10); lv_obj_set_style_local_bg_opa(btnSteps, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_50); lv_obj_t* lblSteps = lv_label_create(btnSteps, nullptr); lv_label_set_text_static(lblSteps, "Steps style"); lv_obj_set_event_cb(btnSteps, event_handler); lv_obj_set_hidden(btnSteps, true); + btnWeather = lv_btn_create(lv_scr_act(), nullptr); + btnWeather->user_data = this; + lv_obj_set_size(btnWeather, 160, 60); + lv_obj_align(btnWeather, lv_scr_act(), LV_ALIGN_CENTER, 0, 60); + lv_obj_set_style_local_bg_opa(btnWeather, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_50); + lv_obj_t* lblWeather = lv_label_create(btnWeather, nullptr); + lv_label_set_text_static(lblWeather, "Weather"); + lv_obj_set_event_cb(btnWeather, event_handler); + lv_obj_set_hidden(btnWeather, true); + btnSetColor = lv_btn_create(lv_scr_act(), nullptr); btnSetColor->user_data = this; lv_obj_set_size(btnSetColor, 150, 60); @@ -337,9 +377,9 @@ WatchFacePineTimeStyle::WatchFacePineTimeStyle(Controllers::DateTime& dateTimeCo lv_obj_set_style_local_radius(btnSetColor, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, 20); lv_obj_set_style_local_bg_opa(btnSetColor, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_50); lv_obj_set_event_cb(btnSetColor, event_handler); - lbl_btnSetColor = lv_label_create(btnSetColor, nullptr); - lv_obj_set_style_local_text_font(lbl_btnSetColor, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_sys_48); - lv_label_set_text_static(lbl_btnSetColor, Symbols::paintbrushLg); + lv_obj_t* lblSetColor = lv_label_create(btnSetColor, nullptr); + lv_obj_set_style_local_text_font(lblSetColor, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_sys_48); + lv_label_set_text_static(lblSetColor, Symbols::paintbrushLg); lv_obj_set_hidden(btnSetColor, true); btnSetOpts = lv_btn_create(lv_scr_act(), nullptr); @@ -349,9 +389,9 @@ WatchFacePineTimeStyle::WatchFacePineTimeStyle(Controllers::DateTime& dateTimeCo lv_obj_set_style_local_radius(btnSetOpts, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, 20); lv_obj_set_style_local_bg_opa(btnSetOpts, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_50); lv_obj_set_event_cb(btnSetOpts, event_handler); - lbl_btnSetOpts = lv_label_create(btnSetOpts, nullptr); - lv_obj_set_style_local_text_font(lbl_btnSetOpts, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_sys_48); - lv_label_set_text_static(lbl_btnSetOpts, Symbols::settings); + lv_obj_t* lblSetOpts = lv_label_create(btnSetOpts, nullptr); + lv_obj_set_style_local_text_font(lblSetOpts, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_sys_48); + lv_label_set_text_static(lblSetOpts, Symbols::settings); lv_obj_set_hidden(btnSetOpts, true); taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); @@ -388,6 +428,7 @@ void WatchFacePineTimeStyle::CloseMenu() { lv_obj_set_hidden(btnRandom, true); lv_obj_set_hidden(btnClose, true); lv_obj_set_hidden(btnSteps, true); + lv_obj_set_hidden(btnWeather, true); } bool WatchFacePineTimeStyle::OnButtonPushed() { @@ -403,17 +444,6 @@ void WatchFacePineTimeStyle::SetBatteryIcon() { batteryIcon.SetBatteryPercentage(batteryPercent); } -void WatchFacePineTimeStyle::AlignIcons() { - if (notificationState.Get() && bleState.Get()) { - lv_obj_align(bleIcon, sidebar, LV_ALIGN_IN_TOP_MID, 8, 25); - lv_obj_align(notificationIcon, sidebar, LV_ALIGN_IN_TOP_MID, -8, 25); - } else if (notificationState.Get() && !bleState.Get()) { - lv_obj_align(notificationIcon, sidebar, LV_ALIGN_IN_TOP_MID, 0, 25); - } else { - lv_obj_align(bleIcon, sidebar, LV_ALIGN_IN_TOP_MID, 0, 25); - } -} - void WatchFacePineTimeStyle::Refresh() { isCharging = batteryController.IsCharging(); if (isCharging.IsUpdated()) { @@ -437,13 +467,12 @@ void WatchFacePineTimeStyle::Refresh() { bleRadioEnabled = bleController.IsRadioEnabled(); if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) { lv_label_set_text_static(bleIcon, BleIcon::GetIcon(bleState.Get())); - AlignIcons(); + lv_obj_realign(bleIcon); } notificationState = notificationManager.AreNewNotificationsAvailable(); if (notificationState.IsUpdated()) { lv_label_set_text_static(notificationIcon, NotificationIcon::GetIcon(notificationState.Get())); - AlignIcons(); } currentDateTime = dateTimeController.CurrentDateTime(); @@ -499,8 +528,7 @@ void WatchFacePineTimeStyle::Refresh() { } stepCount = motionController.NbSteps(); - motionSensorOk = motionController.IsSensorOk(); - if (stepCount.IsUpdated() || motionSensorOk.IsUpdated()) { + if (stepCount.IsUpdated()) { lv_gauge_set_value(stepGauge, 0, (stepCount.Get() / (settingsController.GetStepsGoal() / 100)) % 100); lv_obj_realign(stepGauge); lv_label_set_text_fmt(stepValue, "%luK", (stepCount.Get() / 1000)); @@ -510,6 +538,25 @@ void WatchFacePineTimeStyle::Refresh() { lv_obj_set_style_local_scale_grad_color(stepGauge, LV_GAUGE_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); } } + + currentWeather = weatherService.Current(); + if (currentWeather.IsUpdated()) { + auto optCurrentWeather = currentWeather.Get(); + if (optCurrentWeather) { + int16_t temp = optCurrentWeather->temperature.Celsius(); + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = optCurrentWeather->temperature.Fahrenheit(); + } + lv_label_set_text_fmt(temperature, "%d°", temp); + lv_label_set_text(weatherIcon, Symbols::GetSymbol(optCurrentWeather->iconId)); + } else { + lv_label_set_text(temperature, "--"); + lv_label_set_text(weatherIcon, Symbols::ban); + } + lv_obj_realign(temperature); + lv_obj_realign(weatherIcon); + } + if (!lv_obj_get_hidden(btnSetColor)) { if ((savedTick > 0) && (lv_tick_get() - savedTick > 3000)) { lv_obj_set_hidden(btnSetColor, true); @@ -655,6 +702,37 @@ void WatchFacePineTimeStyle::UpdateSelected(lv_obj_t* object, lv_event_t event) settingsController.SetPTSGaugeStyle(Controllers::Settings::PTSGaugeStyle::Full); } } + if (object == btnWeather) { + if (lv_obj_get_hidden(weatherIcon)) { + // show weather icon and temperature + lv_obj_set_hidden(weatherIcon, false); + lv_obj_set_hidden(temperature, false); + lv_obj_align(calendarOuter, sidebar, LV_ALIGN_CENTER, 0, 20); + lv_obj_realign(calendarInner); + lv_obj_realign(calendarBar1); + lv_obj_realign(calendarBar2); + lv_obj_realign(calendarCrossBar1); + lv_obj_realign(calendarCrossBar2); + lv_obj_realign(dateDayOfWeek); + lv_obj_realign(dateDay); + lv_obj_realign(dateMonth); + settingsController.SetPTSWeather(Controllers::Settings::PTSWeather::On); + } else { + // hide weather + lv_obj_set_hidden(weatherIcon, true); + lv_obj_set_hidden(temperature, true); + lv_obj_align(calendarOuter, sidebar, LV_ALIGN_CENTER, 0, 0); + lv_obj_realign(calendarInner); + lv_obj_realign(calendarBar1); + lv_obj_realign(calendarBar2); + lv_obj_realign(calendarCrossBar1); + lv_obj_realign(calendarCrossBar2); + lv_obj_realign(dateDayOfWeek); + lv_obj_realign(dateDay); + lv_obj_realign(dateMonth); + settingsController.SetPTSWeather(Controllers::Settings::PTSWeather::Off); + } + } if (object == btnSetColor) { lv_obj_set_hidden(btnSetColor, true); lv_obj_set_hidden(btnSetOpts, true); @@ -672,6 +750,7 @@ void WatchFacePineTimeStyle::UpdateSelected(lv_obj_t* object, lv_event_t event) lv_obj_set_hidden(btnSetColor, true); lv_obj_set_hidden(btnSetOpts, true); lv_obj_set_hidden(btnSteps, false); + lv_obj_set_hidden(btnWeather, false); lv_obj_set_hidden(btnClose, false); } } diff --git a/src/displayapp/screens/WatchFacePineTimeStyle.h b/src/displayapp/screens/WatchFacePineTimeStyle.h index bccb224a..72537095 100644 --- a/src/displayapp/screens/WatchFacePineTimeStyle.h +++ b/src/displayapp/screens/WatchFacePineTimeStyle.h @@ -4,11 +4,14 @@ #include <chrono> #include <cstdint> #include <memory> +#include <displayapp/Controllers.h> #include "displayapp/screens/Screen.h" #include "displayapp/screens/BatteryIcon.h" #include "displayapp/Colors.h" #include "components/datetime/DateTimeController.h" +#include "components/ble/SimpleWeatherService.h" #include "components/ble/BleController.h" +#include "utility/DirtyValue.h" namespace Pinetime { namespace Controllers { @@ -29,7 +32,8 @@ namespace Pinetime { const Controllers::Ble& bleController, Controllers::NotificationManager& notificationManager, Controllers::Settings& settingsController, - Controllers::MotionController& motionController); + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weather); ~WatchFacePineTimeStyle() override; bool OnTouchEvent(TouchEvents event) override; @@ -50,14 +54,14 @@ namespace Pinetime { uint8_t currentDay = 0; uint32_t savedTick = 0; - DirtyValue<uint8_t> batteryPercentRemaining {}; - DirtyValue<bool> isCharging {}; - DirtyValue<bool> bleState {}; - DirtyValue<bool> bleRadioEnabled {}; - DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>> currentDateTime {}; - DirtyValue<bool> motionSensorOk {}; - DirtyValue<uint32_t> stepCount {}; - DirtyValue<bool> notificationState {}; + Utility::DirtyValue<uint8_t> batteryPercentRemaining {}; + Utility::DirtyValue<bool> isCharging {}; + Utility::DirtyValue<bool> bleState {}; + Utility::DirtyValue<bool> bleRadioEnabled {}; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>> currentDateTime {}; + Utility::DirtyValue<uint32_t> stepCount {}; + Utility::DirtyValue<bool> notificationState {}; + Utility::DirtyValue<std::optional<Pinetime::Controllers::SimpleWeatherService::CurrentWeather>> currentWeather {}; static Pinetime::Controllers::Settings::Colors GetNext(Controllers::Settings::Colors color); static Pinetime::Controllers::Settings::Colors GetPrevious(Controllers::Settings::Colors color); @@ -72,6 +76,7 @@ namespace Pinetime { lv_obj_t* btnRandom; lv_obj_t* btnClose; lv_obj_t* btnSteps; + lv_obj_t* btnWeather; lv_obj_t* timebar; lv_obj_t* sidebar; lv_obj_t* timeDD1; @@ -81,6 +86,8 @@ namespace Pinetime { lv_obj_t* dateDayOfWeek; lv_obj_t* dateDay; lv_obj_t* dateMonth; + lv_obj_t* weatherIcon; + lv_obj_t* temperature; lv_obj_t* plugIcon; lv_obj_t* bleIcon; lv_obj_t* calendarOuter; @@ -93,8 +100,6 @@ namespace Pinetime { lv_obj_t* stepGauge; lv_obj_t* btnSetColor; lv_obj_t* btnSetOpts; - lv_obj_t* lbl_btnSetColor; - lv_obj_t* lbl_btnSetOpts; lv_obj_t* stepIcon; lv_obj_t* stepValue; lv_color_t needle_colors[1]; @@ -107,13 +112,33 @@ namespace Pinetime { Controllers::NotificationManager& notificationManager; Controllers::Settings& settingsController; Controllers::MotionController& motionController; + Controllers::SimpleWeatherService& weatherService; void SetBatteryIcon(); void CloseMenu(); - void AlignIcons(); lv_task_t* taskRefresh; }; } + + template <> + struct WatchFaceTraits<WatchFace::PineTimeStyle> { + static constexpr WatchFace watchFace = WatchFace::PineTimeStyle; + static constexpr const char* name = "PineTimeStyle"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFacePineTimeStyle(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController, + controllers.motionController, + *controllers.weatherController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; } } diff --git a/src/displayapp/screens/WatchFaceTerminal.cpp b/src/displayapp/screens/WatchFaceTerminal.cpp index e5ff195e..96d77741 100644 --- a/src/displayapp/screens/WatchFaceTerminal.cpp +++ b/src/displayapp/screens/WatchFaceTerminal.cpp @@ -104,45 +104,33 @@ void WatchFaceTerminal::Refresh() { } } - currentDateTime = dateTimeController.CurrentDateTime(); - + currentDateTime = std::chrono::time_point_cast<std::chrono::seconds>(dateTimeController.CurrentDateTime()); if (currentDateTime.IsUpdated()) { - auto hour = dateTimeController.Hours(); - auto minute = dateTimeController.Minutes(); - auto second = dateTimeController.Seconds(); - auto year = dateTimeController.Year(); - auto month = dateTimeController.Month(); - auto dayOfWeek = dateTimeController.DayOfWeek(); - auto day = dateTimeController.Day(); - - if (displayedHour != hour || displayedMinute != minute || displayedSecond != second) { - displayedHour = hour; - displayedMinute = minute; - displayedSecond = second; + uint8_t hour = dateTimeController.Hours(); + uint8_t minute = dateTimeController.Minutes(); + uint8_t second = dateTimeController.Seconds(); - if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { - char ampmChar[3] = "AM"; - if (hour == 0) { - hour = 12; - } else if (hour == 12) { - ampmChar[0] = 'P'; - } else if (hour > 12) { - hour = hour - 12; - ampmChar[0] = 'P'; - } - lv_label_set_text_fmt(label_time, "[TIME]#11cc55 %02d:%02d:%02d %s#", hour, minute, second, ampmChar); - } else { - lv_label_set_text_fmt(label_time, "[TIME]#11cc55 %02d:%02d:%02d", hour, minute, second); + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + char ampmChar[3] = "AM"; + if (hour == 0) { + hour = 12; + } else if (hour == 12) { + ampmChar[0] = 'P'; + } else if (hour > 12) { + hour = hour - 12; + ampmChar[0] = 'P'; } + lv_label_set_text_fmt(label_time, "[TIME]#11cc55 %02d:%02d:%02d %s#", hour, minute, second, ampmChar); + } else { + lv_label_set_text_fmt(label_time, "[TIME]#11cc55 %02d:%02d:%02d", hour, minute, second); } - if ((year != currentYear) || (month != currentMonth) || (dayOfWeek != currentDayOfWeek) || (day != currentDay)) { + currentDate = std::chrono::time_point_cast<std::chrono::days>(currentDateTime.Get()); + if (currentDate.IsUpdated()) { + uint16_t year = dateTimeController.Year(); + Controllers::DateTime::Months month = dateTimeController.Month(); + uint8_t day = dateTimeController.Day(); lv_label_set_text_fmt(label_date, "[DATE]#007fff %04d-%02d-%02d#", short(year), char(month), char(day)); - - currentYear = year; - currentMonth = month; - currentDayOfWeek = dayOfWeek; - currentDay = day; } } @@ -157,8 +145,7 @@ void WatchFaceTerminal::Refresh() { } stepCount = motionController.NbSteps(); - motionSensorOk = motionController.IsSensorOk(); - if (stepCount.IsUpdated() || motionSensorOk.IsUpdated()) { + if (stepCount.IsUpdated()) { lv_label_set_text_fmt(stepValue, "[STEP]#ee3377 %lu steps#", stepCount.Get()); } } diff --git a/src/displayapp/screens/WatchFaceTerminal.h b/src/displayapp/screens/WatchFaceTerminal.h index 67156a50..bf460866 100644 --- a/src/displayapp/screens/WatchFaceTerminal.h +++ b/src/displayapp/screens/WatchFaceTerminal.h @@ -4,8 +4,10 @@ #include <chrono> #include <cstdint> #include <memory> +#include <displayapp/Controllers.h> #include "displayapp/screens/Screen.h" #include "components/datetime/DateTimeController.h" +#include "utility/DirtyValue.h" namespace Pinetime { namespace Controllers { @@ -34,25 +36,16 @@ namespace Pinetime { void Refresh() override; private: - uint8_t displayedHour = -1; - uint8_t displayedMinute = -1; - uint8_t displayedSecond = -1; - - uint16_t currentYear = 1970; - Pinetime::Controllers::DateTime::Months currentMonth = Pinetime::Controllers::DateTime::Months::Unknown; - Pinetime::Controllers::DateTime::Days currentDayOfWeek = Pinetime::Controllers::DateTime::Days::Unknown; - uint8_t currentDay = 0; - - DirtyValue<int> batteryPercentRemaining {}; - DirtyValue<bool> powerPresent {}; - DirtyValue<bool> bleState {}; - DirtyValue<bool> bleRadioEnabled {}; - DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>> currentDateTime {}; - DirtyValue<bool> motionSensorOk {}; - DirtyValue<uint32_t> stepCount {}; - DirtyValue<uint8_t> heartbeat {}; - DirtyValue<bool> heartbeatRunning {}; - DirtyValue<bool> notificationState {}; + Utility::DirtyValue<int> batteryPercentRemaining {}; + Utility::DirtyValue<bool> powerPresent {}; + Utility::DirtyValue<bool> bleState {}; + Utility::DirtyValue<bool> bleRadioEnabled {}; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>> currentDateTime {}; + Utility::DirtyValue<uint32_t> stepCount {}; + Utility::DirtyValue<uint8_t> heartbeat {}; + Utility::DirtyValue<bool> heartbeatRunning {}; + Utility::DirtyValue<bool> notificationState {}; + Utility::DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::days>> currentDate; lv_obj_t* label_time; lv_obj_t* label_date; @@ -75,5 +68,25 @@ namespace Pinetime { lv_task_t* taskRefresh; }; } + + template <> + struct WatchFaceTraits<WatchFace::Terminal> { + static constexpr WatchFace watchFace = WatchFace::Terminal; + static constexpr const char* name = "Terminal"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceTerminal(controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; } } diff --git a/src/displayapp/screens/Weather.cpp b/src/displayapp/screens/Weather.cpp index 4921174c..25464c70 100644 --- a/src/displayapp/screens/Weather.cpp +++ b/src/displayapp/screens/Weather.cpp @@ -1,221 +1,197 @@ -/* Copyright (C) 2021 Avamander +#include "displayapp/screens/Weather.h" - This file is part of InfiniTime. - - InfiniTime is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - InfiniTime is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ -#include "Weather.h" #include <lvgl/lvgl.h> -#include <components/ble/weather/WeatherService.h> -#include "Label.h" -#include "components/battery/BatteryController.h" -#include "components/ble/BleController.h" -#include "components/ble/weather/WeatherData.h" -using namespace Pinetime::Applications::Screens; +#include "components/ble/SimpleWeatherService.h" +#include "components/datetime/DateTimeController.h" +#include "components/settings/Settings.h" +#include "displayapp/DisplayApp.h" +#include "displayapp/screens/WeatherSymbols.h" +#include "displayapp/InfiniTimeTheme.h" -Weather::Weather(Pinetime::Applications::DisplayApp* app, Pinetime::Controllers::WeatherService& weather) - : app {app}, - weatherService(weather), - screens {app, - 0, - {[this]() -> std::unique_ptr<Screen> { - return CreateScreenTemperature(); - }, - [this]() -> std::unique_ptr<Screen> { - return CreateScreenAir(); - }, - [this]() -> std::unique_ptr<Screen> { - return CreateScreenClouds(); - }, - [this]() -> std::unique_ptr<Screen> { - return CreateScreenPrecipitation(); - }, - [this]() -> std::unique_ptr<Screen> { - return CreateScreenHumidity(); - }}, - Screens::ScreenListModes::UpDown} { -} +using namespace Pinetime::Applications::Screens; -Weather::~Weather() { - lv_obj_clean(lv_scr_act()); -} +namespace { + lv_color_t TemperatureColor(Pinetime::Controllers::SimpleWeatherService::Temperature temp) { + if (temp.Celsius() <= 0) { // freezing + return Colors::blue; + } else if (temp.Celsius() <= 4) { // ice + return LV_COLOR_CYAN; + } else if (temp.Celsius() >= 27) { // hot + return Colors::deepOrange; + } + return Colors::orange; // normal + } -void Weather::Refresh() { - if (running) { - // screens.Refresh(); + uint8_t TemperatureStyle(Pinetime::Controllers::SimpleWeatherService::Temperature temp) { + if (temp.Celsius() <= 0) { // freezing + return LV_TABLE_PART_CELL3; + } else if (temp.Celsius() <= 4) { // ice + return LV_TABLE_PART_CELL4; + } else if (temp.Celsius() >= 27) { // hot + return LV_TABLE_PART_CELL6; + } + return LV_TABLE_PART_CELL5; // normal } } -bool Weather::OnButtonPushed() { - running = false; - return true; -} +Weather::Weather(Controllers::Settings& settingsController, Controllers::SimpleWeatherService& weatherService) + : settingsController {settingsController}, weatherService {weatherService} { -bool Weather::OnTouchEvent(Pinetime::Applications::TouchEvents event) { - return screens.OnTouchEvent(event); -} + temperature = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_text_font(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_42); + lv_label_set_text(temperature, "---"); + lv_obj_align(temperature, nullptr, LV_ALIGN_CENTER, 0, -30); + lv_obj_set_auto_realign(temperature, true); -std::unique_ptr<Screen> Weather::CreateScreenTemperature() { - lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr); - lv_label_set_recolor(label, true); - std::unique_ptr<Controllers::WeatherData::Temperature>& current = weatherService.GetCurrentTemperature(); - if (current->timestamp == 0) { - // Do not use the data, it's invalid - lv_label_set_text_fmt(label, - "#FFFF00 Temperature#\n\n" - "#444444 %d#°C \n\n" - "#444444 %d#\n\n" - "%d\n" - "%d\n", - 0, - 0, - 0, - 0); - } else { - lv_label_set_text_fmt(label, - "#FFFF00 Temperature#\n\n" - "#444444 %d#°C \n\n" - "#444444 %hd#\n\n" - "%llu\n" - "%lu\n", - current->temperature / 100, - current->dewPoint, - current->timestamp, - current->expires); - } - lv_label_set_align(label, LV_LABEL_ALIGN_CENTER); - lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); - return std::unique_ptr<Screen>(new Screens::Label(0, 5, label)); -} + minTemperature = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(minTemperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::bg); + lv_label_set_text(minTemperature, ""); + lv_obj_align(minTemperature, temperature, LV_ALIGN_OUT_LEFT_MID, -10, 0); + lv_obj_set_auto_realign(minTemperature, true); + + maxTemperature = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(maxTemperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::bg); + lv_label_set_text(maxTemperature, ""); + lv_obj_align(maxTemperature, temperature, LV_ALIGN_OUT_RIGHT_MID, 10, 0); + lv_obj_set_auto_realign(maxTemperature, true); + + condition = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(condition, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::lightGray); + lv_label_set_text(condition, ""); + lv_obj_align(condition, temperature, LV_ALIGN_OUT_TOP_MID, 0, -10); + lv_obj_set_auto_realign(condition, true); + + icon = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(icon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_text_font(icon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &fontawesome_weathericons); + lv_label_set_text(icon, ""); + lv_obj_align(icon, condition, LV_ALIGN_OUT_TOP_MID, 0, 0); + lv_obj_set_auto_realign(icon, true); -std::unique_ptr<Screen> Weather::CreateScreenAir() { - lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr); - lv_label_set_recolor(label, true); - std::unique_ptr<Controllers::WeatherData::AirQuality>& current = weatherService.GetCurrentQuality(); - if (current->timestamp == 0) { - // Do not use the data, it's invalid - lv_label_set_text_fmt(label, - "#FFFF00 Air quality#\n\n" - "#444444 %s#\n" - "#444444 %d#\n\n" - "%d\n" - "%d\n", - "", - 0, - 0, - 0); - } else { - lv_label_set_text_fmt(label, - "#FFFF00 Air quality#\n\n" - "#444444 %s#\n" - "#444444 %lu#\n\n" - "%llu\n" - "%lu\n", - current->polluter.c_str(), - (current->amount / 100), - current->timestamp, - current->expires); + forecast = lv_table_create(lv_scr_act(), nullptr); + lv_table_set_col_cnt(forecast, Controllers::SimpleWeatherService::MaxNbForecastDays); + lv_table_set_row_cnt(forecast, 4); + // LV_TABLE_PART_CELL1: Default table style + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL1, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL1, LV_STATE_DEFAULT, Colors::lightGray); + // LV_TABLE_PART_CELL2: Condition icon + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL2, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL2, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_style_local_text_font(forecast, LV_TABLE_PART_CELL2, LV_STATE_DEFAULT, &fontawesome_weathericons); + // LV_TABLE_PART_CELL3: Freezing + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL3, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL3, LV_STATE_DEFAULT, Colors::blue); + // LV_TABLE_PART_CELL4: Ice + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL4, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL4, LV_STATE_DEFAULT, LV_COLOR_CYAN); + // LV_TABLE_PART_CELL5: Normal + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL5, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL5, LV_STATE_DEFAULT, Colors::orange); + // LV_TABLE_PART_CELL6: Hot + lv_obj_set_style_local_border_color(forecast, LV_TABLE_PART_CELL6, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_set_style_local_text_color(forecast, LV_TABLE_PART_CELL6, LV_STATE_DEFAULT, Colors::deepOrange); + + lv_obj_align(forecast, nullptr, LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + + for (int i = 0; i < Controllers::SimpleWeatherService::MaxNbForecastDays; i++) { + lv_table_set_col_width(forecast, i, 48); + lv_table_set_cell_type(forecast, 1, i, LV_TABLE_PART_CELL2); + lv_table_set_cell_align(forecast, 0, i, LV_LABEL_ALIGN_CENTER); + lv_table_set_cell_align(forecast, 1, i, LV_LABEL_ALIGN_CENTER); + lv_table_set_cell_align(forecast, 2, i, LV_LABEL_ALIGN_CENTER); + lv_table_set_cell_align(forecast, 3, i, LV_LABEL_ALIGN_CENTER); } - lv_label_set_align(label, LV_LABEL_ALIGN_CENTER); - lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); - return std::unique_ptr<Screen>(new Screens::Label(0, 5, label)); + + taskRefresh = lv_task_create(RefreshTaskCallback, 1000, LV_TASK_PRIO_MID, this); + Refresh(); } -std::unique_ptr<Screen> Weather::CreateScreenClouds() { - lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr); - lv_label_set_recolor(label, true); - std::unique_ptr<Controllers::WeatherData::Clouds>& current = weatherService.GetCurrentClouds(); - if (current->timestamp == 0) { - // Do not use the data, it's invalid - lv_label_set_text_fmt(label, - "#FFFF00 Clouds#\n\n" - "#444444 %d%%#\n\n" - "%d\n" - "%d\n", - 0, - 0, - 0); - } else { - lv_label_set_text_fmt(label, - "#FFFF00 Clouds#\n\n" - "#444444 %hhu%%#\n\n" - "%llu\n" - "%lu\n", - current->amount, - current->timestamp, - current->expires); - } - lv_label_set_align(label, LV_LABEL_ALIGN_CENTER); - lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); - return std::unique_ptr<Screen>(new Screens::Label(0, 5, label)); +Weather::~Weather() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); } -std::unique_ptr<Screen> Weather::CreateScreenPrecipitation() { - lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr); - lv_label_set_recolor(label, true); - std::unique_ptr<Controllers::WeatherData::Precipitation>& current = weatherService.GetCurrentPrecipitation(); - if (current->timestamp == 0) { - // Do not use the data, it's invalid - lv_label_set_text_fmt(label, - "#FFFF00 Precipitation#\n\n" - "#444444 %d%%#\n\n" - "%d\n" - "%d\n", - 0, - 0, - 0); - } else { - lv_label_set_text_fmt(label, - "#FFFF00 Precipitation#\n\n" - "#444444 %hhu%%#\n\n" - "%llu\n" - "%lu\n", - current->amount, - current->timestamp, - current->expires); +void Weather::Refresh() { + currentWeather = weatherService.Current(); + if (currentWeather.IsUpdated()) { + auto optCurrentWeather = currentWeather.Get(); + if (optCurrentWeather) { + int16_t temp = optCurrentWeather->temperature.Celsius(); + int16_t minTemp = optCurrentWeather->minTemperature.Celsius(); + int16_t maxTemp = optCurrentWeather->maxTemperature.Celsius(); + char tempUnit = 'C'; + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = optCurrentWeather->temperature.Fahrenheit(); + minTemp = optCurrentWeather->minTemperature.Fahrenheit(); + maxTemp = optCurrentWeather->maxTemperature.Fahrenheit(); + tempUnit = 'F'; + } + lv_obj_set_style_local_text_color(temperature, + LV_LABEL_PART_MAIN, + LV_STATE_DEFAULT, + TemperatureColor(optCurrentWeather->temperature)); + lv_label_set_text(icon, Symbols::GetSymbol(optCurrentWeather->iconId)); + lv_label_set_text(condition, Symbols::GetCondition(optCurrentWeather->iconId)); + lv_label_set_text_fmt(temperature, "%d°%c", temp, tempUnit); + lv_label_set_text_fmt(minTemperature, "%d°", minTemp); + lv_label_set_text_fmt(maxTemperature, "%d°", maxTemp); + } else { + lv_label_set_text(icon, ""); + lv_label_set_text(condition, ""); + lv_label_set_text(temperature, "---"); + lv_obj_set_style_local_text_color(temperature, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_label_set_text(minTemperature, ""); + lv_label_set_text(maxTemperature, ""); + } } - lv_label_set_align(label, LV_LABEL_ALIGN_CENTER); - lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); - return std::unique_ptr<Screen>(new Screens::Label(0, 5, label)); -} -std::unique_ptr<Screen> Weather::CreateScreenHumidity() { - lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr); - lv_label_set_recolor(label, true); - std::unique_ptr<Controllers::WeatherData::Humidity>& current = weatherService.GetCurrentHumidity(); - if (current->timestamp == 0) { - // Do not use the data, it's invalid - lv_label_set_text_fmt(label, - "#FFFF00 Humidity#\n\n" - "#444444 %d%%#\n\n" - "%d\n" - "%d\n", - 0, - 0, - 0); - } else { - lv_label_set_text_fmt(label, - "#FFFF00 Humidity#\n\n" - "#444444 %hhu%%#\n\n" - "%llu\n" - "%lu\n", - current->humidity, - current->timestamp, - current->expires); + currentForecast = weatherService.GetForecast(); + if (currentForecast.IsUpdated()) { + auto optCurrentForecast = currentForecast.Get(); + if (optCurrentForecast) { + std::tm localTime = *std::localtime(reinterpret_cast<const time_t*>(&optCurrentForecast->timestamp)); + + for (int i = 0; i < optCurrentForecast->nbDays; i++) { + int16_t maxTemp = optCurrentForecast->days[i]->maxTemperature.Celsius(); + int16_t minTemp = optCurrentForecast->days[i]->minTemperature.Celsius(); + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + maxTemp = optCurrentForecast->days[i]->maxTemperature.Fahrenheit(); + minTemp = optCurrentForecast->days[i]->minTemperature.Fahrenheit(); + } + lv_table_set_cell_type(forecast, 2, i, TemperatureStyle(optCurrentForecast->days[i]->maxTemperature)); + lv_table_set_cell_type(forecast, 3, i, TemperatureStyle(optCurrentForecast->days[i]->minTemperature)); + uint8_t wday = localTime.tm_wday + i + 1; + if (wday > 7) { + wday -= 7; + } + const char* dayOfWeek = Controllers::DateTime::DayOfWeekShortToStringLow(static_cast<Controllers::DateTime::Days>(wday)); + lv_table_set_cell_value(forecast, 0, i, dayOfWeek); + lv_table_set_cell_value(forecast, 1, i, Symbols::GetSymbol(optCurrentForecast->days[i]->iconId)); + // Pad cells based on the largest number of digits on each column + char maxPadding[3] = " "; + char minPadding[3] = " "; + int diff = snprintf(nullptr, 0, "%d", maxTemp) - snprintf(nullptr, 0, "%d", minTemp); + if (diff <= 0) { + maxPadding[-diff] = '\0'; + minPadding[0] = '\0'; + } else { + maxPadding[0] = '\0'; + minPadding[diff] = '\0'; + } + lv_table_set_cell_value_fmt(forecast, 2, i, "%s%d", maxPadding, maxTemp); + lv_table_set_cell_value_fmt(forecast, 3, i, "%s%d", minPadding, minTemp); + } + } else { + for (int i = 0; i < Controllers::SimpleWeatherService::MaxNbForecastDays; i++) { + lv_table_set_cell_value(forecast, 0, i, ""); + lv_table_set_cell_value(forecast, 1, i, ""); + lv_table_set_cell_value(forecast, 2, i, ""); + lv_table_set_cell_value(forecast, 3, i, ""); + lv_table_set_cell_type(forecast, 2, i, LV_TABLE_PART_CELL1); + lv_table_set_cell_type(forecast, 3, i, LV_TABLE_PART_CELL1); + } + } } - lv_label_set_align(label, LV_LABEL_ALIGN_CENTER); - lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); - return std::unique_ptr<Screen>(new Screens::Label(0, 5, label)); } diff --git a/src/displayapp/screens/Weather.h b/src/displayapp/screens/Weather.h index 459534aa..6975311e 100644 --- a/src/displayapp/screens/Weather.h +++ b/src/displayapp/screens/Weather.h @@ -1,45 +1,56 @@ #pragma once -#include <memory> -#include <components/ble/weather/WeatherService.h> -#include "Screen.h" -#include "ScreenList.h" +#include <cstdint> +#include <lvgl/lvgl.h> +#include "displayapp/screens/Screen.h" +#include "components/ble/SimpleWeatherService.h" +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" +#include "Symbols.h" +#include "utility/DirtyValue.h" namespace Pinetime { - namespace Applications { - class DisplayApp; + namespace Controllers { + class Settings; + } + + namespace Applications { namespace Screens { + class Weather : public Screen { public: - explicit Weather(DisplayApp* app, Pinetime::Controllers::WeatherService& weather); - + Weather(Controllers::Settings& settingsController, Controllers::SimpleWeatherService& weatherService); ~Weather() override; void Refresh() override; - bool OnButtonPushed() override; - - bool OnTouchEvent(TouchEvents event) override; - private: - DisplayApp* app; - bool running = true; - - Controllers::WeatherService& weatherService; - - ScreenList<5> screens; + Controllers::Settings& settingsController; + Controllers::SimpleWeatherService& weatherService; - std::unique_ptr<Screen> CreateScreenTemperature(); + Utility::DirtyValue<std::optional<Controllers::SimpleWeatherService::CurrentWeather>> currentWeather {}; + Utility::DirtyValue<std::optional<Controllers::SimpleWeatherService::Forecast>> currentForecast {}; - std::unique_ptr<Screen> CreateScreenAir(); + lv_obj_t* icon; + lv_obj_t* condition; + lv_obj_t* temperature; + lv_obj_t* minTemperature; + lv_obj_t* maxTemperature; + lv_obj_t* forecast; - std::unique_ptr<Screen> CreateScreenClouds(); + lv_task_t* taskRefresh; + }; + } - std::unique_ptr<Screen> CreateScreenPrecipitation(); + template <> + struct AppTraits<Apps::Weather> { + static constexpr Apps app = Apps::Weather; + static constexpr const char* icon = Screens::Symbols::cloudSunRain; - std::unique_ptr<Screen> CreateScreenHumidity(); + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Weather(controllers.settingsController, *controllers.weatherController); }; - } + }; } } diff --git a/src/displayapp/screens/WeatherSymbols.cpp b/src/displayapp/screens/WeatherSymbols.cpp new file mode 100644 index 00000000..de66312f --- /dev/null +++ b/src/displayapp/screens/WeatherSymbols.cpp @@ -0,0 +1,61 @@ +#include "displayapp/screens/WeatherSymbols.h" + +const char* Pinetime::Applications::Screens::Symbols::GetSymbol(const Pinetime::Controllers::SimpleWeatherService::Icons icon) { + switch (icon) { + case Pinetime::Controllers::SimpleWeatherService::Icons::Sun: + return Symbols::sun; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudsSun: + return Symbols::cloudSun; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::Clouds: + return Symbols::cloud; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::BrokenClouds: + return Symbols::cloudMeatball; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::Thunderstorm: + return Symbols::bolt; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::Snow: + return Symbols::snowflake; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudShowerHeavy: + return Symbols::cloudShowersHeavy; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudSunRain: + return Symbols::cloudSunRain; + break; + case Pinetime::Controllers::SimpleWeatherService::Icons::Smog: + return Symbols::smog; + break; + default: + return Symbols::ban; + break; + } +} + +const char* Pinetime::Applications::Screens::Symbols::GetCondition(const Pinetime::Controllers::SimpleWeatherService::Icons icon) { + switch (icon) { + case Pinetime::Controllers::SimpleWeatherService::Icons::Sun: + return "Clear sky"; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudsSun: + return "Few clouds"; + case Pinetime::Controllers::SimpleWeatherService::Icons::Clouds: + return "Scattered clouds"; + case Pinetime::Controllers::SimpleWeatherService::Icons::BrokenClouds: + return "Broken clouds"; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudShowerHeavy: + return "Shower rain"; + case Pinetime::Controllers::SimpleWeatherService::Icons::CloudSunRain: + return "Rain"; + case Pinetime::Controllers::SimpleWeatherService::Icons::Thunderstorm: + return "Thunderstorm"; + case Pinetime::Controllers::SimpleWeatherService::Icons::Snow: + return "Snow"; + case Pinetime::Controllers::SimpleWeatherService::Icons::Smog: + return "Mist"; + default: + return ""; + } +} diff --git a/src/displayapp/screens/WeatherSymbols.h b/src/displayapp/screens/WeatherSymbols.h new file mode 100644 index 00000000..f3eeed55 --- /dev/null +++ b/src/displayapp/screens/WeatherSymbols.h @@ -0,0 +1,14 @@ +#pragma once +#include "components/ble/SimpleWeatherService.h" +#include "displayapp/screens/Symbols.h" + +namespace Pinetime { + namespace Applications { + namespace Screens { + namespace Symbols { + const char* GetSymbol(const Pinetime::Controllers::SimpleWeatherService::Icons icon); + const char* GetCondition(const Pinetime::Controllers::SimpleWeatherService::Icons icon); + } + } + } +} diff --git a/src/displayapp/screens/settings/QuickSettings.cpp b/src/displayapp/screens/settings/QuickSettings.cpp index 05484888..c5c3071a 100644 --- a/src/displayapp/screens/settings/QuickSettings.cpp +++ b/src/displayapp/screens/settings/QuickSettings.cpp @@ -33,13 +33,14 @@ QuickSettings::QuickSettings(Pinetime::Applications::DisplayApp* app, Controllers::BrightnessController& brightness, Controllers::MotorController& motorController, Pinetime::Controllers::Settings& settingsController, - const Controllers::Ble& bleController) + const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController) : app {app}, dateTimeController {dateTimeController}, brightness {brightness}, motorController {motorController}, settingsController {settingsController}, - statusIcons(batteryController, bleController) { + statusIcons(batteryController, bleController, alarmController) { statusIcons.Create(); diff --git a/src/displayapp/screens/settings/QuickSettings.h b/src/displayapp/screens/settings/QuickSettings.h index 55da6176..87c126b7 100644 --- a/src/displayapp/screens/settings/QuickSettings.h +++ b/src/displayapp/screens/settings/QuickSettings.h @@ -23,7 +23,8 @@ namespace Pinetime { Controllers::BrightnessController& brightness, Controllers::MotorController& motorController, Pinetime::Controllers::Settings& settingsController, - const Controllers::Ble& bleController); + const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController); ~QuickSettings() override; diff --git a/src/displayapp/screens/settings/SettingBluetooth.cpp b/src/displayapp/screens/settings/SettingBluetooth.cpp index 82c3dee1..e4dc695c 100644 --- a/src/displayapp/screens/settings/SettingBluetooth.cpp +++ b/src/displayapp/screens/settings/SettingBluetooth.cpp @@ -36,17 +36,19 @@ namespace { SettingBluetooth::SettingBluetooth(Pinetime::Applications::DisplayApp* app, Pinetime::Controllers::Settings& settingsController) : app {app}, + settings {settingsController}, checkboxList( 0, 1, "Bluetooth", Symbols::bluetooth, settingsController.GetBleRadioEnabled() ? 0 : 1, - [&settings = settingsController](uint32_t index) { + [this](uint32_t index) { const bool priorMode = settings.GetBleRadioEnabled(); const bool newMode = options[index].radioEnabled; if (newMode != priorMode) { settings.SetBleRadioEnabled(newMode); + this->app->PushMessage(Pinetime::Applications::Display::Messages::BleRadioEnableToggle); } }, CreateOptionArray()) { @@ -54,6 +56,4 @@ SettingBluetooth::SettingBluetooth(Pinetime::Applications::DisplayApp* app, Pine SettingBluetooth::~SettingBluetooth() { lv_obj_clean(lv_scr_act()); - // Pushing the message in the OnValueChanged function causes a freeze? - app->PushMessage(Pinetime::Applications::Display::Messages::BleRadioEnableToggle); } diff --git a/src/displayapp/screens/settings/SettingBluetooth.h b/src/displayapp/screens/settings/SettingBluetooth.h index 1e3f9b81..0cf014f5 100644 --- a/src/displayapp/screens/settings/SettingBluetooth.h +++ b/src/displayapp/screens/settings/SettingBluetooth.h @@ -20,6 +20,7 @@ namespace Pinetime { private: DisplayApp* app; + Pinetime::Controllers::Settings& settings; CheckboxList checkboxList; }; } diff --git a/src/displayapp/screens/settings/SettingDisplay.cpp b/src/displayapp/screens/settings/SettingDisplay.cpp index a9476432..bbc188a9 100644 --- a/src/displayapp/screens/settings/SettingDisplay.cpp +++ b/src/displayapp/screens/settings/SettingDisplay.cpp @@ -9,16 +9,22 @@ using namespace Pinetime::Applications::Screens; namespace { - void event_handler(lv_obj_t* obj, lv_event_t event) { + void TimeoutEventHandler(lv_obj_t* obj, lv_event_t event) { auto* screen = static_cast<SettingDisplay*>(obj->user_data); screen->UpdateSelected(obj, event); } + + void AlwaysOnEventHandler(lv_obj_t* obj, lv_event_t event) { + if (event == LV_EVENT_VALUE_CHANGED) { + auto* screen = static_cast<SettingDisplay*>(obj->user_data); + screen->ToggleAlwaysOn(); + } + } } constexpr std::array<uint16_t, 6> SettingDisplay::options; -SettingDisplay::SettingDisplay(Pinetime::Applications::DisplayApp* app, Pinetime::Controllers::Settings& settingsController) - : app {app}, settingsController {settingsController} { +SettingDisplay::SettingDisplay(Pinetime::Controllers::Settings& settingsController) : settingsController {settingsController} { lv_obj_t* container1 = lv_cont_create(lv_scr_act(), nullptr); @@ -43,19 +49,26 @@ SettingDisplay::SettingDisplay(Pinetime::Applications::DisplayApp* app, Pinetime lv_label_set_align(icon, LV_LABEL_ALIGN_CENTER); lv_obj_align(icon, title, LV_ALIGN_OUT_LEFT_MID, -10, 0); - char buffer[12]; + char buffer[4]; for (unsigned int i = 0; i < options.size(); i++) { cbOption[i] = lv_checkbox_create(container1, nullptr); - sprintf(buffer, "%2ds", options[i] / 1000); + snprintf(buffer, sizeof(buffer), "%2" PRIu16 "s", options[i] / 1000); lv_checkbox_set_text(cbOption[i], buffer); cbOption[i]->user_data = this; - lv_obj_set_event_cb(cbOption[i], event_handler); + lv_obj_set_event_cb(cbOption[i], TimeoutEventHandler); SetRadioButtonStyle(cbOption[i]); if (settingsController.GetScreenTimeOut() == options[i]) { lv_checkbox_set_checked(cbOption[i], true); } } + + alwaysOnCheckbox = lv_checkbox_create(container1, nullptr); + lv_checkbox_set_text(alwaysOnCheckbox, "Always On"); + lv_checkbox_set_checked(alwaysOnCheckbox, settingsController.GetAlwaysOnDisplaySetting()); + lv_obj_add_state(alwaysOnCheckbox, LV_STATE_DEFAULT); + alwaysOnCheckbox->user_data = this; + lv_obj_set_event_cb(alwaysOnCheckbox, AlwaysOnEventHandler); } SettingDisplay::~SettingDisplay() { @@ -63,13 +76,17 @@ SettingDisplay::~SettingDisplay() { settingsController.SaveSettings(); } +void SettingDisplay::ToggleAlwaysOn() { + settingsController.SetAlwaysOnDisplaySetting(!settingsController.GetAlwaysOnDisplaySetting()); + lv_checkbox_set_checked(alwaysOnCheckbox, settingsController.GetAlwaysOnDisplaySetting()); +} + void SettingDisplay::UpdateSelected(lv_obj_t* object, lv_event_t event) { if (event == LV_EVENT_CLICKED) { for (unsigned int i = 0; i < options.size(); i++) { if (object == cbOption[i]) { lv_checkbox_set_checked(cbOption[i], true); settingsController.SetScreenTimeOut(options[i]); - app->PushMessage(Applications::Display::Messages::UpdateTimeOut); } else { lv_checkbox_set_checked(cbOption[i], false); } diff --git a/src/displayapp/screens/settings/SettingDisplay.h b/src/displayapp/screens/settings/SettingDisplay.h index 64212c02..3bd10a62 100644 --- a/src/displayapp/screens/settings/SettingDisplay.h +++ b/src/displayapp/screens/settings/SettingDisplay.h @@ -14,17 +14,18 @@ namespace Pinetime { class SettingDisplay : public Screen { public: - SettingDisplay(DisplayApp* app, Pinetime::Controllers::Settings& settingsController); + SettingDisplay(Pinetime::Controllers::Settings& settingsController); ~SettingDisplay() override; void UpdateSelected(lv_obj_t* object, lv_event_t event); + void ToggleAlwaysOn(); private: - DisplayApp* app; static constexpr std::array<uint16_t, 6> options = {5000, 7000, 10000, 15000, 20000, 30000}; Controllers::Settings& settingsController; lv_obj_t* cbOption[options.size()]; + lv_obj_t* alwaysOnCheckbox; }; } } diff --git a/src/displayapp/screens/settings/SettingSetDateTime.cpp b/src/displayapp/screens/settings/SettingSetDateTime.cpp index cf9b0638..8926ff31 100644 --- a/src/displayapp/screens/settings/SettingSetDateTime.cpp +++ b/src/displayapp/screens/settings/SettingSetDateTime.cpp @@ -15,8 +15,7 @@ bool SettingSetDateTime::OnTouchEvent(Pinetime::Applications::TouchEvents event) SettingSetDateTime::SettingSetDateTime(Pinetime::Applications::DisplayApp* app, Pinetime::Controllers::DateTime& dateTimeController, Pinetime::Controllers::Settings& settingsController) - : app {app}, - dateTimeController {dateTimeController}, + : dateTimeController {dateTimeController}, settingsController {settingsController}, screens {app, 0, diff --git a/src/displayapp/screens/settings/SettingSetDateTime.h b/src/displayapp/screens/settings/SettingSetDateTime.h index 051b1abe..dea283f8 100644 --- a/src/displayapp/screens/settings/SettingSetDateTime.h +++ b/src/displayapp/screens/settings/SettingSetDateTime.h @@ -20,7 +20,6 @@ namespace Pinetime { void Quit(); private: - DisplayApp* app; Controllers::DateTime& dateTimeController; Controllers::Settings& settingsController; diff --git a/src/displayapp/screens/settings/SettingWakeUp.cpp b/src/displayapp/screens/settings/SettingWakeUp.cpp index 8df34c20..4649dc82 100644 --- a/src/displayapp/screens/settings/SettingWakeUp.cpp +++ b/src/displayapp/screens/settings/SettingWakeUp.cpp @@ -8,7 +8,7 @@ using namespace Pinetime::Applications::Screens; -constexpr std::array<SettingWakeUp::Option, 4> SettingWakeUp::options; +constexpr std::array<SettingWakeUp::Option, 5> SettingWakeUp::options; namespace { void event_handler(lv_obj_t* obj, lv_event_t event) { @@ -27,9 +27,9 @@ SettingWakeUp::SettingWakeUp(Pinetime::Controllers::Settings& settingsController lv_obj_set_style_local_pad_inner(container1, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 5); lv_obj_set_style_local_border_width(container1, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, 0); - lv_obj_set_pos(container1, 10, 60); + lv_obj_set_pos(container1, 10, 35); lv_obj_set_width(container1, LV_HOR_RES - 20); - lv_obj_set_height(container1, LV_VER_RES - 50); + lv_obj_set_height(container1, LV_VER_RES - 20); lv_cont_set_layout(container1, LV_LAYOUT_COLUMN_LEFT); lv_obj_t* title = lv_label_create(lv_scr_act(), nullptr); diff --git a/src/displayapp/screens/settings/SettingWakeUp.h b/src/displayapp/screens/settings/SettingWakeUp.h index 28219ca1..61edabce 100644 --- a/src/displayapp/screens/settings/SettingWakeUp.h +++ b/src/displayapp/screens/settings/SettingWakeUp.h @@ -25,11 +25,12 @@ namespace Pinetime { }; Controllers::Settings& settingsController; - static constexpr std::array<Option, 4> options = {{ + static constexpr std::array<Option, 5> options = {{ {Controllers::Settings::WakeUpMode::SingleTap, "Single Tap"}, {Controllers::Settings::WakeUpMode::DoubleTap, "Double Tap"}, {Controllers::Settings::WakeUpMode::RaiseWrist, "Raise Wrist"}, {Controllers::Settings::WakeUpMode::Shake, "Shake Wake"}, + {Controllers::Settings::WakeUpMode::LowerWrist, "Lower Wrist"}, }}; lv_obj_t* cbOption[options.size()]; diff --git a/src/displayapp/screens/settings/SettingWatchFace.cpp b/src/displayapp/screens/settings/SettingWatchFace.cpp index 285efa72..0d5168d2 100644 --- a/src/displayapp/screens/settings/SettingWatchFace.cpp +++ b/src/displayapp/screens/settings/SettingWatchFace.cpp @@ -9,6 +9,37 @@ using namespace Pinetime::Applications::Screens; constexpr const char* SettingWatchFace::title; constexpr const char* SettingWatchFace::symbol; +namespace { + uint32_t IndexOf(const std::array<Pinetime::Applications::Screens::SettingWatchFace::Item, + Pinetime::Applications::UserWatchFaceTypes::Count>& watchfaces, + Pinetime::Applications::WatchFace watchface) { + size_t index = 0; + auto found = std::find_if(watchfaces.begin(), + watchfaces.end(), + [&index, &watchface](const Pinetime::Applications::Screens::SettingWatchFace::Item& item) { + const bool result = item.watchface == watchface; + if (!result) { + index++; + } + return result; + }); + if (found == watchfaces.end()) { + index = 0; + } + + return index; + } + + Pinetime::Applications::WatchFace IndexToWatchFace(const std::array<Pinetime::Applications::Screens::SettingWatchFace::Item, + Pinetime::Applications::UserWatchFaceTypes::Count>& watchfaces, + size_t index) { + if (index >= watchfaces.size()) { + return watchfaces[0].watchface; + } + return watchfaces[index].watchface; + } +} + auto SettingWatchFace::CreateScreenList() const { std::array<std::function<std::unique_ptr<Screen>()>, nScreens> screens; for (size_t i = 0; i < screens.size(); i++) { @@ -20,9 +51,10 @@ auto SettingWatchFace::CreateScreenList() const { } SettingWatchFace::SettingWatchFace(Pinetime::Applications::DisplayApp* app, + std::array<Screens::SettingWatchFace::Item, UserWatchFaceTypes::Count>&& watchfaceItems, Pinetime::Controllers::Settings& settingsController, Pinetime::Controllers::FS& filesystem) - : app {app}, + : watchfaceItems {std::move(watchfaceItems)}, settingsController {settingsController}, filesystem {filesystem}, screens {app, 0, CreateScreenList(), Screens::ScreenListModes::UpDown} { @@ -39,7 +71,12 @@ bool SettingWatchFace::OnTouchEvent(Pinetime::Applications::TouchEvents event) { std::unique_ptr<Screen> SettingWatchFace::CreateScreen(unsigned int screenNum) const { std::array<Screens::CheckboxList::Item, settingsPerScreen> watchfacesOnThisScreen; for (int i = 0; i < settingsPerScreen; i++) { - watchfacesOnThisScreen[i] = watchfaces[screenNum * settingsPerScreen + i]; + if (i + (screenNum * settingsPerScreen) >= watchfaceItems.size()) { + watchfacesOnThisScreen[i] = {"", false}; + } else { + auto& item = watchfaceItems[i + (screenNum * settingsPerScreen)]; + watchfacesOnThisScreen[i] = Screens::CheckboxList::Item {item.name, item.enabled}; + } } return std::make_unique<Screens::CheckboxList>( @@ -47,9 +84,9 @@ std::unique_ptr<Screen> SettingWatchFace::CreateScreen(unsigned int screenNum) c nScreens, title, symbol, - settingsController.GetClockFace(), - [&settings = settingsController](uint32_t clockFace) { - settings.SetClockFace(clockFace); + static_cast<uint32_t>(IndexOf(watchfaceItems, settingsController.GetWatchFace())), + [this, &settings = settingsController](uint32_t index) { + settings.SetWatchFace(IndexToWatchFace(watchfaceItems, index)); settings.SaveSettings(); }, watchfacesOnThisScreen); diff --git a/src/displayapp/screens/settings/SettingWatchFace.h b/src/displayapp/screens/settings/SettingWatchFace.h index 45a50e3d..9edc1f7a 100644 --- a/src/displayapp/screens/settings/SettingWatchFace.h +++ b/src/displayapp/screens/settings/SettingWatchFace.h @@ -19,36 +19,34 @@ namespace Pinetime { class SettingWatchFace : public Screen { public: - SettingWatchFace(DisplayApp* app, Pinetime::Controllers::Settings& settingsController, Pinetime::Controllers::FS& filesystem); + struct Item { + const char* name; + WatchFace watchface; + bool enabled; + }; + + SettingWatchFace(DisplayApp* app, + std::array<Item, UserWatchFaceTypes::Count>&& watchfaceItems, + Pinetime::Controllers::Settings& settingsController, + Pinetime::Controllers::FS& filesystem); ~SettingWatchFace() override; bool OnTouchEvent(TouchEvents event) override; private: - DisplayApp* app; auto CreateScreenList() const; std::unique_ptr<Screen> CreateScreen(unsigned int screenNum) const; + static constexpr int settingsPerScreen = 4; + std::array<Item, UserWatchFaceTypes::Count> watchfaceItems; + static constexpr int nScreens = UserWatchFaceTypes::Count > 0 ? (UserWatchFaceTypes ::Count - 1) / settingsPerScreen + 1 : 1; + Controllers::Settings& settingsController; Pinetime::Controllers::FS& filesystem; static constexpr const char* title = "Watch face"; static constexpr const char* symbol = Symbols::home; - static constexpr int settingsPerScreen = 4; - - // Increment this when more space is needed - static constexpr int nScreens = 2; - - std::array<Screens::CheckboxList::Item, settingsPerScreen * nScreens> watchfaces { - {{"Digital face", true}, - {"Analog face", true}, - {"PineTimeStyle", true}, - {"Terminal", true}, - {"Infineat face", Applications::Screens::WatchFaceInfineat::IsAvailable(filesystem)}, - {"Casio G7710", Applications::Screens::WatchFaceCasioStyleG7710::IsAvailable(filesystem)}, - {"", false}, - {"", false}}}; ScreenList<nScreens> screens; }; } diff --git a/src/displayapp/screens/settings/SettingWeatherFormat.cpp b/src/displayapp/screens/settings/SettingWeatherFormat.cpp new file mode 100644 index 00000000..22d281b2 --- /dev/null +++ b/src/displayapp/screens/settings/SettingWeatherFormat.cpp @@ -0,0 +1,63 @@ +#include "displayapp/screens/settings/SettingWeatherFormat.h" + +#include <lvgl/lvgl.h> + +#include "displayapp/DisplayApp.h" +#include "displayapp/screens/Styles.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/screens/Symbols.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + struct Option { + Pinetime::Controllers::Settings::WeatherFormat weatherFormat; + const char* name; + }; + + constexpr std::array<Option, 2> options = {{ + {Pinetime::Controllers::Settings::WeatherFormat::Metric, "Metric"}, + {Pinetime::Controllers::Settings::WeatherFormat::Imperial, "Imperial"}, + }}; + + std::array<CheckboxList::Item, CheckboxList::MaxItems> CreateOptionArray() { + std::array<Pinetime::Applications::Screens::CheckboxList::Item, CheckboxList::MaxItems> optionArray; + for (size_t i = 0; i < CheckboxList::MaxItems; i++) { + if (i >= options.size()) { + optionArray[i].name = ""; + optionArray[i].enabled = false; + } else { + optionArray[i].name = options[i].name; + optionArray[i].enabled = true; + } + } + return optionArray; + } + + uint32_t GetDefaultOption(Pinetime::Controllers::Settings::WeatherFormat currentOption) { + for (size_t i = 0; i < options.size(); i++) { + if (options[i].weatherFormat == currentOption) { + return i; + } + } + return 0; + } +} + +SettingWeatherFormat::SettingWeatherFormat(Pinetime::Controllers::Settings& settingsController) + : checkboxList( + 0, + 1, + "Weather format", + Symbols::cloudSunRain, + GetDefaultOption(settingsController.GetWeatherFormat()), + [&settings = settingsController](uint32_t index) { + settings.SetWeatherFormat(options[index].weatherFormat); + settings.SaveSettings(); + }, + CreateOptionArray()) { +} + +SettingWeatherFormat::~SettingWeatherFormat() { + lv_obj_clean(lv_scr_act()); +} diff --git a/src/displayapp/screens/settings/SettingWeatherFormat.h b/src/displayapp/screens/settings/SettingWeatherFormat.h new file mode 100644 index 00000000..a3d2bf4b --- /dev/null +++ b/src/displayapp/screens/settings/SettingWeatherFormat.h @@ -0,0 +1,26 @@ +#pragma once + +#include <array> +#include <cstdint> +#include <lvgl/lvgl.h> + +#include "components/settings/Settings.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/screens/CheckboxList.h" + +namespace Pinetime { + + namespace Applications { + namespace Screens { + + class SettingWeatherFormat : public Screen { + public: + explicit SettingWeatherFormat(Pinetime::Controllers::Settings& settingsController); + ~SettingWeatherFormat() override; + + private: + CheckboxList checkboxList; + }; + } + } +} diff --git a/src/displayapp/screens/settings/Settings.cpp b/src/displayapp/screens/settings/Settings.cpp index 065417fa..cb5ba413 100644 --- a/src/displayapp/screens/settings/Settings.cpp +++ b/src/displayapp/screens/settings/Settings.cpp @@ -1,7 +1,7 @@ #include "displayapp/screens/settings/Settings.h" #include <lvgl/lvgl.h> #include <functional> -#include "displayapp/Apps.h" +#include "displayapp/apps/Apps.h" #include "displayapp/DisplayApp.h" using namespace Pinetime::Applications::Screens; diff --git a/src/displayapp/screens/settings/Settings.h b/src/displayapp/screens/settings/Settings.h index 3f809753..3722c2be 100644 --- a/src/displayapp/screens/settings/Settings.h +++ b/src/displayapp/screens/settings/Settings.h @@ -29,7 +29,7 @@ namespace Pinetime { static constexpr int entriesPerScreen = 4; // Increment this when more space is needed - static constexpr int nScreens = 3; + static constexpr int nScreens = 4; static constexpr std::array<List::Applications, entriesPerScreen * nScreens> entries {{ {Symbols::sun, "Display", Apps::SettingDisplay}, @@ -38,13 +38,15 @@ namespace Pinetime { {Symbols::home, "Watch face", Apps::SettingWatchFace}, {Symbols::shoe, "Steps", Apps::SettingSteps}, - {Symbols::clock, "Date&Time", Apps::SettingSetDateTime}, + {Symbols::clock, "Date & Time", Apps::SettingSetDateTime}, + {Symbols::cloudSunRain, "Weather", Apps::SettingWeatherFormat}, {Symbols::batteryHalf, "Battery", Apps::BatteryInfo}, - {Symbols::clock, "Chimes", Apps::SettingChimes}, + {Symbols::clock, "Chimes", Apps::SettingChimes}, {Symbols::tachometer, "Shake Calib.", Apps::SettingShakeThreshold}, {Symbols::check, "Firmware", Apps::FirmwareValidation}, {Symbols::bluetooth, "Bluetooth", Apps::SettingBluetooth}, + {Symbols::list, "About", Apps::SysInfo}, // {Symbols::none, "None", Apps::None}, diff --git a/src/displayapp/widgets/StatusIcons.cpp b/src/displayapp/widgets/StatusIcons.cpp index 423b53d9..777731a5 100644 --- a/src/displayapp/widgets/StatusIcons.cpp +++ b/src/displayapp/widgets/StatusIcons.cpp @@ -1,10 +1,13 @@ #include "displayapp/widgets/StatusIcons.h" #include "displayapp/screens/Symbols.h" +#include "components/alarm/AlarmController.h" using namespace Pinetime::Applications::Widgets; -StatusIcons::StatusIcons(const Controllers::Battery& batteryController, const Controllers::Ble& bleController) - : batteryIcon(true), batteryController {batteryController}, bleController {bleController} { +StatusIcons::StatusIcons(const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController) + : batteryIcon(true), batteryController {batteryController}, bleController {bleController}, alarmController {alarmController} { } void StatusIcons::Create() { @@ -20,6 +23,9 @@ void StatusIcons::Create() { batteryPlug = lv_label_create(container, nullptr); lv_label_set_text_static(batteryPlug, Screens::Symbols::plug); + alarmIcon = lv_label_create(container, nullptr); + lv_label_set_text_static(alarmIcon, Screens::Symbols::bell); + batteryIcon.Create(container); lv_obj_align(container, nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0); @@ -37,6 +43,11 @@ void StatusIcons::Update() { batteryIcon.SetBatteryPercentage(batteryPercent); } + alarmEnabled = alarmController.IsEnabled(); + if (alarmEnabled.IsUpdated()) { + lv_obj_set_hidden(alarmIcon, !alarmEnabled.Get()); + } + bleState = bleController.IsConnected(); bleRadioEnabled = bleController.IsRadioEnabled(); if (bleState.IsUpdated() || bleRadioEnabled.IsUpdated()) { diff --git a/src/displayapp/widgets/StatusIcons.h b/src/displayapp/widgets/StatusIcons.h index 27e8b86a..5524e996 100644 --- a/src/displayapp/widgets/StatusIcons.h +++ b/src/displayapp/widgets/StatusIcons.h @@ -5,14 +5,18 @@ #include "displayapp/screens/Screen.h" #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" +#include "components/alarm/AlarmController.h" #include "displayapp/screens/BatteryIcon.h" +#include "utility/DirtyValue.h" namespace Pinetime { namespace Applications { namespace Widgets { class StatusIcons { public: - StatusIcons(const Controllers::Battery& batteryController, const Controllers::Ble& bleController); + StatusIcons(const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + const Controllers::AlarmController& alarmController); void Align(); void Create(); @@ -26,13 +30,16 @@ namespace Pinetime { Screens::BatteryIcon batteryIcon; const Pinetime::Controllers::Battery& batteryController; const Controllers::Ble& bleController; + const Controllers::AlarmController& alarmController; - Screens::DirtyValue<uint8_t> batteryPercentRemaining {}; - Screens::DirtyValue<bool> powerPresent {}; - Screens::DirtyValue<bool> bleState {}; - Screens::DirtyValue<bool> bleRadioEnabled {}; + Utility::DirtyValue<uint8_t> batteryPercentRemaining {}; + Utility::DirtyValue<bool> powerPresent {}; + Utility::DirtyValue<bool> bleState {}; + Utility::DirtyValue<bool> bleRadioEnabled {}; + Utility::DirtyValue<bool> alarmEnabled {}; lv_obj_t* bleIcon; + lv_obj_t* alarmIcon; lv_obj_t* batteryPlug; lv_obj_t* container; }; diff --git a/src/drivers/Bma421.cpp b/src/drivers/Bma421.cpp index 84d76ab3..74d47d06 100644 --- a/src/drivers/Bma421.cpp +++ b/src/drivers/Bma421.cpp @@ -22,6 +22,16 @@ namespace { void user_delay(uint32_t period_us, void* /*intf_ptr*/) { nrf_delay_us(period_us); } + + // Scale factors to convert accelerometer counts to milli-g + // from datasheet: https://files.pine64.org/doc/datasheet/pinetime/BST-BMA421-FL000.pdf + // The array index to use is stored in accel_conf.range + constexpr int16_t accelScaleFactors[] = { + [BMA4_ACCEL_RANGE_2G] = 1024, // LSB/g +/- 2g range + [BMA4_ACCEL_RANGE_4G] = 512, // LSB/g +/- 4g range + [BMA4_ACCEL_RANGE_8G] = 256, // LSB/g +/- 8g range + [BMA4_ACCEL_RANGE_16G] = 128 // LSB/g +/- 16g range + }; } Bma421::Bma421(TwiMaster& twiMaster, uint8_t twiAddress) : twiMaster {twiMaster}, deviceAddress {twiAddress} { @@ -74,7 +84,6 @@ void Bma421::Init() { if (ret != BMA4_OK) return; - struct bma4_accel_config accel_conf; accel_conf.odr = BMA4_OUTPUT_DATA_RATE_100HZ; accel_conf.range = BMA4_ACCEL_RANGE_2G; accel_conf.bandwidth = BMA4_ACCEL_NORMAL_AVG4; @@ -102,19 +111,21 @@ void Bma421::Write(uint8_t registerAddress, const uint8_t* data, size_t size) { Bma421::Values Bma421::Process() { if (not isOk) return {}; + struct bma4_accel rawData; struct bma4_accel data; - bma4_read_accel_xyz(&data, &bma); + bma4_read_accel_xyz(&rawData, &bma); + + // Scale the measured ADC counts to units of 'binary milli-g' + // where 1g = 1024 'binary milli-g' units. + // See https://github.com/InfiniTimeOrg/InfiniTime/pull/1950 for + // discussion of why we opted for scaling to 1024 rather than 1000. + data.x = 1024 * rawData.x / accelScaleFactors[accel_conf.range]; + data.y = 1024 * rawData.y / accelScaleFactors[accel_conf.range]; + data.z = 1024 * rawData.z / accelScaleFactors[accel_conf.range]; uint32_t steps = 0; bma423_step_counter_output(&steps, &bma); - int32_t temperature; - bma4_get_temperature(&temperature, BMA4_DEG, &bma); - temperature = temperature / 1000; - - uint8_t activity = 0; - bma423_activity_output(&activity, &bma); - // X and Y axis are swapped because of the way the sensor is mounted in the PineTime return {steps, data.y, data.x, data.z}; } diff --git a/src/drivers/Bma421.h b/src/drivers/Bma421.h index fb832514..5269f62b 100644 --- a/src/drivers/Bma421.h +++ b/src/drivers/Bma421.h @@ -41,6 +41,7 @@ namespace Pinetime { TwiMaster& twiMaster; uint8_t deviceAddress = 0x18; struct bma4_dev bma; + struct bma4_accel_config accel_conf; // Store the device configuration for later reference. bool isOk = false; bool isResetOk = false; DeviceTypes deviceType = DeviceTypes::Unknown; diff --git a/src/drivers/Hrs3300.cpp b/src/drivers/Hrs3300.cpp index 6c47ae28..a4b72479 100644 --- a/src/drivers/Hrs3300.cpp +++ b/src/drivers/Hrs3300.cpp @@ -6,6 +6,7 @@ #include "drivers/Hrs3300.h" #include <algorithm> +#include <iterator> #include <nrf_gpio.h> #include <FreeRTOS.h> @@ -14,8 +15,14 @@ using namespace Pinetime::Drivers; +namespace { + static constexpr uint8_t ledDriveCurrentValue = 0x2f; +} + /** Driver for the HRS3300 heart rate sensor. - * Original implementation from wasp-os : https://github.com/daniel-thompson/wasp-os/blob/master/wasp/drivers/hrs3300.py + * Original implementation from wasp-os : https://github.com/wasp-os/wasp-os/blob/master/wasp/drivers/hrs3300.py + * + * Experimentaly derived changes to improve signal/noise (see comments below) - Ceimour */ Hrs3300::Hrs3300(TwiMaster& twiMaster, uint8_t twiAddress) : twiMaster {twiMaster}, twiAddress {twiAddress} { } @@ -26,19 +33,21 @@ void Hrs3300::Init() { Disable(); vTaskDelay(100); - // HRS disabled, 12.5 ms wait time between cycles, (partly) 20mA drive - WriteRegister(static_cast<uint8_t>(Registers::Enable), 0x60); + // HRS disabled, 50ms wait time between ADC conversion period, current 12.5mA + WriteRegister(static_cast<uint8_t>(Registers::Enable), 0x50); - // (partly) 20mA drive, power on, "magic" (datasheet says both - // "reserved" and "set low nibble to 8" but 0xe gives better results - // and is used by at least two other HRS3300 drivers - WriteRegister(static_cast<uint8_t>(Registers::PDriver), 0x6E); + // Current 12.5mA and low nibble 0xF. + // Note: Setting low nibble to 0x8 per the datasheet results in + // modulated LED driver output. Setting to 0xF results in clean, + // steady output during the ADC conversion period. + WriteRegister(static_cast<uint8_t>(Registers::PDriver), ledDriveCurrentValue); - // HRS and ALS both in 16-bit mode - WriteRegister(static_cast<uint8_t>(Registers::Res), 0x88); + // HRS and ALS both in 15-bit mode results in ~50ms LED drive period + // and presumably ~50ms ADC conversion period. + WriteRegister(static_cast<uint8_t>(Registers::Res), 0x77); - // 8x gain, non default, reduced value for better readings - WriteRegister(static_cast<uint8_t>(Registers::Hgain), 0xc); + // Gain set to 1x + WriteRegister(static_cast<uint8_t>(Registers::Hgain), 0x00); } void Hrs3300::Enable() { @@ -46,6 +55,8 @@ void Hrs3300::Enable() { auto value = ReadRegister(static_cast<uint8_t>(Registers::Enable)); value |= 0x80; WriteRegister(static_cast<uint8_t>(Registers::Enable), value); + + WriteRegister(static_cast<uint8_t>(Registers::PDriver), ledDriveCurrentValue); } void Hrs3300::Disable() { @@ -53,42 +64,41 @@ void Hrs3300::Disable() { auto value = ReadRegister(static_cast<uint8_t>(Registers::Enable)); value &= ~0x80; WriteRegister(static_cast<uint8_t>(Registers::Enable), value); -} -uint32_t Hrs3300::ReadHrs() { - auto m = ReadRegister(static_cast<uint8_t>(Registers::C0DataM)); - auto h = ReadRegister(static_cast<uint8_t>(Registers::C0DataH)); - auto l = ReadRegister(static_cast<uint8_t>(Registers::C0dataL)); - return ((l & 0x30) << 12) | (m << 8) | ((h & 0x0f) << 4) | (l & 0x0f); + WriteRegister(static_cast<uint8_t>(Registers::PDriver), 0); } -uint32_t Hrs3300::ReadAls() { - auto m = ReadRegister(static_cast<uint8_t>(Registers::C1dataM)); - auto h = ReadRegister(static_cast<uint8_t>(Registers::C1dataH)); - auto l = ReadRegister(static_cast<uint8_t>(Registers::C1dataL)); - return ((h & 0x3f) << 11) | (m << 3) | (l & 0x07); -} +Hrs3300::PackedHrsAls Hrs3300::ReadHrsAls() { + constexpr Registers dataRegisters[] = + {Registers::C1dataM, Registers::C0DataM, Registers::C0DataH, Registers::C1dataH, Registers::C1dataL, Registers::C0dataL}; + // Calculate smallest register address + constexpr uint8_t baseOffset = static_cast<uint8_t>(*std::min_element(std::begin(dataRegisters), std::end(dataRegisters))); + // Calculate largest address to determine length of read needed + // Add one to largest relative index to find the length + constexpr uint8_t length = static_cast<uint8_t>(*std::max_element(std::begin(dataRegisters), std::end(dataRegisters))) - baseOffset + 1; -void Hrs3300::SetGain(uint8_t gain) { - constexpr uint8_t maxGain = 64U; - gain = std::min(gain, maxGain); - uint8_t hgain = 0; - while ((1 << hgain) < gain) { - ++hgain; + Hrs3300::PackedHrsAls res; + uint8_t buf[length]; + auto ret = twiMaster.Read(twiAddress, baseOffset, buf, length); + if (ret != TwiMaster::ErrorCodes::NoError) { + NRF_LOG_INFO("READ ERROR"); } + // hrs + uint8_t m = static_cast<uint8_t>(Registers::C0DataM) - baseOffset; + uint8_t h = static_cast<uint8_t>(Registers::C0DataH) - baseOffset; + uint8_t l = static_cast<uint8_t>(Registers::C0dataL) - baseOffset; + // There are two extra bits (17 and 18) but they are not read here + // as resolutions >16bit aren't practically useful (too slow) and + // all hrs values throughout InfiniTime are 16bit + res.hrs = (buf[m] << 8) | ((buf[h] & 0x0f) << 4) | (buf[l] & 0x0f); - WriteRegister(static_cast<uint8_t>(Registers::Hgain), hgain << 2); -} - -void Hrs3300::SetDrive(uint8_t drive) { - auto en = ReadRegister(static_cast<uint8_t>(Registers::Enable)); - auto pd = ReadRegister(static_cast<uint8_t>(Registers::PDriver)); - - en = (en & 0xf7) | ((drive & 2) << 2); - pd = (pd & 0xbf) | ((drive & 1) << 6); + // als + m = static_cast<uint8_t>(Registers::C1dataM) - baseOffset; + h = static_cast<uint8_t>(Registers::C1dataH) - baseOffset; + l = static_cast<uint8_t>(Registers::C1dataL) - baseOffset; + res.als = ((buf[h] & 0x3f) << 11) | (buf[m] << 3) | (buf[l] & 0x07); - WriteRegister(static_cast<uint8_t>(Registers::Enable), en); - WriteRegister(static_cast<uint8_t>(Registers::PDriver), pd); + return res; } void Hrs3300::WriteRegister(uint8_t reg, uint8_t data) { diff --git a/src/drivers/Hrs3300.h b/src/drivers/Hrs3300.h index 8bbdc69a..6f721448 100644 --- a/src/drivers/Hrs3300.h +++ b/src/drivers/Hrs3300.h @@ -21,6 +21,11 @@ namespace Pinetime { Hgain = 0x17 }; + struct PackedHrsAls { + uint16_t hrs; + uint16_t als; + }; + Hrs3300(TwiMaster& twiMaster, uint8_t twiAddress); Hrs3300(const Hrs3300&) = delete; Hrs3300& operator=(const Hrs3300&) = delete; @@ -30,10 +35,7 @@ namespace Pinetime { void Init(); void Enable(); void Disable(); - uint32_t ReadHrs(); - uint32_t ReadAls(); - void SetGain(uint8_t gain); - void SetDrive(uint8_t drive); + PackedHrsAls ReadHrsAls(); private: TwiMaster& twiMaster; diff --git a/src/drivers/PinMap.h b/src/drivers/PinMap.h index a70cfc41..238b965d 100644 --- a/src/drivers/PinMap.h +++ b/src/drivers/PinMap.h @@ -34,6 +34,7 @@ namespace Pinetime { static constexpr uint8_t SpiFlashCsn = 5; static constexpr uint8_t SpiLcdCsn = 25; static constexpr uint8_t LcdDataCommand = 18; + static constexpr uint8_t LcdReset = 26; static constexpr uint8_t TwiScl = 7; static constexpr uint8_t TwiSda = 6; diff --git a/src/drivers/Spi.cpp b/src/drivers/Spi.cpp index e477622b..a95a7eae 100644 --- a/src/drivers/Spi.cpp +++ b/src/drivers/Spi.cpp @@ -9,8 +9,8 @@ Spi::Spi(SpiMaster& spiMaster, uint8_t pinCsn) : spiMaster {spiMaster}, pinCsn { nrf_gpio_pin_set(pinCsn); } -bool Spi::Write(const uint8_t* data, size_t size) { - return spiMaster.Write(pinCsn, data, size); +bool Spi::Write(const uint8_t* data, size_t size, const std::function<void()>& preTransactionHook) { + return spiMaster.Write(pinCsn, data, size, preTransactionHook); } bool Spi::Read(uint8_t* cmd, size_t cmdSize, uint8_t* data, size_t dataSize) { @@ -27,7 +27,8 @@ bool Spi::WriteCmdAndBuffer(const uint8_t* cmd, size_t cmdSize, const uint8_t* d } bool Spi::Init() { - nrf_gpio_pin_set(pinCsn); /* disable Set slave select (inactive high) */ + nrf_gpio_cfg_output(pinCsn); + nrf_gpio_pin_set(pinCsn); return true; } diff --git a/src/drivers/Spi.h b/src/drivers/Spi.h index 9b6a30f4..0c5edf08 100644 --- a/src/drivers/Spi.h +++ b/src/drivers/Spi.h @@ -1,6 +1,7 @@ #pragma once #include <cstdint> #include <cstddef> +#include <functional> #include "drivers/SpiMaster.h" namespace Pinetime { @@ -14,7 +15,7 @@ namespace Pinetime { Spi& operator=(Spi&&) = delete; bool Init(); - bool Write(const uint8_t* data, size_t size); + bool Write(const uint8_t* data, size_t size, const std::function<void()>& preTransactionHook); bool Read(uint8_t* cmd, size_t cmdSize, uint8_t* data, size_t dataSize); bool WriteCmdAndBuffer(const uint8_t* cmd, size_t cmdSize, const uint8_t* data, size_t dataSize); void Sleep(); diff --git a/src/drivers/SpiMaster.cpp b/src/drivers/SpiMaster.cpp index 234884ab..19422ef3 100644 --- a/src/drivers/SpiMaster.cpp +++ b/src/drivers/SpiMaster.cpp @@ -94,32 +94,45 @@ bool SpiMaster::Init() { return true; } -void SpiMaster::SetupWorkaroundForFtpan58(NRF_SPIM_Type* spim, uint32_t ppi_channel, uint32_t gpiote_channel) { - // Create an event when SCK toggles. - NRF_GPIOTE->CONFIG[gpiote_channel] = (GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos) | (spim->PSEL.SCK << GPIOTE_CONFIG_PSEL_Pos) | - (GPIOTE_CONFIG_POLARITY_Toggle << GPIOTE_CONFIG_POLARITY_Pos); +void SpiMaster::SetupWorkaroundForErratum58() { + nrfx_gpiote_pin_t pin = spiBaseAddress->PSEL.SCK; + nrfx_gpiote_in_config_t gpioteCfg = {.sense = NRF_GPIOTE_POLARITY_TOGGLE, + .pull = NRF_GPIO_PIN_NOPULL, + .is_watcher = false, + .hi_accuracy = true, + .skip_gpio_setup = true}; + if (!workaroundActive) { + // Create an event when SCK toggles. + APP_ERROR_CHECK(nrfx_gpiote_in_init(pin, &gpioteCfg, NULL)); + nrfx_gpiote_in_event_enable(pin, false); + + // Stop the spim instance when SCK toggles. + nrf_ppi_channel_endpoint_setup(workaroundPpi, nrfx_gpiote_in_event_addr_get(pin), spiBaseAddress->TASKS_STOP); + nrf_ppi_channel_enable(workaroundPpi); + } - // Stop the spim instance when SCK toggles. - NRF_PPI->CH[ppi_channel].EEP = (uint32_t) &NRF_GPIOTE->EVENTS_IN[gpiote_channel]; - NRF_PPI->CH[ppi_channel].TEP = (uint32_t) &spim->TASKS_STOP; - NRF_PPI->CHENSET = 1U << ppi_channel; spiBaseAddress->EVENTS_END = 0; // Disable IRQ - spim->INTENCLR = (1 << 6); - spim->INTENCLR = (1 << 1); - spim->INTENCLR = (1 << 19); + spiBaseAddress->INTENCLR = (1 << 6); + spiBaseAddress->INTENCLR = (1 << 1); + spiBaseAddress->INTENCLR = (1 << 19); + workaroundActive = true; } -void SpiMaster::DisableWorkaroundForFtpan58(NRF_SPIM_Type* spim, uint32_t ppi_channel, uint32_t gpiote_channel) { - NRF_GPIOTE->CONFIG[gpiote_channel] = 0; - NRF_PPI->CH[ppi_channel].EEP = 0; - NRF_PPI->CH[ppi_channel].TEP = 0; - NRF_PPI->CHENSET = ppi_channel; +void SpiMaster::DisableWorkaroundForErratum58() { + nrfx_gpiote_pin_t pin = spiBaseAddress->PSEL.SCK; + if (workaroundActive) { + nrfx_gpiote_in_uninit(pin); + nrf_ppi_channel_disable(workaroundPpi); + } spiBaseAddress->EVENTS_END = 0; - spim->INTENSET = (1 << 6); - spim->INTENSET = (1 << 1); - spim->INTENSET = (1 << 19); + + // Enable IRQ + spiBaseAddress->INTENSET = (1 << 6); + spiBaseAddress->INTENSET = (1 << 1); + spiBaseAddress->INTENSET = (1 << 19); + workaroundActive = false; } void SpiMaster::OnEndEvent() { @@ -131,29 +144,23 @@ void SpiMaster::OnEndEvent() { if (s > 0) { auto currentSize = std::min((size_t) 255, s); PrepareTx(currentBufferAddr, currentSize); - currentBufferAddr += currentSize; - currentBufferSize -= currentSize; + currentBufferAddr = currentBufferAddr + currentSize; + currentBufferSize = currentBufferSize - currentSize; spiBaseAddress->TASKS_START = 1; } else { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - if (taskToNotify != nullptr) { - vTaskNotifyGiveFromISR(taskToNotify, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } - nrf_gpio_pin_set(this->pinCsn); currentBufferAddr = 0; - BaseType_t xHigherPriorityTaskWoken2 = pdFALSE; - xSemaphoreGiveFromISR(mutex, &xHigherPriorityTaskWoken2); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken | xHigherPriorityTaskWoken2); + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + xSemaphoreGiveFromISR(mutex, &xHigherPriorityTaskWoken); + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } void SpiMaster::OnStartedEvent() { } -void SpiMaster::PrepareTx(const volatile uint32_t bufferAddress, const volatile size_t size) { +void SpiMaster::PrepareTx(const uint32_t bufferAddress, const size_t size) { spiBaseAddress->TXD.PTR = bufferAddress; spiBaseAddress->TXD.MAXCNT = size; spiBaseAddress->TXD.LIST = 0; @@ -163,7 +170,7 @@ void SpiMaster::PrepareTx(const volatile uint32_t bufferAddress, const volatile spiBaseAddress->EVENTS_END = 0; } -void SpiMaster::PrepareRx(const volatile uint32_t bufferAddress, const volatile size_t size) { +void SpiMaster::PrepareRx(const uint32_t bufferAddress, const size_t size) { spiBaseAddress->TXD.PTR = 0; spiBaseAddress->TXD.MAXCNT = 0; spiBaseAddress->TXD.LIST = 0; @@ -173,21 +180,23 @@ void SpiMaster::PrepareRx(const volatile uint32_t bufferAddress, const volatile spiBaseAddress->EVENTS_END = 0; } -bool SpiMaster::Write(uint8_t pinCsn, const uint8_t* data, size_t size) { +bool SpiMaster::Write(uint8_t pinCsn, const uint8_t* data, size_t size, const std::function<void()>& preTransactionHook) { if (data == nullptr) return false; auto ok = xSemaphoreTake(mutex, portMAX_DELAY); ASSERT(ok == true); - taskToNotify = xTaskGetCurrentTaskHandle(); this->pinCsn = pinCsn; if (size == 1) { - SetupWorkaroundForFtpan58(spiBaseAddress, 0, 0); + SetupWorkaroundForErratum58(); } else { - DisableWorkaroundForFtpan58(spiBaseAddress, 0, 0); + DisableWorkaroundForErratum58(); } + if (preTransactionHook != nullptr) { + preTransactionHook(); + } nrf_gpio_pin_clear(this->pinCsn); currentBufferAddr = (uint32_t) data; @@ -195,8 +204,8 @@ bool SpiMaster::Write(uint8_t pinCsn, const uint8_t* data, size_t size) { auto currentSize = std::min((size_t) 255, (size_t) currentBufferSize); PrepareTx(currentBufferAddr, currentSize); - currentBufferSize -= currentSize; - currentBufferAddr += currentSize; + currentBufferSize = currentBufferSize - currentSize; + currentBufferAddr = currentBufferAddr + currentSize; spiBaseAddress->TASKS_START = 1; if (size == 1) { @@ -204,6 +213,9 @@ bool SpiMaster::Write(uint8_t pinCsn, const uint8_t* data, size_t size) { ; nrf_gpio_pin_set(this->pinCsn); currentBufferAddr = 0; + + DisableWorkaroundForErratum58(); + xSemaphoreGive(mutex); } @@ -213,10 +225,8 @@ bool SpiMaster::Write(uint8_t pinCsn, const uint8_t* data, size_t size) { bool SpiMaster::Read(uint8_t pinCsn, uint8_t* cmd, size_t cmdSize, uint8_t* data, size_t dataSize) { xSemaphoreTake(mutex, portMAX_DELAY); - taskToNotify = nullptr; - this->pinCsn = pinCsn; - DisableWorkaroundForFtpan58(spiBaseAddress, 0, 0); + DisableWorkaroundForErratum58(); spiBaseAddress->INTENCLR = (1 << 6); spiBaseAddress->INTENCLR = (1 << 1); spiBaseAddress->INTENCLR = (1 << 19); @@ -262,10 +272,8 @@ void SpiMaster::Wakeup() { bool SpiMaster::WriteCmdAndBuffer(uint8_t pinCsn, const uint8_t* cmd, size_t cmdSize, const uint8_t* data, size_t dataSize) { xSemaphoreTake(mutex, portMAX_DELAY); - taskToNotify = nullptr; - this->pinCsn = pinCsn; - DisableWorkaroundForFtpan58(spiBaseAddress, 0, 0); + DisableWorkaroundForErratum58(); spiBaseAddress->INTENCLR = (1 << 6); spiBaseAddress->INTENCLR = (1 << 1); spiBaseAddress->INTENCLR = (1 << 19); diff --git a/src/drivers/SpiMaster.h b/src/drivers/SpiMaster.h index 8b698c57..be6e5351 100644 --- a/src/drivers/SpiMaster.h +++ b/src/drivers/SpiMaster.h @@ -1,10 +1,13 @@ #pragma once #include <cstddef> #include <cstdint> +#include <functional> #include <FreeRTOS.h> #include <semphr.h> #include <task.h> +#include "nrfx_gpiote.h" +#include "nrf_ppi.h" namespace Pinetime { namespace Drivers { @@ -31,7 +34,7 @@ namespace Pinetime { SpiMaster& operator=(SpiMaster&&) = delete; bool Init(); - bool Write(uint8_t pinCsn, const uint8_t* data, size_t size); + bool Write(uint8_t pinCsn, const uint8_t* data, size_t size, const std::function<void()>& preTransactionHook); bool Read(uint8_t pinCsn, uint8_t* cmd, size_t cmdSize, uint8_t* data, size_t dataSize); bool WriteCmdAndBuffer(uint8_t pinCsn, const uint8_t* cmd, size_t cmdSize, const uint8_t* data, size_t dataSize); @@ -43,8 +46,8 @@ namespace Pinetime { void Wakeup(); private: - void SetupWorkaroundForFtpan58(NRF_SPIM_Type* spim, uint32_t ppi_channel, uint32_t gpiote_channel); - void DisableWorkaroundForFtpan58(NRF_SPIM_Type* spim, uint32_t ppi_channel, uint32_t gpiote_channel); + void SetupWorkaroundForErratum58(); + void DisableWorkaroundForErratum58(); void PrepareTx(const volatile uint32_t bufferAddress, const volatile size_t size); void PrepareRx(const volatile uint32_t bufferAddress, const volatile size_t size); @@ -56,8 +59,9 @@ namespace Pinetime { volatile uint32_t currentBufferAddr = 0; volatile size_t currentBufferSize = 0; - volatile TaskHandle_t taskToNotify; SemaphoreHandle_t mutex = nullptr; + static constexpr nrf_ppi_channel_t workaroundPpi = NRF_PPI_CHANNEL0; + bool workaroundActive = false; }; } } diff --git a/src/drivers/SpiNorFlash.cpp b/src/drivers/SpiNorFlash.cpp index 28f82fe6..3bc45cca 100644 --- a/src/drivers/SpiNorFlash.cpp +++ b/src/drivers/SpiNorFlash.cpp @@ -10,7 +10,7 @@ SpiNorFlash::SpiNorFlash(Spi& spi) : spi {spi} { } void SpiNorFlash::Init() { - device_id = ReadIdentificaion(); + device_id = ReadIdentification(); NRF_LOG_INFO("[SpiNorFlash] Manufacturer : %d, Memory type : %d, memory density : %d", device_id.manufacturer, device_id.type, @@ -22,7 +22,7 @@ void SpiNorFlash::Uninit() { void SpiNorFlash::Sleep() { auto cmd = static_cast<uint8_t>(Commands::DeepPowerDown); - spi.Write(&cmd, sizeof(uint8_t)); + spi.Write(&cmd, sizeof(uint8_t), nullptr); NRF_LOG_INFO("[SpiNorFlash] Sleep") } @@ -32,7 +32,7 @@ void SpiNorFlash::Wakeup() { uint8_t cmd[cmdSize] = {static_cast<uint8_t>(Commands::ReleaseFromDeepPowerDown), 0x01, 0x02, 0x03}; uint8_t id = 0; spi.Read(reinterpret_cast<uint8_t*>(&cmd), cmdSize, &id, 1); - auto devId = device_id = ReadIdentificaion(); + auto devId = device_id = ReadIdentification(); if (devId.type != device_id.type) { NRF_LOG_INFO("[SpiNorFlash] ID on Wakeup: Failed"); } else { @@ -41,7 +41,7 @@ void SpiNorFlash::Wakeup() { NRF_LOG_INFO("[SpiNorFlash] Wakeup") } -SpiNorFlash::Identification SpiNorFlash::ReadIdentificaion() { +SpiNorFlash::Identification SpiNorFlash::ReadIdentification() { auto cmd = static_cast<uint8_t>(Commands::ReadIdentification); Identification identification; spi.Read(&cmd, 1, reinterpret_cast<uint8_t*>(&identification), sizeof(Identification)); @@ -145,3 +145,7 @@ void SpiNorFlash::Write(uint32_t address, const uint8_t* buffer, size_t size) { len -= toWrite; } } + +SpiNorFlash::Identification SpiNorFlash::GetIdentification() const { + return device_id; +} diff --git a/src/drivers/SpiNorFlash.h b/src/drivers/SpiNorFlash.h index 8a063fea..028f92b4 100644 --- a/src/drivers/SpiNorFlash.h +++ b/src/drivers/SpiNorFlash.h @@ -20,7 +20,6 @@ namespace Pinetime { uint8_t density = 0; }; - Identification ReadIdentificaion(); uint8_t ReadStatusRegister(); bool WriteInProgress(); bool WriteEnabled(); @@ -33,6 +32,8 @@ namespace Pinetime { bool ProgramFailed(); bool EraseFailed(); + Identification GetIdentification() const; + void Init(); void Uninit(); @@ -40,6 +41,8 @@ namespace Pinetime { void Wakeup(); private: + Identification ReadIdentification(); + enum class Commands : uint8_t { PageProgram = 0x02, Read = 0x03, diff --git a/src/drivers/St7789.cpp b/src/drivers/St7789.cpp index cfd5bd2c..482fbad6 100644 --- a/src/drivers/St7789.cpp +++ b/src/drivers/St7789.cpp @@ -1,66 +1,123 @@ +#include <cstring> #include "drivers/St7789.h" #include <hal/nrf_gpio.h> -#include <libraries/delay/nrf_delay.h> #include <nrfx_log.h> #include "drivers/Spi.h" +#include "task.h" using namespace Pinetime::Drivers; -St7789::St7789(Spi& spi, uint8_t pinDataCommand) : spi {spi}, pinDataCommand {pinDataCommand} { +St7789::St7789(Spi& spi, uint8_t pinDataCommand, uint8_t pinReset) : spi {spi}, pinDataCommand {pinDataCommand}, pinReset {pinReset} { } void St7789::Init() { - spi.Init(); nrf_gpio_cfg_output(pinDataCommand); - nrf_gpio_cfg_output(26); - nrf_gpio_pin_set(26); + nrf_gpio_cfg_output(pinReset); + nrf_gpio_pin_set(pinReset); HardwareReset(); SoftwareReset(); + Command2Enable(); SleepOut(); - ColMod(); + PixelFormat(); MemoryDataAccessControl(); - ColumnAddressSet(); - RowAddressSet(); + SetAddrWindow(0, 0, Width, Height); // P8B Mirrored version does not need display inversion. #ifndef DRIVER_DISPLAY_MIRROR DisplayInversionOn(); #endif + PorchSet(); + FrameRateNormalSet(); + IdleFrameRateOff(); NormalModeOn(); SetVdv(); + PowerControl(); + GateControl(); DisplayOn(); } -void St7789::WriteCommand(uint8_t cmd) { - nrf_gpio_pin_clear(pinDataCommand); - WriteSpi(&cmd, 1); +void St7789::WriteData(uint8_t data) { + WriteData(&data, 1); } -void St7789::WriteData(uint8_t data) { - nrf_gpio_pin_set(pinDataCommand); - WriteSpi(&data, 1); +void St7789::WriteData(const uint8_t* data, size_t size) { + WriteSpi(data, size, [pinDataCommand = pinDataCommand]() { + nrf_gpio_pin_set(pinDataCommand); + }); +} + +void St7789::WriteCommand(uint8_t data) { + WriteCommand(&data, 1); } -void St7789::WriteSpi(const uint8_t* data, size_t size) { - spi.Write(data, size); +void St7789::WriteCommand(const uint8_t* data, size_t size) { + WriteSpi(data, size, [pinDataCommand = pinDataCommand]() { + nrf_gpio_pin_clear(pinDataCommand); + }); +} + +void St7789::WriteSpi(const uint8_t* data, size_t size, const std::function<void()>& preTransactionHook) { + spi.Write(data, size, preTransactionHook); } void St7789::SoftwareReset() { + EnsureSleepOutPostDelay(); WriteCommand(static_cast<uint8_t>(Commands::SoftwareReset)); - nrf_delay_ms(150); + // If sleep in: must wait 120ms before sleep out can sent (see driver datasheet) + // Unconditionally wait as software reset doesn't need to be performant + sleepIn = true; + lastSleepExit = xTaskGetTickCount(); + vTaskDelay(pdMS_TO_TICKS(125)); +} + +void St7789::Command2Enable() { + WriteCommand(static_cast<uint8_t>(Commands::Command2Enable)); + constexpr uint8_t args[] = { + 0x5a, // Constant + 0x69, // Constant + 0x02, // Constant + 0x01, // Enable + }; + WriteData(args, sizeof(args)); } void St7789::SleepOut() { + if (!sleepIn) { + return; + } WriteCommand(static_cast<uint8_t>(Commands::SleepOut)); + // Wait 5ms for clocks to stabilise + // pdMS rounds down => 6 used here + vTaskDelay(pdMS_TO_TICKS(6)); + // Cannot send sleep in or software reset for 120ms + lastSleepExit = xTaskGetTickCount(); + sleepIn = false; +} + +void St7789::EnsureSleepOutPostDelay() { + TickType_t delta = xTaskGetTickCount() - lastSleepExit; + // Due to timer wraparound, there is a chance of delaying when not necessary + // It is very low (pdMS_TO_TICKS(125)/2^32) and waiting an extra 125ms isn't too bad + if (delta < pdMS_TO_TICKS(125)) { + vTaskDelay(pdMS_TO_TICKS(125) - delta); + } } void St7789::SleepIn() { + if (sleepIn) { + return; + } + EnsureSleepOutPostDelay(); WriteCommand(static_cast<uint8_t>(Commands::SleepIn)); + // Wait 5ms for clocks to stabilise + // pdMS rounds down => 6 used here + vTaskDelay(pdMS_TO_TICKS(6)); + sleepIn = true; } -void St7789::ColMod() { - WriteCommand(static_cast<uint8_t>(Commands::ColMod)); +void St7789::PixelFormat() { + WriteCommand(static_cast<uint8_t>(Commands::PixelFormat)); + // 65K colours, 16-bit per pixel WriteData(0x55); - nrf_delay_ms(10); } void St7789::MemoryDataAccessControl() { @@ -79,54 +136,110 @@ void St7789::MemoryDataAccessControl() { #endif } -void St7789::ColumnAddressSet() { - WriteCommand(static_cast<uint8_t>(Commands::ColumnAddressSet)); - WriteData(0x00); - WriteData(0x00); - WriteData(Width >> 8u); - WriteData(Width & 0xffu); -} - -void St7789::RowAddressSet() { - WriteCommand(static_cast<uint8_t>(Commands::RowAddressSet)); - WriteData(0x00); - WriteData(0x00); - WriteData(320u >> 8u); - WriteData(320u & 0xffu); -} - void St7789::DisplayInversionOn() { WriteCommand(static_cast<uint8_t>(Commands::DisplayInversionOn)); - nrf_delay_ms(10); } void St7789::NormalModeOn() { WriteCommand(static_cast<uint8_t>(Commands::NormalModeOn)); - nrf_delay_ms(10); +} + +void St7789::IdleModeOn() { + WriteCommand(static_cast<uint8_t>(Commands::IdleModeOn)); +} + +void St7789::IdleModeOff() { + WriteCommand(static_cast<uint8_t>(Commands::IdleModeOff)); +} + +void St7789::PorchSet() { + WriteCommand(static_cast<uint8_t>(Commands::Porch)); + constexpr uint8_t args[] = { + 0x02, // Normal mode front porch + 0x03, // Normal mode back porch + 0x01, // Porch control enable + 0xed, // Idle mode front:back porch + 0xed, // Partial mode front:back porch (partial mode unused but set anyway) + }; + WriteData(args, sizeof(args)); +} + +void St7789::FrameRateNormalSet() { + WriteCommand(static_cast<uint8_t>(Commands::FrameRateNormal)); + // Note that the datasheet table is imprecise - see formula below table + WriteData(0x0a); +} + +void St7789::IdleFrameRateOn() { + WriteCommand(static_cast<uint8_t>(Commands::FrameRateIdle)); + // According to the datasheet, these controls should apply only to partial/idle mode + // However they appear to apply to normal mode, so we have to enable/disable + // every time we enter/exit always on + constexpr uint8_t args[] = { + 0x12, // Enable frame rate control for partial/idle mode, 4x frame divider + 0x1e, // Idle mode frame rate + 0x1e, // Partial mode frame rate (unused) + }; + WriteData(args, sizeof(args)); +} + +void St7789::IdleFrameRateOff() { + WriteCommand(static_cast<uint8_t>(Commands::FrameRateIdle)); + constexpr uint8_t args[] = { + 0x00, // Disable frame rate control and divider + 0x0a, // Idle mode frame rate (normal) + 0x0a, // Partial mode frame rate (normal, unused) + }; + WriteData(args, sizeof(args)); } void St7789::DisplayOn() { WriteCommand(static_cast<uint8_t>(Commands::DisplayOn)); } +void St7789::PowerControl() { + WriteCommand(static_cast<uint8_t>(Commands::PowerControl1)); + constexpr uint8_t args[] = { + 0xa4, // Constant + 0x00, // Lowest possible voltages + }; + WriteData(args, sizeof(args)); + + WriteCommand(static_cast<uint8_t>(Commands::PowerControl2)); + // Lowest possible boost circuit clocks + WriteData(0xb3); +} + +void St7789::GateControl() { + WriteCommand(static_cast<uint8_t>(Commands::GateControl)); + // Lowest possible VGL/VGH + WriteData(0x00); +} + void St7789::SetAddrWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { WriteCommand(static_cast<uint8_t>(Commands::ColumnAddressSet)); - WriteData(x0 >> 8); - WriteData(x0 & 0xff); - WriteData(x1 >> 8); - WriteData(x1 & 0xff); + uint8_t colArgs[] = { + static_cast<uint8_t>(x0 >> 8), // x start MSB + static_cast<uint8_t>(x0), // x start LSB + static_cast<uint8_t>(x1 >> 8), // x end MSB + static_cast<uint8_t>(x1) // x end LSB + }; + WriteData(colArgs, sizeof(colArgs)); WriteCommand(static_cast<uint8_t>(Commands::RowAddressSet)); - WriteData(y0 >> 8); - WriteData(y0 & 0xff); - WriteData(y1 >> 8); - WriteData(y1 & 0xff); - - WriteToRam(); + uint8_t rowArgs[] = { + static_cast<uint8_t>(y0 >> 8), // y start MSB + static_cast<uint8_t>(y0), // y start LSB + static_cast<uint8_t>(y1 >> 8), // y end MSB + static_cast<uint8_t>(y1) // y end LSB + }; + memcpy(addrWindowArgs, rowArgs, sizeof(rowArgs)); + WriteData(addrWindowArgs, sizeof(addrWindowArgs)); } -void St7789::WriteToRam() { +void St7789::WriteToRam(const uint8_t* data, size_t size) { WriteCommand(static_cast<uint8_t>(Commands::WriteToRam)); + WriteData(data, size); } void St7789::SetVdv() { @@ -138,50 +251,48 @@ void St7789::SetVdv() { void St7789::DisplayOff() { WriteCommand(static_cast<uint8_t>(Commands::DisplayOff)); - nrf_delay_ms(500); -} - -void St7789::VerticalScrollDefinition(uint16_t topFixedLines, uint16_t scrollLines, uint16_t bottomFixedLines) { - WriteCommand(static_cast<uint8_t>(Commands::VerticalScrollDefinition)); - WriteData(topFixedLines >> 8u); - WriteData(topFixedLines & 0x00ffu); - WriteData(scrollLines >> 8u); - WriteData(scrollLines & 0x00ffu); - WriteData(bottomFixedLines >> 8u); - WriteData(bottomFixedLines & 0x00ffu); } void St7789::VerticalScrollStartAddress(uint16_t line) { verticalScrollingStartAddress = line; WriteCommand(static_cast<uint8_t>(Commands::VerticalScrollStartAddress)); - WriteData(line >> 8u); - WriteData(line & 0x00ffu); + uint8_t args[] = { + static_cast<uint8_t>(line >> 8), // Frame memory line pointer MSB + static_cast<uint8_t>(line) // Frame memory line pointer LSB + }; + memcpy(verticalScrollArgs, args, sizeof(args)); + WriteData(verticalScrollArgs, sizeof(verticalScrollArgs)); } void St7789::Uninit() { } -void St7789::DrawPixel(uint16_t x, uint16_t y, uint32_t color) { - if (x >= Width || y >= Height) { - return; - } - - SetAddrWindow(x, y, x + 1, y + 1); - - nrf_gpio_pin_set(pinDataCommand); - WriteSpi(reinterpret_cast<const uint8_t*>(&color), 2); -} - void St7789::DrawBuffer(uint16_t x, uint16_t y, uint16_t width, uint16_t height, const uint8_t* data, size_t size) { SetAddrWindow(x, y, x + width - 1, y + height - 1); - nrf_gpio_pin_set(pinDataCommand); - WriteSpi(data, size); + WriteToRam(data, size); } void St7789::HardwareReset() { - nrf_gpio_pin_clear(26); - nrf_delay_ms(10); - nrf_gpio_pin_set(26); + nrf_gpio_pin_clear(pinReset); + vTaskDelay(pdMS_TO_TICKS(1)); + nrf_gpio_pin_set(pinReset); + // If hardware reset started while sleep out, reset time may be up to 120ms + // Unconditionally wait as hardware reset doesn't need to be performant + sleepIn = true; + lastSleepExit = xTaskGetTickCount(); + vTaskDelay(pdMS_TO_TICKS(125)); +} + +void St7789::LowPowerOn() { + IdleModeOn(); + IdleFrameRateOn(); + NRF_LOG_INFO("[LCD] Low power mode"); +} + +void St7789::LowPowerOff() { + IdleModeOff(); + IdleFrameRateOff(); + NRF_LOG_INFO("[LCD] Normal power mode"); } void St7789::Sleep() { diff --git a/src/drivers/St7789.h b/src/drivers/St7789.h index 8a1bdfca..9c778905 100644 --- a/src/drivers/St7789.h +++ b/src/drivers/St7789.h @@ -1,6 +1,9 @@ #pragma once #include <cstddef> #include <cstdint> +#include <functional> + +#include <FreeRTOS.h> namespace Pinetime { namespace Drivers { @@ -8,7 +11,7 @@ namespace Pinetime { class St7789 { public: - explicit St7789(Spi& spi, uint8_t pinDataCommand); + explicit St7789(Spi& spi, uint8_t pinDataCommand, uint8_t pinReset); St7789(const St7789&) = delete; St7789& operator=(const St7789&) = delete; St7789(St7789&&) = delete; @@ -16,37 +19,51 @@ namespace Pinetime { void Init(); void Uninit(); - void DrawPixel(uint16_t x, uint16_t y, uint32_t color); - void VerticalScrollDefinition(uint16_t topFixedLines, uint16_t scrollLines, uint16_t bottomFixedLines); void VerticalScrollStartAddress(uint16_t line); void DrawBuffer(uint16_t x, uint16_t y, uint16_t width, uint16_t height, const uint8_t* data, size_t size); + void LowPowerOn(); + void LowPowerOff(); void Sleep(); void Wakeup(); private: Spi& spi; uint8_t pinDataCommand; + uint8_t pinReset; uint8_t verticalScrollingStartAddress = 0; + bool sleepIn; + TickType_t lastSleepExit; void HardwareReset(); void SoftwareReset(); + void Command2Enable(); void SleepOut(); + void EnsureSleepOutPostDelay(); void SleepIn(); - void ColMod(); + void PixelFormat(); void MemoryDataAccessControl(); void DisplayInversionOn(); void NormalModeOn(); - void WriteToRam(); + void WriteToRam(const uint8_t* data, size_t size); + void IdleModeOn(); + void IdleModeOff(); + void FrameRateNormalSet(); + void IdleFrameRateOff(); + void IdleFrameRateOn(); void DisplayOn(); void DisplayOff(); + void PowerControl(); + void GateControl(); + void PorchSet(); void SetAddrWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); void SetVdv(); void WriteCommand(uint8_t cmd); - void WriteSpi(const uint8_t* data, size_t size); + void WriteCommand(const uint8_t* data, size_t size); + void WriteSpi(const uint8_t* data, size_t size, const std::function<void()>& preTransactionHook); enum class Commands : uint8_t { SoftwareReset = 0x01, @@ -62,15 +79,26 @@ namespace Pinetime { MemoryDataAccessControl = 0x36, VerticalScrollDefinition = 0x33, VerticalScrollStartAddress = 0x37, - ColMod = 0x3a, + IdleModeOff = 0x38, + IdleModeOn = 0x39, + PixelFormat = 0x3a, + FrameRateIdle = 0xb3, + FrameRateNormal = 0xc6, VdvSet = 0xc4, + Command2Enable = 0xdf, + PowerControl1 = 0xd0, + PowerControl2 = 0xe8, + GateControl = 0xb7, + Porch = 0xb2, }; void WriteData(uint8_t data); - void ColumnAddressSet(); + void WriteData(const uint8_t* data, size_t size); static constexpr uint16_t Width = 240; static constexpr uint16_t Height = 320; - void RowAddressSet(); + + uint8_t addrWindowArgs[4]; + uint8_t verticalScrollArgs[2]; }; } } diff --git a/src/drivers/Watchdog.cpp b/src/drivers/Watchdog.cpp index d0907a65..eeeb6cfd 100644 --- a/src/drivers/Watchdog.cpp +++ b/src/drivers/Watchdog.cpp @@ -2,74 +2,148 @@ #include <mdk/nrf.h> using namespace Pinetime::Drivers; -void Watchdog::Setup(uint8_t timeoutSeconds) { - NRF_WDT->CONFIG &= ~(WDT_CONFIG_SLEEP_Msk << WDT_CONFIG_SLEEP_Pos); - NRF_WDT->CONFIG |= (WDT_CONFIG_HALT_Run << WDT_CONFIG_SLEEP_Pos); +namespace { + /// The watchdog is always driven by a 32768kHz clock + constexpr uint32_t ClockFrequency = 32768; + /// Write this value in the reload register to reload the watchdog + constexpr uint32_t ReloadValue = 0x6E524635UL; - NRF_WDT->CONFIG &= ~(WDT_CONFIG_HALT_Msk << WDT_CONFIG_HALT_Pos); - NRF_WDT->CONFIG |= (WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos); + /// Configures the behaviours (pause or run) of the watchdog while the CPU is sleeping or halted by the debugger + /// + /// @param sleepBehaviour Configure the watchdog to either be paused, or kept running, while the CPU is sleeping + /// @param haltBehaviour Configure the watchdog to either be paused, or kept running, while the CPU is halted by the debugger + void SetBehaviours(Watchdog::SleepBehaviour sleepBehaviour, Watchdog::HaltBehaviour haltBehaviour) { + // NRF_WDT->CONFIG : only the 1st and 4th bits are relevant. + // Bit 0 : Behavior when the CPU is sleeping + // Bit 3 : Behavior when the CPU is halted by the debugger + // O means that the CPU is paused during sleep/halt, 1 means that the watchdog is kept running + NRF_WDT->CONFIG = static_cast<uint32_t>(sleepBehaviour) | static_cast<uint32_t>(haltBehaviour); + } - /* timeout (s) = (CRV + 1) / 32768 */ - // JF : 7500 = 7.5s - uint32_t crv = (((timeoutSeconds * 1000u) << 15u) / 1000) - 1; - NRF_WDT->CRV = crv; + /// Configure the timeout delay of the watchdog (called CRV, Counter Reload Value, in the documentation). + /// + /// @param timeoutSeconds Timeout of the watchdog, expressed in seconds + void SetTimeout(uint8_t timeoutSeconds) { + // According to the documentation: + // Clock = 32768 + // timeout [s] = ( CRV + 1 ) / Clock + // -> CRV = (timeout [s] * Clock) -1 + NRF_WDT->CRV = (timeoutSeconds * ClockFrequency) - 1; + } - /* Enable reload requests */ - NRF_WDT->RREN = (WDT_RREN_RR0_Enabled << WDT_RREN_RR0_Pos); + /// Enables the first reload register + /// + /// The hardware provides 8 reload registers. To reload the watchdog, all enabled + /// register must be refreshed. + /// + /// This driver only enables the first reload register. + void EnableFirstReloadRegister() { + // RRED (Reload Register Enable) is a bitfield of 8 bits. Each bit represent + // one of the eight reload registers available. + // In this case, we enable only the first one. + NRF_WDT->RREN = NRF_WDT->RREN | 1; + } - resetReason = ActualResetReason(); -} + /// Returns the reset reason provided by the POWER subsystem + Watchdog::ResetReason GetResetReason() { + /* NRF_POWER->RESETREAS + * -------------------------------------------------------------------------------------------------------------------- * + * Bit | Reason (if bit is set to 1) + * ----|---------------------------------------------------------------------------------------------------------------- * + * 0 | Reset from the pin reset + * 1 | Reset from the watchdog + * 2 | Reset from soft reset + * 3 | Reset from CPU lock-up + * 16 | Reset due to wake up from System OFF mode when wakeup is triggered from DETECT signal from GPIO + * 17 | Reset due to wake up from System OFF mode when wakeup is triggered from ANADETECT signal from LPCOMP + * 18 | Reset due to wake up from System OFF mode when wakeup is triggered from entering into debug interface mode + * 19 | Reset due to wake up from System OFF mode by NFC field detect + * -------------------------------------------------------------------------------------------------------------------- */ + const uint32_t reason = NRF_POWER->RESETREAS; + NRF_POWER->RESETREAS = 0xffffffff; -void Watchdog::Start() { - NRF_WDT->TASKS_START = 1; + uint32_t value = reason & 0x01; // avoid implicit conversion to bool using this temporary variable. + if (value != 0) { + return Watchdog::ResetReason::ResetPin; + } + + value = (reason >> 1u) & 0x01u; + if (value != 0) { + return Watchdog::ResetReason::Watchdog; + } + + value = (reason >> 2u) & 0x01u; + if (value != 0) { + return Watchdog::ResetReason::SoftReset; + } + + value = (reason >> 3u) & 0x01u; + if (value != 0) { + return Watchdog::ResetReason::CpuLockup; + } + + value = (reason >> 16u) & 0x01u; + if (value != 0) { + return Watchdog::ResetReason::SystemOff; + } + + value = (reason >> 17u) & 0x01u; + if (value != 0) { + return Watchdog::ResetReason::LpComp; + } + + value = (reason >> 18u) & 0x01u; + if (value != 0) { + return Watchdog::ResetReason::DebugInterface; + } + + value = (reason >> 19u) & 0x01u; + if (value != 0) { + return Watchdog::ResetReason::NFC; + } + + return Watchdog::ResetReason::HardReset; + } } -void Watchdog::Kick() { - NRF_WDT->RR[0] = WDT_RR_RR_Reload; +void Watchdog::Setup(uint8_t timeoutSeconds, SleepBehaviour sleepBehaviour, HaltBehaviour haltBehaviour) { + SetBehaviours(sleepBehaviour, haltBehaviour); + SetTimeout(timeoutSeconds); + EnableFirstReloadRegister(); + + resetReason = ::GetResetReason(); } -Watchdog::ResetReasons Watchdog::ActualResetReason() const { - uint32_t reason = NRF_POWER->RESETREAS; - NRF_POWER->RESETREAS = 0xffffffff; +void Watchdog::Start() { + // Write 1 in the START task to start the watchdog + NRF_WDT->TASKS_START = 1; +} - if (reason & 0x01u) - return ResetReasons::ResetPin; - if ((reason >> 1u) & 0x01u) - return ResetReasons::Watchdog; - if ((reason >> 2u) & 0x01u) - return ResetReasons::SoftReset; - if ((reason >> 3u) & 0x01u) - return ResetReasons::CpuLockup; - if ((reason >> 16u) & 0x01u) - return ResetReasons::SystemOff; - if ((reason >> 17u) & 0x01u) - return ResetReasons::LpComp; - if ((reason) &0x01u) - return ResetReasons::DebugInterface; - if ((reason >> 19u) & 0x01u) - return ResetReasons::NFC; - return ResetReasons::HardReset; +void Watchdog::Reload() { + // Write the reload value 0x6E524635UL to the reload register to reload the watchdog. + // NOTE : This driver enables only the 1st reload register. + NRF_WDT->RR[0] = ReloadValue; } -const char* Watchdog::ResetReasonToString(Watchdog::ResetReasons reason) { +const char* Pinetime::Drivers::ResetReasonToString(Watchdog::ResetReason reason) { switch (reason) { - case ResetReasons::ResetPin: + case Watchdog::ResetReason::ResetPin: return "Reset pin"; - case ResetReasons::Watchdog: + case Watchdog::ResetReason::Watchdog: return "Watchdog"; - case ResetReasons::DebugInterface: + case Watchdog::ResetReason::DebugInterface: return "Debug interface"; - case ResetReasons::LpComp: + case Watchdog::ResetReason::LpComp: return "LPCOMP"; - case ResetReasons::SystemOff: + case Watchdog::ResetReason::SystemOff: return "System OFF"; - case ResetReasons::CpuLockup: + case Watchdog::ResetReason::CpuLockup: return "CPU Lock-up"; - case ResetReasons::SoftReset: + case Watchdog::ResetReason::SoftReset: return "Soft reset"; - case ResetReasons::NFC: + case Watchdog::ResetReason::NFC: return "NFC"; - case ResetReasons::HardReset: + case Watchdog::ResetReason::HardReset: return "Hard reset"; default: return "Unknown"; diff --git a/src/drivers/Watchdog.h b/src/drivers/Watchdog.h index 65a505cb..c075232e 100644 --- a/src/drivers/Watchdog.h +++ b/src/drivers/Watchdog.h @@ -1,24 +1,68 @@ #pragma once #include <cstdint> +#include <nrf52_bitfields.h> namespace Pinetime { namespace Drivers { + /// Low level driver for the watchdog based on the nRF52832 Product Specification V1.1 + /// + /// This driver initializes the timeout and sleep and halt behaviours of the watchdog + /// in the method Watchdog::Setup(). + /// + /// The watchdog can then be started using the method Watchdog::Start(). At this point, the watchdog runs + /// and will reset the MCU if it's not reloaded before the timeout elapses. + /// + /// The watchdog can be reloaded using Watchdog::Kick(). + /// + /// The watchdog also provide the cause of the last reset (reset pin, watchdog, soft reset, hard reset,... See + /// Watchdog::ResetReasons). class Watchdog { public: - enum class ResetReasons { ResetPin, Watchdog, SoftReset, CpuLockup, SystemOff, LpComp, DebugInterface, NFC, HardReset }; - void Setup(uint8_t timeoutSeconds); + /// Indicates the reasons of a reset of the MCU + enum class ResetReason { ResetPin, Watchdog, SoftReset, CpuLockup, SystemOff, LpComp, DebugInterface, NFC, HardReset }; + + /// Behaviours of the watchdog when the CPU is sleeping + enum class SleepBehaviour : uint8_t { + /// Pause watchdog while the CPU is sleeping + Pause = 0 << WDT_CONFIG_SLEEP_Pos, + /// Keep the watchdog running while the CPU is sleeping + Run = 1 << WDT_CONFIG_SLEEP_Pos + }; + + /// Behaviours of the watchdog when the CPU is halted by the debugger + enum class HaltBehaviour : uint8_t { + /// Pause watchdog while the CPU is halted by the debugger + Pause = 0 << WDT_CONFIG_HALT_Pos, + /// Keep the watchdog running while the CPU is halted by the debugger + Run = 1 << WDT_CONFIG_HALT_Pos + }; + + /// Configures the watchdog with a specific timeout, behaviour when sleeping and when halted by the debugger + /// + /// @param sleepBehaviour Configure the watchdog to either be paused, or kept running, while the CPU is sleeping + /// @param haltBehaviour Configure the watchdog to either be paused, or kept running, while the CPU is halted by the debugger + void Setup(uint8_t timeoutSeconds, SleepBehaviour sleepBehaviour, HaltBehaviour haltBehaviour); + + /// Starts the watchdog. The watchdog will reset the MCU when the timeout period is elapsed unless you call + /// Watchdog::Kick before the end of the period void Start(); - void Kick(); - ResetReasons ResetReason() const { + /// Reloads the watchdog. + /// + /// Ensure that you call this function regularly with a period shorter + /// than the timeout period to prevent the watchdog from resetting the MCU. + void Reload(); + + /// Returns the reason of the last reset + ResetReason GetResetReason() const { return resetReason; } - static const char* ResetReasonToString(ResetReasons reason); - private: - ResetReasons resetReason; - ResetReasons ActualResetReason() const; + ResetReason resetReason; }; + + /// Converts a reset reason to a human readable string + const char* ResetReasonToString(Watchdog::ResetReason reason); } } diff --git a/src/heartratetask/HeartRateTask.cpp b/src/heartratetask/HeartRateTask.cpp index 50833ab2..8a5a871b 100644 --- a/src/heartratetask/HeartRateTask.cpp +++ b/src/heartratetask/HeartRateTask.cpp @@ -26,10 +26,11 @@ void HeartRateTask::Process(void* instance) { void HeartRateTask::Work() { int lastBpm = 0; while (true) { - auto delay = portMAX_DELAY; + Messages msg; + uint32_t delay; if (state == States::Running) { if (measurementStarted) { - delay = 40; + delay = ppg.deltaTms; } else { delay = 100; } @@ -37,8 +38,7 @@ void HeartRateTask::Work() { delay = portMAX_DELAY; } - Messages msg; - if (xQueueReceive(messageQueue, &msg, delay) == pdTRUE) { + if (xQueueReceive(messageQueue, &msg, delay)) { switch (msg) { case Messages::GoToSleep: StopMeasurement(); @@ -70,12 +70,29 @@ void HeartRateTask::Work() { } if (measurementStarted) { - ppg.Preprocess(static_cast<float>(heartRateSensor.ReadHrs())); - auto bpm = ppg.HeartRate(); + auto sensorData = heartRateSensor.ReadHrsAls(); + int8_t ambient = ppg.Preprocess(sensorData.hrs, sensorData.als); + int bpm = ppg.HeartRate(); + + // If ambient light detected or a reset requested (bpm < 0) + if (ambient > 0) { + // Reset all DAQ buffers + ppg.Reset(true); + // Force state to NotEnoughData (below) + lastBpm = 0; + bpm = 0; + } else if (bpm < 0) { + // Reset all DAQ buffers except HRS buffer + ppg.Reset(false); + // Set HR to zero and update + bpm = 0; + controller.Update(Controllers::HeartRateController::States::Running, bpm); + } if (lastBpm == 0 && bpm == 0) { - controller.Update(Controllers::HeartRateController::States::NotEnoughData, 0); + controller.Update(Controllers::HeartRateController::States::NotEnoughData, bpm); } + if (bpm != 0) { lastBpm = bpm; controller.Update(Controllers::HeartRateController::States::Running, lastBpm); @@ -87,19 +104,17 @@ void HeartRateTask::Work() { void HeartRateTask::PushMessage(HeartRateTask::Messages msg) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xQueueSendFromISR(messageQueue, &msg, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken == pdTRUE) { - /* Actual macro used here is port specific. */ - // TODO : should I do something here? - } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void HeartRateTask::StartMeasurement() { heartRateSensor.Enable(); + ppg.Reset(true); vTaskDelay(100); - ppg.SetOffset(heartRateSensor.ReadHrs()); } void HeartRateTask::StopMeasurement() { heartRateSensor.Disable(); + ppg.Reset(true); vTaskDelay(100); } diff --git a/src/libs/QCBOR b/src/libs/QCBOR deleted file mode 160000 -Subproject 56b17bf9f74096774944bcac0829adcd887d391 diff --git a/src/libs/arduinoFFT b/src/libs/arduinoFFT new file mode 160000 +Subproject 419d7b044e56b87de8efbcf76f09c04759628fb diff --git a/src/libs/lfs_config.h b/src/libs/lfs_config.h new file mode 100644 index 00000000..eaeede0e --- /dev/null +++ b/src/libs/lfs_config.h @@ -0,0 +1,49 @@ +#pragma once + +#include <libraries/log/nrf_log.h> + +#ifndef LFS_TRACE +#ifdef LFS_YES_TRACE +#define LFS_TRACE_(fmt, ...) \ + NRF_LOG_DEBUG("[LFS] %s:%d:trace: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS_TRACE(...) LFS_TRACE_(__VA_ARGS__, "") +#else +#define LFS_TRACE(...) +#endif +#endif + +#ifndef LFS_DEBUG +#ifndef LFS_NO_DEBUG +#define LFS_DEBUG_(fmt, ...) \ + NRF_LOG_DEBUG("[LFS] %s:%d:debug: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS_DEBUG(...) LFS_DEBUG_(__VA_ARGS__, "") +#else +#define LFS_DEBUG(...) +#endif +#endif + +#ifndef LFS_WARN +#ifndef LFS_NO_WARN +#define LFS_WARN_(fmt, ...) \ + NRF_LOG_WARNING("[LFS] %s:%d:warn: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS_WARN(...) LFS_WARN_(__VA_ARGS__, "") +#else +#define LFS_WARN(...) +#endif +#endif + +#ifndef LFS_ERROR +#ifndef LFS_NO_ERROR +#define LFS_ERROR_(fmt, ...) \ + NRF_LOG_ERROR("[LFS] %s:%d:error: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS_ERROR(...) LFS_ERROR_(__VA_ARGS__, "") +#else +#define LFS_ERROR(...) +#endif +#endif + +// This is required in order for the CRC implementation in littlefs/lfs_util.c to be compiled +#undef LFS_CONFIG + +#undef LFS_UTIL_H +#include <littlefs/lfs_util.h> diff --git a/src/libs/lv_conf.h b/src/libs/lv_conf.h index 063f1d34..c23647f2 100644 --- a/src/libs/lv_conf.h +++ b/src/libs/lv_conf.h @@ -71,7 +71,7 @@ typedef int16_t lv_coord_t; * The graphical objects and other related data are stored here. */ /* 1: use custom malloc/free, 0: use the built-in `lv_mem_alloc` and `lv_mem_free` */ -#define LV_MEM_CUSTOM 0 +#define LV_MEM_CUSTOM 1 #if LV_MEM_CUSTOM == 0 /* Size of the memory used by `lv_mem_alloc` in bytes (>= 2kB)*/ #define LV_MEM_SIZE (14U * 1024U) @@ -86,9 +86,9 @@ typedef int16_t lv_coord_t; /* Automatically defrag. on free. Defrag. means joining the adjacent free cells. */ #define LV_MEM_AUTO_DEFRAG 1 #else /*LV_MEM_CUSTOM*/ -#define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /*Header for the dynamic memory function*/ -#define LV_MEM_CUSTOM_ALLOC malloc /*Wrapper to malloc*/ -#define LV_MEM_CUSTOM_FREE free /*Wrapper to free*/ +#define LV_MEM_CUSTOM_INCLUDE <FreeRTOS.h> /*Header for the dynamic memory function*/ +#define LV_MEM_CUSTOM_ALLOC pvPortMalloc /*Wrapper to malloc*/ +#define LV_MEM_CUSTOM_FREE vPortFree /*Wrapper to free*/ #endif /*LV_MEM_CUSTOM*/ /* Use the standard memcpy and memset instead of LVGL's own functions. @@ -418,6 +418,7 @@ typedef void* lv_indev_drv_user_data_t; /*Type of user data in the in LV_FONT_DECLARE(jetbrains_mono_42) \ LV_FONT_DECLARE(jetbrains_mono_76) \ LV_FONT_DECLARE(open_sans_light) \ + LV_FONT_DECLARE(fontawesome_weathericons) \ LV_FONT_DECLARE(lv_font_sys_48) /* Enable it if you have fonts with a lot of characters. @@ -728,7 +729,9 @@ typedef void* lv_obj_user_data_t; #define LV_USE_TABLE 1 #if LV_USE_TABLE #define LV_TABLE_COL_MAX 12 -#define LV_TABLE_CELL_STYLE_CNT 5 +#define LV_TABLE_CELL_STYLE_CNT 6 +#define LV_TABLE_PART_CELL5 5 +#define LV_TABLE_PART_CELL6 6 #endif diff --git a/src/libs/lvgl b/src/libs/lvgl -Subproject 23430cf20e32294549fff9b2879a9466dacc19b +Subproject 7c96fb87e33733a7a67b33bf8057e598c6843e3 diff --git a/src/main.cpp b/src/main.cpp index 74804214..24f13cad 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -68,7 +68,7 @@ Pinetime::Drivers::SpiMaster spi {Pinetime::Drivers::SpiMaster::SpiModule::SPI0, Pinetime::PinMap::SpiMiso}}; Pinetime::Drivers::Spi lcdSpi {spi, Pinetime::PinMap::SpiLcdCsn}; -Pinetime::Drivers::St7789 lcd {lcdSpi, Pinetime::PinMap::LcdDataCommand}; +Pinetime::Drivers::St7789 lcd {lcdSpi, Pinetime::PinMap::LcdDataCommand, Pinetime::PinMap::LcdReset}; Pinetime::Drivers::Spi flashSpi {spi, Pinetime::PinMap::SpiFlashCsn}; Pinetime::Drivers::SpiNorFlash spiNorFlash {flashSpi}; @@ -83,6 +83,7 @@ Pinetime::Drivers::Cst816S touchPanel {twiMaster, touchPanelTwiAddress}; #include "displayapp/DisplayAppRecovery.h" #else #include "displayapp/DisplayApp.h" + #include "main.h" #endif Pinetime::Drivers::Bma421 motionSensor {twiMaster, motionSensorTwiAddress}; Pinetime::Drivers::Hrs3300 heartRateSensor {twiMaster, heartRateSensorTwiAddress}; @@ -103,8 +104,7 @@ Pinetime::Controllers::DateTime dateTimeController {settingsController}; Pinetime::Drivers::Watchdog watchdog; Pinetime::Controllers::NotificationManager notificationManager; Pinetime::Controllers::MotionController motionController; -Pinetime::Controllers::TimerController timerController; -Pinetime::Controllers::AlarmController alarmController {dateTimeController}; +Pinetime::Controllers::AlarmController alarmController {dateTimeController, fs}; Pinetime::Controllers::TouchHandler touchHandler; Pinetime::Controllers::ButtonHandler buttonHandler; Pinetime::Controllers::BrightnessController brightnessController {}; @@ -120,11 +120,11 @@ Pinetime::Applications::DisplayApp displayApp(lcd, settingsController, motorController, motionController, - timerController, alarmController, brightnessController, touchHandler, - fs); + fs, + spiNorFlash); Pinetime::System::SystemTask systemTask(spi, spiNorFlash, @@ -133,7 +133,6 @@ Pinetime::System::SystemTask systemTask(spi, batteryController, bleController, dateTimeController, - timerController, alarmController, watchdog, notificationManager, @@ -147,7 +146,17 @@ Pinetime::System::SystemTask systemTask(spi, fs, touchHandler, buttonHandler); +int mallocFailedCount = 0; +int stackOverflowCount = 0; +extern "C" { +void vApplicationMallocFailedHook() { + mallocFailedCount++; +} +void vApplicationStackOverflowHook(TaskHandle_t /*xTask*/, char* /*pcTaskName*/) { + stackOverflowCount++; +} +} /* Variable Declarations for variables in noinit SRAM Increment NoInit_MagicValue upon adding variables to this area */ @@ -159,7 +168,7 @@ std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> NoI void nrfx_gpiote_evt_handler(nrfx_gpiote_pin_t pin, nrf_gpiote_polarity_t action) { if (pin == Pinetime::PinMap::Cst816sIrq) { - systemTask.OnTouchEvent(); + systemTask.PushMessage(Pinetime::System::Messages::OnTouchEvent); return; } @@ -296,7 +305,12 @@ void calibrate_lf_clock_rc(nrf_drv_clock_evt_type_t /*event*/) { nrf_drv_clock_calibration_start(16, calibrate_lf_clock_rc); } +void enable_dcdc_regulator() { + NRF_POWER->DCDCEN = 1; +} + int main() { + enable_dcdc_regulator(); logger.Init(); nrf_drv_clock_init(); @@ -5,4 +5,7 @@ #include <nrfx_gpiote.h> void nrfx_gpiote_evt_handler(nrfx_gpiote_pin_t pin, nrf_gpiote_polarity_t action); -void DebounceTimerCallback(TimerHandle_t xTimer);
\ No newline at end of file +void DebounceTimerCallback(TimerHandle_t xTimer); + +extern int mallocFailedCount; +extern int stackOverflowCount;
\ No newline at end of file diff --git a/src/recoveryLoader.cpp b/src/recoveryLoader.cpp index d6f8d49b..fc9ab76c 100644 --- a/src/recoveryLoader.cpp +++ b/src/recoveryLoader.cpp @@ -10,7 +10,6 @@ #include <libraries/gpiote/app_gpiote.h> #include <hal/nrf_wdt.h> #include <cstring> -#include <components/gfx/Gfx.h> #include <drivers/St7789.h> #include <components/brightness/BrightnessController.h> #include <algorithm> @@ -46,9 +45,8 @@ Pinetime::Drivers::Spi flashSpi {spi, Pinetime::PinMap::SpiFlashCsn}; Pinetime::Drivers::SpiNorFlash spiNorFlash {flashSpi}; Pinetime::Drivers::Spi lcdSpi {spi, Pinetime::PinMap::SpiLcdCsn}; -Pinetime::Drivers::St7789 lcd {lcdSpi, Pinetime::PinMap::LcdDataCommand}; +Pinetime::Drivers::St7789 lcd {lcdSpi, Pinetime::PinMap::LcdDataCommand, Pinetime::PinMap::LcdReset}; -Pinetime::Components::Gfx gfx {lcd}; Pinetime::Controllers::BrightnessController brightnessController; void DisplayProgressBar(uint8_t percent, uint16_t color); @@ -92,7 +90,6 @@ void Process(void* /*instance*/) { spiNorFlash.Wakeup(); brightnessController.Init(); lcd.Init(); - gfx.Init(); NRF_LOG_INFO("Display logo") DisplayLogo(); @@ -124,7 +121,6 @@ void DisplayLogo() { Pinetime::Tools::RleDecoder rleDecoder(infinitime_nb, sizeof(infinitime_nb)); for (int i = 0; i < displayWidth; i++) { rleDecoder.DecodeNext(displayBuffer, displayWidth * bytesPerPixel); - ulTaskNotifyTake(pdTRUE, 500); lcd.DrawBuffer(0, i, displayWidth, 1, reinterpret_cast<const uint8_t*>(displayBuffer), displayWidth * bytesPerPixel); } } @@ -133,12 +129,23 @@ void DisplayProgressBar(uint8_t percent, uint16_t color) { static constexpr uint8_t barHeight = 20; std::fill(displayBuffer, displayBuffer + (displayWidth * bytesPerPixel), color); for (int i = 0; i < barHeight; i++) { - ulTaskNotifyTake(pdTRUE, 500); uint16_t barWidth = std::min(static_cast<float>(percent) * 2.4f, static_cast<float>(displayWidth)); lcd.DrawBuffer(0, displayWidth - barHeight + i, barWidth, 1, reinterpret_cast<const uint8_t*>(displayBuffer), barWidth * bytesPerPixel); } } +int mallocFailedCount = 0; +int stackOverflowCount = 0; +extern "C" { +void vApplicationMallocFailedHook() { + mallocFailedCount++; +} + +void vApplicationStackOverflowHook(TaskHandle_t /*xTask*/, char* /*pcTaskName*/) { + stackOverflowCount++; +} +} + int main(void) { TaskHandle_t taskHandle; RefreshWatchdog(); diff --git a/src/resources/CMakeLists.txt b/src/resources/CMakeLists.txt index 0983aaff..9181d4a6 100644 --- a/src/resources/CMakeLists.txt +++ b/src/resources/CMakeLists.txt @@ -3,13 +3,14 @@ find_program(LV_FONT_CONV "lv_font_conv" NO_CACHE REQUIRED HINTS "${CMAKE_SOURCE_DIR}/node_modules/.bin") message(STATUS "Using ${LV_FONT_CONV} to generate font files") -find_program(LV_IMG_CONV "lv_img_conv" NO_CACHE REQUIRED - HINTS "${CMAKE_SOURCE_DIR}/node_modules/.bin") +find_program(LV_IMG_CONV "lv_img_conv.py" NO_CACHE REQUIRED + HINTS "${CMAKE_CURRENT_SOURCE_DIR}") message(STATUS "Using ${LV_IMG_CONV} to generate font files") if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.12) # FindPython3 module introduces with CMake 3.12 # https://cmake.org/cmake/help/latest/module/FindPython3.html + set(Python3_FIND_STRATEGY LOCATION) # https://discourse.cmake.org/t/find-package-python3-is-not-finding-the-correct-python/10563 find_package(Python3 REQUIRED) else() set(Python3_EXECUTABLE "python") diff --git a/src/resources/generate-img.py b/src/resources/generate-img.py index cdbfc030..518d2206 100755 --- a/src/resources/generate-img.py +++ b/src/resources/generate-img.py @@ -11,6 +11,9 @@ import subprocess def gen_lvconv_line(lv_img_conv: str, dest: str, color_format: str, output_format: str, binary_format: str, sources: str): args = [lv_img_conv, sources, '--force', '--output-file', dest, '--color-format', color_format, '--output-format', output_format, '--binary-format', binary_format] + if lv_img_conv.endswith(".py"): + # lv_img_conv is a python script, call with current python executable + args = [sys.executable] + args return args diff --git a/src/resources/images.json b/src/resources/images.json index db2ccab0..e4247188 100644 --- a/src/resources/images.json +++ b/src/resources/images.json @@ -5,5 +5,19 @@ "output_format": "bin", "binary_format": "ARGB8565_RBSWAP", "target_path": "/images/" + }, + "navigation0" : { + "sources": "images/navigation0.png", + "color_format": "CF_INDEXED_1_BIT", + "output_format": "bin", + "binary_format": "ARGB8565_RBSWAP", + "target_path": "/images/" + }, + "navigation1" : { + "sources": "images/navigation1.png", + "color_format": "CF_INDEXED_1_BIT", + "output_format": "bin", + "binary_format": "ARGB8565_RBSWAP", + "target_path": "/images/" } } diff --git a/src/resources/images/navigation0.png b/src/resources/images/navigation0.png Binary files differnew file mode 100644 index 00000000..37d7abd8 --- /dev/null +++ b/src/resources/images/navigation0.png diff --git a/src/resources/images/navigation1.png b/src/resources/images/navigation1.png Binary files differnew file mode 100644 index 00000000..f1714a71 --- /dev/null +++ b/src/resources/images/navigation1.png diff --git a/src/resources/lv_img_conv.py b/src/resources/lv_img_conv.py new file mode 100755 index 00000000..4c6b84e3 --- /dev/null +++ b/src/resources/lv_img_conv.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +import argparse +import pathlib +import sys +import decimal +from PIL import Image + + +def classify_pixel(value, bits): + def round_half_up(v): + """python3 implements "propper" "banker's rounding" by rounding to the nearest + even number. Javascript rounds to the nearest integer. + To have the same output as the original JavaScript implementation add a custom + rounding function, which does "school" rounding (to the nearest integer). + + see: https://stackoverflow.com/questions/43851273/how-to-round-float-0-5-up-to-1-0-while-still-rounding-0-45-to-0-0-as-the-usual + """ + return int(decimal.Decimal(v).quantize(decimal.Decimal('1'), rounding=decimal.ROUND_HALF_UP)) + tmp = 1 << (8 - bits) + val = round_half_up(value / tmp) * tmp + if val < 0: + val = 0 + return val + + +def test_classify_pixel(): + # test difference between round() and round_half_up() + assert classify_pixel(18, 5) == 16 + # school rounding 4.5 to 5, but banker's rounding 4.5 to 4 + assert classify_pixel(18, 6) == 20 + + +def main(): + parser = argparse.ArgumentParser() + + parser.add_argument("img", + help="Path to image to convert to C header file") + parser.add_argument("-o", "--output-file", + help="output file path (for single-image conversion)", + required=True) + parser.add_argument("-f", "--force", + help="allow overwriting the output file", + action="store_true") + parser.add_argument("-i", "--image-name", + help="name of image structure (not implemented)") + parser.add_argument("-c", "--color-format", + help="color format of image", + default="CF_TRUE_COLOR_ALPHA", + choices=[ + "CF_ALPHA_1_BIT", "CF_ALPHA_2_BIT", "CF_ALPHA_4_BIT", + "CF_ALPHA_8_BIT", "CF_INDEXED_1_BIT", "CF_INDEXED_2_BIT", "CF_INDEXED_4_BIT", + "CF_INDEXED_8_BIT", "CF_RAW", "CF_RAW_CHROMA", "CF_RAW_ALPHA", + "CF_TRUE_COLOR", "CF_TRUE_COLOR_ALPHA", "CF_TRUE_COLOR_CHROMA", "CF_RGB565A8", + ], + required=True) + parser.add_argument("-t", "--output-format", + help="output format of image", + default="bin", # default in original is 'c' + choices=["c", "bin"]) + parser.add_argument("--binary-format", + help="binary color format (needed if output-format is binary)", + default="ARGB8565_RBSWAP", + choices=["ARGB8332", "ARGB8565", "ARGB8565_RBSWAP", "ARGB8888"]) + parser.add_argument("-s", "--swap-endian", + help="swap endian of image (not implemented)", + action="store_true") + parser.add_argument("-d", "--dither", + help="enable dither (not implemented)", + action="store_true") + args = parser.parse_args() + + img_path = pathlib.Path(args.img) + out = pathlib.Path(args.output_file) + if not img_path.is_file(): + print(f"Input file is missing: '{args.img}'") + return 1 + print(f"Beginning conversion of {args.img}") + if out.exists(): + if args.force: + print(f"overwriting {args.output_file}") + else: + pritn(f"Error: refusing to overwrite {args.output_file} without -f specified.") + return 1 + out.touch() + + # only implemented the bare minimum, everything else is not implemented + if args.color_format not in ["CF_INDEXED_1_BIT", "CF_TRUE_COLOR_ALPHA"]: + raise NotImplementedError(f"argument --color-format '{args.color_format}' not implemented") + if args.output_format != "bin": + raise NotImplementedError(f"argument --output-format '{args.output_format}' not implemented") + if args.binary_format not in ["ARGB8565_RBSWAP", "ARGB8888"]: + raise NotImplementedError(f"argument --binary-format '{args.binary_format}' not implemented") + if args.image_name: + raise NotImplementedError(f"argument --image-name not implemented") + if args.swap_endian: + raise NotImplementedError(f"argument --swap-endian not implemented") + if args.dither: + raise NotImplementedError(f"argument --dither not implemented") + + # open image using Pillow + img = Image.open(img_path) + img_height = img.height + img_width = img.width + if args.color_format == "CF_TRUE_COLOR_ALPHA" and img.mode != "RGBA": + # support pictures stored in other formats like with a color palette 'P' + # see: https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes + img = img.convert(mode="RGBA") + elif args.color_format == "CF_INDEXED_1_BIT" and img.mode != "L": + # for CF_INDEXED_1_BIT we need just a grayscale value per pixel + img = img.convert(mode="L") + if args.color_format == "CF_TRUE_COLOR_ALPHA" and args.binary_format == "ARGB8888": + buf = bytearray(img_height*img_width*4) # 4 bytes (32 bit) per pixel + for y in range(img_height): + for x in range(img_width): + i = (y*img_width + x)*4 # buffer-index + pixel = img.getpixel((x,y)) + r, g, b, a = pixel + buf[i + 0] = r + buf[i + 1] = g + buf[i + 2] = b + buf[i + 3] = a + + elif args.color_format == "CF_TRUE_COLOR_ALPHA" and args.binary_format == "ARGB8565_RBSWAP": + buf = bytearray(img_height*img_width*3) # 3 bytes (24 bit) per pixel + for y in range(img_height): + for x in range(img_width): + i = (y*img_width + x)*3 # buffer-index + pixel = img.getpixel((x,y)) + r_act = classify_pixel(pixel[0], 5) + g_act = classify_pixel(pixel[1], 6) + b_act = classify_pixel(pixel[2], 5) + a = pixel[3] + r_act = min(r_act, 0xF8) + g_act = min(g_act, 0xFC) + b_act = min(b_act, 0xF8) + c16 = ((r_act) << 8) | ((g_act) << 3) | ((b_act) >> 3) # RGR565 + buf[i + 0] = (c16 >> 8) & 0xFF + buf[i + 1] = c16 & 0xFF + buf[i + 2] = a + + elif args.color_format == "CF_INDEXED_1_BIT": # ignore binary format, use color format as binary format + w = img_width >> 3 + if img_width & 0x07: + w+=1 + max_p = w * (img_height-1) + ((img_width-1) >> 3) + 8 # +8 for the palette + buf = bytearray(max_p+1) + + for y in range(img_height): + for x in range(img_width): + c = img.getpixel((x,y)) + p = w * y + (x >> 3) + 8 # +8 for the palette + buf[p] |= (c & 0x1) << (7 - (x & 0x7)) + # write palette information, for indexed-1-bit we need palette with two values + # write 8 palette bytes + buf[0] = 0 + buf[1] = 0 + buf[2] = 0 + buf[3] = 0 + # Normally there is much math behind this, but for the current use case this is close enough + # only needs to be more complicated if we have more than 2 colors in the palette + buf[4] = 255 + buf[5] = 255 + buf[6] = 255 + buf[7] = 255 + else: + # raise just to be sure + raise NotImplementedError(f"args.color_format '{args.color_format}' with args.binary_format '{args.binary_format}' not implemented") + + # write header + match args.color_format: + case "CF_TRUE_COLOR_ALPHA": + lv_cf = 5 + case "CF_INDEXED_1_BIT": + lv_cf = 7 + case _: + # raise just to be sure + raise NotImplementedError(f"args.color_format '{args.color_format}' not implemented") + header_32bit = lv_cf | (img_width << 10) | (img_height << 21) + buf_out = bytearray(4 + len(buf)) + buf_out[0] = header_32bit & 0xFF + buf_out[1] = (header_32bit & 0xFF00) >> 8 + buf_out[2] = (header_32bit & 0xFF0000) >> 16 + buf_out[3] = (header_32bit & 0xFF000000) >> 24 + buf_out[4:] = buf + + # write byte buffer to file + with open(out, "wb") as f: + f.write(buf_out) + return 0 + + +if __name__ == '__main__': + if "--test" in sys.argv: + # run small set of tests and exit + print("running tests") + test_classify_pixel() + print("success!") + sys.exit(0) + # run normal program + sys.exit(main()) diff --git a/src/stdlib.c b/src/stdlib.c new file mode 100644 index 00000000..21b506a8 --- /dev/null +++ b/src/stdlib.c @@ -0,0 +1,51 @@ +#include <stdlib.h> +#include <string.h> +#include <FreeRTOS.h> + +// Override malloc() and free() to use the memory manager from FreeRTOS. +// According to the documentation of libc, we also need to override +// calloc and realloc. +// See https://www.gnu.org/software/libc/manual/html_node/Replacing-malloc.html + +void* malloc(size_t size) { + return pvPortMalloc(size); +} + +void* __wrap_malloc(size_t size) { + return malloc(size); +} + +void* __wrap__malloc_r(struct _reent* reent, size_t size) { + (void) reent; + return malloc(size); +} + +void free(void* ptr) { + vPortFree(ptr); +} + +void __wrap_free(void* ptr) { + free(ptr); +} + +void* calloc(size_t num, size_t size) { + void *ptr = malloc(num * size); + if (ptr) { + memset(ptr, 0, num * size); + } + return ptr; +} + +void* __wrap_calloc(size_t num, size_t size) { + return calloc(num, size); +} + +void* pvPortRealloc(void* ptr, size_t xWantedSize); + +void* realloc(void* ptr, size_t newSize) { + return pvPortRealloc(ptr, newSize); +} + +void* __wrap_realloc(void* ptr, size_t newSize) { + return realloc(ptr, newSize); +} diff --git a/src/systemtask/Messages.h b/src/systemtask/Messages.h index 3768ae9b..fee94bb7 100644 --- a/src/systemtask/Messages.h +++ b/src/systemtask/Messages.h @@ -6,19 +6,17 @@ namespace Pinetime { enum class Messages : uint8_t { GoToSleep, GoToRunning, - TouchWakeUp, OnNewTime, OnNewNotification, - OnTimerDone, OnNewCall, BleConnected, - UpdateTimeOut, BleFirmwareUpdateStarted, BleFirmwareUpdateFinished, OnTouchEvent, HandleButtonEvent, HandleButtonTimerEvent, OnDisplayTaskSleeping, + OnDisplayTaskAOD, EnableSleeping, DisableSleeping, OnNewDay, diff --git a/src/systemtask/SystemMonitor.cpp b/src/systemtask/SystemMonitor.cpp index 2edee7bf..8696c8ce 100644 --- a/src/systemtask/SystemMonitor.cpp +++ b/src/systemtask/SystemMonitor.cpp @@ -1,5 +1,5 @@ #include "systemtask/SystemTask.h" -#if configUSE_TRACE_FACILITY == 1 +#if NRF_LOG_ENABLED // FreeRtosMonitor #include <FreeRTOS.h> #include <task.h> diff --git a/src/systemtask/SystemTask.cpp b/src/systemtask/SystemTask.cpp index 041cb209..8e0435e3 100644 --- a/src/systemtask/SystemTask.cpp +++ b/src/systemtask/SystemTask.cpp @@ -27,20 +27,6 @@ namespace { } } -void DimTimerCallback(TimerHandle_t xTimer) { - - NRF_LOG_INFO("DimTimerCallback"); - auto* sysTask = static_cast<SystemTask*>(pvTimerGetTimerID(xTimer)); - sysTask->OnDim(); -} - -void IdleTimerCallback(TimerHandle_t xTimer) { - - NRF_LOG_INFO("IdleTimerCallback"); - auto* sysTask = static_cast<SystemTask*>(pvTimerGetTimerID(xTimer)); - sysTask->OnIdle(); -} - void MeasureBatteryTimerCallback(TimerHandle_t xTimer) { auto* sysTask = static_cast<SystemTask*>(pvTimerGetTimerID(xTimer)); sysTask->PushMessage(Pinetime::System::Messages::MeasureBatteryTimerExpired); @@ -53,7 +39,6 @@ SystemTask::SystemTask(Drivers::SpiMaster& spi, Controllers::Battery& batteryController, Controllers::Ble& bleController, Controllers::DateTime& dateTimeController, - Controllers::TimerController& timerController, Controllers::AlarmController& alarmController, Drivers::Watchdog& watchdog, Pinetime::Controllers::NotificationManager& notificationManager, @@ -74,7 +59,6 @@ SystemTask::SystemTask(Drivers::SpiMaster& spi, batteryController {batteryController}, bleController {bleController}, dateTimeController {dateTimeController}, - timerController {timerController}, alarmController {alarmController}, watchdog {watchdog}, notificationManager {notificationManager}, @@ -115,10 +99,12 @@ void SystemTask::Process(void* instance) { void SystemTask::Work() { BootErrors bootError = BootErrors::None; - watchdog.Setup(7); + watchdog.Setup(7, Drivers::Watchdog::SleepBehaviour::Run, Drivers::Watchdog::HaltBehaviour::Pause); watchdog.Start(); - NRF_LOG_INFO("Last reset reason : %s", Pinetime::Drivers::Watchdog::ResetReasonToString(watchdog.ResetReason())); - APP_GPIOTE_INIT(2); + NRF_LOG_INFO("Last reset reason : %s", Pinetime::Drivers::ResetReasonToString(watchdog.GetResetReason())); + if (!nrfx_gpiote_is_init()) { + nrfx_gpiote_init(); + } spi.Init(); spiNorFlash.Init(); @@ -141,7 +127,6 @@ void SystemTask::Work() { dateTimeController.Register(this); batteryController.Register(this); motionSensor.SoftReset(); - timerController.Init(this); alarmController.Init(this); // Reset the TWI device because the motion sensor chip most probably crashed it... @@ -153,6 +138,9 @@ void SystemTask::Work() { settingsController.Init(); displayApp.Register(this); + displayApp.Register(&nimbleController.weather()); + displayApp.Register(&nimbleController.music()); + displayApp.Register(&nimbleController.navigation()); displayApp.Start(bootError); heartRateSensor.Init(); @@ -189,10 +177,7 @@ void SystemTask::Work() { batteryController.MeasureVoltage(); - idleTimer = xTimerCreate("idleTimer", pdMS_TO_TICKS(2000), pdFALSE, this, IdleTimerCallback); - dimTimer = xTimerCreate("dimTimer", pdMS_TO_TICKS(settingsController.GetScreenTimeOut() - 2000), pdFALSE, this, DimTimerCallback); measureBatteryTimer = xTimerCreate("measureBattery", batteryMeasurementPeriod, pdTRUE, this, MeasureBatteryTimerCallback); - xTimerStart(dimTimer, 0); xTimerStart(measureBatteryTimer, portMAX_DELAY); #pragma clang diagnostic push @@ -204,131 +189,80 @@ void SystemTask::Work() { if (xQueueReceive(systemTasksMsgQueue, &msg, 100) == pdTRUE) { switch (msg) { case Messages::EnableSleeping: - // Make sure that exiting an app doesn't enable sleeping, - // if the exiting was caused by a firmware update - if (!bleController.IsFirmwareUpdating()) { - doNotGoToSleep = false; - } - ReloadIdleTimer(); + wakeLocksHeld--; break; case Messages::DisableSleeping: - doNotGoToSleep = true; - break; - case Messages::UpdateTimeOut: - xTimerChangePeriod(dimTimer, pdMS_TO_TICKS(settingsController.GetScreenTimeOut() - 2000), 0); + GoToRunning(); + wakeLocksHeld++; break; case Messages::GoToRunning: - spi.Wakeup(); - - // Double Tap needs the touch screen to be in normal mode - if (!settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) { - touchPanel.Wakeup(); - } - - xTimerStart(dimTimer, 0); - spiNorFlash.Wakeup(); - - displayApp.PushMessage(Pinetime::Applications::Display::Messages::GoToRunning); - heartRateApp.PushMessage(Pinetime::Applications::HeartRateTask::Messages::WakeUp); - - if (bleController.IsRadioEnabled() && !bleController.IsConnected()) { - nimbleController.RestartFastAdv(); - } - - state = SystemTaskState::Running; - isDimmed = false; - break; - case Messages::TouchWakeUp: { - if (touchHandler.ProcessTouchInfo(touchPanel.GetTouchInfo())) { - auto gesture = touchHandler.GestureGet(); - if (settingsController.GetNotificationStatus() != Controllers::Settings::Notification::Sleep && - gesture != Pinetime::Applications::TouchEvents::None && - ((gesture == Pinetime::Applications::TouchEvents::DoubleTap && - settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) || - (gesture == Pinetime::Applications::TouchEvents::Tap && - settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::SingleTap)))) { - GoToRunning(); - } - } + GoToRunning(); break; - } case Messages::GoToSleep: - if (doNotGoToSleep) { - break; - } - state = SystemTaskState::GoingToSleep; // Already set in PushMessage() - NRF_LOG_INFO("[systemtask] Going to sleep"); - xTimerStop(idleTimer, 0); - xTimerStop(dimTimer, 0); - displayApp.PushMessage(Pinetime::Applications::Display::Messages::GoToSleep); - heartRateApp.PushMessage(Pinetime::Applications::HeartRateTask::Messages::GoToSleep); + GoToSleep(); break; case Messages::OnNewTime: - ReloadIdleTimer(); - displayApp.PushMessage(Pinetime::Applications::Display::Messages::UpdateDateTime); - if (alarmController.State() == Controllers::AlarmController::AlarmState::Set) { + if (alarmController.IsEnabled()) { alarmController.ScheduleAlarm(); } break; case Messages::OnNewNotification: if (settingsController.GetNotificationStatus() == Pinetime::Controllers::Settings::Notification::On) { - if (state == SystemTaskState::Sleeping) { + if (IsSleeping()) { GoToRunning(); - } else { - ReloadIdleTimer(); } displayApp.PushMessage(Pinetime::Applications::Display::Messages::NewNotification); } break; - case Messages::OnTimerDone: - if (state == SystemTaskState::Sleeping) { - GoToRunning(); - } - displayApp.PushMessage(Pinetime::Applications::Display::Messages::TimerDone); - break; case Messages::SetOffAlarm: - if (state == SystemTaskState::Sleeping) { - GoToRunning(); - } + GoToRunning(); displayApp.PushMessage(Pinetime::Applications::Display::Messages::AlarmTriggered); break; case Messages::BleConnected: - ReloadIdleTimer(); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::NotifyDeviceActivity); isBleDiscoveryTimerRunning = true; bleDiscoveryTimer = 5; break; case Messages::BleFirmwareUpdateStarted: - doNotGoToSleep = true; - if (state == SystemTaskState::Sleeping) { - GoToRunning(); - } + GoToRunning(); + wakeLocksHeld++; displayApp.PushMessage(Pinetime::Applications::Display::Messages::BleFirmwareUpdateStarted); break; case Messages::BleFirmwareUpdateFinished: if (bleController.State() == Pinetime::Controllers::Ble::FirmwareUpdateStates::Validated) { NVIC_SystemReset(); } - doNotGoToSleep = false; - xTimerStart(dimTimer, 0); + wakeLocksHeld--; break; case Messages::StartFileTransfer: NRF_LOG_INFO("[systemtask] FS Started"); - doNotGoToSleep = true; - if (state == SystemTaskState::Sleeping) { - GoToRunning(); - } + GoToRunning(); + wakeLocksHeld++; // TODO add intent of fs access icon or something break; case Messages::StopFileTransfer: NRF_LOG_INFO("[systemtask] FS Stopped"); - doNotGoToSleep = false; - xTimerStart(dimTimer, 0); + wakeLocksHeld--; // TODO add intent of fs access icon or something break; case Messages::OnTouchEvent: - if (touchHandler.ProcessTouchInfo(touchPanel.GetTouchInfo())) { - ReloadIdleTimer(); + // Finish immediately if no new events + if (!touchHandler.ProcessTouchInfo(touchPanel.GetTouchInfo())) { + break; + } + if (state == SystemTaskState::Running) { displayApp.PushMessage(Pinetime::Applications::Display::Messages::TouchEvent); + } else { + // If asleep, check for touch panel wake triggers + auto gesture = touchHandler.GestureGet(); + if (settingsController.GetNotificationStatus() != Controllers::Settings::Notification::Sleep && + gesture != Pinetime::Applications::TouchEvents::None && + ((gesture == Pinetime::Applications::TouchEvents::DoubleTap && + settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) || + (gesture == Pinetime::Applications::TouchEvents::Tap && + settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::SingleTap)))) { + GoToRunning(); + } } break; case Messages::HandleButtonEvent: { @@ -351,19 +285,35 @@ void SystemTask::Work() { HandleButtonAction(action); } break; case Messages::OnDisplayTaskSleeping: + case Messages::OnDisplayTaskAOD: + // The state was set to GoingToSleep when GoToSleep() was called + // If the state is no longer GoingToSleep, we have since transitioned back to Running + // In this case absorb the OnDisplayTaskSleeping/AOD + // as DisplayApp is about to receive GoToRunning + if (state != SystemTaskState::GoingToSleep) { + break; + } if (BootloaderVersion::IsValid()) { // First versions of the bootloader do not expose their version and cannot initialize the SPI NOR FLASH // if it's in sleep mode. Avoid bricked device by disabling sleep mode on these versions. spiNorFlash.Sleep(); } - spi.Sleep(); + + // Must keep SPI awake when still updating the display for always on + if (msg == Messages::OnDisplayTaskSleeping) { + spi.Sleep(); + } // Double Tap needs the touch screen to be in normal mode if (!settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) { touchPanel.Sleep(); } - state = SystemTaskState::Sleeping; + if (msg == Messages::OnDisplayTaskSleeping) { + state = SystemTaskState::Sleeping; + } else { + state = SystemTaskState::AODSleeping; + } break; case Messages::OnNewDay: // We might be sleeping (with TWI device disabled. @@ -373,32 +323,22 @@ void SystemTask::Work() { case Messages::OnNewHour: using Pinetime::Controllers::AlarmController; if (settingsController.GetNotificationStatus() != Controllers::Settings::Notification::Sleep && - settingsController.GetChimeOption() == Controllers::Settings::ChimesOption::Hours && - alarmController.State() != AlarmController::AlarmState::Alerting) { - if (state == SystemTaskState::Sleeping) { - GoToRunning(); - displayApp.PushMessage(Pinetime::Applications::Display::Messages::Chime); - } + settingsController.GetChimeOption() == Controllers::Settings::ChimesOption::Hours && !alarmController.IsAlerting()) { + GoToRunning(); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::Chime); } break; case Messages::OnNewHalfHour: using Pinetime::Controllers::AlarmController; if (settingsController.GetNotificationStatus() != Controllers::Settings::Notification::Sleep && - settingsController.GetChimeOption() == Controllers::Settings::ChimesOption::HalfHours && - alarmController.State() != AlarmController::AlarmState::Alerting) { - if (state == SystemTaskState::Sleeping) { - GoToRunning(); - displayApp.PushMessage(Pinetime::Applications::Display::Messages::Chime); - } + settingsController.GetChimeOption() == Controllers::Settings::ChimesOption::HalfHours && !alarmController.IsAlerting()) { + GoToRunning(); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::Chime); } break; case Messages::OnChargingEvent: batteryController.ReadPowerState(); - displayApp.PushMessage(Applications::Display::Messages::OnChargingEvent); - ReloadIdleTimer(); - if (state == SystemTaskState::Sleeping) { - GoToRunning(); - } + GoToRunning(); break; case Messages::MeasureBatteryTimerExpired: batteryController.MeasureVoltage(); @@ -407,9 +347,7 @@ void SystemTask::Work() { nimbleController.NotifyBatteryLevel(batteryController.PercentRemaining()); break; case Messages::OnPairing: - if (state == SystemTaskState::Sleeping) { - GoToRunning(); - } + GoToRunning(); displayApp.PushMessage(Pinetime::Applications::Display::Messages::ShowPairingKey); break; case Messages::BleRadioEnableToggle: @@ -436,23 +374,66 @@ void SystemTask::Work() { } monitor.Process(); - uint32_t systick_counter = nrf_rtc_counter_get(portNRF_RTC_REG); - dateTimeController.UpdateTime(systick_counter); NoInit_BackUpTime = dateTimeController.CurrentDateTime(); if (nrf_gpio_pin_read(PinMap::Button) == 0) { - watchdog.Kick(); + watchdog.Reload(); } } #pragma clang diagnostic pop } -void SystemTask::UpdateMotion() { - if (state == SystemTaskState::GoingToSleep || state == SystemTaskState::WakingUp) { +void SystemTask::GoToRunning() { + if (state == SystemTaskState::Running) { return; } + if (state == SystemTaskState::Sleeping || state == SystemTaskState::AODSleeping) { + // SPI only switched off when entering Sleeping, not AOD or GoingToSleep + if (state == SystemTaskState::Sleeping) { + spi.Wakeup(); + } + + // Double Tap needs the touch screen to be in normal mode + if (!settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) { + touchPanel.Wakeup(); + } + + spiNorFlash.Wakeup(); + } + + displayApp.PushMessage(Pinetime::Applications::Display::Messages::GoToRunning); + heartRateApp.PushMessage(Pinetime::Applications::HeartRateTask::Messages::WakeUp); + + if (bleController.IsRadioEnabled() && !bleController.IsConnected()) { + nimbleController.RestartFastAdv(); + } + + state = SystemTaskState::Running; +}; +void SystemTask::GoToSleep() { + if (IsSleeping()) { + return; + } + if (IsSleepDisabled()) { + return; + } + NRF_LOG_INFO("[systemtask] Going to sleep"); + if (settingsController.GetAlwaysOnDisplay()) { + displayApp.PushMessage(Pinetime::Applications::Display::Messages::GoToAOD); + } else { + displayApp.PushMessage(Pinetime::Applications::Display::Messages::GoToSleep); + } + heartRateApp.PushMessage(Pinetime::Applications::HeartRateTask::Messages::GoToSleep); + + state = SystemTaskState::GoingToSleep; +}; + +void SystemTask::UpdateMotion() { + // Only consider disabling motion updates specifically in the Sleeping state + // AOD needs motion on to show up to date step counts if (state == SystemTaskState::Sleeping && !(settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::RaiseWrist) || - settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::Shake))) { + settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::Shake) || + motionController.GetService()->IsMotionNotificationSubscribed())) { return; } @@ -463,17 +444,20 @@ void SystemTask::UpdateMotion() { auto motionValues = motionSensor.Process(); - motionController.IsSensorOk(motionSensor.IsOk()); motionController.Update(motionValues.x, motionValues.y, motionValues.z, motionValues.steps); if (settingsController.GetNotificationStatus() != Controllers::Settings::Notification::Sleep) { if ((settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::RaiseWrist) && - motionController.ShouldRaiseWake(state == SystemTaskState::Sleeping)) || + motionController.ShouldRaiseWake()) || (settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::Shake) && motionController.ShouldShakeWake(settingsController.GetShakeThreshold()))) { GoToRunning(); } } + if (settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::LowerWrist) && state == SystemTaskState::Running && + motionController.ShouldLowerSleep()) { + GoToSleep(); + } } void SystemTask::HandleButtonAction(Controllers::ButtonActions action) { @@ -481,14 +465,14 @@ void SystemTask::HandleButtonAction(Controllers::ButtonActions action) { return; } - ReloadIdleTimer(); + displayApp.PushMessage(Pinetime::Applications::Display::Messages::NotifyDeviceActivity); using Actions = Controllers::ButtonActions; switch (action) { case Actions::Click: // If the first action after fast wakeup is a click, it should be ignored. - if (!fastWakeUpDone && state != SystemTaskState::GoingToSleep) { + if (!fastWakeUpDone) { displayApp.PushMessage(Applications::Display::Messages::ButtonPushed); } break; @@ -508,67 +492,12 @@ void SystemTask::HandleButtonAction(Controllers::ButtonActions action) { fastWakeUpDone = false; } -void SystemTask::GoToRunning() { - if (state == SystemTaskState::Sleeping) { - state = SystemTaskState::WakingUp; - PushMessage(Messages::GoToRunning); - } -} - -void SystemTask::OnTouchEvent() { - if (state == SystemTaskState::Running) { - PushMessage(Messages::OnTouchEvent); - } else if (state == SystemTaskState::Sleeping) { - if (settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::SingleTap) or - settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) { - PushMessage(Messages::TouchWakeUp); - } - } -} - void SystemTask::PushMessage(System::Messages msg) { - if (msg == Messages::GoToSleep && !doNotGoToSleep) { - state = SystemTaskState::GoingToSleep; - } - if (in_isr()) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xQueueSendFromISR(systemTasksMsgQueue, &msg, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken == pdTRUE) { - /* Actual macro used here is port specific. */ - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } else { xQueueSend(systemTasksMsgQueue, &msg, portMAX_DELAY); } } - -void SystemTask::OnDim() { - if (doNotGoToSleep) { - return; - } - NRF_LOG_INFO("Dim timeout -> Dim screen") - displayApp.PushMessage(Pinetime::Applications::Display::Messages::DimScreen); - xTimerStart(idleTimer, 0); - isDimmed = true; -} - -void SystemTask::OnIdle() { - if (doNotGoToSleep) { - return; - } - NRF_LOG_INFO("Idle timeout -> Going to sleep") - PushMessage(Messages::GoToSleep); -} - -void SystemTask::ReloadIdleTimer() { - if (state != SystemTaskState::Running) { - return; - } - if (isDimmed) { - displayApp.PushMessage(Pinetime::Applications::Display::Messages::RestoreBrightness); - isDimmed = false; - } - xTimerReset(dimTimer, 0); - xTimerStop(idleTimer, 0); -} diff --git a/src/systemtask/SystemTask.h b/src/systemtask/SystemTask.h index 72e88875..0060e360 100644 --- a/src/systemtask/SystemTask.h +++ b/src/systemtask/SystemTask.h @@ -15,7 +15,6 @@ #include "systemtask/SystemMonitor.h" #include "components/ble/NimbleController.h" #include "components/ble/NotificationManager.h" -#include "components/timer/TimerController.h" #include "components/alarm/AlarmController.h" #include "components/fs/FS.h" #include "touchhandler/TouchHandler.h" @@ -53,7 +52,7 @@ namespace Pinetime { namespace System { class SystemTask { public: - enum class SystemTaskState { Sleeping, Running, GoingToSleep, WakingUp }; + enum class SystemTaskState { Sleeping, Running, GoingToSleep, AODSleeping }; SystemTask(Drivers::SpiMaster& spi, Pinetime::Drivers::SpiNorFlash& spiNorFlash, Drivers::TwiMaster& twiMaster, @@ -61,7 +60,6 @@ namespace Pinetime { Controllers::Battery& batteryController, Controllers::Ble& bleController, Controllers::DateTime& dateTimeController, - Controllers::TimerController& timerController, Controllers::AlarmController& alarmController, Drivers::Watchdog& watchdog, Pinetime::Controllers::NotificationManager& notificationManager, @@ -79,17 +77,16 @@ namespace Pinetime { void Start(); void PushMessage(Messages msg); - void OnTouchEvent(); - - void OnIdle(); - void OnDim(); + bool IsSleepDisabled() { + return wakeLocksHeld > 0; + } Pinetime::Controllers::NimbleController& nimble() { return nimbleController; }; bool IsSleeping() const { - return state == SystemTaskState::Sleeping || state == SystemTaskState::WakingUp; + return state != SystemTaskState::Running; } private: @@ -103,7 +100,6 @@ namespace Pinetime { Pinetime::Controllers::Ble& bleController; Pinetime::Controllers::DateTime& dateTimeController; - Pinetime::Controllers::TimerController& timerController; Pinetime::Controllers::AlarmController& alarmController; QueueHandle_t systemTasksMsgQueue; Pinetime::Drivers::Watchdog& watchdog; @@ -123,20 +119,17 @@ namespace Pinetime { static void Process(void* instance); void Work(); - void ReloadIdleTimer(); bool isBleDiscoveryTimerRunning = false; uint8_t bleDiscoveryTimer = 0; - TimerHandle_t dimTimer; - TimerHandle_t idleTimer; TimerHandle_t measureBatteryTimer; - bool doNotGoToSleep = false; - bool isDimmed = false; + uint8_t wakeLocksHeld = 0; SystemTaskState state = SystemTaskState::Running; void HandleButtonAction(Controllers::ButtonActions action); bool fastWakeUpDone = false; void GoToRunning(); + void GoToSleep(); void UpdateMotion(); bool stepCounterMustBeReset = false; static constexpr TickType_t batteryMeasurementPeriod = pdMS_TO_TICKS(10 * 60 * 1000); diff --git a/src/systemtask/WakeLock.cpp b/src/systemtask/WakeLock.cpp new file mode 100644 index 00000000..2953f7ee --- /dev/null +++ b/src/systemtask/WakeLock.cpp @@ -0,0 +1,27 @@ +#include "systemtask/WakeLock.h" + +using namespace Pinetime::System; + +WakeLock::WakeLock(SystemTask& systemTask) : systemTask {systemTask} { + lockHeld = false; +} + +WakeLock::~WakeLock() { + Release(); +} + +void WakeLock::Lock() { + if (lockHeld) { + return; + } + systemTask.PushMessage(Messages::DisableSleeping); + lockHeld = true; +} + +void WakeLock::Release() { + if (!lockHeld) { + return; + } + systemTask.PushMessage(Messages::EnableSleeping); + lockHeld = false; +} diff --git a/src/systemtask/WakeLock.h b/src/systemtask/WakeLock.h new file mode 100644 index 00000000..5424c009 --- /dev/null +++ b/src/systemtask/WakeLock.h @@ -0,0 +1,19 @@ +#pragma once + +#include "systemtask/SystemTask.h" + +namespace Pinetime { + namespace System { + class WakeLock { + public: + WakeLock(SystemTask& systemTask); + ~WakeLock(); + void Lock(); + void Release(); + + private: + bool lockHeld; + SystemTask& systemTask; + }; + } +} diff --git a/src/utility/CircularBuffer.h b/src/utility/CircularBuffer.h new file mode 100644 index 00000000..c8abe92e --- /dev/null +++ b/src/utility/CircularBuffer.h @@ -0,0 +1,51 @@ +#pragma once + +#include <array> +#include <cstddef> + +namespace Pinetime { + namespace Utility { + template <class T, size_t S> + struct CircularBuffer { + constexpr size_t Size() const { + return S; + } + + size_t Idx() const { + return idx; + } + + T& operator[](size_t n) { + return data[(idx + n) % S]; + } + + const T& operator[](size_t n) const { + return data[(idx + n) % S]; + } + + void operator++() { + idx++; + idx %= S; + } + + void operator++(int) { + operator++(); + } + + void operator--() { + if (idx > 0) { + idx--; + } else { + idx = S - 1; + } + } + + void operator--(int) { + operator--(); + } + + std::array<T, S> data; + size_t idx = 0; + }; + } +} diff --git a/src/utility/DirtyValue.h b/src/utility/DirtyValue.h new file mode 100644 index 00000000..8d5147aa --- /dev/null +++ b/src/utility/DirtyValue.h @@ -0,0 +1,39 @@ +#pragma once + +namespace Pinetime { + namespace Utility { + template <class T> + class DirtyValue { + public: + DirtyValue() = default; // Use NSDMI + + explicit DirtyValue(T const& v) : value {v} { + } // Use MIL and const-lvalue-ref + + bool IsUpdated() { + if (this->isUpdated) { + this->isUpdated = false; + return true; + } + return false; + } + + T const& Get() { + this->isUpdated = false; + return value; + } // never expose a non-const lvalue-ref + + DirtyValue& operator=(const T& other) { + if (this->value != other) { + this->value = other; + this->isUpdated = true; + } + return *this; + } + + private: + T value {}; // NSDMI - default initialise type + bool isUpdated {true}; // NSDMI - use brace initialisation + }; + } +} diff --git a/src/components/utility/LinearApproximation.h b/src/utility/LinearApproximation.h index 1fe58d44..34ceb7f2 100644 --- a/src/components/utility/LinearApproximation.h +++ b/src/utility/LinearApproximation.h @@ -5,7 +5,6 @@ namespace Pinetime { namespace Utility { - // based on: https://github.com/SHristov92/LinearApproximation/blob/main/Linear.h template <typename Key, typename Value, std::size_t Size> class LinearApproximation { diff --git a/src/utility/Math.cpp b/src/utility/Math.cpp new file mode 100644 index 00000000..fee4f64a --- /dev/null +++ b/src/utility/Math.cpp @@ -0,0 +1,49 @@ +#include "utility/Math.h" + +#include <lvgl/src/lv_misc/lv_math.h> + +using namespace Pinetime::Utility; + +#ifndef PINETIME_IS_RECOVERY + +int16_t Pinetime::Utility::Asin(int16_t arg) { + int16_t a = arg < 0 ? -arg : arg; + + int16_t angle = 45; + int16_t low = 0; + int16_t high = 90; + while (low <= high) { + int16_t sinAngle = _lv_trigo_sin(angle); + int16_t sinAngleSub = _lv_trigo_sin(angle - 1); + int16_t sinAngleAdd = _lv_trigo_sin(angle + 1); + + if (a >= sinAngleSub && a <= sinAngleAdd) { + if (a <= (sinAngleSub + sinAngle) / 2) { + angle--; + } else if (a > (sinAngle + sinAngleAdd) / 2) { + angle++; + } + break; + } + + if (a < sinAngle) { + high = angle - 1; + } + + else { + low = angle + 1; + } + + angle = (low + high) / 2; + } + + return arg < 0 ? -angle : angle; +} + +#else + +int16_t Pinetime::Utility::Asin(int16_t /*arg*/) { + return 0; +} + +#endif diff --git a/src/utility/Math.h b/src/utility/Math.h new file mode 100644 index 00000000..e8d190c7 --- /dev/null +++ b/src/utility/Math.h @@ -0,0 +1,10 @@ +#pragma once + +#include <cstdint> + +namespace Pinetime { + namespace Utility { + // returns the arcsin of `arg`. asin(-32767) = -90, asin(32767) = 90 + int16_t Asin(int16_t arg); + } +} diff --git a/src/utility/StaticStack.h b/src/utility/StaticStack.h new file mode 100644 index 00000000..40df9354 --- /dev/null +++ b/src/utility/StaticStack.h @@ -0,0 +1,47 @@ +#include <array> +#include <cstddef> + +namespace Pinetime { + namespace Utility { + template <typename T, size_t N> + class StaticStack { + public: + T Pop(); + void Push(T element); + void Reset(); + T Top(); + + private: + std::array<T, N> elementArray; + // Number of elements in stack, points to the next empty slot + size_t stackPointer = 0; + }; + + // Returns random data when popping from empty array. + template <typename T, size_t N> + T StaticStack<T, N>::Pop() { + if (stackPointer > 0) { + stackPointer--; + } + return elementArray[stackPointer]; + } + + template <typename T, size_t N> + void StaticStack<T, N>::Push(T element) { + if (stackPointer < elementArray.size()) { + elementArray[stackPointer] = element; + stackPointer++; + } + } + + template <typename T, size_t N> + void StaticStack<T, N>::Reset() { + stackPointer = 0; + } + + template <typename T, size_t N> + T StaticStack<T, N>::Top() { + return elementArray[stackPointer - 1]; + } + } +} |
