Merge branch 'recovery-firmware' of JF/PineTime into develop
This commit is contained in:
commit
c5dc4c55a7
|
@ -8,30 +8,10 @@
|
|||
<option name="INDENT_DIRECTIVE_AS_CODE" value="true" />
|
||||
<option name="SPACE_BEFORE_TEMPLATE_DECLARATION_LT" value="true" />
|
||||
<option name="SPACE_BEFORE_POINTER_IN_DECLARATION" value="false" />
|
||||
<!-- Disabled until clang-format can do it <option name="SPACE_AFTER_POINTER_IN_DECLARATION" value="true" /> -->
|
||||
<option name="SPACE_AFTER_POINTER_IN_DECLARATION" value="true" />
|
||||
<option name="SPACE_BEFORE_REFERENCE_IN_DECLARATION" value="false" />
|
||||
<option name="SPACE_AFTER_REFERENCE_IN_DECLARATION" value="true" />
|
||||
</Objective-C>
|
||||
<Objective-C-extensions>
|
||||
<rules>
|
||||
<rule entity="NAMESPACE" visibility="ANY" specifier="ANY" prefix="" style="PASCAL_CASE" suffix="" />
|
||||
<rule entity="MACRO" visibility="ANY" specifier="ANY" prefix="" style="SCREAMING_SNAKE_CASE" suffix="" />
|
||||
<rule entity="CLASS" visibility="ANY" specifier="ANY" prefix="" style="PASCAL_CASE" suffix="" />
|
||||
<rule entity="STRUCT" visibility="ANY" specifier="ANY" prefix="" style="CAMEL_CASE" suffix="" />
|
||||
<rule entity="ENUM" visibility="ANY" specifier="ANY" prefix="" style="PASCAL_CASE" suffix="" />
|
||||
<rule entity="ENUMERATOR" visibility="ANY" specifier="ANY" prefix="" style="PASCAL_CASE" suffix="" />
|
||||
<rule entity="TYPEDEF" visibility="ANY" specifier="ANY" prefix="" style="CAMEL_CASE" suffix="" />
|
||||
<rule entity="UNION" visibility="ANY" specifier="ANY" prefix="" style="CAMEL_CASE" suffix="" />
|
||||
<rule entity="CLASS_MEMBER_FUNCTION" visibility="ANY" specifier="ANY" prefix="" style="PASCAL_CASE" suffix="" />
|
||||
<rule entity="STRUCT_MEMBER_FUNCTION" visibility="ANY" specifier="ANY" prefix="" style="CAMEL_CASE" suffix="" />
|
||||
<rule entity="CLASS_MEMBER_FIELD" visibility="ANY" specifier="ANY" prefix="" style="CAMEL_CASE" suffix="" />
|
||||
<rule entity="STRUCT_MEMBER_FIELD" visibility="ANY" specifier="ANY" prefix="" style="CAMEL_CASE" suffix="" />
|
||||
<rule entity="GLOBAL_FUNCTION" visibility="ANY" specifier="ANY" prefix="" style="PASCAL_CASE" suffix="" />
|
||||
<rule entity="GLOBAL_VARIABLE" visibility="ANY" specifier="ANY" prefix="" style="CAMEL_CASE" suffix="" />
|
||||
<rule entity="PARAMETER" visibility="ANY" specifier="ANY" prefix="" style="CAMEL_CASE" suffix="" />
|
||||
<rule entity="LOCAL_VARIABLE" visibility="ANY" specifier="ANY" prefix="" style="CAMEL_CASE" suffix="" />
|
||||
</rules>
|
||||
</Objective-C-extensions>
|
||||
<codeStyleSettings language="ObjectiveC">
|
||||
<option name="RIGHT_MARGIN" value="140" />
|
||||
<option name="IF_BRACE_FORCE" value="3" />
|
||||
|
@ -47,8 +27,5 @@
|
|||
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<clangFormatSettings>
|
||||
<option name="ENABLED" value="true" />
|
||||
</clangFormatSettings>
|
||||
</code_scheme>
|
||||
</component>
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 .
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:DEBUG>>: ${COMMON_FLAGS} -O0 -g3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:RELEASE>>: ${COMMON_FLAGS} -O3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:DEBUG>>: ${COMMON_FLAGS} -O0 -g3>
|
||||
|
@ -863,21 +929,142 @@ target_compile_options(${EXECUTABLE_GRAPHICS_NAME} PUBLIC
|
|||
$<$<COMPILE_LANGUAGE:ASM>: -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
|
||||
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:DEBUG>>: ${COMMON_FLAGS} -O0 -g3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:RELEASE>>: ${COMMON_FLAGS} -O3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:DEBUG>>: ${COMMON_FLAGS} -O0 -g3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:RELEASE>>: ${COMMON_FLAGS} -O3>
|
||||
$<$<COMPILE_LANGUAGE:ASM>: -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
|
||||
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:DEBUG>>: ${COMMON_FLAGS} -O0 -g3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:RELEASE>>: ${COMMON_FLAGS} -O3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:DEBUG>>: ${COMMON_FLAGS} -O0 -g3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:RELEASE>>: ${COMMON_FLAGS} -O3>
|
||||
$<$<COMPILE_LANGUAGE:ASM>: -MP -MD -x assembler-with-cpp>
|
||||
)
|
||||
target_include_directories(${EXECUTABLE_RECOVERYLOADER_NAME} PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/src>
|
||||
)
|
||||
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
|
||||
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:DEBUG>>: ${COMMON_FLAGS} -O0 -g3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:RELEASE>>: ${COMMON_FLAGS} -O3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:DEBUG>>: ${COMMON_FLAGS} -O0 -g3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:RELEASE>>: ${COMMON_FLAGS} -O3>
|
||||
$<$<COMPILE_LANGUAGE:ASM>: -MP -MD -x assembler-with-cpp>
|
||||
)
|
||||
target_include_directories(${EXECUTABLE_MCUBOOT_RECOVERYLOADER_NAME} PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/src>
|
||||
)
|
||||
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
|
||||
|
|
39
src/components/rle/RleDecoder.cpp
Normal file
39
src/components/rle/RleDecoder.cpp
Normal file
|
@ -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<size; encodedBufferIndex++) {
|
||||
uint8_t rl = buffer[encodedBufferIndex] - processedCount;
|
||||
while (rl) {
|
||||
output[bp] = color >> 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;
|
||||
}
|
||||
}
|
||||
|
33
src/components/rle/RleDecoder.h
Normal file
33
src/components/rle/RleDecoder.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
110
src/displayapp/DisplayAppRecovery.cpp
Normal file
110
src/displayapp/DisplayAppRecovery.cpp
Normal file
|
@ -0,0 +1,110 @@
|
|||
#include "DisplayAppRecovery.h"
|
||||
#include "DisplayAppRecovery.h"
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
#include <libraries/log/nrf_log.h>
|
||||
#include <components/rle/RleDecoder.h>
|
||||
#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<DisplayApp *>(instance);
|
||||
NRF_LOG_INFO("displayapp task started!");
|
||||
|
||||
// Send a dummy notification to unlock the lvgl display driver for the first iteration
|
||||
xTaskNotifyGive(xTaskGetCurrentTaskHandle());
|
||||
|
||||
app->InitHw();
|
||||
while (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<float>(bleController.FirmwareUpdateCurrentBytes()) /
|
||||
static_cast<float>(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<const uint8_t *>(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<float>(percent) * 2.4f, static_cast<float>(displayWidth));
|
||||
lcd.BeginDrawBuffer(0, displayWidth - barHeight + i, barWidth, 1);
|
||||
lcd.NextDrawBuffer(reinterpret_cast<const uint8_t *>(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?
|
||||
}
|
||||
}
|
71
src/displayapp/DisplayAppRecovery.h
Normal file
71
src/displayapp/DisplayAppRecovery.h
Normal file
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
#include <drivers/St7789.h>
|
||||
#include <drivers/SpiMaster.h>
|
||||
#include <bits/unique_ptr.h>
|
||||
#include <queue.h>
|
||||
#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 <date/date.h>
|
||||
#include "displayapp/screens/Clock.h"
|
||||
#include <drivers/Watchdog.h>
|
||||
#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];
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
30
src/displayapp/DummyLittleVgl.h
Normal file
30
src/displayapp/DummyLittleVgl.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include <libs/lvgl/src/lv_core/lv_style.h>
|
||||
#include <libs/lvgl/src/lv_themes/lv_theme.h>
|
||||
#include <libs/lvgl/src/lv_hal/lv_hal.h>
|
||||
#include <drivers/St7789.h>
|
||||
#include <drivers/Cst816s.h>
|
||||
|
||||
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) {}
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
11
src/displayapp/Messages.h
Normal file
11
src/displayapp/Messages.h
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
127
src/displayapp/icons/infinitime/infinitime-nb.c
Normal file
127
src/displayapp/icons/infinitime/infinitime-nb.c
Normal file
|
@ -0,0 +1,127 @@
|
|||
|
||||
#include <unistd.h>
|
||||
|
||||
// 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,
|
||||
};
|
BIN
src/displayapp/icons/infinitime/infinitime-nb.png
Normal file
BIN
src/displayapp/icons/infinitime/infinitime-nb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
|
@ -1 +1 @@
|
|||
Subproject commit b89c30eafe1397a778ae5d65f108a0ef820de124
|
||||
Subproject commit 69a50b97dafffddd212e4b006776b473951be7a6
|
16
src/main.cpp
16
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
#include <drivers/Spi.h>
|
||||
#include <drivers/SpiNorFlash.h>
|
||||
#include <libraries/log/nrf_log.h>
|
||||
#include "bootloader/boot_graphics.h"
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
#include <legacy/nrf_drv_gpiote.h>
|
||||
|
@ -14,6 +13,12 @@
|
|||
#include <components/gfx/Gfx.h>
|
||||
#include <drivers/St7789.h>
|
||||
#include <components/brightness/BrightnessController.h>
|
||||
#include <algorithm>
|
||||
#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<float>(offset) / static_cast<float>(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<uint8_t *>(displayLineBuffer), screenWidth);
|
||||
spiNorFlash.Read((line*screenWidthInBytes)+screenWidth, reinterpret_cast<uint8_t *>(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<const uint8_t *>(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<float>(percent) * 2.4f, static_cast<float>(displayWidth));
|
||||
lcd.BeginDrawBuffer(0, displayWidth - barHeight + i, barWidth, 1);
|
||||
lcd.NextDrawBuffer(reinterpret_cast<const uint8_t *>(displayBuffer), barWidth * bytesPerPixel);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
TaskHandle_t taskHandle;
|
||||
|
||||
RefreshWatchdog();
|
||||
logger.Init();
|
||||
nrf_drv_clock_init();
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
74
tools/bin2c.py
Normal file
74
tools/bin2c.py
Normal file
|
@ -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()
|
1
tools/mcuboot/README
Normal file
1
tools/mcuboot/README
Normal file
|
@ -0,0 +1 @@
|
|||
This whole folder comes from MCUBoot source files (commit 9015a5d404c2c688166cab81067be53c860d98f4).
|
131
tools/mcuboot/assemble.py
Normal file
131
tools/mcuboot/assemble.py
Normal file
|
@ -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()
|
18
tools/mcuboot/flash.sh
Normal file
18
tools/mcuboot/flash.sh
Normal file
|
@ -0,0 +1,18 @@
|
|||
#! /bin/bash
|
||||
|
||||
source $(dirname $0)/../target.sh
|
||||
|
||||
lscript=/tmp/flash$$.jlink
|
||||
|
||||
cat >$lscript <<EOF
|
||||
h
|
||||
r
|
||||
loadfile outdir/$BOARD/zephyr.bin $BASE_BOOT
|
||||
loadfile hello.signed.bin $BASE_PRIMARY_SLOT
|
||||
loadfile shell.signed.bin $BASE_SECONDARY_SLOT
|
||||
q
|
||||
EOF
|
||||
|
||||
JLinkExe -device $SOC -si SWD -speed auto \
|
||||
-CommanderScript $lscript
|
||||
rm $lscript
|
27
tools/mcuboot/gdb-boot.sh
Normal file
27
tools/mcuboot/gdb-boot.sh
Normal file
|
@ -0,0 +1,27 @@
|
|||
#! /bin/bash
|
||||
|
||||
source $(dirname $0)/../target.sh
|
||||
|
||||
gscript=/tmp/init$$.gdb
|
||||
|
||||
cat > $gscript <<EOF
|
||||
target remote localhost:2331
|
||||
symbol-file outdir/$BOARD/zephyr.elf
|
||||
# symbol-file ../zephyr/samples/shell/outdir/$BOARD/zephyr.elf
|
||||
# dir apps/boot/src
|
||||
# dir libs/bootutil/src
|
||||
# dir hw/mcu/stm/stm32f4xx/src
|
||||
b main
|
||||
# b __reset
|
||||
# b bootutil_img_validate
|
||||
# b cmp_rsasig
|
||||
# b bootutil_verify_sig
|
||||
# b mbedtls_rsa_public
|
||||
# b boot_calloc
|
||||
mon reset 2
|
||||
layout src
|
||||
focus cmd
|
||||
EOF
|
||||
|
||||
$gdbexe -x $gscript
|
||||
rm $gscript
|
30
tools/mcuboot/imgtool.nix
Normal file
30
tools/mcuboot/imgtool.nix
Normal file
|
@ -0,0 +1,30 @@
|
|||
#
|
||||
# Nix environment for imgtool
|
||||
#
|
||||
# To install the environment
|
||||
#
|
||||
# $ nix-env --file imgtool.nix --install env-imgtool
|
||||
#
|
||||
# To load the environment
|
||||
#
|
||||
# $ load-env-imgtool
|
||||
#
|
||||
with import <nixpkgs> {};
|
||||
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 ];
|
||||
}
|
20
tools/mcuboot/imgtool.py
Executable file
20
tools/mcuboot/imgtool.py
Executable file
|
@ -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()
|
15
tools/mcuboot/imgtool/__init__.py
Normal file
15
tools/mcuboot/imgtool/__init__.py
Normal file
|
@ -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"
|
47
tools/mcuboot/imgtool/boot_record.py
Normal file
47
tools/mcuboot/imgtool/boot_record.py
Normal file
|
@ -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)
|
552
tools/mcuboot/imgtool/image.py
Normal file
552
tools/mcuboot/imgtool/image.py
Normal file
|
@ -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 "<Image version={}, header_size={}, security_counter={}, \
|
||||
base_addr={}, load_addr={}, align={}, slot_size={}, \
|
||||
max_sectors={}, overwrite_only={}, endian={} format={}, \
|
||||
payloadlen=0x{:x}>".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
|
94
tools/mcuboot/imgtool/keys/__init__.py
Normal file
94
tools/mcuboot/imgtool/keys/__init__.py
Normal file
|
@ -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)))
|
157
tools/mcuboot/imgtool/keys/ecdsa.py
Normal file
157
tools/mcuboot/imgtool/keys/ecdsa.py
Normal file
|
@ -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
|
99
tools/mcuboot/imgtool/keys/ecdsa_test.py
Normal file
99
tools/mcuboot/imgtool/keys/ecdsa_test.py
Normal file
|
@ -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()
|
105
tools/mcuboot/imgtool/keys/ed25519.py
Normal file
105
tools/mcuboot/imgtool/keys/ed25519.py
Normal file
|
@ -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)
|
103
tools/mcuboot/imgtool/keys/ed25519_test.py
Normal file
103
tools/mcuboot/imgtool/keys/ed25519_test.py
Normal file
|
@ -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()
|
45
tools/mcuboot/imgtool/keys/general.py
Normal file
45
tools/mcuboot/imgtool/keys/general.py
Normal file
|
@ -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)
|
163
tools/mcuboot/imgtool/keys/rsa.py
Normal file
163
tools/mcuboot/imgtool/keys/rsa.py
Normal file
|
@ -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())
|
115
tools/mcuboot/imgtool/keys/rsa_test.py
Normal file
115
tools/mcuboot/imgtool/keys/rsa_test.py
Normal file
|
@ -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()
|
107
tools/mcuboot/imgtool/keys/x25519.py
Normal file
107
tools/mcuboot/imgtool/keys/x25519.py
Normal file
|
@ -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)
|
352
tools/mcuboot/imgtool/main.py
Normal file
352
tools/mcuboot/imgtool/main.py
Normal file
|
@ -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:
|
||||
"(<image_ID>,<image_version>), ... "''')
|
||||
@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()
|
53
tools/mcuboot/imgtool/version.py
Normal file
53
tools/mcuboot/imgtool/version.py
Normal file
|
@ -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"))
|
6
tools/mcuboot/jgdb.sh
Normal file
6
tools/mcuboot/jgdb.sh
Normal file
|
@ -0,0 +1,6 @@
|
|||
#! /bin/bash
|
||||
|
||||
source $(dirname $0)/../target.sh
|
||||
|
||||
# Start the jlink gdb server
|
||||
JLinkGDBServer -if swd -device $SOC -speed auto
|
5
tools/mcuboot/jl.sh
Normal file
5
tools/mcuboot/jl.sh
Normal file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
source $(dirname $0)/../target.sh
|
||||
|
||||
JLinkExe -speed auto -si SWD -device $SOC
|
135
tools/mcuboot/mcubin.bt
Normal file
135
tools/mcuboot/mcubin.bt
Normal file
|
@ -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 <format=hex>;
|
||||
uint32 load_addr <format=hex>;
|
||||
uint16 hdr_size <format=hex>;
|
||||
uint16 protect_size <format=hex>;
|
||||
uint32 img_size <format=hex>;
|
||||
uint32 flags;
|
||||
struct IMAGE_VERSION ver;
|
||||
uint32 _pad1;
|
||||
};
|
||||
|
||||
struct TLV_HDR {
|
||||
uint16 magic;
|
||||
uint16 tlv_tot;
|
||||
};
|
||||
|
||||
struct TLV {
|
||||
uchar type <format=hex>;
|
||||
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;
|
||||
}
|
||||
}
|
4
tools/mcuboot/requirements.txt
Normal file
4
tools/mcuboot/requirements.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
cryptography>=2.6
|
||||
intelhex
|
||||
click
|
||||
cbor>=1.0.0
|
29
tools/mcuboot/setup.py
Normal file
29
tools/mcuboot/setup.py
Normal file
|
@ -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",
|
||||
],
|
||||
)
|
379
tools/rle_encode.py
Normal file
379
tools/rle_encode.py
Normal file
|
@ -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)
|
Loading…
Reference in a new issue