diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 60a86f76..7bdfbcb1 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -8,30 +8,10 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -47,8 +27,5 @@
-
-
-
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ac7c8903..c63d169a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -41,6 +41,10 @@ if(DEFINED USE_DEBUG_PINS AND USE_DEBUG_PINS)
add_definitions(-DUSE_DEBUG_PINS)
endif()
+if(BUILD_DFU)
+ set(BUILD_DFU true)
+endif()
+
message("BUILD CONFIGURATION")
message("-------------------")
message(" * Version : " ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH})
@@ -62,6 +66,11 @@ if(USE_DEBUG_PINS)
else()
message(" * Debug pins : Disabled")
endif()
+if(BUILD_DFU)
+ message(" * Build DFU (using adafruit-nrfutil) : Enabled")
+else()
+ message(" * Build DFU (using adafruit-nrfutil) : Disabled")
+endif()
set(VERSION_EDIT_WARNING "// Do not edit this file, it is automatically generated by CMAKE!")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/Version.h.in ${CMAKE_CURRENT_SOURCE_DIR}/src/Version.h)
diff --git a/doc/buildAndProgram.md b/doc/buildAndProgram.md
index 79ca519d..8d9c9382 100644
--- a/doc/buildAndProgram.md
+++ b/doc/buildAndProgram.md
@@ -26,7 +26,10 @@ CMake configures the project according to variables you specify the command line
**NRFJPROG**|Path to the NRFJProg executable. Used only if `USE_JLINK` is 1.|`-DNRFJPROG=/opt/nrfjprog/nrfjprog`
**GDB_CLIENT_BIN_PATH**|Path to arm-none-eabi-gdb executable. Used only if `USE_GDB_CLIENT` is 1.|`-DGDB_CLIENT_BIN_PATH=/home/jf/nrf52/gcc-arm-none-eabi-9-2019-q4-major/bin/arm-none-eabi-gdb`
**GDB_CLIENT_TARGET_REMOTE**|Target remote connection string. Used only if `USE_GDB_CLIENT` is 1.|`-DGDB_CLIENT_TARGET_REMOTE=/dev/ttyACM0`
+**BUILD_DFU (\*)**|Build DFU files while building (needs [adafruit-nrfutil](https://github.com/adafruit/Adafruit_nRF52_nrfutil)).|`-BUILD_DFU=1`
+####(*) Note about **BUILD_DFU**:
+DFU files are the files you'll need to install your build of InfiniTime using OTA (over-the-air) mecanism. To generate the DFU file, the Python tool [adafruit-nrfutil](https://github.com/adafruit/Adafruit_nRF52_nrfutil) is needed on your system. Check that this tool is properly installed before enabling this option.
#### CMake command line for JLink
```
@@ -45,11 +48,14 @@ cmake -DARM_NONE_EABI_TOOLCHAIN_PATH=... -DNRF5_SDK_PATH=... -DUSE_OPENOCD=1 -DG
### Build the project
During the project generation, CMake created the following targets:
-- FLASH_ERASE : mass erase the flash memory of the NRF52.
-- FLASH_pinetime-app : flash the firmware into the NRF52.
-- pinetime-app : build the standalone (without bootloader support) version of the firmware.
-- pinetime-mcuboot-app : build the firmware with the support of the bootloader (based on MCUBoot).
-- pinetime-graphics : small firmware that writes the boot graphics into the SPI flash.
+- **FLASH_ERASE** : mass erase the flash memory of the NRF52.
+- **FLASH_pinetime-app** : flash the firmware into the NRF52.
+- **pinetime-app** : build the standalone (without bootloader support) version of the firmware.
+- **pinetime-recovery** : build the standalone recovery version of infinitime (light firmware that only supports OTA and basic UI)
+- **pinetime-recovery-loader** : build the standalone tool that flashes the recovery firmware into the external SPI flash
+- **pinetime-mcuboot-app** : build the firmware with the support of the bootloader (based on MCUBoot).
+- **pinetime-mcuboot-recovery** : build pinetime-recovery with bootloader support
+- **pinetime-mcuboot-recovery-loader** : build pinetime-recovery-loader with bootloader support
If you just want to build the project and run it on the Pinetime, using *pinetime-app* is recommanded. See [this page](../bootloader/README.md) for more info about bootloader support.
@@ -64,8 +70,11 @@ Binary files are generated into the folder `src`:
- **pinetime-app.map** : map file
- **pinetime-mcuboot-app.bin, .hex and .out** : firmware with bootloader support in bin, hex and out formats.
- **pinetime-mcuboot-app.map** : map file
- - **pinetime-graphics.bin, .hex and .out** : firmware for the boot graphic in bin, hex and out formats.
- - **pinetime-graphics.map** : map file
+ - **pinetime-mcuboot-app-image** : MCUBoot image of the firmware
+ - **pinetime-mcuboot-ap-dfu** : DFU file of the firmware
+
+The same files are generated for **pinetime-recovery** and **pinetime-recoveryloader**
+
### Program and run
#### Using CMake targets
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 894e534b..a6caa24e 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -17,7 +17,8 @@ RUN apt-get update -qq \
# aarch64 packages
libffi-dev \
libssl-dev \
- python3-dev \
+ python3-dev \
+ python \
&& rm -rf /var/cache/apt/* /var/lib/apt/lists/*;
RUN pip3 install adafruit-nrfutil
diff --git a/docker/build.sh b/docker/build.sh
index 8f0d0fa9..2fa7d920 100755
--- a/docker/build.sh
+++ b/docker/build.sh
@@ -63,6 +63,7 @@ CmakeGenerate() {
-DUSE_OPENOCD=1 \
-DARM_NONE_EABI_TOOLCHAIN_PATH="$TOOLS_DIR/$GCC_ARM_VER" \
-DNRF5_SDK_PATH="$TOOLS_DIR/$NRF_SDK_VER" \
+ -DBUILD_DFU=1 \
"$SOURCES_DIR"
cmake -L -N .
}
diff --git a/docker/post_build.sh.in b/docker/post_build.sh.in
index 53ae343a..db6e7a94 100755
--- a/docker/post_build.sh.in
+++ b/docker/post_build.sh.in
@@ -9,15 +9,12 @@ export PROJECT_VERSION="@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT
mkdir -p "$OUTPUT_DIR"
cp "$SOURCES_DIR"/bootloader/bootloader-5.0.4.bin $OUTPUT_DIR/bootloader.bin
+cp "$BUILD_DIR/src/pinetime-mcuboot-app-image-$PROJECT_VERSION.bin" "$OUTPUT_DIR/pinetime-mcuboot-app-image-$PROJECT_VERSION.bin"
+cp "$BUILD_DIR/src/pinetime-mcuboot-app-dfu-$PROJECT_VERSION.zip" "$OUTPUT_DIR/pinetime-mcuboot-app-dfu-$PROJECT_VERSION.zip"
-"$TOOLS_DIR"/mcuboot/scripts/imgtool.py create --version 1.0.0 \
- --align 4 --header-size 32 --slot-size 475136 --pad-header \
- "$BUILD_DIR/src/pinetime-mcuboot-app-$PROJECT_VERSION.bin" \
- "$OUTPUT_DIR/image-$PROJECT_VERSION.bin"
+cp "$BUILD_DIR/src/pinetime-mcuboot-recovery-loader-image-$PROJECT_VERSION.bin" "$OUTPUT_DIR/pinetime-mcuboot-recovery-loader-image-$PROJECT_VERSION.bin"
+cp "$BUILD_DIR/src/pinetime-mcuboot-recovery-loader-dfu-$PROJECT_VERSION.zip" "$OUTPUT_DIR/pinetime-mcuboot-recovery-loader-dfu-$PROJECT_VERSION.zip"
-adafruit-nrfutil dfu genpkg --dev-type 0x0052 \
- --application "$OUTPUT_DIR/image-$PROJECT_VERSION.bin" \
- "$OUTPUT_DIR/dfu-$PROJECT_VERSION.zip"
mkdir -p "$OUTPUT_DIR/src"
cd "$BUILD_DIR"
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2cf570be..47b34335 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -548,7 +548,58 @@ list(APPEND SOURCE_FILES
components/heartrate/HeartRateController.cpp
)
-list(APPEND GRAPHICS_SOURCE_FILES
+list(APPEND RECOVERY_SOURCE_FILES
+ BootloaderVersion.cpp
+ logging/NrfLogger.cpp
+ displayapp/DisplayAppRecovery.cpp
+
+ main.cpp
+ drivers/St7789.cpp
+ drivers/SpiNorFlash.cpp
+ drivers/SpiMaster.cpp
+ drivers/Spi.cpp
+ drivers/Watchdog.cpp
+ drivers/DebugPins.cpp
+ drivers/InternalFlash.cpp
+ drivers/Hrs3300.cpp
+ components/battery/BatteryController.cpp
+ components/ble/BleController.cpp
+ components/ble/NotificationManager.cpp
+ components/datetime/DateTimeController.cpp
+ components/brightness/BrightnessController.cpp
+ components/ble/NimbleController.cpp
+ components/ble/DeviceInformationService.cpp
+ components/ble/CurrentTimeClient.cpp
+ components/ble/AlertNotificationClient.cpp
+ components/ble/DfuService.cpp
+ components/ble/CurrentTimeService.cpp
+ components/ble/AlertNotificationService.cpp
+ components/ble/MusicService.cpp
+ components/ble/BatteryInformationService.cpp
+ components/ble/ImmediateAlertService.cpp
+ components/ble/ServiceDiscovery.cpp
+ components/ble/NavigationService.cpp
+ components/ble/HeartRateService.cpp
+ components/firmwarevalidator/FirmwareValidator.cpp
+ drivers/Cst816s.cpp
+ FreeRTOS/port.c
+ FreeRTOS/port_cmsis_systick.c
+ FreeRTOS/port_cmsis.c
+
+ systemtask/SystemTask.cpp
+ drivers/TwiMaster.cpp
+ components/gfx/Gfx.cpp
+ displayapp/icons/infinitime/infinitime-nb.c
+ 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
+ )
+
+list(APPEND RECOVERYLOADER_SOURCE_FILES
# FreeRTOS
FreeRTOS/port.c
FreeRTOS/port_cmsis_systick.c
@@ -559,18 +610,23 @@ list(APPEND GRAPHICS_SOURCE_FILES
drivers/Spi.cpp
logging/NrfLogger.cpp
+ components/rle/RleDecoder.cpp
+
components/gfx/Gfx.cpp
drivers/St7789.cpp
components/brightness/BrightnessController.cpp
- graphics.cpp
+ displayapp/icons/infinitime/infinitime-nb.c
+ recoveryLoader.cpp
)
+
set(INCLUDE_FILES
BootloaderVersion.h
logging/Logger.h
logging/NrfLogger.h
displayapp/DisplayApp.h
+ displayapp/Messages.h
displayapp/TouchEvents.h
displayapp/screens/Screen.h
displayapp/screens/Clock.h
@@ -820,8 +876,8 @@ add_custom_command(TARGET ${EXECUTABLE_NAME}
# Build binary intended to be used by bootloader
set(EXECUTABLE_MCUBOOT_NAME "pinetime-mcuboot-app")
set(EXECUTABLE_MCUBOOT_FILE_NAME ${EXECUTABLE_MCUBOOT_NAME}-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH})
-set(IMAGE_MCUBOOT_FILE_NAME image-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}.bin)
-set(DFU_FILE_NAME dfu-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}.zip)
+set(IMAGE_MCUBOOT_FILE_NAME ${EXECUTABLE_MCUBOOT_NAME}-image-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}.bin)
+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)
@@ -846,16 +902,26 @@ add_custom_command(TARGET ${EXECUTABLE_MCUBOOT_NAME}
COMMAND ${CMAKE_SIZE_UTIL} ${EXECUTABLE_MCUBOOT_FILE_NAME}.out
COMMAND ${CMAKE_OBJCOPY} -O binary ${EXECUTABLE_MCUBOOT_FILE_NAME}.out "${EXECUTABLE_MCUBOOT_FILE_NAME}.bin"
COMMAND ${CMAKE_OBJCOPY} -O ihex ${EXECUTABLE_MCUBOOT_FILE_NAME}.out "${EXECUTABLE_MCUBOOT_FILE_NAME}.hex"
+ COMMAND ${CMAKE_SOURCE_DIR}/tools/mcuboot/imgtool.py create --align 4 --version 1.0.0 --header-size 32 --slot-size 475136 --pad-header ${EXECUTABLE_MCUBOOT_FILE_NAME}.bin ${IMAGE_MCUBOOT_FILE_NAME}
COMMENT "post build steps for ${EXECUTABLE_MCUBOOT_FILE_NAME}"
)
-# Build binary that writes the graphic assets for the bootloader
-set(EXECUTABLE_GRAPHICS_NAME "pinetime-graphics")
-set(EXECUTABLE_GRAPHICS_FILE_NAME ${EXECUTABLE_GRAPHICS_NAME}-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH})
-add_executable(${EXECUTABLE_GRAPHICS_NAME} ${GRAPHICS_SOURCE_FILES})
-target_link_libraries(${EXECUTABLE_GRAPHICS_NAME} nrf-sdk)
-set_target_properties(${EXECUTABLE_GRAPHICS_NAME} PROPERTIES OUTPUT_NAME ${EXECUTABLE_GRAPHICS_FILE_NAME})
-target_compile_options(${EXECUTABLE_GRAPHICS_NAME} PUBLIC
+if(BUILD_DFU)
+ add_custom_command(TARGET ${EXECUTABLE_MCUBOOT_NAME}
+ POST_BUILD
+ COMMAND adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application ${IMAGE_MCUBOOT_FILE_NAME} ${DFU_MCUBOOT_FILE_NAME}
+ COMMENT "post build (DFU) steps for ${EXECUTABLE_MCUBOOT_FILE_NAME}"
+ )
+endif()
+
+# InfiniTime recovery firmware (autonomous)
+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)
+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
$<$,$>: ${COMMON_FLAGS} -O0 -g3>
$<$,$>: ${COMMON_FLAGS} -O3>
$<$,$>: ${COMMON_FLAGS} -O0 -g3>
@@ -863,21 +929,142 @@ target_compile_options(${EXECUTABLE_GRAPHICS_NAME} PUBLIC
$<$: -MP -MD -x assembler-with-cpp>
)
-set_target_properties(${EXECUTABLE_GRAPHICS_NAME} PROPERTIES
+set_target_properties(${EXECUTABLE_RECOVERY_NAME} PROPERTIES
SUFFIX ".out"
- LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections --specs=nano.specs -lc -lnosys -lm -Wl,-Map=${EXECUTABLE_GRAPHICS_FILE_NAME}.map"
+ LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections --specs=nano.specs -lc -lnosys -lm -Wl,-Map=${EXECUTABLE_RECOVERY_FILE_NAME}.map"
CXX_STANDARD 11
C_STANDARD 99
)
-add_custom_command(TARGET ${EXECUTABLE_GRAPHICS_NAME}
+add_custom_command(TARGET ${EXECUTABLE_RECOVERY_NAME}
POST_BUILD
- COMMAND ${CMAKE_SIZE_UTIL} ${EXECUTABLE_GRAPHICS_FILE_NAME}.out
- COMMAND ${CMAKE_OBJCOPY} -O binary ${EXECUTABLE_GRAPHICS_FILE_NAME}.out "${EXECUTABLE_GRAPHICS_FILE_NAME}.bin"
- COMMAND ${CMAKE_OBJCOPY} -O ihex ${EXECUTABLE_GRAPHICS_FILE_NAME}.out "${EXECUTABLE_GRAPHICS_FILE_NAME}.hex"
- COMMENT "post build steps for ${EXECUTABLE_GRAPHICS_FILE_NAME}"
+ COMMAND ${CMAKE_SIZE_UTIL} ${EXECUTABLE_RECOVERY_FILE_NAME}.out
+ COMMAND ${CMAKE_OBJCOPY} -O binary ${EXECUTABLE_RECOVERY_FILE_NAME}.out "${EXECUTABLE_RECOVERY_FILE_NAME}.bin"
+ COMMAND ${CMAKE_OBJCOPY} -O ihex ${EXECUTABLE_RECOVERY_FILE_NAME}.out "${EXECUTABLE_RECOVERY_FILE_NAME}.hex"
+ COMMENT "post build steps for ${EXECUTABLE_RECOVERY_FILE_NAME}"
)
+# InfiniTime recovery firmware (mcuboot)
+set(EXECUTABLE_RECOVERY_MCUBOOT_NAME "pinetime-mcuboot-recovery")
+set(EXECUTABLE_RECOVERY_MCUBOOT_FILE_NAME ${EXECUTABLE_RECOVERY_MCUBOOT_NAME}-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH})
+set(IMAGE_RECOVERY_MCUBOOT_FILE_NAME ${EXECUTABLE_RECOVERY_MCUBOOT_NAME}-image-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}.bin)
+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)
+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
+ $<$,$>: ${COMMON_FLAGS} -O0 -g3>
+ $<$,$>: ${COMMON_FLAGS} -O3>
+ $<$,$>: ${COMMON_FLAGS} -O0 -g3>
+ $<$,$>: ${COMMON_FLAGS} -O3>
+ $<$: -MP -MD -x assembler-with-cpp>
+ )
+
+set_target_properties(${EXECUTABLE_RECOVERY_MCUBOOT_NAME} PROPERTIES
+ SUFFIX ".out"
+ LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections --specs=nano.specs -lc -lnosys -lm -Wl,-Map=${EXECUTABLE_RECOVERY_MCUBOOT_FILE_NAME}.map"
+ CXX_STANDARD 11
+ C_STANDARD 99
+ )
+
+add_custom_command(TARGET ${EXECUTABLE_RECOVERY_MCUBOOT_NAME}
+ POST_BUILD
+ COMMAND ${CMAKE_SIZE_UTIL} ${EXECUTABLE_RECOVERY_MCUBOOT_FILE_NAME}.out
+ COMMAND ${CMAKE_OBJCOPY} -O binary ${EXECUTABLE_RECOVERY_MCUBOOT_FILE_NAME}.out "${EXECUTABLE_RECOVERY_MCUBOOT_FILE_NAME}.bin"
+ COMMAND ${CMAKE_OBJCOPY} -O ihex ${EXECUTABLE_RECOVERY_MCUBOOT_FILE_NAME}.out "${EXECUTABLE_RECOVERYY_MCUBOOT_FILE_NAME}.hex"
+ COMMAND ${CMAKE_SOURCE_DIR}/tools/mcuboot/imgtool.py create --align 4 --version 1.0.0 --header-size 32 --slot-size 475136 --pad-header ${EXECUTABLE_RECOVERY_MCUBOOT_FILE_NAME}.bin ${IMAGE_RECOVERY_MCUBOOT_FILE_NAME}
+ COMMAND python ${CMAKE_SOURCE_DIR}/tools/bin2c.py ${IMAGE_RECOVERY_MCUBOOT_FILE_NAME} recoveryImage > recoveryImage.h
+ COMMENT "post build steps for ${EXECUTABLE_RECOVERY_MCUBOOT_FILE_NAME}"
+ )
+
+if(BUILD_DFU)
+ add_custom_command(TARGET ${EXECUTABLE_RECOVERY_MCUBOOT_NAME}
+ POST_BUILD
+ COMMAND adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application ${IMAGE_RECOVERY_MCUBOOT_FILE_NAME} ${DFU_RECOVERY_MCUBOOT_FILE_NAME}
+ COMMENT "post build (DFU) steps for ${EXECUTABLE_RECOVERY_MCUBOOT_FILE_NAME}"
+ )
+endif()
+
+# Build binary that writes the recovery image into the SPI flash memory
+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)
+set_target_properties(${EXECUTABLE_RECOVERYLOADER_NAME} PROPERTIES OUTPUT_NAME ${EXECUTABLE_RECOVERYLOADER_FILE_NAME})
+target_compile_options(${EXECUTABLE_RECOVERYLOADER_NAME} PUBLIC
+ $<$,$>: ${COMMON_FLAGS} -O0 -g3>
+ $<$,$>: ${COMMON_FLAGS} -O3>
+ $<$,$>: ${COMMON_FLAGS} -O0 -g3>
+ $<$,$>: ${COMMON_FLAGS} -O3>
+ $<$: -MP -MD -x assembler-with-cpp>
+ )
+target_include_directories(${EXECUTABLE_RECOVERYLOADER_NAME} PUBLIC
+ $
+ )
+add_dependencies(${EXECUTABLE_RECOVERYLOADER_NAME} ${EXECUTABLE_RECOVERY_MCUBOOT_NAME})
+
+set_target_properties(${EXECUTABLE_RECOVERYLOADER_NAME} PROPERTIES
+ SUFFIX ".out"
+ LINK_FLAGS "-mthumb -mabi=aapcs -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections --specs=nano.specs -lc -lnosys -lm -Wl,-Map=${EXECUTABLE_RECOVERYLOADER_FILE_NAME}.map"
+ CXX_STANDARD 11
+ C_STANDARD 99
+ )
+
+add_custom_command(TARGET ${EXECUTABLE_RECOVERYLOADER_NAME}
+ POST_BUILD
+ COMMAND ${CMAKE_SIZE_UTIL} ${EXECUTABLE_RECOVERYLOADER_FILE_NAME}.out
+ COMMAND ${CMAKE_OBJCOPY} -O binary ${EXECUTABLE_RECOVERYLOADER_FILE_NAME}.out "${EXECUTABLE_RECOVERYLOADER_FILE_NAME}.bin"
+ COMMAND ${CMAKE_OBJCOPY} -O ihex ${EXECUTABLE_RECOVERYLOADER_FILE_NAME}.out "${EXECUTABLE_RECOVERYLOADER_FILE_NAME}.hex"
+ COMMENT "post build steps for ${EXECUTABLE_RECOVERYLOADER_FILE_NAME}"
+ )
+
+# Build binary that writes the recovery image (MCUBoot version)
+set(EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME "pinetime-mcuboot-recovery-loader")
+set(EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME}-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH})
+set(IMAGE_MCUBOOT_RECOVERYLOADER_FILE_NAME ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME}-image-${pinetime_VERSION_MAJOR}.${pinetime_VERSION_MINOR}.${pinetime_VERSION_PATCH}.bin)
+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)
+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} -O0 -g3>
+ $<$,$>: ${COMMON_FLAGS} -O3>
+ $<$,$>: ${COMMON_FLAGS} -O0 -g3>
+ $<$,$>: ${COMMON_FLAGS} -O3>
+ $<$: -MP -MD -x assembler-with-cpp>
+ )
+target_include_directories(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} PUBLIC
+ $
+ )
+add_dependencies(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} ${EXECUTABLE_RECOVERY_MCUBOOT_NAME})
+
+set_target_properties(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} PROPERTIES
+ SUFFIX ".out"
+ LINK_FLAGS "-mthumb -mabi=aapcs -std=gnu++98 -std=c99 -L ${NRF5_SDK_PATH}/modules/nrfx/mdk -T${NRF5_LINKER_SCRIPT_MCUBOOT} -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wl,--gc-sections --specs=nano.specs -lc -lnosys -lm -Wl,-Map=${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.map"
+ CXX_STANDARD 11
+ C_STANDARD 99
+ )
+
+add_custom_command(TARGET ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME}
+ POST_BUILD
+ COMMAND ${CMAKE_SIZE_UTIL} ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.out
+ COMMAND ${CMAKE_OBJCOPY} -O binary ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.out "${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.bin"
+ COMMAND ${CMAKE_OBJCOPY} -O ihex ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.out "${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.hex"
+ COMMAND ${CMAKE_SOURCE_DIR}/tools/mcuboot/imgtool.py create --align 4 --version 1.0.0 --header-size 32 --slot-size 475136 --pad-header ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}.bin ${IMAGE_MCUBOOT_RECOVERYLOADER_FILE_NAME}
+ COMMAND python ${CMAKE_SOURCE_DIR}/tools/bin2c.py ${IMAGE_MCUBOOT_RECOVERYLOADER_FILE_NAME} recoveryLoaderImage > recoveryLoaderImage.h
+ COMMENT "post build steps for ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}"
+ )
+
+if(BUILD_DFU)
+ add_custom_command(TARGET ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME}
+ POST_BUILD
+ COMMAND adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application ${IMAGE_MCUBOOT_RECOVERYLOADER_FILE_NAME} ${DFU_MCUBOOT_RECOVERYLOADER_FILE_NAME}
+ COMMENT "post build (DFU) steps for ${EXECUTABLE_MCUBOOT_RECOVERYLOADER_FILE_NAME}"
+ )
+endif()
+
+
# FLASH
if (USE_JLINK)
add_custom_target(FLASH_ERASE
diff --git a/src/components/rle/RleDecoder.cpp b/src/components/rle/RleDecoder.cpp
new file mode 100644
index 00000000..19a90fda
--- /dev/null
+++ b/src/components/rle/RleDecoder.cpp
@@ -0,0 +1,39 @@
+#include "RleDecoder.h"
+
+using namespace Pinetime::Tools;
+
+RleDecoder::RleDecoder(const uint8_t *buffer, size_t size) : buffer{buffer}, size{size} {
+
+}
+
+RleDecoder::RleDecoder(const uint8_t *buffer, size_t size, uint16_t foregroundColor, uint16_t backgroundColor) : RleDecoder{buffer, size} {
+ this->foregroundColor = foregroundColor;
+ this->backgroundColor = backgroundColor;
+}
+
+
+void RleDecoder::DecodeNext(uint8_t *output, size_t maxBytes) {
+ for (;encodedBufferIndex> 8;
+ output[bp + 1] = color & 0xff;
+ bp += 2;
+ rl -= 1;
+ processedCount++;
+
+ if (bp >= maxBytes) {
+ bp = 0;
+ y += 1;
+ return;
+ }
+ }
+ processedCount = 0;
+
+ if (color == backgroundColor)
+ color = foregroundColor;
+ else
+ color = backgroundColor;
+ }
+}
+
diff --git a/src/components/rle/RleDecoder.h b/src/components/rle/RleDecoder.h
new file mode 100644
index 00000000..0f607fb8
--- /dev/null
+++ b/src/components/rle/RleDecoder.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include
+#include
+
+namespace Pinetime {
+ namespace Tools {
+ /* 1-bit RLE decoder. Provide the encoded buffer to the constructor and then call DecodeNext() by
+ * specifying the output (decoded) buffer and the maximum number of bytes this buffer can handle.
+ *
+ * Code from https://github.com/daniel-thompson/wasp-bootloader by Daniel Thompson released under the MIT license.
+ */
+ class RleDecoder {
+ public:
+ RleDecoder(const uint8_t* buffer, size_t size);
+ RleDecoder(const uint8_t* buffer, size_t size, uint16_t foregroundColor, uint16_t backgroundColor);
+
+ void DecodeNext(uint8_t* output, size_t maxBytes);
+
+ private:
+ const uint8_t* buffer;
+ size_t size;
+
+ int encodedBufferIndex = 0;
+ int y = 0;
+ uint16_t bp = 0;
+ uint16_t foregroundColor = 0xffff;
+ uint16_t backgroundColor = 0;
+ uint16_t color = backgroundColor;
+ int processedCount = 0;
+ };
+ }
+}
diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp
index 6e3fd0bf..4e9042ab 100644
--- a/src/displayapp/DisplayApp.cpp
+++ b/src/displayapp/DisplayApp.cpp
@@ -25,6 +25,7 @@
#include "systemtask/SystemTask.h"
using namespace Pinetime::Applications;
+using namespace Pinetime::Applications::Display;
DisplayApp::DisplayApp(Drivers::St7789 &lcd, Components::LittleVgl &lvgl, Drivers::Cst816S &touchPanel,
Controllers::Battery &batteryController, Controllers::Ble &bleController,
@@ -220,7 +221,7 @@ void DisplayApp::IdleState() {
}
-void DisplayApp::PushMessage(DisplayApp::Messages msg) {
+void DisplayApp::PushMessage(Messages msg) {
BaseType_t xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(msgQueue, &msg, &xHigherPriorityTaskWoken);
diff --git a/src/displayapp/DisplayApp.h b/src/displayapp/DisplayApp.h
index c38404ba..9a3d1191 100644
--- a/src/displayapp/DisplayApp.h
+++ b/src/displayapp/DisplayApp.h
@@ -10,6 +10,7 @@
#include "components/brightness/BrightnessController.h"
#include "components/firmwarevalidator/FirmwareValidator.h"
#include "displayapp/screens/Screen.h"
+#include "Messages.h"
namespace Pinetime {
@@ -33,9 +34,6 @@ namespace Pinetime {
class DisplayApp {
public:
enum class States {Idle, Running};
- enum class Messages : uint8_t {GoToSleep, GoToRunning, UpdateDateTime, UpdateBleConnection, UpdateBatteryLevel, TouchEvent, ButtonPushed,
- NewNotification, BleFirmwareUpdateStarted };
-
enum class FullRefreshDirections { None, Up, Down };
enum class TouchModes { Gestures, Polling };
@@ -46,7 +44,7 @@ namespace Pinetime {
Pinetime::Controllers::NotificationManager& notificationManager,
Pinetime::Controllers::HeartRateController& heartRateController);
void Start();
- void PushMessage(Messages msg);
+ void PushMessage(Display::Messages msg);
void StartApp(Apps app);
diff --git a/src/displayapp/DisplayAppRecovery.cpp b/src/displayapp/DisplayAppRecovery.cpp
new file mode 100644
index 00000000..cccb72d3
--- /dev/null
+++ b/src/displayapp/DisplayAppRecovery.cpp
@@ -0,0 +1,110 @@
+#include "DisplayAppRecovery.h"
+#include "DisplayAppRecovery.h"
+#include
+#include
+#include
+#include
+#include "displayapp/icons/infinitime/infinitime-nb.c"
+
+using namespace Pinetime::Applications;
+
+DisplayApp::DisplayApp(Drivers::St7789 &lcd, Components::LittleVgl &lvgl, Drivers::Cst816S &touchPanel,
+ Controllers::Battery &batteryController, Controllers::Ble &bleController,
+ Controllers::DateTime &dateTimeController, Drivers::WatchdogView &watchdog,
+ System::SystemTask &systemTask,
+ Pinetime::Controllers::NotificationManager& notificationManager,
+ Pinetime::Controllers::HeartRateController& heartRateController):
+ lcd{lcd}, bleController{bleController} {
+ msgQueue = xQueueCreate(queueSize, itemSize);
+
+}
+
+void DisplayApp::Start() {
+ if (pdPASS != xTaskCreate(DisplayApp::Process, "displayapp", 512, this, 0, &taskHandle))
+ APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
+}
+
+void DisplayApp::Process(void *instance) {
+ auto *app = static_cast(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 (1) {
+ app->Refresh();
+ }
+}
+
+void DisplayApp::InitHw() {
+ DisplayLogo(colorWhite);
+}
+
+void DisplayApp::Refresh() {
+ Display::Messages msg;
+ if (xQueueReceive(msgQueue, &msg, 200)) {
+ switch (msg) {
+ case Display::Messages::UpdateBleConnection:
+ if (bleController.IsConnected())
+ DisplayLogo(colorBlue);
+ else
+ DisplayLogo(colorWhite);
+ break;
+ case Display::Messages::BleFirmwareUpdateStarted:
+ DisplayLogo(colorGreen);
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (bleController.IsFirmwareUpdating()) {
+ uint8_t percent = (static_cast(bleController.FirmwareUpdateCurrentBytes()) /
+ static_cast(bleController.FirmwareUpdateTotalBytes())) * 100.0f;
+ switch (bleController.State()) {
+ case Controllers::Ble::FirmwareUpdateStates::Running:
+ DisplayOtaProgress(percent, colorWhite);
+ break;
+ case Controllers::Ble::FirmwareUpdateStates::Validated:
+ DisplayOtaProgress(100, colorGreenSwapped);
+ break;
+ case Controllers::Ble::FirmwareUpdateStates::Error:
+ DisplayOtaProgress(100, colorRedSwapped);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+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.BeginDrawBuffer(0, i, displayWidth, 1);
+ lcd.NextDrawBuffer(reinterpret_cast(displayBuffer), displayWidth * bytesPerPixel);
+ }
+}
+
+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(percent) * 2.4f, static_cast(displayWidth));
+ lcd.BeginDrawBuffer(0, displayWidth - barHeight + i, barWidth, 1);
+ lcd.NextDrawBuffer(reinterpret_cast(displayBuffer), barWidth * bytesPerPixel);
+ }
+}
+
+void DisplayApp::PushMessage(Display::Messages msg) {
+ BaseType_t xHigherPriorityTaskWoken;
+ xHigherPriorityTaskWoken = pdFALSE;
+ xQueueSendFromISR(msgQueue, &msg, &xHigherPriorityTaskWoken);
+ if (xHigherPriorityTaskWoken) {
+ /* Actual macro used here is port specific. */
+ // TODO : should I do something here?
+ }
+}
\ No newline at end of file
diff --git a/src/displayapp/DisplayAppRecovery.h b/src/displayapp/DisplayAppRecovery.h
new file mode 100644
index 00000000..bcabea3a
--- /dev/null
+++ b/src/displayapp/DisplayAppRecovery.h
@@ -0,0 +1,71 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include "components/gfx/Gfx.h"
+#include "components/battery/BatteryController.h"
+#include "components/brightness/BrightnessController.h"
+#include "components/ble/BleController.h"
+#include "components/datetime/DateTimeController.h"
+#include "components/ble/NotificationManager.h"
+#include "components/firmwarevalidator/FirmwareValidator.h"
+#include "drivers/Cst816s.h"
+#include
+#include "displayapp/screens/Clock.h"
+#include
+#include "TouchEvents.h"
+#include "Apps.h"
+#include "Messages.h"
+#include "DummyLittleVgl.h"
+
+namespace Pinetime {
+ namespace System {
+ class SystemTask;
+ };
+ namespace Applications {
+ class DisplayApp {
+ public:
+ DisplayApp(Drivers::St7789 &lcd, Components::LittleVgl &lvgl, Drivers::Cst816S &,
+ Controllers::Battery &batteryController, Controllers::Ble &bleController,
+ Controllers::DateTime &dateTimeController, Drivers::WatchdogView &watchdog,
+ System::SystemTask &systemTask,
+ Pinetime::Controllers::NotificationManager& notificationManager,
+ Pinetime::Controllers::HeartRateController& heartRateController);
+ void Start();
+ void PushMessage(Pinetime::Applications::Display::Messages msg);
+
+ private:
+ TaskHandle_t taskHandle;
+ static void Process(void* instance);
+ void DisplayLogo(uint16_t color);
+ void DisplayOtaProgress(uint8_t percent, uint16_t color);
+ void InitHw();
+ void Refresh();
+ Pinetime::Drivers::St7789& lcd;
+ Controllers::Ble &bleController;
+
+ static constexpr uint8_t queueSize = 10;
+ static constexpr uint8_t itemSize = 1;
+ QueueHandle_t msgQueue;
+ static constexpr uint8_t displayWidth = 240;
+ static constexpr uint8_t displayHeight = 240;
+ static constexpr uint8_t bytesPerPixel = 2;
+
+ static constexpr uint16_t colorWhite = 0xFFFF;
+ static constexpr uint16_t colorGreen = 0x07E0;
+ static constexpr uint16_t colorGreenSwapped = 0xE007;
+ static constexpr uint16_t colorBlue = 0x0000ff;
+ static constexpr uint16_t colorRed = 0xff00;
+ static constexpr uint16_t colorRedSwapped = 0x00ff;
+ static constexpr uint16_t colorBlack = 0x0000;
+ uint8_t displayBuffer[displayWidth * bytesPerPixel];
+
+
+ };
+ }
+}
+
+
diff --git a/src/displayapp/DummyLittleVgl.h b/src/displayapp/DummyLittleVgl.h
new file mode 100644
index 00000000..1c60911c
--- /dev/null
+++ b/src/displayapp/DummyLittleVgl.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+namespace Pinetime {
+ namespace Components {
+ class LittleVgl {
+ public:
+ enum class FullRefreshDirections { None, Up, Down };
+ LittleVgl(Pinetime::Drivers::St7789& lcd, Pinetime::Drivers::Cst816S& touchPanel) {}
+
+ LittleVgl(const LittleVgl&) = delete;
+ LittleVgl& operator=(const LittleVgl&) = delete;
+ LittleVgl(LittleVgl&&) = delete;
+ LittleVgl& operator=(LittleVgl&&) = delete;
+
+ void FlushDisplay(const lv_area_t * area, lv_color_t * color_p) {}
+ bool GetTouchPadInfo(lv_indev_data_t *ptr) {return false;}
+ void SetFullRefresh(FullRefreshDirections direction) {}
+ void SetNewTapEvent(uint16_t x, uint16_t y) {}
+
+
+ };
+ }
+}
+
diff --git a/src/displayapp/Messages.h b/src/displayapp/Messages.h
new file mode 100644
index 00000000..f617774d
--- /dev/null
+++ b/src/displayapp/Messages.h
@@ -0,0 +1,11 @@
+#pragma once
+namespace Pinetime {
+ namespace Applications {
+ namespace Display {
+ enum class Messages : uint8_t {
+ GoToSleep, GoToRunning, UpdateDateTime, UpdateBleConnection, UpdateBatteryLevel, TouchEvent, ButtonPushed,
+ NewNotification, BleFirmwareUpdateStarted
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/displayapp/icons/infinitime/infinitime-nb.c b/src/displayapp/icons/infinitime/infinitime-nb.c
new file mode 100644
index 00000000..52f18541
--- /dev/null
+++ b/src/displayapp/icons/infinitime/infinitime-nb.c
@@ -0,0 +1,127 @@
+
+#include
+
+// 1-bit RLE, generated from ./infinitime-nb.png, 1445 bytes
+static const uint8_t infinitime_nb[] = {
+ 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0,
+ 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0,
+ 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0x66, 0x2, 0xed, 0x4, 0xec, 0x5,
+ 0xea, 0x7, 0xe8, 0x9, 0xe6, 0xa, 0xe5, 0xc, 0xe3, 0xe, 0xe1, 0x10,
+ 0xdf, 0x12, 0xde, 0x12, 0xdd, 0x14, 0xdb, 0x16, 0xd9, 0x18, 0xd7, 0x1a,
+ 0xd5, 0x1b, 0xd4, 0x1d, 0xd3, 0xd, 0x3, 0xe, 0xd1, 0xd, 0x5, 0xe,
+ 0xcf, 0xe, 0x5, 0xf, 0xcd, 0xf, 0x5, 0xf, 0xcc, 0x10, 0x5, 0x10,
+ 0xca, 0x11, 0x5, 0x11, 0xc8, 0x12, 0x5, 0x12, 0xc6, 0x13, 0x5, 0x13,
+ 0xc5, 0x13, 0x5, 0x13, 0xc4, 0x14, 0x5, 0x14, 0xc2, 0x15, 0x5, 0x15,
+ 0xc0, 0x17, 0x3, 0x17, 0xbe, 0x33, 0xbc, 0x34, 0xbb, 0x36, 0xba, 0x37,
+ 0xb8, 0x39, 0xb6, 0x3b, 0xb4, 0x3c, 0xb3, 0x3e, 0xb1, 0x40, 0xaf, 0x9,
+ 0x2, 0x2e, 0x1, 0x8, 0xad, 0x9, 0x4, 0x2c, 0x3, 0x8, 0xac, 0x8,
+ 0x6, 0x2a, 0x5, 0x7, 0xab, 0x9, 0x6, 0x29, 0x6, 0x8, 0xa9, 0xb,
+ 0x5, 0x29, 0x5, 0xa, 0xa7, 0xd, 0x3, 0x2b, 0x3, 0xc, 0xa5, 0x4c,
+ 0xa3, 0x4d, 0xa2, 0x4f, 0xa0, 0x51, 0x9f, 0x52, 0x9d, 0x54, 0x9b, 0x55,
+ 0x9a, 0x57, 0x98, 0x59, 0x96, 0x5b, 0x94, 0x5d, 0x93, 0x5d, 0x92, 0x5f,
+ 0x90, 0x61, 0x8e, 0x63, 0x8c, 0x65, 0x8a, 0x66, 0x89, 0x68, 0x87, 0x8,
+ 0x2, 0x59, 0x2, 0x5, 0x86, 0x7, 0x4, 0x57, 0x4, 0x5, 0x84, 0x8,
+ 0x5, 0x55, 0x6, 0x5, 0x82, 0x9, 0x6, 0x54, 0x6, 0x5, 0x81, 0xa,
+ 0x5, 0x55, 0x5, 0x7, 0x7f, 0xc, 0x4, 0x56, 0x3, 0x9, 0x7d, 0x74,
+ 0x7b, 0x76, 0x79, 0x77, 0x79, 0x78, 0x77, 0x7a, 0x75, 0x7c, 0x73, 0x7e,
+ 0x71, 0x7f, 0x70, 0x81, 0x6e, 0x83, 0x6c, 0x85, 0x6b, 0x86, 0x69, 0x87,
+ 0x68, 0x89, 0x66, 0x8b, 0x64, 0x8d, 0x62, 0x8f, 0x60, 0x90, 0x60, 0x91,
+ 0x5e, 0x93, 0x5c, 0x95, 0x5a, 0xe, 0x7, 0x71, 0x7, 0xa, 0x58, 0xd,
+ 0xb, 0x6d, 0xb, 0x8, 0x57, 0xe, 0xc, 0x6c, 0xc, 0x8, 0x55, 0xf,
+ 0xc, 0x6c, 0xb, 0xa, 0x53, 0x11, 0xa, 0x6d, 0xb, 0xb, 0x52, 0x9f,
+ 0x50, 0xa0, 0x4f, 0xa2, 0x4d, 0xa4, 0x4b, 0xa6, 0x49, 0xa8, 0x48, 0xa8,
+ 0xff, 0x0, 0xe3, 0x44, 0xad, 0x43, 0xae, 0x41, 0xb0, 0x40, 0xb1, 0x3e,
+ 0xb2, 0x3e, 0xb3, 0x3c, 0xb5, 0x3a, 0xb7, 0x39, 0xb8, 0x37, 0xb9, 0x36,
+ 0xbb, 0x35, 0xe, 0x1, 0x66, 0x1, 0x3c, 0x1, 0x9, 0x33, 0xe, 0x3,
+ 0x15, 0x5, 0xe, 0x4, 0x16, 0x15, 0xd, 0x3, 0x11, 0x5, 0xe, 0x4,
+ 0x12, 0x3, 0x9, 0x31, 0xf, 0x4, 0x14, 0x6, 0xd, 0x4, 0x16, 0x15,
+ 0xd, 0x4, 0x10, 0x5, 0xe, 0x4, 0x12, 0x4, 0x9, 0x30, 0xf, 0x4,
+ 0x14, 0x6, 0xd, 0x4, 0x16, 0x15, 0xd, 0x4, 0x10, 0x6, 0xd, 0x4,
+ 0x12, 0x4, 0x9, 0x2f, 0x10, 0x4, 0x14, 0x7, 0xc, 0x4, 0x16, 0x15,
+ 0xd, 0x4, 0x10, 0x6, 0xd, 0x4, 0x12, 0x4, 0xa, 0x2d, 0x11, 0x4,
+ 0x14, 0x7, 0xc, 0x4, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x7, 0xc, 0x4,
+ 0x12, 0x4, 0xb, 0x2c, 0x11, 0x4, 0x14, 0x8, 0xb, 0x4, 0x16, 0x4,
+ 0x1e, 0x4, 0x10, 0x7, 0xc, 0x4, 0x12, 0x4, 0xc, 0x2a, 0x12, 0x4,
+ 0x14, 0x8, 0xb, 0x4, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x8, 0xb, 0x4,
+ 0x12, 0x4, 0xd, 0x28, 0x13, 0x4, 0x14, 0x4, 0x1, 0x4, 0xa, 0x4,
+ 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4, 0x1, 0x3, 0xb, 0x4, 0x12, 0x4,
+ 0xd, 0x28, 0x13, 0x4, 0x14, 0x4, 0x1, 0x4, 0xa, 0x4, 0x16, 0x4,
+ 0x1e, 0x4, 0x10, 0x4, 0x1, 0x4, 0xa, 0x4, 0x12, 0x4, 0xe, 0x26,
+ 0x14, 0x4, 0x14, 0x4, 0x2, 0x4, 0x9, 0x4, 0x16, 0x4, 0x1e, 0x4,
+ 0x10, 0x4, 0x2, 0x3, 0xa, 0x4, 0x12, 0x4, 0xf, 0x24, 0x15, 0x4,
+ 0x14, 0x4, 0x2, 0x4, 0x9, 0x4, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4,
+ 0x2, 0x4, 0x9, 0x4, 0x12, 0x4, 0x10, 0x23, 0x15, 0x4, 0x14, 0x4,
+ 0x3, 0x4, 0x8, 0x4, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4, 0x2, 0x4,
+ 0x9, 0x4, 0x12, 0x4, 0x11, 0x21, 0x16, 0x4, 0x14, 0x4, 0x3, 0x4,
+ 0x8, 0x4, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4, 0x3, 0x4, 0x8, 0x4,
+ 0x12, 0x4, 0x11, 0x20, 0x17, 0x4, 0x14, 0x4, 0x4, 0x3, 0x8, 0x4,
+ 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4, 0x3, 0x4, 0x8, 0x4, 0x12, 0x4,
+ 0x12, 0x1f, 0x17, 0x4, 0x14, 0x4, 0x4, 0x4, 0x7, 0x4, 0x16, 0x4,
+ 0x1e, 0x4, 0x10, 0x4, 0x4, 0x3, 0x8, 0x4, 0x12, 0x4, 0x13, 0x1d,
+ 0x18, 0x4, 0x14, 0x4, 0x5, 0x3, 0x7, 0x4, 0x16, 0x13, 0xf, 0x4,
+ 0x10, 0x4, 0x4, 0x4, 0x7, 0x4, 0x12, 0x4, 0x14, 0x1b, 0x1a, 0x3,
+ 0x14, 0x4, 0x5, 0x4, 0x6, 0x4, 0x16, 0x13, 0x10, 0x3, 0x10, 0x4,
+ 0x5, 0x3, 0x7, 0x4, 0x13, 0x3, 0x15, 0x1a, 0x1b, 0x1, 0x15, 0x4,
+ 0x6, 0x3, 0x6, 0x4, 0x16, 0x13, 0x11, 0x1, 0x11, 0x4, 0x5, 0x4,
+ 0x6, 0x4, 0x14, 0x1, 0x16, 0x19, 0x32, 0x4, 0x6, 0x4, 0x5, 0x4,
+ 0x16, 0x13, 0x23, 0x4, 0x6, 0x3, 0x6, 0x4, 0x2c, 0x17, 0x33, 0x4,
+ 0x7, 0x3, 0x5, 0x4, 0x16, 0x4, 0x32, 0x4, 0x6, 0x4, 0x5, 0x4,
+ 0x2d, 0x16, 0x1d, 0x1, 0x15, 0x4, 0x7, 0x4, 0x4, 0x4, 0x16, 0x4,
+ 0x20, 0x1, 0x11, 0x4, 0x7, 0x3, 0x5, 0x4, 0x14, 0x1, 0x19, 0x14,
+ 0x1d, 0x3, 0x14, 0x4, 0x7, 0x4, 0x4, 0x4, 0x16, 0x4, 0x1f, 0x3,
+ 0x10, 0x4, 0x7, 0x4, 0x4, 0x4, 0x13, 0x3, 0x19, 0x12, 0x1d, 0x4,
+ 0x14, 0x4, 0x8, 0x4, 0x3, 0x4, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4,
+ 0x8, 0x3, 0x4, 0x4, 0x12, 0x4, 0x19, 0x12, 0x1d, 0x4, 0x14, 0x4,
+ 0x8, 0x4, 0x3, 0x4, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4, 0x8, 0x4,
+ 0x3, 0x4, 0x12, 0x4, 0x1a, 0x10, 0x1e, 0x4, 0x14, 0x4, 0x9, 0x3,
+ 0x3, 0x4, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4, 0x8, 0x4, 0x3, 0x4,
+ 0x12, 0x4, 0x1b, 0xe, 0x1f, 0x4, 0x14, 0x4, 0x9, 0x4, 0x2, 0x4,
+ 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4, 0x9, 0x4, 0x2, 0x4, 0x12, 0x4,
+ 0x1c, 0xd, 0x1f, 0x4, 0x14, 0x4, 0xa, 0x3, 0x2, 0x4, 0x16, 0x4,
+ 0x1e, 0x4, 0x10, 0x4, 0x9, 0x4, 0x2, 0x4, 0x12, 0x4, 0x1d, 0xb,
+ 0x20, 0x4, 0x14, 0x4, 0xa, 0x4, 0x1, 0x4, 0x16, 0x4, 0x1e, 0x4,
+ 0x10, 0x4, 0xa, 0x3, 0x2, 0x4, 0x12, 0x4, 0x1d, 0xb, 0x20, 0x4,
+ 0x14, 0x4, 0xb, 0x3, 0x1, 0x4, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4,
+ 0xa, 0x4, 0x1, 0x4, 0x12, 0x4, 0x1e, 0x9, 0x21, 0x4, 0x14, 0x4,
+ 0xb, 0x8, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4, 0xb, 0x3, 0x1, 0x4,
+ 0x12, 0x4, 0x1f, 0x7, 0x22, 0x4, 0x14, 0x4, 0xc, 0x7, 0x16, 0x4,
+ 0x1e, 0x4, 0x10, 0x4, 0xb, 0x8, 0x12, 0x4, 0x20, 0x6, 0x22, 0x4,
+ 0x14, 0x4, 0xc, 0x7, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4, 0xc, 0x7,
+ 0x12, 0x4, 0x21, 0x4, 0x23, 0x4, 0x14, 0x4, 0xd, 0x6, 0x16, 0x4,
+ 0x1e, 0x4, 0x10, 0x4, 0xc, 0x7, 0x12, 0x4, 0x21, 0x3, 0x24, 0x4,
+ 0x14, 0x4, 0xd, 0x6, 0x16, 0x4, 0x1e, 0x4, 0x10, 0x4, 0xd, 0x6,
+ 0x12, 0x4, 0x22, 0x2, 0x24, 0x4, 0x14, 0x4, 0xd, 0x6, 0x16, 0x4,
+ 0x1e, 0x4, 0x10, 0x4, 0xd, 0x6, 0x12, 0x4, 0x48, 0x3, 0x15, 0x4,
+ 0xe, 0x5, 0x16, 0x4, 0x1e, 0x3, 0x11, 0x4, 0xd, 0x6, 0x12, 0x3,
+ 0x4a, 0x1, 0x66, 0x1, 0x3c, 0x1, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0,
+ 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0x10, 0x11,
+ 0xf, 0x9, 0xf, 0x4, 0x9, 0x4, 0xd, 0xf, 0x8b, 0x11, 0xf, 0x9,
+ 0xf, 0x5, 0x7, 0x5, 0xd, 0xf, 0x8b, 0x11, 0xf, 0x9, 0xf, 0x5,
+ 0x7, 0x5, 0xd, 0xf, 0x92, 0x3, 0x19, 0x3, 0x12, 0x6, 0x5, 0x6,
+ 0xd, 0x3, 0x9e, 0x3, 0x19, 0x3, 0x12, 0x6, 0x5, 0x6, 0xd, 0x3,
+ 0x9e, 0x3, 0x19, 0x3, 0x12, 0x6, 0x5, 0x6, 0xd, 0x3, 0x9e, 0x3,
+ 0x19, 0x3, 0x12, 0x3, 0x1, 0x3, 0x3, 0x3, 0x1, 0x3, 0xd, 0x3,
+ 0x9e, 0x3, 0x19, 0x3, 0x12, 0x3, 0x2, 0x2, 0x3, 0x2, 0x2, 0x3,
+ 0xd, 0x3, 0x9e, 0x3, 0x19, 0x3, 0x12, 0x3, 0x2, 0x3, 0x1, 0x3,
+ 0x2, 0x3, 0xd, 0x3, 0x9e, 0x3, 0x19, 0x3, 0x12, 0x3, 0x2, 0x3,
+ 0x1, 0x3, 0x2, 0x3, 0xd, 0x3, 0x9e, 0x3, 0x19, 0x3, 0x12, 0x3,
+ 0x3, 0x5, 0x3, 0x3, 0xd, 0xd, 0x94, 0x3, 0x19, 0x3, 0x12, 0x3,
+ 0x3, 0x5, 0x3, 0x3, 0xd, 0xd, 0x94, 0x3, 0x19, 0x3, 0x12, 0x3,
+ 0x4, 0x3, 0x4, 0x3, 0xd, 0xd, 0x94, 0x3, 0x19, 0x3, 0x12, 0x3,
+ 0x4, 0x3, 0x4, 0x3, 0xd, 0x3, 0x9e, 0x3, 0x19, 0x3, 0x12, 0x3,
+ 0x5, 0x1, 0x5, 0x3, 0xd, 0x3, 0x9e, 0x3, 0x19, 0x3, 0x12, 0x3,
+ 0x5, 0x1, 0x5, 0x3, 0xd, 0x3, 0x9e, 0x3, 0x19, 0x3, 0x12, 0x3,
+ 0xb, 0x3, 0xd, 0x3, 0x9e, 0x3, 0x19, 0x3, 0x12, 0x3, 0xb, 0x3,
+ 0xd, 0x3, 0x9e, 0x3, 0x19, 0x3, 0x12, 0x3, 0xb, 0x3, 0xd, 0x3,
+ 0x9e, 0x3, 0x19, 0x3, 0x12, 0x3, 0xb, 0x3, 0xd, 0x3, 0x9e, 0x3,
+ 0x19, 0x3, 0x12, 0x3, 0xb, 0x3, 0xd, 0x3, 0x9e, 0x3, 0x19, 0x3,
+ 0x12, 0x3, 0xb, 0x3, 0xd, 0xf, 0x92, 0x3, 0x16, 0x9, 0xf, 0x3,
+ 0xb, 0x3, 0xd, 0xf, 0x92, 0x3, 0x16, 0x9, 0xf, 0x3, 0xb, 0x3,
+ 0xd, 0xf, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0,
+ 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0,
+ 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0,
+ 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0,
+ 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0,
+ 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0, 0xff, 0x0,
+ 0xff, 0x0, 0xff, 0x0, 0xec,
+};
diff --git a/src/displayapp/icons/infinitime/infinitime-nb.png b/src/displayapp/icons/infinitime/infinitime-nb.png
new file mode 100644
index 00000000..e425b060
Binary files /dev/null and b/src/displayapp/icons/infinitime/infinitime-nb.png differ
diff --git a/src/libs/lvgl b/src/libs/lvgl
index b89c30ea..69a50b97 160000
--- a/src/libs/lvgl
+++ b/src/libs/lvgl
@@ -1 +1 @@
-Subproject commit b89c30eafe1397a778ae5d65f108a0ef820de124
+Subproject commit 69a50b97dafffddd212e4b006776b473951be7a6
diff --git a/src/main.cpp b/src/main.cpp
index fe177d0d..03cb1687 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -32,8 +32,6 @@
#include "components/ble/NotificationManager.h"
#include "components/motor/MotorController.h"
#include "components/datetime/DateTimeController.h"
-#include "displayapp/DisplayApp.h"
-#include "displayapp/LittleVgl.h"
#include "drivers/Spi.h"
#include "drivers/SpiMaster.h"
#include "drivers/SpiNorFlash.h"
@@ -85,7 +83,18 @@ Pinetime::Drivers::TwiMaster twiMaster{Pinetime::Drivers::TwiMaster::Modules::TW
Pinetime::Drivers::TwiMaster::Parameters {
MaxTwiFrequencyWithoutHardwareBug, pinTwiSda, pinTwiScl}};
Pinetime::Drivers::Cst816S touchPanel {twiMaster, touchPanelTwiAddress};
+#ifdef PINETIME_IS_RECOVERY
+static constexpr bool isFactory = true;
+#include "displayapp/DummyLittleVgl.h"
+#include "displayapp/DisplayAppRecovery.h"
Pinetime::Components::LittleVgl lvgl {lcd, touchPanel};
+#else
+static constexpr bool isFactory = false;
+#include "displayapp/LittleVgl.h"
+#include "displayapp/DisplayApp.h"
+Pinetime::Components::LittleVgl lvgl {lcd, touchPanel};
+#endif
+
Pinetime::Drivers::Hrs3300 heartRateSensor {twiMaster, heartRateSensorTwiAddress};
@@ -114,7 +123,8 @@ void nrfx_gpiote_evt_handler(nrfx_gpiote_pin_t pin, nrf_gpiote_polarity_t action
extern "C" {
void vApplicationIdleHook(void) {
- lv_tick_inc(1);
+ if(!isFactory)
+ lv_tick_inc(1);
}
}
diff --git a/src/graphics.cpp b/src/recoveryLoader.cpp
similarity index 52%
rename from src/graphics.cpp
rename to src/recoveryLoader.cpp
index 288b5e9a..40cd66da 100644
--- a/src/graphics.cpp
+++ b/src/recoveryLoader.cpp
@@ -4,7 +4,6 @@
#include
#include
#include
-#include "bootloader/boot_graphics.h"
#include
#include
#include
@@ -14,6 +13,12 @@
#include
#include
#include
+#include
+#include "recoveryImage.h"
+
+#include "displayapp/icons/infinitime/infinitime-nb.c"
+#include "components/rle/RleDecoder.h"
+
#if NRF_LOG_ENABLED
#include "logging/NrfLogger.h"
@@ -30,14 +35,21 @@ static constexpr uint8_t pinSpiFlashCsn = 5;
static constexpr uint8_t pinLcdCsn = 25;
static constexpr uint8_t pinLcdDataCommand = 18;
+static constexpr uint8_t displayWidth = 240;
+static constexpr uint8_t displayHeight = 240;
+static constexpr uint8_t bytesPerPixel = 2;
+
+static constexpr uint16_t colorWhite = 0xFFFF;
+static constexpr uint16_t colorGreen = 0xE007;
+
Pinetime::Drivers::SpiMaster spi{Pinetime::Drivers::SpiMaster::SpiModule::SPI0, {
- Pinetime::Drivers::SpiMaster::BitOrder::Msb_Lsb,
- Pinetime::Drivers::SpiMaster::Modes::Mode3,
- Pinetime::Drivers::SpiMaster::Frequencies::Freq8Mhz,
- pinSpiSck,
- pinSpiMosi,
- pinSpiMiso
- }
+ Pinetime::Drivers::SpiMaster::BitOrder::Msb_Lsb,
+ Pinetime::Drivers::SpiMaster::Modes::Mode3,
+ Pinetime::Drivers::SpiMaster::Frequencies::Freq8Mhz,
+ pinSpiSck,
+ pinSpiMosi,
+ pinSpiMiso
+}
};
Pinetime::Drivers::Spi flashSpi{spi, pinSpiFlashCsn};
Pinetime::Drivers::SpiNorFlash spiNorFlash{flashSpi};
@@ -48,6 +60,10 @@ Pinetime::Drivers::St7789 lcd {lcdSpi, pinLcdDataCommand};
Pinetime::Components::Gfx gfx{lcd};
Pinetime::Controllers::BrightnessController brightnessController;
+void DisplayProgressBar(uint8_t percent, uint16_t color);
+
+void DisplayLogo();
+
extern "C" {
void vApplicationIdleHook(void) {
@@ -70,10 +86,13 @@ void SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQHandler(void) {
}
}
-void Process(void* instance) {
- // Wait before erasing the memory to let the time to the SWD debugger to flash a new firmware before running this one.
- vTaskDelay(5000);
+void RefreshWatchdog() {
+ NRF_WDT->RR[0] = WDT_RR_RR_Reload;
+}
+uint8_t displayBuffer[displayWidth * bytesPerPixel];
+void Process(void* instance) {
+ RefreshWatchdog();
APP_GPIOTE_INIT(2);
NRF_LOG_INFO("Init...");
@@ -83,45 +102,57 @@ void Process(void* instance) {
brightnessController.Init();
lcd.Init();
gfx.Init();
- NRF_LOG_INFO("Init Done!")
+
+ NRF_LOG_INFO("Display logo")
+ DisplayLogo();
NRF_LOG_INFO("Erasing...");
- for (uint32_t erased = 0; erased < graphicSize; erased += 0x1000) {
+ for (uint32_t erased = 0; erased < sizeof(recoveryImage); erased += 0x1000) {
spiNorFlash.SectorErase(erased);
+ RefreshWatchdog();
}
- NRF_LOG_INFO("Erase done!");
- NRF_LOG_INFO("Writing graphic...");
+ NRF_LOG_INFO("Writing factory image...");
static constexpr uint32_t memoryChunkSize = 200;
uint8_t writeBuffer[memoryChunkSize];
- for(int offset = 0; offset < 115200; offset+=memoryChunkSize) {
- std::memcpy(writeBuffer, &graphicBuffer[offset], memoryChunkSize);
+ for(size_t offset = 0; offset < sizeof(recoveryImage); offset+=memoryChunkSize) {
+ std::memcpy(writeBuffer, &recoveryImage[offset], memoryChunkSize);
spiNorFlash.Write(offset, writeBuffer, memoryChunkSize);
+ DisplayProgressBar((static_cast(offset) / static_cast(sizeof(recoveryImage))) * 100.0f, colorWhite);
+ RefreshWatchdog();
}
- NRF_LOG_INFO("Writing graphic done!");
-
- NRF_LOG_INFO("Read memory and display the graphic...");
- static constexpr uint32_t screenWidth = 240;
- static constexpr uint32_t screenWidthInBytes = screenWidth*2; // LCD display 16bits color (1 pixel = 2 bytes)
- uint16_t displayLineBuffer[screenWidth];
- for(uint32_t line = 0; line < screenWidth; line++) {
- spiNorFlash.Read(line*screenWidthInBytes, reinterpret_cast(displayLineBuffer), screenWidth);
- spiNorFlash.Read((line*screenWidthInBytes)+screenWidth, reinterpret_cast(displayLineBuffer) + screenWidth, screenWidth);
- for(uint32_t col = 0; col < screenWidth; col++) {
- gfx.pixel_draw(col, line, displayLineBuffer[col]);
- }
- }
-
- NRF_LOG_INFO("Done!");
+ NRF_LOG_INFO("Writing factory image done!");
+ DisplayProgressBar(100.0f, colorGreen);
while(1) {
asm("nop" );
}
}
+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.BeginDrawBuffer(0, i, displayWidth, 1);
+ lcd.NextDrawBuffer(reinterpret_cast(displayBuffer), displayWidth * bytesPerPixel);
+ }
+}
+
+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(percent) * 2.4f, static_cast(displayWidth));
+ lcd.BeginDrawBuffer(0, displayWidth - barHeight + i, barWidth, 1);
+ lcd.NextDrawBuffer(reinterpret_cast(displayBuffer), barWidth * bytesPerPixel);
+ }
+}
+
int main(void) {
TaskHandle_t taskHandle;
-
+ RefreshWatchdog();
logger.Init();
nrf_drv_clock_init();
diff --git a/src/systemtask/SystemTask.cpp b/src/systemtask/SystemTask.cpp
index 6e6360a4..a4f2b14a 100644
--- a/src/systemtask/SystemTask.cpp
+++ b/src/systemtask/SystemTask.cpp
@@ -14,7 +14,6 @@
#include "BootloaderVersion.h"
#include "components/ble/BleController.h"
-#include "displayapp/LittleVgl.h"
#include "drivers/Cst816s.h"
#include "drivers/St7789.h"
#include "drivers/InternalFlash.h"
@@ -74,6 +73,7 @@ void SystemTask::Work() {
spiNorFlash.Wakeup();
nimbleController.Init();
nimbleController.StartAdvertising();
+ brightnessController.Init();
lcd.Init();
twiMaster.Init();
@@ -88,7 +88,7 @@ void SystemTask::Work() {
displayApp->Start();
batteryController.Update();
- displayApp->PushMessage(Pinetime::Applications::DisplayApp::Messages::UpdateBatteryLevel);
+ displayApp->PushMessage(Pinetime::Applications::Display::Messages::UpdateBatteryLevel);
heartRateSensor.Init();
heartRateSensor.Disable();
@@ -141,8 +141,8 @@ void SystemTask::Work() {
touchPanel.Wakeup();
lcd.Wakeup();
- displayApp->PushMessage(Applications::DisplayApp::Messages::GoToRunning);
- displayApp->PushMessage(Applications::DisplayApp::Messages::UpdateBatteryLevel);
+ displayApp->PushMessage(Pinetime::Applications::Display::Messages::GoToRunning);
+ displayApp->PushMessage(Pinetime::Applications::Display::Messages::UpdateBatteryLevel);
heartRateApp->PushMessage(Pinetime::Applications::HeartRateTask::Messages::WakeUp);
isSleeping = false;
@@ -152,17 +152,17 @@ void SystemTask::Work() {
isGoingToSleep = true;
NRF_LOG_INFO("[systemtask] Going to sleep");
xTimerStop(idleTimer, 0);
- displayApp->PushMessage(Pinetime::Applications::DisplayApp::Messages::GoToSleep);
+ displayApp->PushMessage(Pinetime::Applications::Display::Messages::GoToSleep);
heartRateApp->PushMessage(Pinetime::Applications::HeartRateTask::Messages::GoToSleep);
break;
case Messages::OnNewTime:
ReloadIdleTimer();
- displayApp->PushMessage(Pinetime::Applications::DisplayApp::Messages::UpdateDateTime);
+ displayApp->PushMessage(Pinetime::Applications::Display::Messages::UpdateDateTime);
break;
case Messages::OnNewNotification:
if(isSleeping && !isWakingUp) GoToRunning();
if(notificationManager.IsVibrationEnabled()) motorController.SetDuration(35);
- displayApp->PushMessage(Pinetime::Applications::DisplayApp::Messages::NewNotification);
+ displayApp->PushMessage(Pinetime::Applications::Display::Messages::NewNotification);
break;
case Messages::BleConnected:
ReloadIdleTimer();
@@ -172,7 +172,7 @@ void SystemTask::Work() {
case Messages::BleFirmwareUpdateStarted:
doNotGoToSleep = true;
if(isSleeping && !isWakingUp) GoToRunning();
- displayApp->PushMessage(Pinetime::Applications::DisplayApp::Messages::BleFirmwareUpdateStarted);
+ displayApp->PushMessage(Pinetime::Applications::Display::Messages::BleFirmwareUpdateStarted);
break;
case Messages::BleFirmwareUpdateFinished:
doNotGoToSleep = false;
@@ -230,7 +230,7 @@ void SystemTask::OnButtonPushed() {
if(!isSleeping) {
NRF_LOG_INFO("[systemtask] Button pushed");
PushMessage(Messages::OnButtonEvent);
- displayApp->PushMessage(Pinetime::Applications::DisplayApp::Messages::ButtonPushed);
+ displayApp->PushMessage(Pinetime::Applications::Display::Messages::ButtonPushed);
}
else {
if(!isWakingUp) {
@@ -250,7 +250,7 @@ void SystemTask::OnTouchEvent() {
NRF_LOG_INFO("[systemtask] Touch event");
if(!isSleeping) {
PushMessage(Messages::OnTouchEvent);
- displayApp->PushMessage(Pinetime::Applications::DisplayApp::Messages::TouchEvent);
+ displayApp->PushMessage(Pinetime::Applications::Display::Messages::TouchEvent);
}
}
diff --git a/src/systemtask/SystemTask.h b/src/systemtask/SystemTask.h
index c650d085..eadbc72d 100644
--- a/src/systemtask/SystemTask.h
+++ b/src/systemtask/SystemTask.h
@@ -13,7 +13,14 @@
#include "components/ble/NimbleController.h"
#include "components/ble/NotificationManager.h"
#include "components/motor/MotorController.h"
+#ifdef PINETIME_IS_RECOVERY
+#include "displayapp/DisplayAppRecovery.h"
+#include "displayapp/DummyLittleVgl.h"
+#else
#include "displayapp/DisplayApp.h"
+#include "displayapp/LittleVgl.h"
+#endif
+
#include "drivers/Watchdog.h"
namespace Pinetime {
@@ -78,6 +85,7 @@ namespace Pinetime {
Pinetime::Controllers::MotorController& motorController;
Pinetime::Drivers::Hrs3300& heartRateSensor;
Pinetime::Controllers::NimbleController nimbleController;
+ Controllers::BrightnessController brightnessController;
static constexpr uint8_t pinSpiSck = 2;
static constexpr uint8_t pinSpiMosi = 3;
diff --git a/tools/bin2c.py b/tools/bin2c.py
new file mode 100644
index 00000000..1d66656a
--- /dev/null
+++ b/tools/bin2c.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+#-*- coding: utf-8 -*-
+"""
+ bin2c
+ ~~~~~
+
+ Simple tool for creating C array from a binary file.
+
+ :copyright: (c) 2016 by Dmitry Alimov.
+ :license: The MIT License (MIT), see LICENSE for more details.
+"""
+
+import argparse
+import os
+import re
+import sys
+
+PY3 = sys.version_info[0] == 3
+
+
+def bin2c(filename, varname='data', linesize=80, indent=4):
+ """ Read binary data from file and return as a C array
+
+ :param filename: a filename of a file to read.
+ :param varname: a C array variable name.
+ :param linesize: a size of a line (min value is 40).
+ :param indent: an indent (number of spaces) that prepend each line.
+ """
+ if not os.path.isfile(filename):
+ print('File "%s" is not found!' % filename)
+ return ''
+ if not re.match('[a-zA-Z_][a-zA-Z0-9_]*', varname):
+ print('Invalid variable name "%s"' % varname)
+ return
+ with open(filename, 'rb') as in_file:
+ data = in_file.read()
+ # limit the line length
+ if linesize < 40:
+ linesize = 40
+ byte_len = 6 # '0x00, '
+ out = 'const char %s[%d] = {\n' % (varname, len(data))
+ line = ''
+ for byte in data:
+ line += '0x%02x, ' % (byte if PY3 else ord(byte))
+ if len(line) + indent + byte_len >= linesize:
+ out += ' ' * indent + line + '\n'
+ line = ''
+ # add the last line
+ if len(line) + indent + byte_len < linesize:
+ out += ' ' * indent + line + '\n'
+ # strip the last comma
+ out = out.rstrip(', \n') + '\n'
+ out += '};'
+ return out
+
+
+def main():
+ """ Main func """
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ 'filename', help='filename to convert to C array')
+ parser.add_argument(
+ 'varname', nargs='?', help='variable name', default='data')
+ parser.add_argument(
+ 'linesize', nargs='?', help='line length', default=80, type=int)
+ parser.add_argument(
+ 'indent', nargs='?', help='indent size', default=4, type=int)
+ args = parser.parse_args()
+ # print out the data
+ print(bin2c(args.filename, args.varname, args.linesize, args.indent))
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/tools/mcuboot/README b/tools/mcuboot/README
new file mode 100644
index 00000000..feb5d2f9
--- /dev/null
+++ b/tools/mcuboot/README
@@ -0,0 +1 @@
+This whole folder comes from MCUBoot source files (commit 9015a5d404c2c688166cab81067be53c860d98f4).
\ No newline at end of file
diff --git a/tools/mcuboot/assemble.py b/tools/mcuboot/assemble.py
new file mode 100644
index 00000000..f2ce4a1b
--- /dev/null
+++ b/tools/mcuboot/assemble.py
@@ -0,0 +1,131 @@
+#! /usr/bin/env python3
+#
+# Copyright 2017 Linaro Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Assemble multiple images into a single image that can be flashed on the device.
+"""
+
+import argparse
+import errno
+import io
+import re
+import os.path
+import sys
+
+ZEPHYR_BASE = os.getenv("ZEPHYR_BASE")
+if not ZEPHYR_BASE:
+ sys.exit("$ZEPHYR_BASE environment variable undefined")
+
+sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts", "dts"))
+import edtlib
+
+def same_keys(a, b):
+ """Determine if the dicts a and b have the same keys in them"""
+ for ak in a.keys():
+ if ak not in b:
+ return False
+ for bk in b.keys():
+ if bk not in a:
+ return False
+ return True
+
+offset_re = re.compile(r"^#define DT_FLASH_AREA_([0-9A-Z_]+)_OFFSET(_0)?\s+(0x[0-9a-fA-F]+|[0-9]+)$")
+size_re = re.compile(r"^#define DT_FLASH_AREA_([0-9A-Z_]+)_SIZE(_0)?\s+(0x[0-9a-fA-F]+|[0-9]+)$")
+
+class Assembly():
+ def __init__(self, output, bootdir, edt):
+ self.find_slots(edt)
+ try:
+ os.unlink(output)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ self.output = output
+
+ def find_slots(self, edt):
+ offsets = {}
+ sizes = {}
+
+ part_nodes = edt.compat2nodes["fixed-partitions"]
+ for node in part_nodes:
+ for child in node.children.values():
+ if "label" in child.props:
+ label = child.props["label"].val
+ offsets[label] = child.regs[0].addr
+ sizes[label] = child.regs[0].size
+
+ if not same_keys(offsets, sizes):
+ raise Exception("Inconsistent data in devicetree.h")
+
+ # We care about the mcuboot, image-0, and image-1 partitions.
+ if 'mcuboot' not in offsets:
+ raise Exception("Board partition table does not have mcuboot partition")
+
+ if 'image-0' not in offsets:
+ raise Exception("Board partition table does not have image-0 partition")
+
+ if 'image-1' not in offsets:
+ raise Exception("Board partition table does not have image-1 partition")
+
+ self.offsets = offsets
+ self.sizes = sizes
+
+ def add_image(self, source, partition):
+ with open(self.output, 'ab') as ofd:
+ pos = ofd.tell()
+ print("partition {}, pos={}, offset={}".format(partition, pos, self.offsets[partition]))
+ if pos > self.offsets[partition]:
+ raise Exception("Partitions not in order, unsupported")
+ if pos < self.offsets[partition]:
+ buf = b'\xFF' * (self.offsets[partition] - pos)
+ ofd.write(buf)
+ with open(source, 'rb') as rfd:
+ ibuf = rfd.read()
+ if len(ibuf) > self.sizes[partition]:
+ raise Exception("Image {} is too large for partition".format(source))
+ ofd.write(ibuf)
+
+def main():
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument('-b', '--bootdir', required=True,
+ help='Directory of built bootloader')
+ parser.add_argument('-p', '--primary', required=True,
+ help='Signed image file for primary image')
+ parser.add_argument('-s', '--secondary',
+ help='Signed image file for secondary image')
+ parser.add_argument('-o', '--output', required=True,
+ help='Filename to write full image to')
+
+ args = parser.parse_args()
+
+ # Extract board name from path
+ board = os.path.split(os.path.split(args.bootdir)[0])[1]
+
+ dts_path = os.path.join(args.bootdir, "zephyr", board + ".dts.pre.tmp")
+
+ edt = edtlib.EDT(dts_path, [os.path.join(ZEPHYR_BASE, "dts", "bindings")],
+ warn_reg_unit_address_mismatch=False)
+
+ output = Assembly(args.output, args.bootdir, edt)
+
+ output.add_image(os.path.join(args.bootdir, 'zephyr', 'zephyr.bin'), 'mcuboot')
+ output.add_image(args.primary, "image-0")
+ if args.secondary is not None:
+ output.add_image(args.secondary, "image-1")
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/mcuboot/flash.sh b/tools/mcuboot/flash.sh
new file mode 100644
index 00000000..a2c58c75
--- /dev/null
+++ b/tools/mcuboot/flash.sh
@@ -0,0 +1,18 @@
+#! /bin/bash
+
+source $(dirname $0)/../target.sh
+
+lscript=/tmp/flash$$.jlink
+
+cat >$lscript < $gscript < {};
+let
+ # Nixpkgs has fairly recent versions of the dependencies, so we can
+ # rely on them without having to build our own derivations.
+ imgtoolPythonEnv = python37.withPackages (
+ _: [
+ python37.pkgs.click
+ python37.pkgs.cryptography
+ python37.pkgs.intelhex
+ python37.pkgs.setuptools
+ python37.pkgs.cbor
+ ]
+ );
+in
+myEnvFun {
+ name = "imgtool";
+
+ buildInputs = [ imgtoolPythonEnv ];
+}
diff --git a/tools/mcuboot/imgtool.py b/tools/mcuboot/imgtool.py
new file mode 100755
index 00000000..78614745
--- /dev/null
+++ b/tools/mcuboot/imgtool.py
@@ -0,0 +1,20 @@
+#! /usr/bin/env python3
+#
+# Copyright 2017 Linaro Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from imgtool import main
+
+if __name__ == '__main__':
+ main.imgtool()
diff --git a/tools/mcuboot/imgtool/__init__.py b/tools/mcuboot/imgtool/__init__.py
new file mode 100644
index 00000000..c0c3ef21
--- /dev/null
+++ b/tools/mcuboot/imgtool/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2017 Linaro Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+imgtool_version = "1.6.0rc2"
diff --git a/tools/mcuboot/imgtool/boot_record.py b/tools/mcuboot/imgtool/boot_record.py
new file mode 100644
index 00000000..4112b225
--- /dev/null
+++ b/tools/mcuboot/imgtool/boot_record.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2019, Arm Limited.
+# Copyright (c) 2020, Linaro Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from enum import Enum
+import cbor
+
+
+class SwComponent(int, Enum):
+ """
+ Software component property IDs specified by
+ Arm's PSA Attestation API 1.0 document.
+ """
+ TYPE = 1
+ MEASUREMENT_VALUE = 2
+ VERSION = 4
+ SIGNER_ID = 5
+ MEASUREMENT_DESCRIPTION = 6
+
+
+def create_sw_component_data(sw_type, sw_version, sw_measurement_description,
+ sw_measurement_value, sw_signer_id):
+
+ # List of software component properties (Key ID + value)
+ properties = {
+ SwComponent.TYPE: sw_type,
+ SwComponent.VERSION: sw_version,
+ SwComponent.SIGNER_ID: sw_signer_id,
+ SwComponent.MEASUREMENT_DESCRIPTION: sw_measurement_description,
+ }
+
+ # Note: The measurement value must be the last item of the property
+ # list because later it will be modified by the bootloader.
+ properties[SwComponent.MEASUREMENT_VALUE] = sw_measurement_value
+
+ return cbor.dumps(properties)
diff --git a/tools/mcuboot/imgtool/image.py b/tools/mcuboot/imgtool/image.py
new file mode 100644
index 00000000..291134d7
--- /dev/null
+++ b/tools/mcuboot/imgtool/image.py
@@ -0,0 +1,552 @@
+# Copyright 2018 Nordic Semiconductor ASA
+# Copyright 2017 Linaro Limited
+# Copyright 2019-2020 Arm Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Image signing and management.
+"""
+
+from . import version as versmod
+from .boot_record import create_sw_component_data
+import click
+from enum import Enum
+from intelhex import IntelHex
+import hashlib
+import struct
+import os.path
+from .keys import rsa, ecdsa, x25519
+from cryptography.hazmat.primitives.asymmetric import ec, padding
+from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes, hmac
+from cryptography.exceptions import InvalidSignature
+
+IMAGE_MAGIC = 0x96f3b83d
+IMAGE_HEADER_SIZE = 32
+BIN_EXT = "bin"
+INTEL_HEX_EXT = "hex"
+DEFAULT_MAX_SECTORS = 128
+MAX_ALIGN = 8
+DEP_IMAGES_KEY = "images"
+DEP_VERSIONS_KEY = "versions"
+MAX_SW_TYPE_LENGTH = 12 # Bytes
+
+# Image header flags.
+IMAGE_F = {
+ 'PIC': 0x0000001,
+ 'NON_BOOTABLE': 0x0000010,
+ 'RAM_LOAD': 0x0000020,
+ 'ENCRYPTED': 0x0000004,
+}
+
+TLV_VALUES = {
+ 'KEYHASH': 0x01,
+ 'PUBKEY': 0x02,
+ 'SHA256': 0x10,
+ 'RSA2048': 0x20,
+ 'ECDSA224': 0x21,
+ 'ECDSA256': 0x22,
+ 'RSA3072': 0x23,
+ 'ED25519': 0x24,
+ 'ENCRSA2048': 0x30,
+ 'ENCKW128': 0x31,
+ 'ENCEC256': 0x32,
+ 'ENCX25519': 0x33,
+ 'DEPENDENCY': 0x40,
+ 'SEC_CNT': 0x50,
+ 'BOOT_RECORD': 0x60,
+}
+
+TLV_SIZE = 4
+TLV_INFO_SIZE = 4
+TLV_INFO_MAGIC = 0x6907
+TLV_PROT_INFO_MAGIC = 0x6908
+
+boot_magic = bytes([
+ 0x77, 0xc2, 0x95, 0xf3,
+ 0x60, 0xd2, 0xef, 0x7f,
+ 0x35, 0x52, 0x50, 0x0f,
+ 0x2c, 0xb6, 0x79, 0x80, ])
+
+STRUCT_ENDIAN_DICT = {
+ 'little': '<',
+ 'big': '>'
+}
+
+VerifyResult = Enum('VerifyResult',
+ """
+ OK INVALID_MAGIC INVALID_TLV_INFO_MAGIC INVALID_HASH
+ INVALID_SIGNATURE
+ """)
+
+
+class TLV():
+ def __init__(self, endian, magic=TLV_INFO_MAGIC):
+ self.magic = magic
+ self.buf = bytearray()
+ self.endian = endian
+
+ def __len__(self):
+ return TLV_INFO_SIZE + len(self.buf)
+
+ def add(self, kind, payload):
+ """
+ Add a TLV record. Kind should be a string found in TLV_VALUES above.
+ """
+ e = STRUCT_ENDIAN_DICT[self.endian]
+ buf = struct.pack(e + 'BBH', TLV_VALUES[kind], 0, len(payload))
+ self.buf += buf
+ self.buf += payload
+
+ def get(self):
+ if len(self.buf) == 0:
+ return bytes()
+ e = STRUCT_ENDIAN_DICT[self.endian]
+ header = struct.pack(e + 'HH', self.magic, len(self))
+ return header + bytes(self.buf)
+
+
+class Image():
+
+ def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE,
+ pad_header=False, pad=False, confirm=False, align=1,
+ slot_size=0, max_sectors=DEFAULT_MAX_SECTORS,
+ overwrite_only=False, endian="little", load_addr=0,
+ erased_val=None, save_enctlv=False, security_counter=None):
+ self.version = version or versmod.decode_version("0")
+ self.header_size = header_size
+ self.pad_header = pad_header
+ self.pad = pad
+ self.confirm = confirm
+ self.align = align
+ self.slot_size = slot_size
+ self.max_sectors = max_sectors
+ self.overwrite_only = overwrite_only
+ self.endian = endian
+ self.base_addr = None
+ self.load_addr = 0 if load_addr is None else load_addr
+ self.erased_val = 0xff if erased_val is None else int(erased_val, 0)
+ self.payload = []
+ self.enckey = None
+ self.save_enctlv = save_enctlv
+ self.enctlv_len = 0
+
+ if security_counter == 'auto':
+ # Security counter has not been explicitly provided,
+ # generate it from the version number
+ self.security_counter = ((self.version.major << 24)
+ + (self.version.minor << 16)
+ + self.version.revision)
+ else:
+ self.security_counter = security_counter
+
+ def __repr__(self):
+ return "".format(
+ self.version,
+ self.header_size,
+ self.security_counter,
+ self.base_addr if self.base_addr is not None else "N/A",
+ self.load_addr,
+ self.align,
+ self.slot_size,
+ self.max_sectors,
+ self.overwrite_only,
+ self.endian,
+ self.__class__.__name__,
+ len(self.payload))
+
+ def load(self, path):
+ """Load an image from a given file"""
+ ext = os.path.splitext(path)[1][1:].lower()
+ try:
+ if ext == INTEL_HEX_EXT:
+ ih = IntelHex(path)
+ self.payload = ih.tobinarray()
+ self.base_addr = ih.minaddr()
+ else:
+ with open(path, 'rb') as f:
+ self.payload = f.read()
+ except FileNotFoundError:
+ raise click.UsageError("Input file not found")
+
+ # Add the image header if needed.
+ if self.pad_header and self.header_size > 0:
+ if self.base_addr:
+ # Adjust base_addr for new header
+ self.base_addr -= self.header_size
+ self.payload = bytes([self.erased_val] * self.header_size) + \
+ self.payload
+
+ self.check_header()
+
+ def save(self, path, hex_addr=None):
+ """Save an image from a given file"""
+ ext = os.path.splitext(path)[1][1:].lower()
+ if ext == INTEL_HEX_EXT:
+ # input was in binary format, but HEX needs to know the base addr
+ if self.base_addr is None and hex_addr is None:
+ raise click.UsageError("No address exists in input file "
+ "neither was it provided by user")
+ h = IntelHex()
+ if hex_addr is not None:
+ self.base_addr = hex_addr
+ h.frombytes(bytes=self.payload, offset=self.base_addr)
+ if self.pad:
+ trailer_size = self._trailer_size(self.align, self.max_sectors,
+ self.overwrite_only,
+ self.enckey,
+ self.save_enctlv,
+ self.enctlv_len)
+ trailer_addr = (self.base_addr + self.slot_size) - trailer_size
+ padding = bytes([self.erased_val] *
+ (trailer_size - len(boot_magic))) + boot_magic
+ h.puts(trailer_addr, padding)
+ h.tofile(path, 'hex')
+ else:
+ if self.pad:
+ self.pad_to(self.slot_size)
+ with open(path, 'wb') as f:
+ f.write(self.payload)
+
+ def check_header(self):
+ if self.header_size > 0 and not self.pad_header:
+ if any(v != 0 for v in self.payload[0:self.header_size]):
+ raise click.UsageError("Header padding was not requested and "
+ "image does not start with zeros")
+
+ def check_trailer(self):
+ if self.slot_size > 0:
+ tsize = self._trailer_size(self.align, self.max_sectors,
+ self.overwrite_only, self.enckey,
+ self.save_enctlv, self.enctlv_len)
+ padding = self.slot_size - (len(self.payload) + tsize)
+ if padding < 0:
+ msg = "Image size (0x{:x}) + trailer (0x{:x}) exceeds " \
+ "requested size 0x{:x}".format(
+ len(self.payload), tsize, self.slot_size)
+ raise click.UsageError(msg)
+
+ def ecies_hkdf(self, enckey, plainkey):
+ if isinstance(enckey, ecdsa.ECDSA256P1Public):
+ newpk = ec.generate_private_key(ec.SECP256R1(), default_backend())
+ shared = newpk.exchange(ec.ECDH(), enckey._get_public())
+ else:
+ newpk = X25519PrivateKey.generate()
+ shared = newpk.exchange(enckey._get_public())
+ derived_key = HKDF(
+ algorithm=hashes.SHA256(), length=48, salt=None,
+ info=b'MCUBoot_ECIES_v1', backend=default_backend()).derive(shared)
+ encryptor = Cipher(algorithms.AES(derived_key[:16]),
+ modes.CTR(bytes([0] * 16)),
+ backend=default_backend()).encryptor()
+ cipherkey = encryptor.update(plainkey) + encryptor.finalize()
+ mac = hmac.HMAC(derived_key[16:], hashes.SHA256(),
+ backend=default_backend())
+ mac.update(cipherkey)
+ ciphermac = mac.finalize()
+ if isinstance(enckey, ecdsa.ECDSA256P1Public):
+ pubk = newpk.public_key().public_bytes(
+ encoding=Encoding.X962,
+ format=PublicFormat.UncompressedPoint)
+ else:
+ pubk = newpk.public_key().public_bytes(
+ encoding=Encoding.Raw,
+ format=PublicFormat.Raw)
+ return cipherkey, ciphermac, pubk
+
+ def create(self, key, public_key_format, enckey, dependencies=None,
+ sw_type=None):
+ self.enckey = enckey
+
+ # Calculate the hash of the public key
+ if key is not None:
+ pub = key.get_public_bytes()
+ sha = hashlib.sha256()
+ sha.update(pub)
+ pubbytes = sha.digest()
+ else:
+ pubbytes = bytes(hashlib.sha256().digest_size)
+
+ protected_tlv_size = 0
+
+ if self.security_counter is not None:
+ # Size of the security counter TLV: header ('HH') + payload ('I')
+ # = 4 + 4 = 8 Bytes
+ protected_tlv_size += TLV_SIZE + 4
+
+ if sw_type is not None:
+ if len(sw_type) > MAX_SW_TYPE_LENGTH:
+ msg = "'{}' is too long ({} characters) for sw_type. Its " \
+ "maximum allowed length is 12 characters.".format(
+ sw_type, len(sw_type))
+ raise click.UsageError(msg)
+
+ image_version = (str(self.version.major) + '.'
+ + str(self.version.minor) + '.'
+ + str(self.version.revision))
+
+ # The image hash is computed over the image header, the image
+ # itself and the protected TLV area. However, the boot record TLV
+ # (which is part of the protected area) should contain this hash
+ # before it is even calculated. For this reason the script fills
+ # this field with zeros and the bootloader will insert the right
+ # value later.
+ digest = bytes(hashlib.sha256().digest_size)
+
+ # Create CBOR encoded boot record
+ boot_record = create_sw_component_data(sw_type, image_version,
+ "SHA256", digest,
+ pubbytes)
+
+ protected_tlv_size += TLV_SIZE + len(boot_record)
+
+ if dependencies is not None:
+ # Size of a Dependency TLV = Header ('HH') + Payload('IBBHI')
+ # = 4 + 12 = 16 Bytes
+ dependencies_num = len(dependencies[DEP_IMAGES_KEY])
+ protected_tlv_size += (dependencies_num * 16)
+
+ if protected_tlv_size != 0:
+ # Add the size of the TLV info header
+ protected_tlv_size += TLV_INFO_SIZE
+
+ # At this point the image is already on the payload, this adds
+ # the header to the payload as well
+ self.add_header(enckey, protected_tlv_size)
+
+ prot_tlv = TLV(self.endian, TLV_PROT_INFO_MAGIC)
+
+ # Protected TLVs must be added first, because they are also included
+ # in the hash calculation
+ protected_tlv_off = None
+ if protected_tlv_size != 0:
+
+ e = STRUCT_ENDIAN_DICT[self.endian]
+
+ if self.security_counter is not None:
+ payload = struct.pack(e + 'I', self.security_counter)
+ prot_tlv.add('SEC_CNT', payload)
+
+ if sw_type is not None:
+ prot_tlv.add('BOOT_RECORD', boot_record)
+
+ if dependencies is not None:
+ for i in range(dependencies_num):
+ payload = struct.pack(
+ e + 'B3x'+'BBHI',
+ int(dependencies[DEP_IMAGES_KEY][i]),
+ dependencies[DEP_VERSIONS_KEY][i].major,
+ dependencies[DEP_VERSIONS_KEY][i].minor,
+ dependencies[DEP_VERSIONS_KEY][i].revision,
+ dependencies[DEP_VERSIONS_KEY][i].build
+ )
+ prot_tlv.add('DEPENDENCY', payload)
+
+ protected_tlv_off = len(self.payload)
+ self.payload += prot_tlv.get()
+
+ tlv = TLV(self.endian)
+
+ # Note that ecdsa wants to do the hashing itself, which means
+ # we get to hash it twice.
+ sha = hashlib.sha256()
+ sha.update(self.payload)
+ digest = sha.digest()
+
+ tlv.add('SHA256', digest)
+
+ if key is not None:
+ if public_key_format == 'hash':
+ tlv.add('KEYHASH', pubbytes)
+ else:
+ tlv.add('PUBKEY', pub)
+
+ # `sign` expects the full image payload (sha256 done internally),
+ # while `sign_digest` expects only the digest of the payload
+
+ if hasattr(key, 'sign'):
+ sig = key.sign(bytes(self.payload))
+ else:
+ sig = key.sign_digest(digest)
+ tlv.add(key.sig_tlv(), sig)
+
+ # At this point the image was hashed + signed, we can remove the
+ # protected TLVs from the payload (will be re-added later)
+ if protected_tlv_off is not None:
+ self.payload = self.payload[:protected_tlv_off]
+
+ if enckey is not None:
+ plainkey = os.urandom(16)
+
+ if isinstance(enckey, rsa.RSAPublic):
+ cipherkey = enckey._get_public().encrypt(
+ plainkey, padding.OAEP(
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
+ algorithm=hashes.SHA256(),
+ label=None))
+ self.enctlv_len = len(cipherkey)
+ tlv.add('ENCRSA2048', cipherkey)
+ elif isinstance(enckey, (ecdsa.ECDSA256P1Public,
+ x25519.X25519Public)):
+ cipherkey, mac, pubk = self.ecies_hkdf(enckey, plainkey)
+ enctlv = pubk + mac + cipherkey
+ self.enctlv_len = len(enctlv)
+ if isinstance(enckey, ecdsa.ECDSA256P1Public):
+ tlv.add('ENCEC256', enctlv)
+ else:
+ tlv.add('ENCX25519', enctlv)
+
+ nonce = bytes([0] * 16)
+ cipher = Cipher(algorithms.AES(plainkey), modes.CTR(nonce),
+ backend=default_backend())
+ encryptor = cipher.encryptor()
+ img = bytes(self.payload[self.header_size:])
+ self.payload[self.header_size:] = \
+ encryptor.update(img) + encryptor.finalize()
+
+ self.payload += prot_tlv.get()
+ self.payload += tlv.get()
+
+ self.check_trailer()
+
+ def add_header(self, enckey, protected_tlv_size):
+ """Install the image header."""
+
+ flags = 0
+ if enckey is not None:
+ flags |= IMAGE_F['ENCRYPTED']
+ if self.load_addr != 0:
+ # Indicates that this image should be loaded into RAM
+ # instead of run directly from flash.
+ flags |= IMAGE_F['RAM_LOAD']
+
+ e = STRUCT_ENDIAN_DICT[self.endian]
+ fmt = (e +
+ # type ImageHdr struct {
+ 'I' + # Magic uint32
+ 'I' + # LoadAddr uint32
+ 'H' + # HdrSz uint16
+ 'H' + # PTLVSz uint16
+ 'I' + # ImgSz uint32
+ 'I' + # Flags uint32
+ 'BBHI' + # Vers ImageVersion
+ 'I' # Pad1 uint32
+ ) # }
+ assert struct.calcsize(fmt) == IMAGE_HEADER_SIZE
+ header = struct.pack(fmt,
+ IMAGE_MAGIC,
+ self.load_addr,
+ self.header_size,
+ protected_tlv_size, # TLV Info header + Protected TLVs
+ len(self.payload) - self.header_size, # ImageSz
+ flags,
+ self.version.major,
+ self.version.minor or 0,
+ self.version.revision or 0,
+ self.version.build or 0,
+ 0) # Pad1
+ self.payload = bytearray(self.payload)
+ self.payload[:len(header)] = header
+
+ def _trailer_size(self, write_size, max_sectors, overwrite_only, enckey,
+ save_enctlv, enctlv_len):
+ # NOTE: should already be checked by the argument parser
+ magic_size = 16
+ if overwrite_only:
+ return MAX_ALIGN * 2 + magic_size
+ else:
+ if write_size not in set([1, 2, 4, 8]):
+ raise click.BadParameter("Invalid alignment: {}".format(
+ write_size))
+ m = DEFAULT_MAX_SECTORS if max_sectors is None else max_sectors
+ trailer = m * 3 * write_size # status area
+ if enckey is not None:
+ if save_enctlv:
+ # TLV saved by the bootloader is aligned
+ keylen = (int((enctlv_len - 1) / MAX_ALIGN) + 1) * MAX_ALIGN
+ else:
+ keylen = 16
+ trailer += keylen * 2 # encryption keys
+ trailer += MAX_ALIGN * 4 # image_ok/copy_done/swap_info/swap_size
+ trailer += magic_size
+ return trailer
+
+ def pad_to(self, size):
+ """Pad the image to the given size, with the given flash alignment."""
+ tsize = self._trailer_size(self.align, self.max_sectors,
+ self.overwrite_only, self.enckey,
+ self.save_enctlv, self.enctlv_len)
+ padding = size - (len(self.payload) + tsize)
+ pbytes = bytearray([self.erased_val] * padding)
+ pbytes += bytearray([self.erased_val] * (tsize - len(boot_magic)))
+ if self.confirm and not self.overwrite_only:
+ pbytes[-MAX_ALIGN] = 0x01 # image_ok = 0x01
+ pbytes += boot_magic
+ self.payload += pbytes
+
+ @staticmethod
+ def verify(imgfile, key):
+ with open(imgfile, "rb") as f:
+ b = f.read()
+
+ magic, _, header_size, _, img_size = struct.unpack('IIHHI', b[:16])
+ version = struct.unpack('BBHI', b[20:28])
+
+ if magic != IMAGE_MAGIC:
+ return VerifyResult.INVALID_MAGIC, None
+
+ tlv_info = b[header_size+img_size:header_size+img_size+TLV_INFO_SIZE]
+ magic, tlv_tot = struct.unpack('HH', tlv_info)
+ if magic != TLV_INFO_MAGIC:
+ return VerifyResult.INVALID_TLV_INFO_MAGIC, None
+
+ sha = hashlib.sha256()
+ sha.update(b[:header_size+img_size])
+ digest = sha.digest()
+
+ tlv_off = header_size + img_size
+ tlv_end = tlv_off + tlv_tot
+ tlv_off += TLV_INFO_SIZE # skip tlv info
+ while tlv_off < tlv_end:
+ tlv = b[tlv_off:tlv_off+TLV_SIZE]
+ tlv_type, _, tlv_len = struct.unpack('BBH', tlv)
+ if tlv_type == TLV_VALUES["SHA256"]:
+ off = tlv_off + TLV_SIZE
+ if digest == b[off:off+tlv_len]:
+ if key is None:
+ return VerifyResult.OK, version
+ else:
+ return VerifyResult.INVALID_HASH, None
+ elif key is not None and tlv_type == TLV_VALUES[key.sig_tlv()]:
+ off = tlv_off + TLV_SIZE
+ tlv_sig = b[off:off+tlv_len]
+ payload = b[:header_size+img_size]
+ try:
+ if hasattr(key, 'verify'):
+ key.verify(tlv_sig, payload)
+ else:
+ key.verify_digest(tlv_sig, digest)
+ return VerifyResult.OK, version
+ except InvalidSignature:
+ # continue to next TLV
+ pass
+ tlv_off += TLV_SIZE + tlv_len
+ return VerifyResult.INVALID_SIGNATURE, None
diff --git a/tools/mcuboot/imgtool/keys/__init__.py b/tools/mcuboot/imgtool/keys/__init__.py
new file mode 100644
index 00000000..f25e2aae
--- /dev/null
+++ b/tools/mcuboot/imgtool/keys/__init__.py
@@ -0,0 +1,94 @@
+# Copyright 2017 Linaro Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Cryptographic key management for imgtool.
+"""
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric.rsa import (
+ RSAPrivateKey, RSAPublicKey)
+from cryptography.hazmat.primitives.asymmetric.ec import (
+ EllipticCurvePrivateKey, EllipticCurvePublicKey)
+from cryptography.hazmat.primitives.asymmetric.ed25519 import (
+ Ed25519PrivateKey, Ed25519PublicKey)
+from cryptography.hazmat.primitives.asymmetric.x25519 import (
+ X25519PrivateKey, X25519PublicKey)
+
+from .rsa import RSA, RSAPublic, RSAUsageError, RSA_KEY_SIZES
+from .ecdsa import ECDSA256P1, ECDSA256P1Public, ECDSAUsageError
+from .ed25519 import Ed25519, Ed25519Public, Ed25519UsageError
+from .x25519 import X25519, X25519Public, X25519UsageError
+
+
+class PasswordRequired(Exception):
+ """Raised to indicate that the key is password protected, but a
+ password was not specified."""
+ pass
+
+
+def load(path, passwd=None):
+ """Try loading a key from the given path. Returns None if the password wasn't specified."""
+ with open(path, 'rb') as f:
+ raw_pem = f.read()
+ try:
+ pk = serialization.load_pem_private_key(
+ raw_pem,
+ password=passwd,
+ backend=default_backend())
+ # Unfortunately, the crypto library raises unhelpful exceptions,
+ # so we have to look at the text.
+ except TypeError as e:
+ msg = str(e)
+ if "private key is encrypted" in msg:
+ return None
+ raise e
+ except ValueError:
+ # This seems to happen if the key is a public key, let's try
+ # loading it as a public key.
+ pk = serialization.load_pem_public_key(
+ raw_pem,
+ backend=default_backend())
+
+ if isinstance(pk, RSAPrivateKey):
+ if pk.key_size not in RSA_KEY_SIZES:
+ raise Exception("Unsupported RSA key size: " + pk.key_size)
+ return RSA(pk)
+ elif isinstance(pk, RSAPublicKey):
+ if pk.key_size not in RSA_KEY_SIZES:
+ raise Exception("Unsupported RSA key size: " + pk.key_size)
+ return RSAPublic(pk)
+ elif isinstance(pk, EllipticCurvePrivateKey):
+ if pk.curve.name != 'secp256r1':
+ raise Exception("Unsupported EC curve: " + pk.curve.name)
+ if pk.key_size != 256:
+ raise Exception("Unsupported EC size: " + pk.key_size)
+ return ECDSA256P1(pk)
+ elif isinstance(pk, EllipticCurvePublicKey):
+ if pk.curve.name != 'secp256r1':
+ raise Exception("Unsupported EC curve: " + pk.curve.name)
+ if pk.key_size != 256:
+ raise Exception("Unsupported EC size: " + pk.key_size)
+ return ECDSA256P1Public(pk)
+ elif isinstance(pk, Ed25519PrivateKey):
+ return Ed25519(pk)
+ elif isinstance(pk, Ed25519PublicKey):
+ return Ed25519Public(pk)
+ elif isinstance(pk, X25519PrivateKey):
+ return X25519(pk)
+ elif isinstance(pk, X25519PublicKey):
+ return X25519Public(pk)
+ else:
+ raise Exception("Unknown key type: " + str(type(pk)))
diff --git a/tools/mcuboot/imgtool/keys/ecdsa.py b/tools/mcuboot/imgtool/keys/ecdsa.py
new file mode 100644
index 00000000..139d583d
--- /dev/null
+++ b/tools/mcuboot/imgtool/keys/ecdsa.py
@@ -0,0 +1,157 @@
+"""
+ECDSA key management
+"""
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.hashes import SHA256
+
+from .general import KeyClass
+
+class ECDSAUsageError(Exception):
+ pass
+
+class ECDSA256P1Public(KeyClass):
+ def __init__(self, key):
+ self.key = key
+
+ def shortname(self):
+ return "ecdsa"
+
+ def _unsupported(self, name):
+ raise ECDSAUsageError("Operation {} requires private key".format(name))
+
+ def _get_public(self):
+ return self.key
+
+ def get_public_bytes(self):
+ # The key is embedded into MBUboot in "SubjectPublicKeyInfo" format
+ return self._get_public().public_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
+
+ def get_private_bytes(self, minimal):
+ self._unsupported('get_private_bytes')
+
+ def export_private(self, path, passwd=None):
+ self._unsupported('export_private')
+
+ def export_public(self, path):
+ """Write the public key to the given file."""
+ pem = self._get_public().public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
+ with open(path, 'wb') as f:
+ f.write(pem)
+
+ def sig_type(self):
+ return "ECDSA256_SHA256"
+
+ def sig_tlv(self):
+ return "ECDSA256"
+
+ def sig_len(self):
+ # Early versions of MCUboot (< v1.5.0) required ECDSA
+ # signatures to be padded to 72 bytes. Because the DER
+ # encoding is done with signed integers, the size of the
+ # signature will vary depending on whether the high bit is set
+ # in each value. This padding was done in a
+ # not-easily-reversible way (by just adding zeros).
+ #
+ # The signing code no longer requires this padding, and newer
+ # versions of MCUboot don't require it. But, continue to
+ # return the total length so that the padding can be done if
+ # requested.
+ return 72
+
+ def verify(self, signature, payload):
+ # strip possible paddings added during sign
+ signature = signature[:signature[1] + 2]
+ k = self.key
+ if isinstance(self.key, ec.EllipticCurvePrivateKey):
+ k = self.key.public_key()
+ return k.verify(signature=signature, data=payload,
+ signature_algorithm=ec.ECDSA(SHA256()))
+
+
+class ECDSA256P1(ECDSA256P1Public):
+ """
+ Wrapper around an ECDSA private key.
+ """
+
+ def __init__(self, key):
+ """key should be an instance of EllipticCurvePrivateKey"""
+ self.key = key
+ self.pad_sig = False
+
+ @staticmethod
+ def generate():
+ pk = ec.generate_private_key(
+ ec.SECP256R1(),
+ backend=default_backend())
+ return ECDSA256P1(pk)
+
+ def _get_public(self):
+ return self.key.public_key()
+
+ def _build_minimal_ecdsa_privkey(self, der):
+ '''
+ Builds a new DER that only includes the EC private key, removing the
+ public key that is added as an "optional" BITSTRING.
+ '''
+ offset_PUB = 68
+ EXCEPTION_TEXT = "Error parsing ecdsa key. Please submit an issue!"
+ if der[offset_PUB] != 0xa1:
+ raise ECDSAUsageError(EXCEPTION_TEXT)
+ len_PUB = der[offset_PUB + 1]
+ b = bytearray(der[:-offset_PUB])
+ offset_SEQ = 29
+ if b[offset_SEQ] != 0x30:
+ raise ECDSAUsageError(EXCEPTION_TEXT)
+ b[offset_SEQ + 1] -= len_PUB
+ offset_OCT_STR = 27
+ if b[offset_OCT_STR] != 0x04:
+ raise ECDSAUsageError(EXCEPTION_TEXT)
+ b[offset_OCT_STR + 1] -= len_PUB
+ if b[0] != 0x30 or b[1] != 0x81:
+ raise ECDSAUsageError(EXCEPTION_TEXT)
+ b[2] -= len_PUB
+ return b
+
+ def get_private_bytes(self, minimal):
+ priv = self.key.private_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.NoEncryption())
+ if minimal:
+ priv = self._build_minimal_ecdsa_privkey(priv)
+ return priv
+
+ def export_private(self, path, passwd=None):
+ """Write the private key to the given file, protecting it with the optional password."""
+ if passwd is None:
+ enc = serialization.NoEncryption()
+ else:
+ enc = serialization.BestAvailableEncryption(passwd)
+ pem = self.key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=enc)
+ with open(path, 'wb') as f:
+ f.write(pem)
+
+ def raw_sign(self, payload):
+ """Return the actual signature"""
+ return self.key.sign(
+ data=payload,
+ signature_algorithm=ec.ECDSA(SHA256()))
+
+ def sign(self, payload):
+ sig = self.raw_sign(payload)
+ if self.pad_sig:
+ # To make fixed length, pad with one or two zeros.
+ sig += b'\000' * (self.sig_len() - len(sig))
+ return sig
+ else:
+ return sig
diff --git a/tools/mcuboot/imgtool/keys/ecdsa_test.py b/tools/mcuboot/imgtool/keys/ecdsa_test.py
new file mode 100644
index 00000000..7982cad9
--- /dev/null
+++ b/tools/mcuboot/imgtool/keys/ecdsa_test.py
@@ -0,0 +1,99 @@
+"""
+Tests for ECDSA keys
+"""
+
+import io
+import os.path
+import sys
+import tempfile
+import unittest
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.hashes import SHA256
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
+
+from imgtool.keys import load, ECDSA256P1, ECDSAUsageError
+
+class EcKeyGeneration(unittest.TestCase):
+
+ def setUp(self):
+ self.test_dir = tempfile.TemporaryDirectory()
+
+ def tname(self, base):
+ return os.path.join(self.test_dir.name, base)
+
+ def tearDown(self):
+ self.test_dir.cleanup()
+
+ def test_keygen(self):
+ name1 = self.tname("keygen.pem")
+ k = ECDSA256P1.generate()
+ k.export_private(name1, b'secret')
+
+ self.assertIsNone(load(name1))
+
+ k2 = load(name1, b'secret')
+
+ pubname = self.tname('keygen-pub.pem')
+ k2.export_public(pubname)
+ pk2 = load(pubname)
+
+ # We should be able to export the public key from the loaded
+ # public key, but not the private key.
+ pk2.export_public(self.tname('keygen-pub2.pem'))
+ self.assertRaises(ECDSAUsageError,
+ pk2.export_private, self.tname('keygen-priv2.pem'))
+
+ def test_emit(self):
+ """Basic sanity check on the code emitters."""
+ k = ECDSA256P1.generate()
+
+ ccode = io.StringIO()
+ k.emit_c_public(ccode)
+ self.assertIn("ecdsa_pub_key", ccode.getvalue())
+ self.assertIn("ecdsa_pub_key_len", ccode.getvalue())
+
+ rustcode = io.StringIO()
+ k.emit_rust_public(rustcode)
+ self.assertIn("ECDSA_PUB_KEY", rustcode.getvalue())
+
+ def test_emit_pub(self):
+ """Basic sanity check on the code emitters."""
+ pubname = self.tname("public.pem")
+ k = ECDSA256P1.generate()
+ k.export_public(pubname)
+
+ k2 = load(pubname)
+
+ ccode = io.StringIO()
+ k2.emit_c_public(ccode)
+ self.assertIn("ecdsa_pub_key", ccode.getvalue())
+ self.assertIn("ecdsa_pub_key_len", ccode.getvalue())
+
+ rustcode = io.StringIO()
+ k2.emit_rust_public(rustcode)
+ self.assertIn("ECDSA_PUB_KEY", rustcode.getvalue())
+
+ def test_sig(self):
+ k = ECDSA256P1.generate()
+ buf = b'This is the message'
+ sig = k.raw_sign(buf)
+
+ # The code doesn't have any verification, so verify this
+ # manually.
+ k.key.public_key().verify(
+ signature=sig,
+ data=buf,
+ signature_algorithm=ec.ECDSA(SHA256()))
+
+ # Modify the message to make sure the signature fails.
+ self.assertRaises(InvalidSignature,
+ k.key.public_key().verify,
+ signature=sig,
+ data=b'This is thE message',
+ signature_algorithm=ec.ECDSA(SHA256()))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/mcuboot/imgtool/keys/ed25519.py b/tools/mcuboot/imgtool/keys/ed25519.py
new file mode 100644
index 00000000..fb000cd9
--- /dev/null
+++ b/tools/mcuboot/imgtool/keys/ed25519.py
@@ -0,0 +1,105 @@
+"""
+ED25519 key management
+"""
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import ed25519
+
+from .general import KeyClass
+
+
+class Ed25519UsageError(Exception):
+ pass
+
+
+class Ed25519Public(KeyClass):
+ def __init__(self, key):
+ self.key = key
+
+ def shortname(self):
+ return "ed25519"
+
+ def _unsupported(self, name):
+ raise Ed25519UsageError("Operation {} requires private key".format(name))
+
+ def _get_public(self):
+ return self.key
+
+ def get_public_bytes(self):
+ # The key is embedded into MBUboot in "SubjectPublicKeyInfo" format
+ return self._get_public().public_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
+
+ def get_private_bytes(self, minimal):
+ self._unsupported('get_private_bytes')
+
+ def export_private(self, path, passwd=None):
+ self._unsupported('export_private')
+
+ def export_public(self, path):
+ """Write the public key to the given file."""
+ pem = self._get_public().public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
+ with open(path, 'wb') as f:
+ f.write(pem)
+
+ def sig_type(self):
+ return "ED25519"
+
+ def sig_tlv(self):
+ return "ED25519"
+
+ def sig_len(self):
+ return 64
+
+
+class Ed25519(Ed25519Public):
+ """
+ Wrapper around an ED25519 private key.
+ """
+
+ def __init__(self, key):
+ """key should be an instance of EllipticCurvePrivateKey"""
+ self.key = key
+
+ @staticmethod
+ def generate():
+ pk = ed25519.Ed25519PrivateKey.generate()
+ return Ed25519(pk)
+
+ def _get_public(self):
+ return self.key.public_key()
+
+ def get_private_bytes(self, minimal):
+ raise Ed25519UsageError("Operation not supported with {} keys".format(
+ self.shortname()))
+
+ def export_private(self, path, passwd=None):
+ """
+ Write the private key to the given file, protecting it with the
+ optional password.
+ """
+ if passwd is None:
+ enc = serialization.NoEncryption()
+ else:
+ enc = serialization.BestAvailableEncryption(passwd)
+ pem = self.key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=enc)
+ with open(path, 'wb') as f:
+ f.write(pem)
+
+ def sign_digest(self, digest):
+ """Return the actual signature"""
+ return self.key.sign(data=digest)
+
+ def verify_digest(self, signature, digest):
+ """Verify that signature is valid for given digest"""
+ k = self.key
+ if isinstance(self.key, ed25519.Ed25519PrivateKey):
+ k = self.key.public_key()
+ return k.verify(signature=signature, data=digest)
diff --git a/tools/mcuboot/imgtool/keys/ed25519_test.py b/tools/mcuboot/imgtool/keys/ed25519_test.py
new file mode 100644
index 00000000..31f43fe9
--- /dev/null
+++ b/tools/mcuboot/imgtool/keys/ed25519_test.py
@@ -0,0 +1,103 @@
+"""
+Tests for ECDSA keys
+"""
+
+import hashlib
+import io
+import os.path
+import sys
+import tempfile
+import unittest
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives.asymmetric import ed25519
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
+
+from imgtool.keys import load, Ed25519, Ed25519UsageError
+
+
+class Ed25519KeyGeneration(unittest.TestCase):
+
+ def setUp(self):
+ self.test_dir = tempfile.TemporaryDirectory()
+
+ def tname(self, base):
+ return os.path.join(self.test_dir.name, base)
+
+ def tearDown(self):
+ self.test_dir.cleanup()
+
+ def test_keygen(self):
+ name1 = self.tname("keygen.pem")
+ k = Ed25519.generate()
+ k.export_private(name1, b'secret')
+
+ self.assertIsNone(load(name1))
+
+ k2 = load(name1, b'secret')
+
+ pubname = self.tname('keygen-pub.pem')
+ k2.export_public(pubname)
+ pk2 = load(pubname)
+
+ # We should be able to export the public key from the loaded
+ # public key, but not the private key.
+ pk2.export_public(self.tname('keygen-pub2.pem'))
+ self.assertRaises(Ed25519UsageError,
+ pk2.export_private, self.tname('keygen-priv2.pem'))
+
+ def test_emit(self):
+ """Basic sanity check on the code emitters."""
+ k = Ed25519.generate()
+
+ ccode = io.StringIO()
+ k.emit_c_public(ccode)
+ self.assertIn("ed25519_pub_key", ccode.getvalue())
+ self.assertIn("ed25519_pub_key_len", ccode.getvalue())
+
+ rustcode = io.StringIO()
+ k.emit_rust_public(rustcode)
+ self.assertIn("ED25519_PUB_KEY", rustcode.getvalue())
+
+ def test_emit_pub(self):
+ """Basic sanity check on the code emitters."""
+ pubname = self.tname("public.pem")
+ k = Ed25519.generate()
+ k.export_public(pubname)
+
+ k2 = load(pubname)
+
+ ccode = io.StringIO()
+ k2.emit_c_public(ccode)
+ self.assertIn("ed25519_pub_key", ccode.getvalue())
+ self.assertIn("ed25519_pub_key_len", ccode.getvalue())
+
+ rustcode = io.StringIO()
+ k2.emit_rust_public(rustcode)
+ self.assertIn("ED25519_PUB_KEY", rustcode.getvalue())
+
+ def test_sig(self):
+ k = Ed25519.generate()
+ buf = b'This is the message'
+ sha = hashlib.sha256()
+ sha.update(buf)
+ digest = sha.digest()
+ sig = k.sign_digest(digest)
+
+ # The code doesn't have any verification, so verify this
+ # manually.
+ k.key.public_key().verify(signature=sig, data=digest)
+
+ # Modify the message to make sure the signature fails.
+ sha = hashlib.sha256()
+ sha.update(b'This is thE message')
+ new_digest = sha.digest()
+ self.assertRaises(InvalidSignature,
+ k.key.public_key().verify,
+ signature=sig,
+ data=new_digest)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/mcuboot/imgtool/keys/general.py b/tools/mcuboot/imgtool/keys/general.py
new file mode 100644
index 00000000..ce7a2d26
--- /dev/null
+++ b/tools/mcuboot/imgtool/keys/general.py
@@ -0,0 +1,45 @@
+"""General key class."""
+
+import sys
+
+AUTOGEN_MESSAGE = "/* Autogenerated by imgtool.py, do not edit. */"
+
+class KeyClass(object):
+ def _emit(self, header, trailer, encoded_bytes, indent, file=sys.stdout, len_format=None):
+ print(AUTOGEN_MESSAGE, file=file)
+ print(header, end='', file=file)
+ for count, b in enumerate(encoded_bytes):
+ if count % 8 == 0:
+ print("\n" + indent, end='', file=file)
+ else:
+ print(" ", end='', file=file)
+ print("0x{:02x},".format(b), end='', file=file)
+ print("\n" + trailer, file=file)
+ if len_format is not None:
+ print(len_format.format(len(encoded_bytes)), file=file)
+
+ def emit_c_public(self, file=sys.stdout):
+ self._emit(
+ header="const unsigned char {}_pub_key[] = {{".format(self.shortname()),
+ trailer="};",
+ encoded_bytes=self.get_public_bytes(),
+ indent=" ",
+ len_format="const unsigned int {}_pub_key_len = {{}};".format(self.shortname()),
+ file=file)
+
+ def emit_rust_public(self, file=sys.stdout):
+ self._emit(
+ header="static {}_PUB_KEY: &'static [u8] = &[".format(self.shortname().upper()),
+ trailer="];",
+ encoded_bytes=self.get_public_bytes(),
+ indent=" ",
+ file=file)
+
+ def emit_private(self, minimal, file=sys.stdout):
+ self._emit(
+ header="const unsigned char enc_priv_key[] = {",
+ trailer="};",
+ encoded_bytes=self.get_private_bytes(minimal),
+ indent=" ",
+ len_format="const unsigned int enc_priv_key_len = {};",
+ file=file)
diff --git a/tools/mcuboot/imgtool/keys/rsa.py b/tools/mcuboot/imgtool/keys/rsa.py
new file mode 100644
index 00000000..f8273bf5
--- /dev/null
+++ b/tools/mcuboot/imgtool/keys/rsa.py
@@ -0,0 +1,163 @@
+"""
+RSA Key management
+"""
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.primitives.asymmetric.padding import PSS, MGF1
+from cryptography.hazmat.primitives.hashes import SHA256
+
+from .general import KeyClass
+
+
+# Sizes that bootutil will recognize
+RSA_KEY_SIZES = [2048, 3072]
+
+
+class RSAUsageError(Exception):
+ pass
+
+
+class RSAPublic(KeyClass):
+ """The public key can only do a few operations"""
+ def __init__(self, key):
+ self.key = key
+
+ def key_size(self):
+ return self.key.key_size
+
+ def shortname(self):
+ return "rsa"
+
+ def _unsupported(self, name):
+ raise RSAUsageError("Operation {} requires private key".format(name))
+
+ def _get_public(self):
+ return self.key
+
+ def get_public_bytes(self):
+ # The key embedded into MCUboot is in PKCS1 format.
+ return self._get_public().public_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PublicFormat.PKCS1)
+
+ def get_private_bytes(self, minimal):
+ self._unsupported('get_private_bytes')
+
+ def export_private(self, path, passwd=None):
+ self._unsupported('export_private')
+
+ def export_public(self, path):
+ """Write the public key to the given file."""
+ pem = self._get_public().public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
+ with open(path, 'wb') as f:
+ f.write(pem)
+
+ def sig_type(self):
+ return "PKCS1_PSS_RSA{}_SHA256".format(self.key_size())
+
+ def sig_tlv(self):
+ return"RSA{}".format(self.key_size())
+
+ def sig_len(self):
+ return self.key_size() / 8
+
+ def verify(self, signature, payload):
+ k = self.key
+ if isinstance(self.key, rsa.RSAPrivateKey):
+ k = self.key.public_key()
+ return k.verify(signature=signature, data=payload,
+ padding=PSS(mgf=MGF1(SHA256()), salt_length=32),
+ algorithm=SHA256())
+
+
+class RSA(RSAPublic):
+ """
+ Wrapper around an RSA key, with imgtool support.
+ """
+
+ def __init__(self, key):
+ """The key should be a private key from cryptography"""
+ self.key = key
+
+ @staticmethod
+ def generate(key_size=2048):
+ if key_size not in RSA_KEY_SIZES:
+ raise RSAUsageError("Key size {} is not supported by MCUboot"
+ .format(key_size))
+ pk = rsa.generate_private_key(
+ public_exponent=65537,
+ key_size=key_size,
+ backend=default_backend())
+ return RSA(pk)
+
+ def _get_public(self):
+ return self.key.public_key()
+
+ def _build_minimal_rsa_privkey(self, der):
+ '''
+ Builds a new DER that only includes N/E/D/P/Q RSA parameters;
+ standard DER private bytes provided by OpenSSL also includes
+ CRT params (DP/DQ/QP) which can be removed.
+ '''
+ OFFSET_N = 7 # N is always located at this offset
+ b = bytearray(der)
+ off = OFFSET_N
+ if b[off + 1] != 0x82:
+ raise RSAUsageError("Error parsing N while minimizing")
+ len_N = (b[off + 2] << 8) + b[off + 3] + 4
+ off += len_N
+ if b[off + 1] != 0x03:
+ raise RSAUsageError("Error parsing E while minimizing")
+ len_E = b[off + 2] + 4
+ off += len_E
+ if b[off + 1] != 0x82:
+ raise RSAUsageError("Error parsing D while minimizing")
+ len_D = (b[off + 2] << 8) + b[off + 3] + 4
+ off += len_D
+ if b[off + 1] != 0x81:
+ raise RSAUsageError("Error parsing P while minimizing")
+ len_P = b[off + 2] + 3
+ off += len_P
+ if b[off + 1] != 0x81:
+ raise RSAUsageError("Error parsing Q while minimizing")
+ len_Q = b[off + 2] + 3
+ off += len_Q
+ # adjust DER size for removed elements
+ b[2] = (off - 4) >> 8
+ b[3] = (off - 4) & 0xff
+ return b[:off]
+
+ def get_private_bytes(self, minimal):
+ priv = self.key.private_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption())
+ if minimal:
+ priv = self._build_minimal_rsa_privkey(priv)
+ return priv
+
+ def export_private(self, path, passwd=None):
+ """Write the private key to the given file, protecting it with the
+ optional password."""
+ if passwd is None:
+ enc = serialization.NoEncryption()
+ else:
+ enc = serialization.BestAvailableEncryption(passwd)
+ pem = self.key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=enc)
+ with open(path, 'wb') as f:
+ f.write(pem)
+
+ def sign(self, payload):
+ # The verification code only allows the salt length to be the
+ # same as the hash length, 32.
+ return self.key.sign(
+ data=payload,
+ padding=PSS(mgf=MGF1(SHA256()), salt_length=32),
+ algorithm=SHA256())
diff --git a/tools/mcuboot/imgtool/keys/rsa_test.py b/tools/mcuboot/imgtool/keys/rsa_test.py
new file mode 100644
index 00000000..b0afa835
--- /dev/null
+++ b/tools/mcuboot/imgtool/keys/rsa_test.py
@@ -0,0 +1,115 @@
+"""
+Tests for RSA keys
+"""
+
+import io
+import os
+import sys
+import tempfile
+import unittest
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives.asymmetric.padding import PSS, MGF1
+from cryptography.hazmat.primitives.hashes import SHA256
+
+# Setup sys path so 'imgtool' is in it.
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '../..')))
+
+from imgtool.keys import load, RSA, RSAUsageError
+from imgtool.keys.rsa import RSA_KEY_SIZES
+
+
+class KeyGeneration(unittest.TestCase):
+
+ def setUp(self):
+ self.test_dir = tempfile.TemporaryDirectory()
+
+ def tname(self, base):
+ return os.path.join(self.test_dir.name, base)
+
+ def tearDown(self):
+ self.test_dir.cleanup()
+
+ def test_keygen(self):
+ # Try generating a RSA key with non-supported size
+ with self.assertRaises(RSAUsageError):
+ RSA.generate(key_size=1024)
+
+ for key_size in RSA_KEY_SIZES:
+ name1 = self.tname("keygen.pem")
+ k = RSA.generate(key_size=key_size)
+ k.export_private(name1, b'secret')
+
+ # Try loading the key without a password.
+ self.assertIsNone(load(name1))
+
+ k2 = load(name1, b'secret')
+
+ pubname = self.tname('keygen-pub.pem')
+ k2.export_public(pubname)
+ pk2 = load(pubname)
+
+ # We should be able to export the public key from the loaded
+ # public key, but not the private key.
+ pk2.export_public(self.tname('keygen-pub2.pem'))
+ self.assertRaises(RSAUsageError, pk2.export_private,
+ self.tname('keygen-priv2.pem'))
+
+ def test_emit(self):
+ """Basic sanity check on the code emitters."""
+ for key_size in RSA_KEY_SIZES:
+ k = RSA.generate(key_size=key_size)
+
+ ccode = io.StringIO()
+ k.emit_c_public(ccode)
+ self.assertIn("rsa_pub_key", ccode.getvalue())
+ self.assertIn("rsa_pub_key_len", ccode.getvalue())
+
+ rustcode = io.StringIO()
+ k.emit_rust_public(rustcode)
+ self.assertIn("RSA_PUB_KEY", rustcode.getvalue())
+
+ def test_emit_pub(self):
+ """Basic sanity check on the code emitters, from public key."""
+ pubname = self.tname("public.pem")
+ for key_size in RSA_KEY_SIZES:
+ k = RSA.generate(key_size=key_size)
+ k.export_public(pubname)
+
+ k2 = load(pubname)
+
+ ccode = io.StringIO()
+ k2.emit_c_public(ccode)
+ self.assertIn("rsa_pub_key", ccode.getvalue())
+ self.assertIn("rsa_pub_key_len", ccode.getvalue())
+
+ rustcode = io.StringIO()
+ k2.emit_rust_public(rustcode)
+ self.assertIn("RSA_PUB_KEY", rustcode.getvalue())
+
+ def test_sig(self):
+ for key_size in RSA_KEY_SIZES:
+ k = RSA.generate(key_size=key_size)
+ buf = b'This is the message'
+ sig = k.sign(buf)
+
+ # The code doesn't have any verification, so verify this
+ # manually.
+ k.key.public_key().verify(
+ signature=sig,
+ data=buf,
+ padding=PSS(mgf=MGF1(SHA256()), salt_length=32),
+ algorithm=SHA256())
+
+ # Modify the message to make sure the signature fails.
+ self.assertRaises(InvalidSignature,
+ k.key.public_key().verify,
+ signature=sig,
+ data=b'This is thE message',
+ padding=PSS(mgf=MGF1(SHA256()), salt_length=32),
+ algorithm=SHA256())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/mcuboot/imgtool/keys/x25519.py b/tools/mcuboot/imgtool/keys/x25519.py
new file mode 100644
index 00000000..63c0b5a7
--- /dev/null
+++ b/tools/mcuboot/imgtool/keys/x25519.py
@@ -0,0 +1,107 @@
+"""
+X25519 key management
+"""
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import x25519
+
+from .general import KeyClass
+
+
+class X25519UsageError(Exception):
+ pass
+
+
+class X25519Public(KeyClass):
+ def __init__(self, key):
+ self.key = key
+
+ def shortname(self):
+ return "x25519"
+
+ def _unsupported(self, name):
+ raise X25519UsageError("Operation {} requires private key".format(name))
+
+ def _get_public(self):
+ return self.key
+
+ def get_public_bytes(self):
+ # The key is embedded into MBUboot in "SubjectPublicKeyInfo" format
+ return self._get_public().public_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
+
+ def get_private_bytes(self, minimal):
+ self._unsupported('get_private_bytes')
+
+ def export_private(self, path, passwd=None):
+ self._unsupported('export_private')
+
+ def export_public(self, path):
+ """Write the public key to the given file."""
+ pem = self._get_public().public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
+ with open(path, 'wb') as f:
+ f.write(pem)
+
+ def sig_type(self):
+ return "X25519"
+
+ def sig_tlv(self):
+ return "X25519"
+
+ def sig_len(self):
+ return 32
+
+
+class X25519(X25519Public):
+ """
+ Wrapper around an X25519 private key.
+ """
+
+ def __init__(self, key):
+ """key should be an instance of EllipticCurvePrivateKey"""
+ self.key = key
+
+ @staticmethod
+ def generate():
+ pk = x25519.X25519PrivateKey.generate()
+ return X25519(pk)
+
+ def _get_public(self):
+ return self.key.public_key()
+
+ def get_private_bytes(self, minimal):
+ return self.key.private_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.NoEncryption())
+
+ def export_private(self, path, passwd=None):
+ """
+ Write the private key to the given file, protecting it with the
+ optional password.
+ """
+ if passwd is None:
+ enc = serialization.NoEncryption()
+ else:
+ enc = serialization.BestAvailableEncryption(passwd)
+ pem = self.key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=enc)
+ with open(path, 'wb') as f:
+ f.write(pem)
+
+ def sign_digest(self, digest):
+ """Return the actual signature"""
+ return self.key.sign(data=digest)
+
+ def verify_digest(self, signature, digest):
+ """Verify that signature is valid for given digest"""
+ k = self.key
+ if isinstance(self.key, x25519.X25519PrivateKey):
+ k = self.key.public_key()
+ return k.verify(signature=signature, data=digest)
diff --git a/tools/mcuboot/imgtool/main.py b/tools/mcuboot/imgtool/main.py
new file mode 100644
index 00000000..c93addc0
--- /dev/null
+++ b/tools/mcuboot/imgtool/main.py
@@ -0,0 +1,352 @@
+#! /usr/bin/env python3
+#
+# Copyright 2017-2020 Linaro Limited
+# Copyright 2019-2020 Arm Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+import click
+import getpass
+import imgtool.keys as keys
+import sys
+from imgtool import image, imgtool_version
+from imgtool.version import decode_version
+from .keys import (
+ RSAUsageError, ECDSAUsageError, Ed25519UsageError, X25519UsageError)
+
+MIN_PYTHON_VERSION = (3, 6)
+if sys.version_info < MIN_PYTHON_VERSION:
+ sys.exit("Python %s.%s or newer is required by imgtool."
+ % MIN_PYTHON_VERSION)
+
+
+def gen_rsa2048(keyfile, passwd):
+ keys.RSA.generate().export_private(path=keyfile, passwd=passwd)
+
+
+def gen_rsa3072(keyfile, passwd):
+ keys.RSA.generate(key_size=3072).export_private(path=keyfile,
+ passwd=passwd)
+
+
+def gen_ecdsa_p256(keyfile, passwd):
+ keys.ECDSA256P1.generate().export_private(keyfile, passwd=passwd)
+
+
+def gen_ecdsa_p224(keyfile, passwd):
+ print("TODO: p-224 not yet implemented")
+
+
+def gen_ed25519(keyfile, passwd):
+ keys.Ed25519.generate().export_private(path=keyfile, passwd=passwd)
+
+
+def gen_x25519(keyfile, passwd):
+ keys.X25519.generate().export_private(path=keyfile, passwd=passwd)
+
+
+valid_langs = ['c', 'rust']
+keygens = {
+ 'rsa-2048': gen_rsa2048,
+ 'rsa-3072': gen_rsa3072,
+ 'ecdsa-p256': gen_ecdsa_p256,
+ 'ecdsa-p224': gen_ecdsa_p224,
+ 'ed25519': gen_ed25519,
+ 'x25519': gen_x25519,
+}
+
+
+def load_key(keyfile):
+ # TODO: better handling of invalid pass-phrase
+ key = keys.load(keyfile)
+ if key is not None:
+ return key
+ passwd = getpass.getpass("Enter key passphrase: ").encode('utf-8')
+ return keys.load(keyfile, passwd)
+
+
+def get_password():
+ while True:
+ passwd = getpass.getpass("Enter key passphrase: ")
+ passwd2 = getpass.getpass("Reenter passphrase: ")
+ if passwd == passwd2:
+ break
+ print("Passwords do not match, try again")
+
+ # Password must be bytes, always use UTF-8 for consistent
+ # encoding.
+ return passwd.encode('utf-8')
+
+
+@click.option('-p', '--password', is_flag=True,
+ help='Prompt for password to protect key')
+@click.option('-t', '--type', metavar='type', required=True,
+ type=click.Choice(keygens.keys()), prompt=True,
+ help='{}'.format('One of: {}'.format(', '.join(keygens.keys()))))
+@click.option('-k', '--key', metavar='filename', required=True)
+@click.command(help='Generate pub/private keypair')
+def keygen(type, key, password):
+ password = get_password() if password else None
+ keygens[type](key, password)
+
+
+@click.option('-l', '--lang', metavar='lang', default=valid_langs[0],
+ type=click.Choice(valid_langs))
+@click.option('-k', '--key', metavar='filename', required=True)
+@click.command(help='Dump public key from keypair')
+def getpub(key, lang):
+ key = load_key(key)
+ if key is None:
+ print("Invalid passphrase")
+ elif lang == 'c':
+ key.emit_c_public()
+ elif lang == 'rust':
+ key.emit_rust_public()
+ else:
+ raise ValueError("BUG: should never get here!")
+
+
+@click.option('--minimal', default=False, is_flag=True,
+ help='Reduce the size of the dumped private key to include only '
+ 'the minimum amount of data required to decrypt. This '
+ 'might require changes to the build config. Check the docs!'
+ )
+@click.option('-k', '--key', metavar='filename', required=True)
+@click.command(help='Dump private key from keypair')
+def getpriv(key, minimal):
+ key = load_key(key)
+ if key is None:
+ print("Invalid passphrase")
+ try:
+ key.emit_private(minimal)
+ except (RSAUsageError, ECDSAUsageError, Ed25519UsageError,
+ X25519UsageError) as e:
+ raise click.UsageError(e)
+
+
+@click.argument('imgfile')
+@click.option('-k', '--key', metavar='filename')
+@click.command(help="Check that signed image can be verified by given key")
+def verify(key, imgfile):
+ key = load_key(key) if key else None
+ ret, version = image.Image.verify(imgfile, key)
+ if ret == image.VerifyResult.OK:
+ print("Image was correctly validated")
+ print("Image version: {}.{}.{}+{}".format(*version))
+ return
+ elif ret == image.VerifyResult.INVALID_MAGIC:
+ print("Invalid image magic; is this an MCUboot image?")
+ elif ret == image.VerifyResult.INVALID_TLV_INFO_MAGIC:
+ print("Invalid TLV info magic; is this an MCUboot image?")
+ elif ret == image.VerifyResult.INVALID_HASH:
+ print("Image has an invalid sha256 digest")
+ elif ret == image.VerifyResult.INVALID_SIGNATURE:
+ print("No signature found for the given key")
+ else:
+ print("Unknown return code: {}".format(ret))
+ sys.exit(1)
+
+
+def validate_version(ctx, param, value):
+ try:
+ decode_version(value)
+ return value
+ except ValueError as e:
+ raise click.BadParameter("{}".format(e))
+
+
+def validate_security_counter(ctx, param, value):
+ if value is not None:
+ if value.lower() == 'auto':
+ return 'auto'
+ else:
+ try:
+ return int(value, 0)
+ except ValueError:
+ raise click.BadParameter(
+ "{} is not a valid integer. Please use code literals "
+ "prefixed with 0b/0B, 0o/0O, or 0x/0X as necessary."
+ .format(value))
+
+
+def validate_header_size(ctx, param, value):
+ min_hdr_size = image.IMAGE_HEADER_SIZE
+ if value < min_hdr_size:
+ raise click.BadParameter(
+ "Minimum value for -H/--header-size is {}".format(min_hdr_size))
+ return value
+
+
+def get_dependencies(ctx, param, value):
+ if value is not None:
+ versions = []
+ images = re.findall(r"\((\d+)", value)
+ if len(images) == 0:
+ raise click.BadParameter(
+ "Image dependency format is invalid: {}".format(value))
+ raw_versions = re.findall(r",\s*([0-9.+]+)\)", value)
+ if len(images) != len(raw_versions):
+ raise click.BadParameter(
+ '''There's a mismatch between the number of dependency images
+ and versions in: {}'''.format(value))
+ for raw_version in raw_versions:
+ try:
+ versions.append(decode_version(raw_version))
+ except ValueError as e:
+ raise click.BadParameter("{}".format(e))
+ dependencies = dict()
+ dependencies[image.DEP_IMAGES_KEY] = images
+ dependencies[image.DEP_VERSIONS_KEY] = versions
+ return dependencies
+
+
+class BasedIntParamType(click.ParamType):
+ name = 'integer'
+
+ def convert(self, value, param, ctx):
+ try:
+ return int(value, 0)
+ except ValueError:
+ self.fail('%s is not a valid integer. Please use code literals '
+ 'prefixed with 0b/0B, 0o/0O, or 0x/0X as necessary.'
+ % value, param, ctx)
+
+
+@click.argument('outfile')
+@click.argument('infile')
+@click.option('-R', '--erased-val', type=click.Choice(['0', '0xff']),
+ required=False,
+ help='The value that is read back from erased flash.')
+@click.option('-x', '--hex-addr', type=BasedIntParamType(), required=False,
+ help='Adjust address in hex output file.')
+@click.option('-L', '--load-addr', type=BasedIntParamType(), required=False,
+ help='Load address for image when it should run from RAM.')
+@click.option('--save-enctlv', default=False, is_flag=True,
+ help='When upgrading, save encrypted key TLVs instead of plain '
+ 'keys. Enable when BOOT_SWAP_SAVE_ENCTLV config option '
+ 'was set.')
+@click.option('-E', '--encrypt', metavar='filename',
+ help='Encrypt image using the provided public key')
+@click.option('-e', '--endian', type=click.Choice(['little', 'big']),
+ default='little', help="Select little or big endian")
+@click.option('--overwrite-only', default=False, is_flag=True,
+ help='Use overwrite-only instead of swap upgrades')
+@click.option('--boot-record', metavar='sw_type', help='Create CBOR encoded '
+ 'boot record TLV. The sw_type represents the role of the '
+ 'software component (e.g. CoFM for coprocessor firmware). '
+ '[max. 12 characters]')
+@click.option('-M', '--max-sectors', type=int,
+ help='When padding allow for this amount of sectors (defaults '
+ 'to 128)')
+@click.option('--confirm', default=False, is_flag=True,
+ help='When padding the image, mark it as confirmed')
+@click.option('--pad', default=False, is_flag=True,
+ help='Pad image to --slot-size bytes, adding trailer magic')
+@click.option('-S', '--slot-size', type=BasedIntParamType(), required=True,
+ help='Size of the slot where the image will be written')
+@click.option('--pad-header', default=False, is_flag=True,
+ help='Add --header-size zeroed bytes at the beginning of the '
+ 'image')
+@click.option('-H', '--header-size', callback=validate_header_size,
+ type=BasedIntParamType(), required=True)
+@click.option('--pad-sig', default=False, is_flag=True,
+ help='Add 0-2 bytes of padding to ECDSA signature '
+ '(for mcuboot <1.5)')
+@click.option('-d', '--dependencies', callback=get_dependencies,
+ required=False, help='''Add dependence on another image, format:
+ "(,), ... "''')
+@click.option('-s', '--security-counter', callback=validate_security_counter,
+ help='Specify the value of security counter. Use the `auto` '
+ 'keyword to automatically generate it from the image version.')
+@click.option('-v', '--version', callback=validate_version, required=True)
+@click.option('--align', type=click.Choice(['1', '2', '4', '8']),
+ required=True)
+@click.option('--public-key-format', type=click.Choice(['hash', 'full']),
+ default='hash', help='In what format to add the public key to '
+ 'the image manifest: full key or hash of the key.')
+@click.option('-k', '--key', metavar='filename')
+@click.command(help='''Create a signed or unsigned image\n
+ INFILE and OUTFILE are parsed as Intel HEX if the params have
+ .hex extension, otherwise binary format is used''')
+def sign(key, public_key_format, align, version, pad_sig, header_size,
+ pad_header, slot_size, pad, confirm, max_sectors, overwrite_only,
+ endian, encrypt, infile, outfile, dependencies, load_addr, hex_addr,
+ erased_val, save_enctlv, security_counter, boot_record):
+ img = image.Image(version=decode_version(version), header_size=header_size,
+ pad_header=pad_header, pad=pad, confirm=confirm,
+ align=int(align), slot_size=slot_size,
+ max_sectors=max_sectors, overwrite_only=overwrite_only,
+ endian=endian, load_addr=load_addr, erased_val=erased_val,
+ save_enctlv=save_enctlv,
+ security_counter=security_counter)
+ img.load(infile)
+ key = load_key(key) if key else None
+ enckey = load_key(encrypt) if encrypt else None
+ if enckey and key:
+ if ((isinstance(key, keys.ECDSA256P1) and
+ not isinstance(enckey, keys.ECDSA256P1Public))
+ or (isinstance(key, keys.RSA) and
+ not isinstance(enckey, keys.RSAPublic))):
+ # FIXME
+ raise click.UsageError("Signing and encryption must use the same "
+ "type of key")
+
+ if pad_sig and hasattr(key, 'pad_sig'):
+ key.pad_sig = True
+
+ img.create(key, public_key_format, enckey, dependencies, boot_record)
+ img.save(outfile, hex_addr)
+
+
+class AliasesGroup(click.Group):
+
+ _aliases = {
+ "create": "sign",
+ }
+
+ def list_commands(self, ctx):
+ cmds = [k for k in self.commands]
+ aliases = [k for k in self._aliases]
+ return sorted(cmds + aliases)
+
+ def get_command(self, ctx, cmd_name):
+ rv = click.Group.get_command(self, ctx, cmd_name)
+ if rv is not None:
+ return rv
+ if cmd_name in self._aliases:
+ return click.Group.get_command(self, ctx, self._aliases[cmd_name])
+ return None
+
+
+@click.command(help='Print imgtool version information')
+def version():
+ print(imgtool_version)
+
+
+@click.command(cls=AliasesGroup,
+ context_settings=dict(help_option_names=['-h', '--help']))
+def imgtool():
+ pass
+
+
+imgtool.add_command(keygen)
+imgtool.add_command(getpub)
+imgtool.add_command(getpriv)
+imgtool.add_command(verify)
+imgtool.add_command(sign)
+imgtool.add_command(version)
+
+
+if __name__ == '__main__':
+ imgtool()
diff --git a/tools/mcuboot/imgtool/version.py b/tools/mcuboot/imgtool/version.py
new file mode 100644
index 00000000..030b012c
--- /dev/null
+++ b/tools/mcuboot/imgtool/version.py
@@ -0,0 +1,53 @@
+# Copyright 2017 Linaro Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Semi Semantic Versioning
+
+Implements a subset of semantic versioning that is supportable by the image
+header.
+"""
+
+from collections import namedtuple
+import re
+
+SemiSemVersion = namedtuple('SemiSemVersion', ['major', 'minor', 'revision',
+ 'build'])
+
+version_re = re.compile(
+ r"""^([1-9]\d*|0)(\.([1-9]\d*|0)(\.([1-9]\d*|0)(\+([1-9]\d*|0))?)?)?$""")
+
+
+def decode_version(text):
+ """Decode the version string, which should be of the form maj.min.rev+build
+ """
+ m = version_re.match(text)
+ if m:
+ result = SemiSemVersion(
+ int(m.group(1)) if m.group(1) else 0,
+ int(m.group(3)) if m.group(3) else 0,
+ int(m.group(5)) if m.group(5) else 0,
+ int(m.group(7)) if m.group(7) else 0)
+ return result
+ else:
+ msg = "Invalid version number, should be maj.min.rev+build with later "
+ msg += "parts optional"
+ raise ValueError(msg)
+
+
+if __name__ == '__main__':
+ print(decode_version("1.2"))
+ print(decode_version("1.0"))
+ print(decode_version("0.0.2+75"))
+ print(decode_version("0.0.0+00"))
diff --git a/tools/mcuboot/jgdb.sh b/tools/mcuboot/jgdb.sh
new file mode 100644
index 00000000..a79c87c6
--- /dev/null
+++ b/tools/mcuboot/jgdb.sh
@@ -0,0 +1,6 @@
+#! /bin/bash
+
+source $(dirname $0)/../target.sh
+
+# Start the jlink gdb server
+JLinkGDBServer -if swd -device $SOC -speed auto
diff --git a/tools/mcuboot/jl.sh b/tools/mcuboot/jl.sh
new file mode 100644
index 00000000..260206d5
--- /dev/null
+++ b/tools/mcuboot/jl.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+source $(dirname $0)/../target.sh
+
+JLinkExe -speed auto -si SWD -device $SOC
diff --git a/tools/mcuboot/mcubin.bt b/tools/mcuboot/mcubin.bt
new file mode 100644
index 00000000..e2ec3614
--- /dev/null
+++ b/tools/mcuboot/mcubin.bt
@@ -0,0 +1,135 @@
+// Copyright (C) 2019, Linaro Ltd
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file is a Binary Template file for the 010 Editor
+// (http://www.sweetscape.com/010editor/) to allow it to show the
+// structure of an MCUboot image.
+
+LittleEndian();
+
+struct ENTRY {
+ uint32 id;
+ uint32 offset;
+ uint32 size;
+ uint32 pad;
+};
+
+// The simulator writes the partition table at the beginning of the
+// image, so that we can tell where the partitions are. If you are
+// trying to view an image captured from a device, you can either
+// construct a synthetic partition table in the file, or change code
+// described below to hardcode one.
+struct PTABLE {
+ uchar pheader[8];
+ if (ptable.pheader != "mcuboot\0") {
+ // NOTE: Put code here to hard code a partition table, and
+ // continue.
+ Warning("Invalid magic on ptable header");
+ return -1;
+ } else {
+ uint32 count;
+ struct ENTRY entries[count];
+ }
+};
+
+struct PTABLE ptable;
+
+struct IMAGE_VERSION {
+ uchar major;
+ uchar minor;
+ uint16 revision;
+ uint32 build_num;
+};
+
+struct IHDR {
+ uint32 magic ;
+ uint32 load_addr ;
+ uint16 hdr_size ;
+ uint16 protect_size ;
+ uint32 img_size ;
+ uint32 flags;
+ struct IMAGE_VERSION ver;
+ uint32 _pad1;
+};
+
+struct TLV_HDR {
+ uint16 magic;
+ uint16 tlv_tot;
+};
+
+struct TLV {
+ uchar type ;
+ uchar pad;
+ uint16 len;
+
+ switch (type) {
+ case 0x01: // keyhash
+ uchar keyhash[len];
+ break;
+ case 0x40: // dependency
+ if (len != 12) {
+ Warning("Invalid dependency size");
+ return -1;
+ }
+ uchar image_id;
+ uchar pad1;
+ uint16 pad2;
+ struct IMAGE_VERSION version;
+ break;
+ default:
+ // Other, just consume the data.
+ uchar data[len];
+ }
+};
+
+local int i;
+local int epos;
+
+for (i = 0; i < ptable.count; i++) {
+ FSeek(ptable.entries[i].offset);
+ switch (ptable.entries[i].id) {
+ case 1:
+ case 2:
+ case 4:
+ case 5:
+ struct IMAGE {
+ struct IHDR ihdr;
+
+ if (ihdr.magic == 0x96f3b83d) {
+ uchar payload[ihdr.img_size];
+
+ epos = FTell();
+ struct TLV_HDR tlv_hdr;
+
+ if (tlv_hdr.magic == 0x6907) {
+ epos += tlv_hdr.tlv_tot;
+ while (FTell() < epos) {
+ struct TLV tlv;
+ }
+ }
+ }
+ // uchar block[ptable.entries[i].size];
+ } image;
+ break;
+ case 3:
+ struct SCRATCH {
+ uchar data[ptable.entries[i].size];
+ } scratch;
+ break;
+ default:
+ break;
+ }
+}
diff --git a/tools/mcuboot/requirements.txt b/tools/mcuboot/requirements.txt
new file mode 100644
index 00000000..9481e2c1
--- /dev/null
+++ b/tools/mcuboot/requirements.txt
@@ -0,0 +1,4 @@
+cryptography>=2.6
+intelhex
+click
+cbor>=1.0.0
diff --git a/tools/mcuboot/setup.py b/tools/mcuboot/setup.py
new file mode 100644
index 00000000..058d0cb4
--- /dev/null
+++ b/tools/mcuboot/setup.py
@@ -0,0 +1,29 @@
+import setuptools
+from imgtool import imgtool_version
+
+setuptools.setup(
+ name="imgtool",
+ version=imgtool_version,
+ author="The MCUboot committers",
+ author_email="dev-mcuboot@lists.runtime.co",
+ description=("MCUboot's image signing and key management"),
+ license="Apache Software License",
+ url="http://github.com/JuulLabs-OSS/mcuboot",
+ packages=setuptools.find_packages(),
+ python_requires='>=3.6',
+ install_requires=[
+ 'cryptography>=2.4.2',
+ 'intelhex>=2.2.1',
+ 'click',
+ 'cbor>=1.0.0',
+ ],
+ entry_points={
+ "console_scripts": ["imgtool=imgtool.main:imgtool"]
+ },
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "Development Status :: 4 - Beta",
+ "Topic :: Software Development :: Build Tools",
+ "License :: OSI Approved :: Apache Software License",
+ ],
+)
diff --git a/tools/rle_encode.py b/tools/rle_encode.py
new file mode 100644
index 00000000..80a0926f
--- /dev/null
+++ b/tools/rle_encode.py
@@ -0,0 +1,379 @@
+#!/usr/bin/env python3
+
+# SPDX-License-Identifier: LGPL-3.0-or-later
+# Copyright (C) 2020 Daniel Thompson
+
+import argparse
+import sys
+import os.path
+from PIL import Image
+
+def clut8_rgb888(i):
+ """Reference CLUT for wasp-os.
+
+ Technically speaking this is not a CLUT because the we lookup the colours
+ algorithmically to avoid the cost of a genuine CLUT. The palette is
+ designed to be fairly easy to generate algorithmically.
+
+ The palette includes all 216 web-safe colours together 4 grays and
+ 36 additional colours that target "gaps" at the brighter end of the web
+ safe set. There are 11 greys (plus black and white) although two are
+ fairly close together.
+
+ :param int i: Index (from 0..255 inclusive) into the CLUT
+ :return: 24-bit colour in RGB888 format
+ """
+ if i < 216:
+ rgb888 = ( i % 6) * 0x33
+ rg = i // 6
+ rgb888 += (rg % 6) * 0x3300
+ rgb888 += (rg // 6) * 0x330000
+ elif i < 252:
+ i -= 216
+ rgb888 = 0x7f + (( i % 3) * 0x33)
+ rg = i // 3
+ rgb888 += 0x4c00 + ((rg % 4) * 0x3300)
+ rgb888 += 0x7f0000 + ((rg // 4) * 0x330000)
+ else:
+ i -= 252
+ rgb888 = 0x2c2c2c + (0x101010 * i)
+
+ return rgb888
+
+def clut8_rgb565(i):
+ """RBG565 CLUT for wasp-os.
+
+ This CLUT implements the same palette as :py:meth:`clut8_888` but
+ outputs RGB565 pixels.
+
+ .. note::
+
+ This function is unused within this file but needs to be
+ maintained alongside the reference clut so it is reproduced
+ here.
+
+ :param int i: Index (from 0..255 inclusive) into the CLUT
+ :return: 16-bit colour in RGB565 format
+ """
+ if i < 216:
+ rgb565 = (( i % 6) * 0x33) >> 3
+ rg = i // 6
+ rgb565 += ((rg % 6) * (0x33 << 3)) & 0x07e0
+ rgb565 += ((rg // 6) * (0x33 << 8)) & 0xf800
+ elif i < 252:
+ i -= 216
+ rgb565 = (0x7f + (( i % 3) * 0x33)) >> 3
+ rg = i // 3
+ rgb565 += ((0x4c << 3) + ((rg % 4) * (0x33 << 3))) & 0x07e0
+ rgb565 += ((0x7f << 8) + ((rg // 4) * (0x33 << 8))) & 0xf800
+ else:
+ i -= 252
+ gr6 = (0x2c + (0x10 * i)) >> 2
+ gr5 = gr6 >> 1
+ rgb565 = (gr5 << 11) + (gr6 << 5) + gr5
+
+ return rgb565
+
+class ReverseCLUT:
+ def __init__(self, clut):
+ l = []
+ for i in range(256):
+ l.append(clut(i))
+ self.clut = tuple(l)
+ self.lookup = {}
+
+ def __call__(self, rgb888):
+ """Compare rgb888 to every element of the CLUT and pick the
+ closest match.
+ """
+ if rgb888 in self.lookup:
+ return self.lookup[rgb888]
+
+ best = 200000
+ index = -1
+ clut = self.clut
+ r = rgb888 >> 16
+ g = (rgb888 >> 8) & 0xff
+ b = rgb888 & 0xff
+
+ for i in range(256):
+ candidate = clut[i]
+ rd = r - (candidate >> 16)
+ gd = g - ((candidate >> 8) & 0xff)
+ bd = b - (candidate & 0xff)
+ # This is the Euclidian distance (squared)
+ distance = rd * rd + gd * gd + bd * bd
+ if distance < best:
+ best = distance
+ index = i
+
+ self.lookup[rgb888] = index
+ #print(f'# #{rgb888:06x} -> #{clut8_rgb888(index):06x}')
+ return index
+
+def varname(p):
+ return os.path.basename(os.path.splitext(p)[0])
+
+def encode(im):
+ pixels = im.load()
+
+ rle = []
+ rl = 0
+ px = pixels[0, 0]
+
+ def encode_pixel(px, rl):
+ while rl > 255:
+ rle.append(255)
+ rle.append(0)
+ rl -= 255
+ rle.append(rl)
+
+ for y in range(im.height):
+ for x in range(im.width):
+ newpx = pixels[x, y]
+ if newpx == px:
+ rl += 1
+ assert(rl < (1 << 21))
+ continue
+
+ # Code the previous run
+ encode_pixel(px, rl)
+
+ # Start a new run
+ rl = 1
+ px = newpx
+
+ # Handle the final run
+ encode_pixel(px, rl)
+
+ return (im.width, im.height, bytes(rle))
+
+def encode_2bit(im):
+ """2-bit palette based RLE encoder.
+
+ This encoder has a reprogrammable 2-bit palette. This allows it to encode
+ arbitrary images with a full 8-bit depth but the 2-byte overhead each time
+ a new colour is introduced means it is not efficient unless the image is
+ carefully constructed to keep a good locality of reference for the three
+ non-background colours.
+
+ The encoding competes well with the 1-bit encoder for small monochrome
+ images but once run-lengths longer than 62 start to become frequent then
+ this encoding is about 30% larger than a 1-bit encoding.
+ """
+ pixels = im.load()
+ assert(im.width <= 255)
+ assert(im.height <= 255)
+
+ full_palette = ReverseCLUT(clut8_rgb888)
+
+ rle = []
+ rl = 0
+ px = pixels[0, 0]
+ # black, grey25, grey50, white
+ palette = [0, 254, 219, 215]
+ next_color = 1
+
+ def encode_pixel(px, rl):
+ nonlocal next_color
+ px = full_palette((px[0] << 16) + (px[1] << 8) + px[2])
+ if px not in palette:
+ rle.append(next_color << 6)
+ rle.append(px)
+ palette[next_color] = px
+ next_color += 1
+ if next_color >= len(palette):
+ next_color = 1
+ px = palette.index(px)
+ if rl >= 63:
+ rle.append((px << 6) + 63)
+ rl -= 63
+ while rl >= 255:
+ rle.append(255)
+ rl -= 255
+ rle.append(rl)
+ else:
+ rle.append((px << 6) + rl)
+
+ # Issue the descriptor
+ rle.append(2)
+ rle.append(im.width)
+ rle.append(im.height)
+
+ for y in range(im.height):
+ for x in range(im.width):
+ newpx = pixels[x, y]
+ if newpx == px:
+ rl += 1
+ assert(rl < (1 << 21))
+ continue
+
+ # Code the previous run
+ encode_pixel(px, rl)
+
+ # Start a new run
+ rl = 1
+ px = newpx
+
+ # Handle the final run
+ encode_pixel(px, rl)
+
+ return bytes(rle)
+
+def encode_8bit(im):
+ """Experimental 8-bit RLE encoder.
+
+ For monochrome images this is about 3x less efficient than the 1-bit
+ encoder. This encoder is not currently used anywhere in wasp-os and
+ currently there is no decoder either (so don't assume this code
+ actually works).
+ """
+ pixels = im.load()
+
+ rle = []
+ rl = 0
+ px = pixels[0, 0]
+
+ def encode_pixel(px, rl):
+ px = (px[0] & 0xe0) | ((px[1] & 0xe0) >> 3) | ((px[2] & 0xc0) >> 6)
+
+ rle.append(px)
+ if rl > 0:
+ rle.append(px)
+ rl -= 2
+ if rl > (1 << 14):
+ rle.append(0x80 | ((rl >> 14) & 0x7f))
+ if rl > (1 << 7):
+ rle.append(0x80 | ((rl >> 7) & 0x7f))
+ if rl >= 0:
+ rle.append( rl & 0x7f )
+
+ for y in range(im.height):
+ for x in range(im.width):
+ newpx = pixels[x, y]
+ if newpx == px:
+ rl += 1
+ assert(rl < (1 << 21))
+ continue
+
+ # Code the previous run
+ encode_pixel(px, rl)
+
+ # Start a new run
+ rl = 1
+ px = newpx
+
+ # Handle the final run
+ encode_pixel(px, rl)
+
+ return (im.width, im.height, bytes(rle))
+
+def render_c(image, fname, indent, depth):
+ extra_indent = ' ' * indent
+ if len(image) == 3:
+ print(f'{extra_indent}// {depth}-bit RLE, generated from {fname}, '
+ f'{len(image[2])} bytes')
+ (x, y, pixels) = image
+ else:
+ print(f'{extra_indent}// {depth}-bit RLE, generated from {fname}, '
+ f'{len(image)} bytes')
+ pixels = image
+
+ print(f'{extra_indent}static const uint8_t {varname(fname)}[] = {{')
+ print(f'{extra_indent} ', end='')
+ i = 0
+ for rl in pixels:
+ print(f' {hex(rl)},', end='')
+
+ i += 1
+ if i == 12:
+ print(f'\n{extra_indent} ', end='')
+ i = 0
+ print('\n};')
+
+def render_py(image, fname, indent, depth):
+ extra_indent = ' ' * indent
+ if len(image) == 3:
+ print(f'{extra_indent}# {depth}-bit RLE, generated from {fname}, '
+ f'{len(image[2])} bytes')
+ (x, y, pixels) = image
+ print(f'{extra_indent}{varname(fname)} = (')
+ print(f'{extra_indent} {x}, {y},')
+ else:
+ print(f'{extra_indent}# {depth}-bit RLE, generated from {fname}, '
+ f'{len(image)} bytes')
+ pixels = image[3:]
+ print(f'{extra_indent}{varname(fname)} = (')
+ print(f'{extra_indent} {image[0:1]}')
+ print(f'{extra_indent} {image[1:3]}')
+
+ # Split the bytestring to ensure each line is short enough to
+ # be absorbed on the target if needed.
+ for i in range(0, len(pixels), 16):
+ print(f'{extra_indent} {pixels[i:i+16]}')
+ print(f'{extra_indent})')
+
+
+def decode_to_ascii(image):
+ (sx, sy, rle) = image
+ data = bytearray(2*sx)
+ dp = 0
+ black = ord('#')
+ white = ord(' ')
+ color = black
+
+ for rl in rle:
+ while rl:
+ data[dp] = color
+ data[dp+1] = color
+ dp += 2
+ rl -= 1
+
+ if dp >= (2*sx):
+ print(data.decode('utf-8'))
+ dp = 0
+
+ if color == black:
+ color = white
+ else:
+ color = black
+
+ # Check the image is the correct length
+ assert(dp == 0)
+
+parser = argparse.ArgumentParser(description='RLE encoder tool.')
+parser.add_argument('files', nargs='+',
+ help='files to be encoded')
+parser.add_argument('--ascii', action='store_true',
+ help='Run the resulting image(s) through an ascii art decoder')
+parser.add_argument('--c', action='store_true',
+ help='Render the output as C instead of python')
+parser.add_argument('--indent', default=0, type=int,
+ help='Add extra indentation in the generated code')
+parser.add_argument('--2bit', action='store_true', dest='twobit',
+ help='Generate 2-bit image')
+parser.add_argument('--8bit', action='store_true', dest='eightbit',
+ help='Generate 8-bit image')
+
+args = parser.parse_args()
+if args.eightbit:
+ encoder = encode_8bit
+ depth = 8
+elif args.twobit:
+ encoder = encode_2bit
+ depth = 2
+else:
+ encoder = encode
+ depth =1
+
+for fname in args.files:
+ image = encoder(Image.open(fname))
+
+ if args.c:
+ render_c(image, fname, args.indent, depth)
+ else:
+ render_py(image, fname, args.indent, depth)
+
+ if args.ascii:
+ print()
+ decode_to_ascii(image)
\ No newline at end of file