diff --git a/Makefile b/Makefile index 32383f66..ab249aeb 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ ifeq ($(BOLOS_SDK),) $(error Environment variable BOLOS_SDK is not set) endif - +DISABLE_UI = 1 include $(BOLOS_SDK)/Makefile.defines ######################################## @@ -34,7 +34,19 @@ APPVERSION_P = 0 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" # Application source files -APP_SOURCE_PATH += src +APP_SOURCE_PATH += src sdk +ifeq ($(TARGET_NAME),TARGET_NANOS) +DEFINES += HAVE_NBGL NBGL_USE_CASE NBGL_STEP +DEFINES += TARGET_NANOS=1 +DEFINES += HAVE_BAGL +DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=32 +DISABLE_STANDARD_BAGL_UX_FLOW = 1 +SDK_SOURCE_PATH += lib_bagl +APP_SOURCE_PATH += sdk_lib_nbgl/src sdk_lib_ux_stax + +INCLUDES_PATH += sdk_lib_nbgl/include sdk_lib_ux_stax +CFLAGS += -Wno-macro-redefined +endif # Application icons following guidelines: # https://developers.ledger.com/docs/embedded-app/design-requirements/#device-icon diff --git a/ledger_app.toml b/ledger_app.toml index e557cc8f..f1139219 100644 --- a/ledger_app.toml +++ b/ledger_app.toml @@ -1,7 +1,7 @@ [app] build_directory = "./" sdk = "C" -devices = ["nanos", "nanox", "nanos+", "stax"] +devices = ["stax"] [tests] unit_directory = "./unit-tests/" diff --git a/sdk/io.c b/sdk/io.c new file mode 100644 index 00000000..588054d1 --- /dev/null +++ b/sdk/io.c @@ -0,0 +1,224 @@ +/***************************************************************************** + * (c) 2021 Ledger SAS. + * + * 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. + *****************************************************************************/ + +#include +#include + +#include "os.h" +#include "io.h" +#include "write.h" + +#ifdef HAVE_SWAP +#include "swap.h" +#endif + +// TODO: Temporary workaround, at some point all status words should be defined by the SDK and +// removed from the application +#define SW_OK 0x9000 +#define SW_WRONG_RESPONSE_LENGTH 0xB000 + +// Avoid conflict with SDK version +//uint8_t G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; + +/** + * Variable containing the length of the APDU response to send back. + */ +static uint32_t G_output_len = 0; + +/** + * IO state (READY, RECEIVING, WAITING). + */ +static io_state_e G_io_state = READY; + +#ifdef HAVE_BAGL +void io_seproxyhal_display(const bagl_element_t *element) +{ + io_seproxyhal_display_default(element); +} +#endif // HAVE_BAGL + +// This function can be used to declare a callback to SEPROXYHAL_TAG_TICKER_EVENT in the application +void app_ticker_event_callback(void) {} + +uint8_t io_event(uint8_t channel) +{ + (void) channel; + + switch (G_io_seproxyhal_spi_buffer[0]) { + case SEPROXYHAL_TAG_BUTTON_PUSH_EVENT: +#ifdef HAVE_BAGL + UX_BUTTON_PUSH_EVENT(G_io_seproxyhal_spi_buffer); +#endif // HAVE_BAGL + break; + case SEPROXYHAL_TAG_STATUS_EVENT: + if (G_io_apdu_media == IO_APDU_MEDIA_USB_HID && // + !(U4BE(G_io_seproxyhal_spi_buffer, 3) & // + SEPROXYHAL_TAG_STATUS_EVENT_FLAG_USB_POWERED)) { + THROW(EXCEPTION_IO_RESET); + } + __attribute__((fallthrough)); + case SEPROXYHAL_TAG_DISPLAY_PROCESSED_EVENT: +#ifdef HAVE_BAGL + UX_DISPLAYED_EVENT({}); +#endif // HAVE_BAGL +#ifdef HAVE_NBGL + UX_DEFAULT_EVENT(); +#endif // HAVE_NBGL + break; +#ifdef HAVE_NBGL + case SEPROXYHAL_TAG_FINGER_EVENT: + UX_FINGER_EVENT(G_io_seproxyhal_spi_buffer); + break; +#endif // HAVE_NBGL + case SEPROXYHAL_TAG_TICKER_EVENT: + app_ticker_event_callback(); + UX_TICKER_EVENT(G_io_seproxyhal_spi_buffer, {}); + break; + default: + UX_DEFAULT_EVENT(); + break; + } + + if (!io_seproxyhal_spi_is_status_sent()) { + io_seproxyhal_general_status(); + } + + return 1; +} + +uint16_t io_exchange_al(uint8_t channel, uint16_t tx_len) +{ + switch (channel & ~(IO_FLAGS)) { + case CHANNEL_KEYBOARD: + break; + case CHANNEL_SPI: + if (tx_len) { + io_seproxyhal_spi_send(G_io_apdu_buffer, tx_len); + + if (channel & IO_RESET_AFTER_REPLIED) { + halt(); + } + + return 0; + } + else { + return io_seproxyhal_spi_recv(G_io_apdu_buffer, sizeof(G_io_apdu_buffer), 0); + } + default: + THROW(INVALID_PARAMETER); + } + + return 0; +} + +void io_init() +{ + // Reset length of APDU response + G_output_len = 0; + G_io_state = READY; +} + +int io_recv_command() +{ + int ret = -1; + + switch (G_io_state) { + case READY: + G_io_state = RECEIVED; + // IO_CONTINUE_RX needed in case an APDU is received during a sync NBGL call + // so that we will process it at the next io_recv_command() call. + // Note that this needs a small change in io_exchange(). + // local definition for cases where it's not yet implemented in the SDK + #ifndef IO_CONTINUE_RX + #define IO_CONTINUE_RX 0 + #endif + ret = io_exchange(CHANNEL_APDU | IO_CONTINUE_RX, G_output_len); + break; + case RECEIVED: + G_io_state = WAITING; + ret = io_exchange(CHANNEL_APDU | IO_ASYNCH_REPLY, G_output_len); + G_io_state = RECEIVED; + break; + case WAITING: + G_io_state = READY; + ret = -1; + break; + } + + return ret; +} + +int io_send_response_buffers(const buffer_t *rdatalist, size_t count, uint16_t sw) +{ + int ret = -1; + + G_output_len = 0; + if (rdatalist && count > 0) { + for (size_t i = 0; i < count; i++) { + const buffer_t *rdata = &rdatalist[i]; + + if (!buffer_copy(rdata, + G_io_apdu_buffer + G_output_len, + sizeof(G_io_apdu_buffer) - G_output_len - 2)) { + return io_send_sw(SW_WRONG_RESPONSE_LENGTH); + } + G_output_len += rdata->size - rdata->offset; + if (count > 1) { + PRINTF("<= FRAG (%u/%u) RData=%.*H\n", i + 1, count, rdata->size, rdata->ptr); + } + } + PRINTF("<= SW=%04X | RData=%.*H\n", sw, G_output_len, G_io_apdu_buffer); + } + else { + PRINTF("<= SW=%04X | RData=\n", sw); + } + + write_u16_be(G_io_apdu_buffer, G_output_len, sw); + G_output_len += 2; + +#ifdef HAVE_SWAP + // If we are in swap mode and have validated a TX, we send it and immediately quit + if (G_called_from_swap && G_swap_response_ready) { + PRINTF("Swap answer is processed. Send it\n"); + if (io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, G_output_len) == 0) { + swap_finalize_exchange_sign_transaction(sw == SW_OK); + } + else { + PRINTF("Unrecoverable\n"); + os_sched_exit(-1); + } + } +#endif // HAVE_SWAP + + switch (G_io_state) { + case READY: + ret = -1; + break; + case RECEIVED: +#if 0 // enforce io_send_xxx to be synchronous + G_io_state = READY; + ret = 0; + break; +#endif + case WAITING: + ret = io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, G_output_len); + G_output_len = 0; + G_io_state = READY; + break; + } + + return ret; +} diff --git a/sdk/nbgl_sync.c b/sdk/nbgl_sync.c new file mode 100644 index 00000000..540242f7 --- /dev/null +++ b/sdk/nbgl_sync.c @@ -0,0 +1,94 @@ +#include "nbgl_use_case.h" +#include "nbgl_sync.h" +#include "nbgl_wrapper.h" +#include "io.h" +#include "ledger_assert.h" +#include "os.h" + +static sync_nbgl_ret_t ret; +static bool ended; + + +static void choice_callback(bool confirm) { + if (confirm) { + ret = NBGL_SYNC_RET_SUCCESS; + } else { + ret = NBGL_SYNC_RET_REJECTED; + } + + ended = true; +} + +static void quit_callback(void) { + ret = NBGL_SYNC_RET_SUCCESS; + ended = true; +} + +static void sync_nbgl_init(void) { + ended = false; + ret = NBGL_SYNC_RET_ERROR; +} + +static sync_nbgl_ret_t sync_nbgl_wait(void) { + int apdu_state = G_io_app.apdu_state; + while (!ended) { + + // Check for a apdu_state change. + // This would means an APDU was received. + // In such case immediately stop the loop. + // We could expose another version that would ignore such APDU event. + // Anyway, it will be handled only at next app call of io_recv_command(). + if (apdu_state == APDU_IDLE && G_io_app.apdu_state != APDU_IDLE) { + return NBGL_SYNC_RET_RX_APDU; + } + // - Looping on os_io_seph_recv_and_process(0); + // - This will send a general_status and then wait for an event. + // - Upon event reception this will call io_seproxyhal_handle_event() + // - On some case this will call io_event() which usually forward the + // event to the UX lib. + os_io_seph_recv_and_process(0); + } + + return ret; +} + +sync_nbgl_ret_t sync_nbgl_useCaseTransactionReview( + const nbgl_layoutTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, /* Most often this is empty, but sometimes indicates a path / index */ + const char *finish_page_text /*unused on Nano*/) +{ + sync_nbgl_init(); + nbgl_useCaseTransactionReview(tagValueList, + icon, + reviewTitle, + reviewSubTitle, + finish_page_text, + choice_callback); + return sync_nbgl_wait(); +} + +sync_nbgl_ret_t sync_nbgl_useCaseAddressReview( + const char *address, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle) +{ + sync_nbgl_init(); + + nbgl_useCaseAddressReview(address, + icon, + reviewTitle, + reviewSubTitle, + choice_callback); + + return sync_nbgl_wait(); +} + +sync_nbgl_ret_t sync_nbgl_useCaseStatus(const char *message, bool isSuccess) +{ + sync_nbgl_init(); + nbgl_useCaseStatus(message, isSuccess, quit_callback); + return sync_nbgl_wait(); +} diff --git a/sdk/nbgl_sync.h b/sdk/nbgl_sync.h new file mode 100644 index 00000000..ef9baf54 --- /dev/null +++ b/sdk/nbgl_sync.h @@ -0,0 +1,23 @@ +#include "nbgl_use_case.h" + +typedef enum { + NBGL_SYNC_RET_SUCCESS, + NBGL_SYNC_RET_REJECTED, + NBGL_SYNC_RET_RX_APDU, + NBGL_SYNC_RET_ERROR +} sync_nbgl_ret_t; + +sync_nbgl_ret_t sync_nbgl_useCaseTransactionReview( + const nbgl_layoutTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, /* Most often this is empty, but sometimes indicates a path / index */ + const char *finish_page_text /*unused on Nano*/); + +sync_nbgl_ret_t sync_nbgl_useCaseAddressReview( + const char *address, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle); + +sync_nbgl_ret_t sync_nbgl_useCaseStatus(const char *message, bool isSuccess); diff --git a/sdk/nbgl_wrapper.c b/sdk/nbgl_wrapper.c new file mode 100644 index 00000000..5a03e438 --- /dev/null +++ b/sdk/nbgl_wrapper.c @@ -0,0 +1,110 @@ + +#include "nbgl_use_case.h" +#include "ledger_assert.h" +#include "os_pic.h" +#include "os.h" +#include "glyphs.h" + +#ifndef TARGET_NANOS +static nbgl_pageInfoLongPress_t infoLongPress; + +static const nbgl_layoutTagValueList_t *review_tagValueList; +static const nbgl_icon_details_t *review_icon; +static const char *review_finish_page_text; + +static const char *review_address; + +static nbgl_choiceCallback_t review_choice_callback; + + +static void tx_confirm_transaction_rejection(void) { + review_choice_callback(false); +} + +static void tx_ask_transaction_rejection_confirmation(void) { + // display a choice to confirm/cancel rejection + nbgl_useCaseConfirm("Reject transaction?", + NULL, + "Yes, Reject", + "Go back to transaction", + tx_confirm_transaction_rejection); +} + +static void tx_review_choice(bool confirm) { + if (confirm) { + review_choice_callback(confirm); + } else { + tx_ask_transaction_rejection_confirmation(); + } +} + +static void tx_review_continue(void) { + // Info long press + infoLongPress.icon = review_icon; + infoLongPress.text = review_finish_page_text; + infoLongPress.longPressText = "Hold to sign"; + + nbgl_useCaseStaticReview(review_tagValueList, &infoLongPress, "Reject transaction", tx_review_choice); +} +#endif + +void nbgl_useCaseTransactionReview( + const nbgl_layoutTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, /* Most often this is empty, but sometimes indicates a path / index */ + const char *finish_page_text, /*unused on Nano*/ + nbgl_choiceCallback_t choice_callback) +{ +#ifndef TARGET_NANOS + review_tagValueList = tagValueList; + review_icon = icon; + review_finish_page_text = finish_page_text; + review_choice_callback = choice_callback; + + nbgl_useCaseReviewStart(icon, + reviewTitle, + reviewSubTitle, + "Reject transaction", + tx_review_continue, + tx_ask_transaction_rejection_confirmation); +#else + UNUSED(reviewSubTitle); + UNUSED(finish_page_text); + nbgl_useCaseStaticReview(tagValueList, icon, reviewTitle, "Approve", "Reject", choice_callback); +#endif + +} + +#ifndef TARGET_NANOS +static void addr_review_continue(void) { + nbgl_useCaseAddressConfirmation(review_address, review_choice_callback); +} + +static void addr_review_rejection(void) { + review_choice_callback(false); +} +#endif + +void nbgl_useCaseAddressReview( + const char *address, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + nbgl_choiceCallback_t choice_callback) +{ +#ifndef TARGET_NANOS + review_address = address; + review_choice_callback = choice_callback; + + nbgl_useCaseReviewStart(icon, + reviewTitle, + reviewSubTitle, + "Cancel", + addr_review_continue, + addr_review_rejection); +#else + UNUSED(reviewSubTitle); + nbgl_useCaseAddressConfirmation(icon, reviewTitle, address, choice_callback); +#endif +} diff --git a/sdk/nbgl_wrapper.h b/sdk/nbgl_wrapper.h new file mode 100644 index 00000000..ce4db583 --- /dev/null +++ b/sdk/nbgl_wrapper.h @@ -0,0 +1,16 @@ +#include "nbgl_use_case.h" + +void nbgl_useCaseTransactionReview( + const nbgl_layoutTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, /* Most often this is empty, but sometimes indicates a path / index */ + const char *finish_page_text, /*unused on Nano*/ + nbgl_choiceCallback_t choice_callback); + +void nbgl_useCaseAddressReview( + const char *address, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + nbgl_choiceCallback_t choice_callback); \ No newline at end of file diff --git a/sdk_lib_nbgl/README.md b/sdk_lib_nbgl/README.md new file mode 100644 index 00000000..39459c42 --- /dev/null +++ b/sdk_lib_nbgl/README.md @@ -0,0 +1,2 @@ +# NBGL library +Graphic library for Stax diff --git a/sdk_lib_nbgl/glyphs_nano/icon_back_x.gif b/sdk_lib_nbgl/glyphs_nano/icon_back_x.gif new file mode 100644 index 00000000..ff043615 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_back_x.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_backspace.gif b/sdk_lib_nbgl/glyphs_nano/icon_backspace.gif new file mode 100644 index 00000000..7374da39 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_backspace.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_backspace_invert.gif b/sdk_lib_nbgl/glyphs_nano/icon_backspace_invert.gif new file mode 100644 index 00000000..0cffcc03 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_backspace_invert.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_certificate.gif b/sdk_lib_nbgl/glyphs_nano/icon_certificate.gif new file mode 100644 index 00000000..89b529f7 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_certificate.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_classes.gif b/sdk_lib_nbgl/glyphs_nano/icon_classes.gif new file mode 100644 index 00000000..459d545b Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_classes.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_classes_invert.gif b/sdk_lib_nbgl/glyphs_nano/icon_classes_invert.gif new file mode 100644 index 00000000..4fcaad1c Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_classes_invert.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_crossmark.gif b/sdk_lib_nbgl/glyphs_nano/icon_crossmark.gif new file mode 100644 index 00000000..2dcf9d9e Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_crossmark.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_dashboard_x.gif b/sdk_lib_nbgl/glyphs_nano/icon_dashboard_x.gif new file mode 100644 index 00000000..33d9b0a7 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_dashboard_x.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_digits.gif b/sdk_lib_nbgl/glyphs_nano/icon_digits.gif new file mode 100644 index 00000000..70da9c66 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_digits.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_digits_invert.gif b/sdk_lib_nbgl/glyphs_nano/icon_digits_invert.gif new file mode 100644 index 00000000..22487c72 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_digits_invert.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_down.gif b/sdk_lib_nbgl/glyphs_nano/icon_down.gif new file mode 100644 index 00000000..4f4e39ee Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_down.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_eye.gif b/sdk_lib_nbgl/glyphs_nano/icon_eye.gif new file mode 100644 index 00000000..df4bb829 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_eye.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_left.gif b/sdk_lib_nbgl/glyphs_nano/icon_left.gif new file mode 100644 index 00000000..524226ba Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_left.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_lowercase.gif b/sdk_lib_nbgl/glyphs_nano/icon_lowercase.gif new file mode 100644 index 00000000..c167cc7b Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_lowercase.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_lowercase_invert.gif b/sdk_lib_nbgl/glyphs_nano/icon_lowercase_invert.gif new file mode 100644 index 00000000..cdd5ed41 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_lowercase_invert.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_right.gif b/sdk_lib_nbgl/glyphs_nano/icon_right.gif new file mode 100644 index 00000000..15ff3cf5 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_right.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_up.gif b/sdk_lib_nbgl/glyphs_nano/icon_up.gif new file mode 100644 index 00000000..4e13c064 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_up.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_uppercase.gif b/sdk_lib_nbgl/glyphs_nano/icon_uppercase.gif new file mode 100644 index 00000000..ebff3704 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_uppercase.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_uppercase_invert.gif b/sdk_lib_nbgl/glyphs_nano/icon_uppercase_invert.gif new file mode 100644 index 00000000..098c6a27 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_uppercase_invert.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_validate.gif b/sdk_lib_nbgl/glyphs_nano/icon_validate.gif new file mode 100644 index 00000000..5d1bc5ac Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_validate.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_validate_10.gif b/sdk_lib_nbgl/glyphs_nano/icon_validate_10.gif new file mode 100755 index 00000000..f69bbee9 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_validate_10.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_validate_14.gif b/sdk_lib_nbgl/glyphs_nano/icon_validate_14.gif new file mode 100644 index 00000000..ccb5cabe Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_validate_14.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/icon_validate_invert.gif b/sdk_lib_nbgl/glyphs_nano/icon_validate_invert.gif new file mode 100644 index 00000000..c1464d90 Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/icon_validate_invert.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/pin_bullet_empty.gif b/sdk_lib_nbgl/glyphs_nano/pin_bullet_empty.gif new file mode 100644 index 00000000..1387dc9e Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/pin_bullet_empty.gif differ diff --git a/sdk_lib_nbgl/glyphs_nano/pin_bullet_filled.gif b/sdk_lib_nbgl/glyphs_nano/pin_bullet_filled.gif new file mode 100644 index 00000000..b6f61d2e Binary files /dev/null and b/sdk_lib_nbgl/glyphs_nano/pin_bullet_filled.gif differ diff --git a/sdk_lib_nbgl/include/nbgl_buttons.h b/sdk_lib_nbgl/include/nbgl_buttons.h new file mode 100644 index 00000000..fd008c1e --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_buttons.h @@ -0,0 +1,47 @@ +/** + * @file nbgl_buttons.h + * Buttons management of NBGL + * + */ + +#ifndef NBGL_BUTTONS_H +#define NBGL_BUTTONS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "nbgl_types.h" +#include "nbgl_obj.h" + +/********************* + * DEFINES + *********************/ +///< Time after the beginning of continuous press on button(s) after which "continuous press" event +///< start to be sent (in 100ms) +#define CONTINOUS_PRESS_THRESHOLD 8 +///< Periodicity of "continuous press" events (in 100ms) +#define CONTINUOUS_PRESS_PERIOD 3 + +/********************** + * TYPEDEFS + **********************/ + +/********************** + * GLOBAL PROTOTYPES + **********************/ +void nbgl_buttonsHandler(uint8_t buttonState, uint32_t currentTimeMs); +void nbgl_buttonsReset(void); + +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_BUTTONS_H */ diff --git a/sdk_lib_nbgl/include/nbgl_debug.h b/sdk_lib_nbgl/include/nbgl_debug.h new file mode 100644 index 00000000..e0a69a88 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_debug.h @@ -0,0 +1,95 @@ +/** + * @file nbgl_debug.h + * @brief debug traces management + * + */ + +#ifndef NBGL_DEBUG_H +#define NBGL_DEBUG_H + +#ifdef __cplusplus +extern "C" { +#endif + +// #define WITH_STDIO 1 + +/********************* + * INCLUDES + *********************/ +#ifdef WITH_STDIO +#include +#include +#endif + +/********************* + * DEFINES + *********************/ +enum { + LOW_LOGGER, + DRAW_LOGGER, + OBJ_LOGGER, + OBJ_POOL_LOGGER, + SCREEN_LOGGER, + LAYOUT_LOGGER, + PAGE_LOGGER, + TOUCH_LOGGER, + APP_LOGGER, + UX_LOGGER, + MISC_LOGGER, + STEP_LOGGER, + FLOW_LOGGER +}; +/********************** + * TYPEDEFS + **********************/ + +/********************** + * GLOBAL PROTOTYPES + **********************/ +extern unsigned long gLogger; + +/********************** + * MACROS + **********************/ +#ifdef WITH_STDIO +extern void mainExit(int exitCode); +#define LOG_DEBUG(__logger, ...) \ + { \ + if (gLogger & (1 << __logger)) \ + printf(__VA_ARGS__); \ + } +#define LOG_WARN(__logger, ...) printf(__VA_ARGS__) +#define LOG_FATAL(__logger, ...) \ + { \ + printf(__VA_ARGS__); \ + mainExit(-1); \ + } + +#else // WITH_STDIO +#ifdef NBGL_DEBUG +#include +#define LOG_DEBUG(__logger, ...) \ + do { \ + PRINTF(__VA_ARGS__); \ + } while (0) +#define LOG_WARN(__logger, ...) \ + do { \ + PRINTF(__VA_ARGS__); \ + } while (0) +#define LOG_FATAL(__logger, ...) \ + do { \ + PRINTF(__VA_ARGS__); \ + halt(); \ + } while (0) +#else +#define LOG_DEBUG(__logger, ...) +#define LOG_WARN(__logger, ...) +#define LOG_FATAL(__logger, ...) +#endif // NBGL_DEBUG +#endif // WITH_STDIO + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_DEBUG_H */ diff --git a/sdk_lib_nbgl/include/nbgl_draw.h b/sdk_lib_nbgl/include/nbgl_draw.h new file mode 100644 index 00000000..2c52b0b1 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_draw.h @@ -0,0 +1,60 @@ +/** + * @file nbgl_draw.h + * @brief Middle Level API of the new BOLOS Graphical Library + * + */ + +#ifndef NBGL_DRAW_H +#define NBGL_DRAW_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "nbgl_types.h" +#include "nbgl_fonts.h" + +/********************* + * DEFINES + *********************/ +#define QR_V4_NB_PIX_SIZE 33 // qr V4 number of pixels +#define QR_V10_NB_PIX_SIZE 57 // qr V10 number of pixels +#define QR_MAX_PIX_SIZE QR_V10_NB_PIX_SIZE // we support up to V10 qr code version + +/********************** + * TYPEDEFS + **********************/ + +/********************** + * GLOBAL PROTOTYPES + **********************/ + +void nbgl_drawIcon(nbgl_area_t *area, nbgl_color_map_t color_map, const nbgl_icon_details_t *icon); +void nbgl_drawRoundedRect(const nbgl_area_t *area, nbgl_radius_t radius, color_t innerColor); +void nbgl_drawRoundedBorderedRect(const nbgl_area_t *area, + nbgl_radius_t radius, + uint8_t stroke, + color_t innerColor, + color_t borderColor); +nbgl_font_id_e nbgl_drawText(const nbgl_area_t *area, + const char *text, + uint16_t textLen, + nbgl_font_id_e fontId, + color_t fontColor); +void nbgl_drawQrCode(const nbgl_area_t *area, + uint8_t version, + const char *text, + color_t backgroundColor); + +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_DRAW_H */ diff --git a/sdk_lib_nbgl/include/nbgl_flow.h b/sdk_lib_nbgl/include/nbgl_flow.h new file mode 100644 index 00000000..f12c1ae0 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_flow.h @@ -0,0 +1,86 @@ +/** + * @file nbgl_flow.h + * @brief Flow construction API of NBGL + * + */ + +#ifndef NBGL_FLOW_H +#define NBGL_FLOW_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ + +#include "nbgl_step.h" +#include "nbgl_obj.h" +#include "nbgl_types.h" + +/********************* + * DEFINES + *********************/ + +/********************** + * TYPEDEFS + **********************/ +/** + * @brief type shared externally + * + */ +typedef void *nbgl_flow_t; + +/** + * @brief prototype of function to be called when a step is using a callback on "double-key" action + */ +typedef void (*nbgl_stepCallback_t)(void); + +/** + * @brief Structure containing all specific information when creating a NBGL step. + */ +typedef struct nbgl_stepDesc_s { + nbgl_stepCallback_t init; ///< if not NULL, function to be called when the step is entered + nbgl_stepCallback_t callback; ///< if not NULL, function to be called on "double-key" action + const char *text; ///< text to display in step (can be multi-pages if icon == NULL) + const char *subText; ///< sub-text to display in step (NULL most of the time) + const nbgl_icon_details_t *icon; ///< icon to display in step (text must be single-page) +#ifdef HAVE_LANGUAGE_PACK + UX_LOC_STRINGS_INDEX textId; ///< text Id to display in step (if text field is NULL) +#endif // HAVE_LANGUAGE_PACK +} nbgl_stepDesc_t; + +typedef nbgl_stepDesc_t nbgl_pageContent_t; + +/** + * @brief This structure contains data to build a centered info + long press button page content + */ +typedef struct nbgl_pageInfoLongPress_s { + const char *text; ///< centered text in large case + const nbgl_icon_details_t *icon; ///< a buffer containing the 1BPP icon + const char *longPressText; ///< text of the long press button + uint8_t longPressToken; ///< the token used as argument of the onActionCallback when button is + ///< long pressed +} nbgl_pageInfoLongPress_t; + +/********************** + * GLOBAL PROTOTYPES + **********************/ + +nbgl_flow_t nbgl_flowDraw(const nbgl_stepDesc_t *steps, + uint8_t nbSteps, + uint8_t initStep, + bool loop, + bool modal); +void nbgl_flowRelease(nbgl_flow_t flow); + +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_FLOW_H */ diff --git a/sdk_lib_nbgl/include/nbgl_fonts.h b/sdk_lib_nbgl/include/nbgl_fonts.h new file mode 100644 index 00000000..213e73fc --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_fonts.h @@ -0,0 +1,212 @@ +/** + * @file nbgl_fonts.h + * Fonts types of the new BOLOS Graphical Library + * + */ + +#ifndef NBGL_FONTS_H +#define NBGL_FONTS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "nbgl_types.h" + +/********************* + * DEFINES + *********************/ +#define PIC_CHAR(x) ((const nbgl_font_character_t *) PIC(x)) +#define PIC_BMP(x) ((uint8_t const *) PIC(x)) + +/** + * @brief fonts nicknames to be used for various wallet size targets (non-Nano) + * + */ +#define SMALL_REGULAR_FONT BAGL_FONT_INTER_REGULAR_24px +#define SMALL_BOLD_FONT BAGL_FONT_INTER_SEMIBOLD_24px +#define LARGE_MEDIUM_FONT BAGL_FONT_INTER_MEDIUM_32px +#define SMALL_REGULAR_1BPP_FONT BAGL_FONT_INTER_REGULAR_24px_1bpp +#define SMALL_BOLD_1BPP_FONT BAGL_FONT_INTER_SEMIBOLD_24px_1bpp +#define LARGE_MEDIUM_1BPP_FONT BAGL_FONT_INTER_MEDIUM_32px_1bpp + +/********************** + * TYPEDEFS + **********************/ + +/** + * @brief structure defining an ASCII character (except the bitmap) + * + */ +// WARNING: please DON'T CHANGE the order/values of the fields below! +// (otherwise python tools that generate data will need to be modified too) +typedef struct { + uint32_t bitmap_offset; ///< offset of this character in chars buffer + uint32_t encoding : 1; ///< method used to encode bitmap data + uint32_t width : 6; ///< width of character in pixels + uint32_t x_min_offset : 4; ///< x_min = x_min_offset + uint32_t y_min_offset : 6; ///< y_min = (y_min + y_min_offset) + uint32_t x_max_offset : 4; ///< x_max = width - x_max_offset + uint32_t y_max_offset : 6; ///< y_max = (height - y_max_offset) +} nbgl_font_character_t; + +/** + * @brief structure defining an ASCII font + * + */ +typedef struct { + uint32_t bitmap_len; ///< Size in bytes of the associated bitmap + uint8_t font_id; ///< ID of the font, from @ref nbgl_font_id_e + uint8_t bpp; ///< number of bits per pixels + uint8_t height; ///< height of all characters in pixels + uint8_t line_height; ///< height of a line for all characters in pixels + uint8_t char_kerning; ///< kerning for the font + uint8_t crop; ///< If false, x_min_offset+y_min_offset=bytes to skip + uint8_t y_min; ///< Most top Y coordinate of any char in the font + uint8_t + first_char; ///< ASCII code of the first character in \b bitmap and in \b characters fields + uint8_t + last_char; ///< ASCII code of the last character in \b bitmap and in \b characters fields + const nbgl_font_character_t + *const characters; ///< array containing definitions of all characters + uint8_t const *bitmap; ///< array containing bitmaps of all characters +} nbgl_font_t; + +#define BAGL_ENCODING_LATIN1 0 +#define BAGL_ENCODING_UTF8 1 +#define BAGL_ENCODING_DEFAULT BAGL_ENCODING_UTF8 + +/** + * @brief structure defining a unicode character (except the bitmap) + * + */ +// WARNING: please DON'T CHANGE the order of the fields below! +// (otherwise python tools that generate data will need to be modified too) +typedef struct { + uint32_t char_unicode : 21; ///< plane value from 0 to 16 then 16-bit code. + uint32_t encoding : 1; ///< method used to encode bitmap data + uint32_t width : 6; ///< width of character in pixels + uint32_t x_min_offset : 4; ///< x_min = x_min_offset + uint32_t y_min_offset : 6; ///< y_min = (y_min + y_min_offset) + uint32_t x_max_offset : 4; ///< x_max = width - x_max_offset + uint32_t y_max_offset : 6; ///< y_max = (height - y_max_offset) + uint32_t bitmap_offset : 16; ///< offset of this character in chars buffer +} nbgl_font_unicode_character_t; +/** + * @brief structure defining a unicode font + * + */ +typedef struct { + uint16_t bitmap_len; ///< Size in bytes of all characters bitmaps + uint8_t font_id; ///< ID of the font, from @ref nbgl_font_id_e + uint8_t bpp; ///< Number of bits per pixels, (interpreted as nbgl_bpp_t) + uint8_t height; ///< height of all characters in pixels + uint8_t line_height; ///< height of a line for all characters in pixels + uint8_t char_kerning; ///< kerning for the font + uint8_t crop; ///< If false, x_min_offset+y_min_offset=bytes to skip + uint8_t y_min; ///< Most top Y coordinate of any char in the font + uint8_t unused[3]; ///< for alignment +#if !defined(HAVE_LANGUAGE_PACK) + // When using language packs, those 2 pointers does not exists + const nbgl_font_unicode_character_t + *const characters; ///< array containing definitions of all characters + uint8_t const *bitmap; ///< array containing bitmaps of all characters +#endif //! defined(HAVE_LANGUAGE_PACK) +} nbgl_font_unicode_t; + +typedef enum { + BAGL_FONT_INTER_REGULAR_24px, + BAGL_FONT_INTER_SEMIBOLD_24px, + BAGL_FONT_INTER_MEDIUM_32px, + BAGL_FONT_INTER_REGULAR_24px_1bpp, + BAGL_FONT_INTER_SEMIBOLD_24px_1bpp, + BAGL_FONT_INTER_MEDIUM_32px_1bpp, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px_1bpp = 8u, // on Nano + BAGL_FONT_OPEN_SANS_LIGHT_16px_1bpp = 9u, // on Nano + BAGL_FONT_OPEN_SANS_REGULAR_11px_1bpp = 10u, // on Nano + BAGL_FONT_INTER_REGULAR_28px = 11u, + BAGL_FONT_INTER_SEMIBOLD_28px = 12u, + BAGL_FONT_INTER_MEDIUM_36px = 13u, + BAGL_FONT_INTER_REGULAR_28px_1bpp = 14u, + BAGL_FONT_INTER_SEMIBOLD_28px_1bpp = 15u, + BAGL_FONT_INTER_MEDIUM_36px_1bpp = 16u, +#ifndef TARGET_NANOS + BAGL_FONT_LAST // MUST ALWAYS BE THE LAST, FOR AUTOMATED INVALID VALUE CHECKS +#endif +} nbgl_font_id_e; + +typedef struct nbgl_unicode_ctx_s { + const nbgl_font_unicode_t *font; + const nbgl_font_unicode_character_t *characters; + const uint8_t *bitmap; + uint32_t unicode_character_byte_count; +} nbgl_unicode_ctx_t; + +/********************** + * GLOBAL PROTOTYPES + **********************/ +const nbgl_font_t *nbgl_font_getFont(unsigned int fontId); +const nbgl_font_t *nbgl_getFont(nbgl_font_id_e fontId); +uint16_t nbgl_getSingleLineTextWidth(nbgl_font_id_e fontId, const char *text); +uint16_t nbgl_getSingleLineTextWidthInLen(nbgl_font_id_e fontId, const char *text, uint16_t maxLen); +uint16_t nbgl_getTextWidth(nbgl_font_id_e fontId, const char *text); +uint8_t nbgl_getCharWidth(nbgl_font_id_e fontId, const char *text); +uint8_t nbgl_getFontHeight(nbgl_font_id_e fontId); +uint8_t nbgl_getFontLineHeight(nbgl_font_id_e fontId); +uint16_t nbgl_getTextNbLines(const char *text); +uint16_t nbgl_getTextHeight(nbgl_font_id_e fontId, const char *text); +uint16_t nbgl_getTextLength(const char *text); +void nbgl_getTextMaxLenAndWidth(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + uint16_t *len, + uint16_t *width, + bool wrapping); +uint16_t nbgl_getTextNbLinesInWidth(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + bool wrapping); +uint16_t nbgl_getTextHeightInWidth(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + bool wrapping); +bool nbgl_getTextMaxLenAndWidthFromEnd(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + uint16_t *len, + uint16_t *width); +bool nbgl_getTextMaxLenInNbLines(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + uint16_t maxNbLines, + uint16_t *len, + bool wrapping); +void nbgl_textWrapOnNbLines(nbgl_font_id_e fontId, char *text, uint16_t maxWidth, uint8_t nbLines); +uint8_t nbgl_getTextNbPagesInWidth(nbgl_font_id_e fontId, + const char *text, + uint8_t nbLinesPerPage, + uint16_t maxWidth); + +uint32_t nbgl_popUnicodeChar(const uint8_t **text, uint16_t *text_length, bool *is_unicode); +#ifdef HAVE_UNICODE_SUPPORT +nbgl_unicode_ctx_t *nbgl_getUnicodeFont(nbgl_font_id_e font_id); +const nbgl_font_unicode_character_t *nbgl_getUnicodeFontCharacter(uint32_t unicode); +uint32_t nbgl_getUnicodeFontCharacterByteCount(void); +#ifdef HAVE_LANGUAGE_PACK +void nbgl_refreshUnicodeFont(void); +#endif +#endif // HAVE_UNICODE_SUPPORT + +/********************** + * MACROS + **********************/ +#define IS_UNICODE(__value) ((__value) > 0xF0) + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_FONTS_H */ diff --git a/sdk_lib_nbgl/include/nbgl_front.h b/sdk_lib_nbgl/include/nbgl_front.h new file mode 100644 index 00000000..08ed3934 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_front.h @@ -0,0 +1,57 @@ +/** + * @file nbgl_front.h + * @brief Font screen low-Level driver API, to draw elementary forms + * + */ + +#ifndef NBGL_FRONT_H +#define NBGL_FRONT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "nbgl_types.h" + +/********************* + * DEFINES + *********************/ + +/********************** + * TYPEDEFS + **********************/ + +/********************** + * GLOBAL PROTOTYPES + **********************/ +void nbgl_frontDrawRect(const nbgl_area_t *area); +void nbgl_frontDrawHorizontalLine(const nbgl_area_t *area, uint8_t mask, color_t lineColor); +void nbgl_frontDrawImage(const nbgl_area_t *area, + const uint8_t *buffer, + nbgl_transformation_t transformation, + nbgl_color_map_t colorMap); +void nbgl_frontDrawImageFile(const nbgl_area_t *area, + const uint8_t *buffer, + nbgl_color_map_t colorMap, + const uint8_t *uzlib_chunk_buffer); +void nbgl_frontDrawImageRle(const nbgl_area_t *area, + const uint8_t *buffer, + uint32_t buffer_len, + color_t fore_color, + uint8_t nb_skipped_bytes); +void nbgl_frontRefreshArea(const nbgl_area_t *area, + nbgl_refresh_mode_t mode, + nbgl_post_refresh_t post_refresh); + +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_FRONT_H */ diff --git a/sdk_lib_nbgl/include/nbgl_image_utils.h b/sdk_lib_nbgl/include/nbgl_image_utils.h new file mode 100644 index 00000000..0c951b05 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_image_utils.h @@ -0,0 +1,102 @@ +/** + * @file nbgl_image_utils.h + * @brief Utilities for Ledger image files processing + * + */ + +#ifndef NBGL_IMAGE_UTILS_H +#define NBGL_IMAGE_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "nbgl_types.h" + +/********************* + * DEFINES + *********************/ +/** + * Width offset in image file + */ +#define WIDTH_OFFSET 0 + +/** + * Height offset in image file + */ +#define HEIGHT_OFFSET 2 + +/** + * BPP & Compression offset in image file + */ +#define BPP_COMPRESS_OFFSET 4 + +/** + * Buffer length offset in image file + * + */ +#define BUFFER_LEN_OFFSET 5 + +/** + * Buffer offset in image file + * + */ +#define BUFFER_OFFSET 8 + +/** + * Size in bytes of an image file header + */ +#define IMAGE_FILE_HEADER_SIZE 8 + +/********************** + * MACROS + **********************/ + +/** + * @brief macro to get the width of the image file + * @param image_file uint8_t* buffer representing the image file + */ +#define GET_IMAGE_FILE_WIDTH(image_file) \ + (image_file[WIDTH_OFFSET] | (image_file[WIDTH_OFFSET + 1] << 8)) + +/** + * @brief macro to get the height of the image file + * @param image_file uint8_t* buffer representing the image file + */ +#define GET_IMAGE_FILE_HEIGHT(image_file) \ + (image_file[HEIGHT_OFFSET] | (image_file[HEIGHT_OFFSET + 1] << 8)) + +/** + * @brief macro to get the bpp of the image file + * @param image_file uint8_t* buffer representing the image file + */ +#define GET_IMAGE_FILE_BPP(image_file) (image_file[BPP_COMPRESS_OFFSET] >> 4) + +/** + * @brief macro to get the compression of the image file + * @param image_file uint8_t* buffer representing the image file + */ +#define GET_IMAGE_FILE_COMPRESSION(image_file) (image_file[BPP_COMPRESS_OFFSET] & 0xF) + +/** + * @brief macro to get the buffer len of the image file + * @param image_file uint8_t* buffer representing the image file + */ +#define GET_IMAGE_FILE_BUFFER_LEN(image_file) \ + (image_file[BUFFER_LEN_OFFSET] | (image_file[BUFFER_LEN_OFFSET + 1] << 8) \ + | (image_file[BUFFER_LEN_OFFSET + 2] << 16)) + +/** + * @brief macro to get the buffer of the image file + * @param image_file uint8_t* buffer representing the image file + */ +#define GET_IMAGE_FILE_BUFFER(image_file) (&image_file[BUFFER_OFFSET]) + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_IMAGE_UTILS_H */ diff --git a/sdk_lib_nbgl/include/nbgl_layout.h b/sdk_lib_nbgl/include/nbgl_layout.h new file mode 100644 index 00000000..81b88d44 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_layout.h @@ -0,0 +1,554 @@ +/** + * @file nbgl_layout.h + * @brief API of the Advanced BOLOS Graphical Library, for predefined layouts + * + */ + +#ifndef NBGL_LAYOUT_H +#define NBGL_LAYOUT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "nbgl_obj.h" +#include "nbgl_screen.h" +#include "nbgl_types.h" +#include "os_io_seproxyhal.h" + +/********************* + * INCLUDES + *********************/ + +/********************* + * DEFINES + *********************/ +#define NO_MORE_OBJ_ERROR -3 +#define NBGL_NO_TUNE NB_TUNES + +#define NB_MAX_SUGGESTION_BUTTONS 4 + +#ifdef HAVE_SE_TOUCH +#define AVAILABLE_WIDTH (SCREEN_WIDTH - 2 * BORDER_MARGIN) +#else // HAVE_SE_TOUCH +// 7 pixels on each side +#define AVAILABLE_WIDTH (SCREEN_WIDTH - 2 * 7) +// maximum number of lines in screen +#define NB_MAX_LINES 4 + +#endif // HAVE_SE_TOUCH + +/********************** + * TYPEDEFS + **********************/ + +/** + * @brief type shared externally + * + */ +typedef void *nbgl_layout_t; + +/** + * @brief prototype of function to be called when an object is touched + * @param token integer passed when registering callback + * @param index when the object touched is a list of radio buttons, gives the index of the activated + * button + */ +typedef void (*nbgl_layoutTouchCallback_t)(int token, uint8_t index); + +/** + * @brief prototype of function to be called when buttons are touched on a screen + * @param layout layout concerned by the event + * @param event type of button event + */ +#ifndef TARGET_NANOS +typedef void (*nbgl_layoutButtonCallback_t)(nbgl_layout_t *layout, nbgl_buttonEvent_t event); +#else +typedef void (*nbgl_layoutButtonCallback_t)(nbgl_buttonEvent_t event); +#endif + +/** + * @brief This structure contains info to build a navigation bar at the bottom of the screen + * @note this widget is incompatible with a footer. + * + */ +typedef struct { + uint8_t token; ///< the token that will be used as argument of the callback + uint8_t nbPages; ///< number of pages. (if 0, no navigation) + uint8_t activePage; ///< index of active page (from 0 to nbPages-1). + bool withExitKey; ///< if set to true, an exit button is drawn, either on the left of + ///< navigation keys or in the center if no navigation + bool withSeparationLine; ///< if set to true, an horizontal line is drawn on top of bar in + ///< light gray +#ifdef HAVE_PIEZO_SOUND + tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when pressing keys) +#endif // HAVE_PIEZO_SOUND +} nbgl_layoutNavigationBar_t; + +/** + * @brief possible directions for Navigation arrows + * + */ +typedef enum { + HORIZONTAL_NAV, ///< '<' and '>' are displayed, to navigate between pages and steps + VERTICAL_NAV ///< '\/' and '/\' are displayed, to navigate in a list (vertical scrolling) +} nbgl_layoutNavDirection_t; + +/** + * @brief possible styles for Navigation arrows (it's a bit field) + * + */ +typedef enum { + NO_ARROWS = 0, + LEFT_ARROW, ///< left arrow is used + RIGHT_ARROW, ///< right arrow is used +} nbgl_layoutNavIndication_t; + +/** + * @brief This structure contains info to build a navigation bar at the bottom of the screen + * @note this widget is incompatible with a footer. + * + */ +typedef struct { + nbgl_layoutNavDirection_t direction; ///< vertical or horizontal navigation + nbgl_layoutNavIndication_t indication; ///< specifies which arrows to use (left or right) +} nbgl_layoutNavigation_t; + +/** + * @brief Structure containing all information when creating a layout. This structure must be passed + * as argument to @ref nbgl_layoutGet + * @note It shall not be used + * + */ +typedef struct nbgl_layoutDescription_s { + bool modal; ///< if true, puts the layout on top of screen stack (modal). Otherwise puts on + ///< background (for apps) +#ifdef HAVE_SE_TOUCH + bool withLeftBorder; ///< if true, draws a light gray left border on the whole height of the + ///< screen + const char *tapActionText; ///< Light gray text used when main container is "tapable" + uint8_t tapActionToken; ///< the token that will be used as argument of the onActionCallback + ///< when main container is "tapped" +#ifdef HAVE_PIEZO_SOUND + tune_index_e tapTuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when tapping on + ///< main container +#endif // HAVE_PIEZO_SOUND + nbgl_layoutTouchCallback_t + onActionCallback; ///< the callback to be called on any action on the layout +#else // HAVE_SE_TOUCH + nbgl_layoutButtonCallback_t + onActionCallback; ///< the callback to be called on any action on the layout +#endif // HAVE_SE_TOUCH + nbgl_screenTickerConfiguration_t ticker; // configuration of ticker (timeout) +} nbgl_layoutDescription_t; + +/** + * @brief This structure contains info to build a clickable "bar" with a text and an icon + * + */ +typedef struct { + const nbgl_icon_details_t + *iconLeft; ///< a buffer containing the 1BPP icon for icon on left (can be NULL) + const char *text; ///< text (can be NULL) + const nbgl_icon_details_t *iconRight; ///< a buffer containing the 1BPP icon for icon 2 (can be + ///< NULL). Dimensions must be the same as iconLeft + const char *subText; ///< sub text (can be NULL) + uint8_t token; ///< the token that will be used as argument of the callback + bool inactive; ///< if set to true, the bar is grayed-out and cannot be touched + bool centered; ///< if set to true, the text is centered horizontaly in the bar +#ifdef HAVE_PIEZO_SOUND + tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played +#endif // HAVE_PIEZO_SOUND +} nbgl_layoutBar_t; + +/** + * @brief This structure contains info to build a switch (on the right) with a description (on the + * left), with a potential sub-description (in gray) + * + */ +typedef struct { + const char *text; ///< main text for the switch + const char + *subText; ///< description under main text (NULL terminated, single line, may be null) + nbgl_state_t initState; ///< initial state of the switch + uint8_t token; ///< the token that will be used as argument of the callback +#ifdef HAVE_PIEZO_SOUND + tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played +#endif // HAVE_PIEZO_SOUND +} nbgl_layoutSwitch_t; + +/** + * @brief This structure contains a list of names to build a list of radio + * buttons (on the right part of screen), with for each a description (names array) + * The chosen item index is provided is the "index" argument of the callback + */ +typedef struct { + union { + const char *const *names; ///< array of strings giving the choices (nbChoices) +#if defined(HAVE_LANGUAGE_PACK) + UX_LOC_STRINGS_INDEX *nameIds; ///< array of string Ids giving the choices (nbChoices) +#endif // HAVE_LANGUAGE_PACK + }; + bool localized; ///< if set to true, use nameIds and not names + uint8_t nbChoices; ///< number of choices + uint8_t initChoice; ///< index of the current choice + uint8_t token; ///< the token that will be used as argument of the callback +#ifdef HAVE_PIEZO_SOUND + tune_index_e + tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when selecting a radio button) +#endif // HAVE_PIEZO_SOUND +} nbgl_layoutRadioChoice_t; + +/** + * @brief prototype of menu list item retrieval callback + * @param choiceIndex index of the menu list item to retrieve (from 0 (to nbChoices-1)) + * @return a pointer on a string + */ +typedef const char *(*nbgl_menuListCallback_t)(uint8_t choiceIndex); + +/** + * @brief This structure contains a list of names to build a menu list on Nanos, with for each item + * a description (names array) + */ +typedef struct { + nbgl_menuListCallback_t callback; ///< function to call to retrieve a menu list item text + uint8_t nbChoices; ///< total number of choices in the menu list + uint8_t selectedChoice; ///< index of the selected choice (centered, in bold) +} nbgl_layoutMenuList_t; + +/** + * @brief This structure contains a [tag,value] pair + */ +typedef struct { + const char *item; ///< string giving the tag name + const char *value; ///< string giving the value name +#ifdef SCREEN_SIZE_WALLET + const nbgl_icon_details_t *valueIcon; ///< a buffer containing the 32px 1BPP icon for icon on + ///< right of value (can be NULL) + int8_t force_page_start : 1; ///< if set to 1, the tag will be displayed at the top of a new + ///< review page +#endif +} nbgl_layoutTagValue_t; + +/** + * @brief prototype of tag/value pair retrieval callback + * @param pairIndex index of the tag/value pair to retrieve (from 0 (to nbPairs-1)) + * @return a pointer on a static tag/value pair + */ +typedef nbgl_layoutTagValue_t *(*nbgl_tagValueCallback_t)(uint8_t pairIndex); + +/** + * @brief This structure contains a list of [tag,value] pairs + */ +typedef struct { + const nbgl_layoutTagValue_t + *pairs; ///< array of [tag,value] pairs (nbPairs items). If NULL, callback is used instead + nbgl_tagValueCallback_t callback; ///< function to call to retrieve a given pair + uint8_t nbPairs; ///< number of pairs in pairs array (or max number of pairs to retrieve with + ///< callback) + uint8_t startIndex; ///< index of the first pair to get with callback + uint8_t nbMaxLinesForValue; ///< if > 0, set the max number of lines for value field. And the + ///< last line is ended with "..." instead of the 3 last chars + uint8_t token; ///< the token that will be used as argument of the callback if icon in any + ///< tag/value pair is touched (index is the index of the pair in pairs[]) + bool smallCaseForValue; ///< if set to true, a 24px font is used for value text, otherwise a + ///< 32px font is used + bool wrapping; ///< if set to true, value text will be wrapped on ' ' to avoid cutting words +} nbgl_layoutTagValueList_t; + +/** + * @brief possible styles for Centered Info Area + * + */ +typedef enum { +#ifdef HAVE_SE_TOUCH + LARGE_CASE_INFO, ///< text in BLACK and large case (INTER 32px), subText in black in Inter24px + LARGE_CASE_BOLD_INFO, ///< text in BLACK and large case (INTER 32px), subText in black bold + ///< Inter24px, text3 in black Inter24px + NORMAL_INFO, ///< Icon in black, a potential text in black bold 24px under it, a potential text + ///< in dark gray (24px) under it, a potential text in black (24px) under it + PLUGIN_INFO ///< A potential text in black 32px, a potential text in black (24px) under it, a + ///< small horizontal line under it, a potential icon under it, a potential text in + ///< black (24px) under it +#else // HAVE_SE_TOUCH + REGULAR_INFO = 0, ///< both texts regular (but '\\b' can switch to bold) + BOLD_TEXT1_INFO ///< bold is used for text1 (but '\\b' can switch to regular) +#endif // HAVE_SE_TOUCH +} nbgl_centeredInfoStyle_t; + +/** + * @brief This structure contains info to build a centered (vertically and horizontally) area, with + * a possible Icon, a possible text under it, and a possible sub-text gray under it. + * + */ +typedef struct { + const char *text1; ///< first text (can be null) + const char *text2; ///< second text (can be null) +#ifdef HAVE_SE_TOUCH + const char *text3; ///< third text (can be null) +#endif // HAVE_SE_TOUCH + const nbgl_icon_details_t *icon; ///< a buffer containing the 1BPP icon + bool onTop; ///< if set to true, align only horizontaly + nbgl_centeredInfoStyle_t style; ///< style to apply to this info +#ifdef HAVE_SE_TOUCH + int16_t offsetY; ///< vertical shift to apply to this info (if >0, shift to bottom) +#endif // HAVE_SE_TOUCH +} nbgl_layoutCenteredInfo_t; + +/** + * @brief This structure contains info to build a centered (vertically and horizontally) area, with + * a QR Code, a possible text (black, bold) under it, and a possible sub-text (black, regular) under + * it. + * + */ +typedef struct { + const char *url; ///< URL for QR code + const char *text1; ///< first text (can be null) + const char *text2; ///< second text (can be null) + bool largeText1; ///< if set to true, use 32px font for text1 +} nbgl_layoutQRCode_t; + +/** + * @brief The different styles for a pair of buttons + * + */ +typedef enum { + ROUNDED_AND_FOOTER_STYLE + = 0, ///< A rounded black background full width button on top of a footer + BOTH_ROUNDED_STYLE ///< A rounded black background full width button on top of a rounded white + ///< background full width button +} nbgl_layoutChoiceButtonsStyle_t; + +/** + * @brief This structure contains info to build a pair of buttons, one on top of the other. + * + * @note the pair of button is automatically put on bottom of screen + */ +typedef struct { + const char *topText; ///< up-button text (index 0) + const char *bottomText; ///< bottom-button text (index 1) + uint8_t token; ///< the token that will be used as argument of the callback + nbgl_layoutChoiceButtonsStyle_t style; ///< the style of the pair +#ifdef HAVE_PIEZO_SOUND + tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played +#endif // HAVE_PIEZO_SOUND +} nbgl_layoutChoiceButtons_t; + +/** + * @brief The different styles for a button + * + */ +typedef enum { + BLACK_BACKGROUND + = 0, ///< rounded bordered button, with text/icon in white, on black background + WHITE_BACKGROUND, ///< rounded bordered button, with text/icon in black, on white background + NO_BORDER, ///< simple clickable text, in black + LONG_PRESS ///< long press button, with progress indicator +} nbgl_layoutButtonStyle_t; + +/** + * @brief This structure contains info to build a single button + */ +typedef struct { + const char *text; ///< button text + const nbgl_icon_details_t *icon; ///< a buffer containing the 1BPP icon for button1 + uint8_t token; ///< the token that will be used as argument of the callback + nbgl_layoutButtonStyle_t style; + bool fittingContent; ///< if set to true, fit the width of button to text, otherwise full width + bool onBottom; ///< if set to true, align on bottom of page, otherwise put on bottom of + ///< previous object +#ifdef HAVE_PIEZO_SOUND + tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played +#endif // HAVE_PIEZO_SOUND +} nbgl_layoutButton_t; + +/** + * @brief This structure contains info to build a progress bar with info. The progress bar itself is + * 120px width * 12px height + * + */ +typedef struct { + uint8_t percentage; ///< percentage of completion, from 0 to 100. + const char *text; ///< text in black, on top of progress bar + const char *subText; ///< text in gray, under progress bar +} nbgl_layoutProgressBar_t; + +/** + * @brief This structure contains info to build a keyboard with @ref nbgl_layoutAddKeyboard() + * + */ +typedef struct { + uint32_t keyMask; ///< mask used to disable some keys in letters only mod. The 26 LSB bits of + ///< mask are used, for the 26 letters of a QWERTY keyboard. Bit[0] for Q, + ///< Bit[1] for W and so on + keyboardCallback_t callback; ///< function called when an active key is pressed + bool lettersOnly; ///< if true, only display letter keys and Backspace + keyboardMode_t mode; ///< keyboard mode to start with +#ifdef HAVE_SE_TOUCH + keyboardCase_t casing; ///< keyboard casing mode (lower, upper once or upper locked) +#else // HAVE_SE_TOUCH + bool enableBackspace; ///< if true, Backspace key is enabled + bool enableValidate; ///< if true, Validate key is enabled + uint8_t selectedCharIndex; +#endif // HAVE_SE_TOUCH +} nbgl_layoutKbd_t; + +/********************** + * GLOBAL PROTOTYPES + **********************/ +#ifndef TARGET_NANOS +nbgl_layout_t *nbgl_layoutGet(const nbgl_layoutDescription_t *description); +int nbgl_layoutAddCenteredInfo(nbgl_layout_t *layout, const nbgl_layoutCenteredInfo_t *info); +int nbgl_layoutAddProgressBar(nbgl_layout_t *layout, const nbgl_layoutProgressBar_t *barLayout); + +#ifdef HAVE_SE_TOUCH +int nbgl_layoutAddTopRightButton(nbgl_layout_t *layout, + const nbgl_icon_details_t *icon, + uint8_t token, + tune_index_e tuneId); +int nbgl_layoutAddTouchableBar(nbgl_layout_t *layout, const nbgl_layoutBar_t *barLayout); +int nbgl_layoutAddSwitch(nbgl_layout_t *layout, const nbgl_layoutSwitch_t *switchLayout); +int nbgl_layoutAddText(nbgl_layout_t *layout, const char *text, const char *subText); +int nbgl_layoutAddRadioChoice(nbgl_layout_t *layout, const nbgl_layoutRadioChoice_t *choices); +int nbgl_layoutAddQRCode(nbgl_layout_t *layout, const nbgl_layoutQRCode_t *info); +int nbgl_layoutAddChoiceButtons(nbgl_layout_t *layout, const nbgl_layoutChoiceButtons_t *info); +int nbgl_layoutAddTagValueList(nbgl_layout_t *layout, const nbgl_layoutTagValueList_t *list); +int nbgl_layoutAddLargeCaseText(nbgl_layout_t *layout, const char *text); +int nbgl_layoutAddSeparationLine(nbgl_layout_t *layout); + +int nbgl_layoutAddButton(nbgl_layout_t *layout, const nbgl_layoutButton_t *buttonInfo); +int nbgl_layoutAddLongPressButton(nbgl_layout_t *layout, + const char *text, + uint8_t token, + tune_index_e tuneId); +int nbgl_layoutAddFooter(nbgl_layout_t *layout, + const char *text, + uint8_t token, + tune_index_e tuneId); +int nbgl_layoutAddSplitFooter(nbgl_layout_t *layout, + const char *leftText, + uint8_t leftToken, + const char *rightText, + uint8_t rightToken, + tune_index_e tuneId); +int nbgl_layoutAddNavigationBar(nbgl_layout_t *layout, const nbgl_layoutNavigationBar_t *info); +int nbgl_layoutAddBottomButton(nbgl_layout_t *layout, + const nbgl_icon_details_t *icon, + uint8_t token, + bool separationLine, + tune_index_e tuneId); +int nbgl_layoutAddProgressIndicator(nbgl_layout_t *layout, + uint8_t activePage, + uint8_t nbPages, + bool withBack, + uint8_t backToken, + tune_index_e tuneId); +int nbgl_layoutAddSpinner(nbgl_layout_t *layout, const char *text, bool fixed); +#else // HAVE_SE_TOUCH +int nbgl_layoutAddText(nbgl_layout_t *layout, + const char *text, + const char *subText, + nbgl_centeredInfoStyle_t style); +int nbgl_layoutAddNavigation(nbgl_layout_t *layout, nbgl_layoutNavigation_t *info); +int nbgl_layoutAddMenuList(nbgl_layout_t *layout, nbgl_layoutMenuList_t *list); +#endif // HAVE_SE_TOUCH + +#ifdef NBGL_KEYBOARD +/* layout objects for page with keyboard */ +int nbgl_layoutAddKeyboard(nbgl_layout_t *layout, const nbgl_layoutKbd_t *kbdInfo); +#ifdef HAVE_SE_TOUCH +int nbgl_layoutUpdateKeyboard(nbgl_layout_t *layout, + uint8_t index, + uint32_t keyMask, + bool updateCasing, + keyboardCase_t casing); +bool nbgl_layoutKeyboardNeedsRefresh(nbgl_layout_t *layout, uint8_t index); +int nbgl_layoutAddSuggestionButtons(nbgl_layout_t *layout, + uint8_t nbUsedButtons, + const char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS], + int firstButtonToken, + tune_index_e tuneId); +int nbgl_layoutUpdateSuggestionButtons(nbgl_layout_t *layout, + uint8_t index, + uint8_t nbUsedButtons, + const char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS]); +int nbgl_layoutAddEnteredText(nbgl_layout_t *layout, + bool numbered, + uint8_t number, + const char *text, + bool grayedOut, + int offsetY, + int token); +int nbgl_layoutUpdateEnteredText(nbgl_layout_t *layout, + uint8_t index, + bool numbered, + uint8_t number, + const char *text, + bool grayedOut); +int nbgl_layoutAddConfirmationButton(nbgl_layout_t *layout, + bool active, + const char *text, + int token, + tune_index_e tuneId); +int nbgl_layoutUpdateConfirmationButton(nbgl_layout_t *layout, + uint8_t index, + bool active, + const char *text); +#else // HAVE_SE_TOUCH +int nbgl_layoutUpdateKeyboard(nbgl_layout_t *layout, uint8_t index, uint32_t keyMask); +int nbgl_layoutAddEnteredText(nbgl_layout_t *layout, const char *text, bool lettersOnly); +int nbgl_layoutUpdateEnteredText(nbgl_layout_t *layout, uint8_t index, const char *text); +#endif // HAVE_SE_TOUCH +#endif // NBGL_KEYBOARD + +#ifdef NBGL_KEYPAD +#ifdef HAVE_SE_TOUCH +/* layout objects for page with keypad (Stax) */ +int nbgl_layoutAddKeypad(nbgl_layout_t *layout, keyboardCallback_t callback, bool shuffled); +int nbgl_layoutUpdateKeypad(nbgl_layout_t *layout, + uint8_t index, + bool enableValidate, + bool enableBackspace, + bool enableDigits); +int nbgl_layoutAddHiddenDigits(nbgl_layout_t *layout, uint8_t nbDigits); +int nbgl_layoutUpdateHiddenDigits(nbgl_layout_t *layout, uint8_t index, uint8_t nbActive); +#else // HAVE_SE_TOUCH +/* layout objects for pages with keypad (nanos) */ +int nbgl_layoutAddKeypad(nbgl_layout_t *layout, + keyboardCallback_t callback, + const char *text, + bool shuffled); +int nbgl_layoutUpdateKeypad(nbgl_layout_t *layout, + uint8_t index, + bool enableValidate, + bool enableBackspace); +int nbgl_layoutAddHiddenDigits(nbgl_layout_t *layout, uint8_t nbDigits); +int nbgl_layoutUpdateHiddenDigits(nbgl_layout_t *layout, uint8_t index, uint8_t nbActive); +#endif // HAVE_SE_TOUCH +#endif // NBGL_KEYPAD + +/* generic functions */ +int nbgl_layoutDraw(nbgl_layout_t *layout); +int nbgl_layoutRelease(nbgl_layout_t *layout); +#else // TARGET_NANOS +void nbgl_screenHandler(uint32_t intervaleMs); +void nbgl_refresh(void); +void nbgl_layoutGet(const nbgl_layoutDescription_t *description); +void nbgl_layoutAddNavigation(nbgl_layoutNavigation_t *info); +void nbgl_layoutAddText(const char *text, + int line, + bool bold, + bool centered); +void nbgl_layoutAddCenteredInfo(const nbgl_layoutCenteredInfo_t *info); +void nbgl_layoutDraw(void); +void nbgl_layoutRelease(void); +#endif +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_LAYOUT_H */ diff --git a/sdk_lib_nbgl/include/nbgl_obj.h b/sdk_lib_nbgl/include/nbgl_obj.h new file mode 100644 index 00000000..8341fd70 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_obj.h @@ -0,0 +1,612 @@ +/** + * @file nbgl_obj.h + * @brief API to draw all basic graphic objects + * + */ + +#ifndef NBGL_OBJ_H +#define NBGL_OBJ_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "nbgl_types.h" +#include "nbgl_fonts.h" +#ifndef TARGET_NANOS +#include "ux_loc.h" +#endif + +/********************* + * INCLUDES + *********************/ + +/********************* + * DEFINES + *********************/ +// Keypad special key values +#define BACKSPACE_KEY 8 +#define VALIDATE_KEY '\r' + +// for Keyboard +#ifdef HAVE_SE_TOUCH +#define KEYBOARD_KEY_HEIGHT 60 +#else // HAVE_SE_TOUCH +#define KEYBOARD_KEY_WIDTH 14 +#define KEYBOARD_KEY_HEIGHT 14 +#define KEYBOARD_WIDTH (5 * KEYBOARD_KEY_WIDTH) +#endif // HAVE_SE_TOUCH + +// for Keypad +#ifdef HAVE_SE_TOUCH +#define KEYPAD_KEY_HEIGHT 104 +#else // HAVE_SE_TOUCH +#define KEYPAD_WIDTH 114 +#define KEYPAD_HEIGHT 18 +#endif // HAVE_SE_TOUCH + +#ifdef HAVE_SE_TOUCH +///< special code used by given callback of @ref nbgl_navigationPopulate to inform when Exit key is +///< pressed +#define EXIT_PAGE 0xFF + +// external margin in pixels +#define BORDER_MARGIN 24 + +// Back button header height +#define BACK_BUTTON_HEADER_HEIGHT 88 + +// common dimensions for buttons +#define BUTTON_RADIUS RADIUS_40_PIXELS +#define BUTTON_DIAMETER 80 +#endif // HAVE_SE_TOUCH + +/********************** + * TYPEDEFS + **********************/ + +/** + * @brief All types of graphical objects. + * + */ +typedef enum { + SCREEN, ///< Main screen + CONTAINER, ///< Empty container + IMAGE, ///< Bitmap (y and height must be multiple of 4 on Stax) + LINE, ///< Vertical or Horizontal line + TEXT_AREA, ///< Area to contain text line(s) + BUTTON, ///< Rounded rectangle button with icon and/or text + SWITCH, ///< Switch to turn on/off something + PAGE_INDICATOR, ///< horizontal bar to indicate position within pages + PROGRESS_BAR, ///< horizontal bar to indicate progression of something (between 0% and 100%) + RADIO_BUTTON, ///< Indicator to inform whether something is on or off + QR_CODE, ///< QR Code + KEYBOARD, ///< Keyboard + KEYPAD, ///< Keypad + SPINNER, ///< Spinner + IMAGE_FILE, ///< Image file (with Ledger compression) + TEXT_ENTRY ///< area for entered text, only for Nanos +} nbgl_obj_type_t; + +/** + * @brief All types of alignments. + * + */ +typedef enum { + NO_ALIGNMENT, ///< used when parent container layout is used + TOP_LEFT, + TOP_MIDDLE, + TOP_RIGHT, + MID_LEFT, + CENTER, + MID_RIGHT, + BOTTOM_LEFT, + BOTTOM_MIDDLE, + BOTTOM_RIGHT, + LEFT_TOP, ///< on outside left + LEFT_BOTTOM, ///< on outside left + RIGHT_TOP, ///< on outside right + RIGHT_BOTTOM, ///< on outside right +} nbgl_aligment_t; + +/** + * @brief to represent a boolean state. + */ +typedef enum { + OFF_STATE, + ON_STATE +} nbgl_state_t; + +/** + * @brief Directions for layout or lines + * + */ +typedef enum { + VERTICAL, ///< from top to bottom + HORIZONTAL ///< from left to right +} nbgl_direction_t; + +/** + * @brief possible styles for text area border + * + */ +typedef enum { + NO_STYLE, ///< no border + INVERTED_COLORS ///< Inverted background and rounded corners, only for @ref TEXT_AREA +} nbgl_style_t; + +/** + * @brief possible modes for QR Code + * @note if text len <= 114 chars, V4 can be used, otherwise use V10 + * + */ +typedef enum { + QRCODE_V4 = 0, ///< QRCode V4, can encode text len up to 114 chars + QRCODE_V10 ///< QRCode V10, can encode text len up to 1500 chars +} nbgl_qrcode_version_t; + +/** + * @brief the 2 possible states of a finger on the Touchscreen + * + */ +typedef enum { + RELEASED, ///< the finger has been released from the screen + PRESSED, ///< the finger is currently pressing the screen +} nbgl_touchState_t; + +/** + * @brief The different types of Touchscreen events + * + */ +typedef enum { + TOUCHED, ///< corresponding to an object touched and released at least SHORT_TOUCH_DURATION ms + ///< later but less than LONG_TOUCH_DURATION ms + LONG_TOUCHED, ///< corresponding to an object touched and released at least LONG_TOUCH_DURATION + ///< ms later. + TOUCHING, ///< corresponding to an object that is currently touched + OUT_OF_TOUCH, ///< corresponding to an object that was touched but that has lost the focus (the + ///< finger has moved) + TOUCH_PRESSED, ///< corresponding to an object that was not touched and where the finger has + ///< been pressed. + TOUCH_RELEASED, ///< corresponding to an object that was touched and where the finger has been + ///< released. + VALUE_CHANGED ///< corresponding to a change of state of the object (indirect event) +} nbgl_touchType_t; + +/** + * @brief The different pressed buttons + * + */ +#define LEFT_BUTTON 0x01 ///< Left button event +#define RIGHT_BUTTON 0x02 ///< Right button event +#define BOTH_BUTTONS 0x03 ///< Both buttons event +#define RELEASED_MASK 0x80 ///< released (see LSB bits to know what buttons are released) +#define CONTINUOUS_MASK \ + 0x40 ///< if set, means that the button(s) is continuously pressed (this event is sent every + ///< 300ms after the first 800ms) + +typedef enum { + BUTTON_LEFT_PRESSED = 0, ///< Sent when Left button is released + BUTTON_RIGHT_PRESSED, ///< Send when Right button is released + BUTTON_LEFT_CONTINUOUS_PRESSED, ///< Send when Left button is continuouly pressed (sent every + ///< 300ms after the first 800ms) + BUTTON_RIGHT_CONTINUOUS_PRESSED, ///< Send when Left button is continuouly pressed (sent every + ///< 300ms after the first 800ms) + BUTTON_BOTH_PRESSED, ///< Sent when both buttons are released + BUTTON_BOTH_TOUCHED, ///< Sent when both buttons are touched + INVALID_BUTTON_EVENT +} nbgl_buttonEvent_t; + +/** + * @brief prototype of function to be called when a button event is received by an object (TODO: + * change to screen?) + * @param obj the concerned object + * @param buttonState event on buttons + */ +typedef void (*nbgl_buttonCallback_t)(void *obj, nbgl_buttonEvent_t buttonEvent); + +/** + * @brief The low level Touchscreen event, coming from driver + * + */ +typedef struct { + nbgl_touchState_t state; ///< state of the touch event, e.g @ref PRESSED or @ref RELEASED + int16_t + x; ///< horizontal position of the touch (or for a @ref RELEASED the last touched point) + int16_t y; ///< vertical position of the touch (or for a @ref RELEASED the last touched point) +} nbgl_touchStatePosition_t; + +/** + * @brief prototype of function to be called when a touch event is received by an object + * @param obj the concerned object + * @param eventType type of touch event + */ +typedef void (*nbgl_touchCallback_t)(void *obj, nbgl_touchType_t eventType); + +/** + * @brief Common structure for all graphical objects + * + * @note this type must never be instantiated + */ +typedef struct PACKED__ nbgl_obj_s { + nbgl_area_t area; ///< absolute position, backGround color and size of the object. DO NOT MOVE + ///< THIS FIELD + int16_t + rel_x0; ///< horizontal position of top-left corner relative to parent's top-left corner + int16_t rel_y0; ///< vertical position of top-left corner relative to parent's top-left corner, + ///< must be multiple of 4 + struct nbgl_obj_s *parent; ///< parent of this object + struct nbgl_obj_s *alignTo; ///< object to align to (parent by default) + nbgl_aligment_t alignment; ///< type of alignment + int16_t alignmentMarginX; ///< horizontal margin when aligning + int16_t alignmentMarginY; ///< vertical margin when aligning + nbgl_obj_type_t type; ///< type of the graphical object, must be explicitly set + uint8_t touchMask; ///< bit mask to tell engine which touch events are handled by this object + uint8_t touchId; ///< a unique identifier (by screen) to be used by external test environment + ///< (TTYT or Screenshots) +} nbgl_obj_t; + +/** + * @brief struct to represent a container (@ref CONTAINER type) + * + * @note the main screen is a kind of container + * + */ +typedef struct PACKED__ nbgl_container_s { + nbgl_obj_t obj; ///< common part + nbgl_direction_t layout; ///< layout of children inside this object + uint8_t nbChildren; + bool forceClean; ///< if set to true, a paint will be done with background color + struct nbgl_obj_s **children; ///< children of this object (nbChildren size) +} nbgl_container_t; + +/** + * @brief struct to represent a vertical or horizontal line + * + */ +typedef struct PACKED__ nbgl_line_s { + nbgl_obj_t obj; ///< common part + nbgl_direction_t direction; ///< direction of the line, e.g @ref VERTICAL or @ref HORIZONTAL + color_t lineColor; ///< color of the line + uint8_t thickness; ///< thickness of the line in pixel, maybe different from height for + ///< horizontal line + uint8_t offset; ///< the object height being always 4, with a y0 multiple of 4, this offset is + ///< use to move the line within these 4 pixels +} nbgl_line_t; + +/** + * @brief prototype of function to be called when a @ref IMAGE object is drawned, and no buffer was + * provided + * @param token provided token in @ref IMAGE object + * @return the icn details to be drawned in image object + */ +typedef nbgl_icon_details_t *(*onImageDrawCallback_t)(uint8_t token); + +/** + * @brief struct to represent an image (@ref IMAGE type) + * + */ +typedef struct PACKED__ nbgl_image_s { + nbgl_obj_t obj; // common part + color_t foregroundColor; ///< color set to '1' bits, for 1PBB images. '0' are set to background + ///< color. + const nbgl_icon_details_t *buffer; ///< buffer containing bitmap, with exact same size as + ///< object (width*height*bpp/8 bytes) + onImageDrawCallback_t onDrawCallback; ///< function called if buffer is NULL, with above token + ///< as parameter. Can be NULL + uint8_t token; ///< token to use as param of onDrawCallback +} nbgl_image_t; + +/** + * @brief struct to represent an image file object (@ref IMAGE_FILE type) + * The source of the data is an image file with header. width and height are given in this header + * + */ +typedef struct PACKED__ nbgl_image_file_s { + nbgl_obj_t obj; // common part + const uint8_t *buffer; ///< buffer containing image file +} nbgl_image_file_t; + +/** + * @brief struct to represent a QR code (@ref QR_CODE type), whose size is fixed + * + */ +typedef struct PACKED__ nbgl_qrcode_s { + nbgl_obj_t obj; // common part + color_t foregroundColor; ///< color set to '1' bits, for 1PBB images. '0' are set to background + ///< color. + nbgl_qrcode_version_t version; ///< requested version, if V10, size will be fixed to 228*228, + ///< if V4, size will be fixed to 132*132 + const char *text; ///< text single line (NULL terminated) +} nbgl_qrcode_t; + +/** + * @brief struct to represent a radio button (@ref RADIO_BUTTON type) + * + * @note size is fixed + * + */ +typedef struct PACKED__ nbgl_radio_s { + nbgl_obj_t obj; // common part + color_t activeColor; ///< color set to to inner circle, when active. + color_t borderColor; ///< color set to border. + nbgl_state_t state; ///< state of the radio button. Active is when state == @ref ON_STATE +} nbgl_radio_t; + +/** + * @brief struct to represent a switch (size is fixed) (@ref SWITCH type) + * + */ +typedef struct PACKED__ nbgl_switch_s { + nbgl_obj_t obj; // common part + color_t onColor; ///< color set to border and knob, when ON (knob on the right). + color_t offColor; ///< color set to border and knob, when OFF (knob on the left). + nbgl_state_t state; ///< state of the switch. +} nbgl_switch_t; + +/** + * @brief struct to represent a progress bar (@ref PROGRESS_BAR type) + * @note if withBorder, the stroke of the border is fixed (3 pixels) + */ +typedef struct PACKED__ nbgl_progress_bar_s { + nbgl_obj_t obj; // common part + bool withBorder; ///< if set to true, a border in black surround the whole object + uint8_t state; ///< state of the progress, in % (from 0 to 100). + color_t foregroundColor; ///< color of the inner progress bar and border (if applicable) +} nbgl_progress_bar_t; + +/** + * @brief struct to represent a navigation bar (@ref PAGE_INDICATOR type) + * There can be up to 5 page indicators, whose shape is fixed. + * If there are more than 5 pages, the middle indicator will be "..." + * + * @note height is fixed + */ +typedef struct PACKED__ nbgl_navigation_bar_s { + nbgl_obj_t obj; ///< common part + uint8_t nbPages; ///< number of pages. + uint8_t activePage; ///< index of active page (from 0 to nbPages-1). +} nbgl_page_indicator_t; + +/** + * @brief prototype of function to be called when a @ref TEXT_AREA object is drawned, and no text + * was provided + * @param token provided token in @ref TEXT_AREA object + * @return an ASCII string (null terminated) to be drawned in text area + */ +typedef char *(*onTextDrawCallback_t)(uint8_t token); + +/** + * @brief struct to represent a button (@ref BUTTON type) + * that can contain a text and/or an icon + * @note border width is fixed (2 pixels) + * + */ +typedef struct PACKED__ nbgl_button_s { + nbgl_obj_t obj; ///< common part + color_t innerColor; ///< color set inside of the button + color_t borderColor; ///< color set to button's border + color_t foregroundColor; ///< color set to '1' bits in icon, and text. '0' are set to + ///< innerColor color. + nbgl_radius_t radius; ///< radius of the corners, must be a multiple of 4. + nbgl_font_id_e fontId; ///< id of the font to use, if any + bool localized; ///< if set to true, means the following 'text' field is considered as a + const char *text; ///< single line UTF-8 text (NULL terminated) +#if defined(HAVE_LANGUAGE_PACK) + UX_LOC_STRINGS_INDEX textId; ///< id of the text single line UTF-8 text +#endif // HAVE_LANGUAGE_PACK + onTextDrawCallback_t onDrawCallback; ///< function called if not NULL, with above token as + ///< parameter to get the text of the button + uint8_t token; ///< token to use as param of onDrawCallback + const nbgl_icon_details_t *icon; ///< buffer containing icons bitmap. Set to NULL when no icon +} nbgl_button_t; + +/** + * @brief struct to represent a text area (@ref TEXT_AREA type) + * + */ +typedef struct PACKED__ nbgl_text_area_s { + nbgl_obj_t obj; ///< common part + color_t textColor; ///< color set to '1' bits in text. '0' are set to backgroundColor color. + nbgl_aligment_t textAlignment; ///< alignment of text within the area + nbgl_style_t style; ///< to define the style of border + nbgl_font_id_e fontId; ///< id of the font to use + bool localized; ///< if set to true, use textId instead of text + bool autoHideLongLine; ///< if set to true, replace beginning of line by ... to keep it single + ///< line + bool wrapping; ///< if set to true, break lines on ' ' when possible + uint8_t nbMaxLines; ///< if >0, replace end (3 last chars) of line (nbMaxLines-1) by "..." and + ///< stop display here + const char *text; ///< ASCII text to draw (NULL terminated). Can be NULL. + uint16_t len; ///< number of bytes to write (if 0, max number of chars or strlen is used) +#if defined(HAVE_LANGUAGE_PACK) + UX_LOC_STRINGS_INDEX textId; ///< id of the UTF-8 text +#endif // HAVE_LANGUAGE_PACK + onTextDrawCallback_t + onDrawCallback; ///< function called if not NULL to get the text of the text area + uint8_t token; ///< token to use as param of onDrawCallback +} nbgl_text_area_t; + +/** + * @brief struct to represent a text entry area (@ref TEXT_ENTRY type) + * + */ +typedef struct PACKED__ nbgl_text_entry_s { + nbgl_obj_t obj; ///< common part + nbgl_font_id_e fontId; ///< id of the font to use + uint8_t nbChars; ///< number of char placeholders to display (8 or 9 chars). + const char *text; ///< text to display (up to nbChars chars). +} nbgl_text_entry_t; + +/** + * @brief struct to represent a "spinner", represented by the Ledger corners, in gray, with one of + * the corners in black (@ref SPINNER type) + * + */ +typedef struct PACKED__ nbgl_spinner_s { + nbgl_obj_t obj; ///< common part + uint8_t position; ///< position of the spinner (from 0 to 3). If set to 0xFF, the spinner is + ///< entirely black +} nbgl_spinner_t; + +/** + * @brief prototype of function to be called when a valid key is pressed on keyboard + * Backspace is equal to 0x8 (ASCII code), Validate (for Keypad) is equal to 15 ('\\r') + * @param touchedKey char typed on keyboard + */ +typedef void (*keyboardCallback_t)(char touchedKey); + +/** + * @brief Mode in which to open/set the keyboard + * + */ +typedef enum { +#ifdef HAVE_SE_TOUCH + MODE_LETTERS = 0, ///< letters mode + MODE_DIGITS, ///< digits and some special characters mode + MODE_SPECIAL ///< extended special characters mode +#else // HAVE_SE_TOUCH + MODE_LOWER_LETTERS, ///< lower case letters mode + MODE_UPPER_LETTERS, ///< upper case letters mode + MODE_DIGITS_AND_SPECIALS, ///< digits and some special characters mode + MODE_NONE ///< no mode defined (only for Nanos) +#endif // HAVE_SE_TOUCH +} keyboardMode_t; + +/** + * @brief Letters casing in which to open/set the keyboard + * + */ +typedef enum { + LOWER_CASE = 0, ///< lower case mode + UPPER_CASE, ///< upper case mode for one character + LOCKED_UPPER_CASE ///< locked upper case mode +} keyboardCase_t; + +/** + * @brief struct to represent a keyboard (@ref KEYBOARD type) + * + */ +typedef struct PACKED__ nbgl_keyboard_s { + nbgl_obj_t obj; ///< common part + color_t textColor; ///< color set to letters. + color_t borderColor; ///< color set to key borders + bool lettersOnly; ///< if true, only display letter keys and Backspace +#ifdef HAVE_SE_TOUCH + bool needsRefresh; ///< if true, means that the keyboard has been redrawn and needs a refresh + keyboardCase_t casing; ///< keyboard casing mode (lower, upper once or upper locked) +#else // HAVE_SE_TOUCH + bool enableBackspace; ///< if true, Backspace key is enabled + bool enableValidate; ///< if true, Validate key is enabled + uint8_t selectedCharIndex; +#endif // HAVE_SE_TOUCH + keyboardMode_t mode; ///< keyboard mode to start with + uint32_t keyMask; ///< mask used to disable some keys in letters only mod. The 26 LSB bits of + ///< mask are used, for the 26 letters of a QWERTY keyboard. Bit[0] for Q, + ///< Bit[1] for W and so on + keyboardCallback_t callback; ///< function called when an active key is pressed +} nbgl_keyboard_t; + +/** + * @brief struct to represent a keypad (@ref KEYPAD type) + * + */ +typedef struct PACKED__ nbgl_keypad_s { + nbgl_obj_t obj; ///< common part +#ifdef HAVE_SE_TOUCH + color_t textColor; ///< color set to digits. + color_t borderColor; ///< color set to key borders + bool enableDigits; ///< if true, Digit keys are enabled + uint8_t digitIndexes[5]; ///< array of digits indexes, 4 bits per digit +#else // HAVE_SE_TOUCH + uint8_t selectedKey; ///< selected key position +#endif // HAVE_SE_TOUCH + bool enableBackspace; ///< if true, Backspace key is enabled + bool enableValidate; ///< if true, Validate key is enabled + bool shuffled; ///< if true, Digit keys are shuffled + keyboardCallback_t callback; ///< function called when an active key is pressed +} nbgl_keypad_t; + +/** + * @brief ids of touchable objects, for external stimulus (by Testing environment) + * + */ +enum { + BOTTOM_BUTTON_ID = 1, + LEFT_BUTTON_ID, + RIGHT_BUTTON_ID, + WHOLE_SCREEN_ID, + TOP_RIGHT_BUTTON_ID, + BACK_BUTTON_ID, + SINGLE_BUTTON_ID, + CHOICE_1_ID, + CHOICE_2_ID, + KEYPAD_ID, + KEYBOARD_ID, + ENTERED_TEXT_ID, + LONG_PRESS_BUTTON_ID, + CONTROLS_ID, // when multiple controls in the same pages (buttons, switches, radios) + NB_CONTROL_IDS +}; + +/********************** + * GLOBAL PROTOTYPES + **********************/ +void nbgl_redrawObject(nbgl_obj_t *obj, nbgl_obj_t *prevObj, bool computePosition); + +void nbgl_refresh(void); +void nbgl_refreshSpecial(nbgl_refresh_mode_t mode); +void nbgl_refreshSpecialWithPostRefresh(nbgl_refresh_mode_t mode, nbgl_post_refresh_t post_refresh); +bool nbgl_refreshIsNeeded(void); +void nbgl_refreshReset(void); + +void nbgl_objInit(void); +void nbgl_objAllowDrawing(bool enable); + +void nbgl_objPoolRelease(uint8_t layer); +nbgl_obj_t *nbgl_objPoolGet(nbgl_obj_type_t type, uint8_t layer); +nbgl_obj_t *nbgl_objPoolGetPrevious(nbgl_obj_t *obj, uint8_t layer); +uint8_t nbgl_objPoolGetId(nbgl_obj_t *obj); +int nbgl_objPoolGetArray(nbgl_obj_type_t type, + uint8_t nbObjs, + uint8_t layer, + nbgl_obj_t **objArray); +uint8_t nbgl_objPoolGetNbUsed(uint8_t layer); +void nbgl_containerPoolRelease(uint8_t layer); +nbgl_obj_t **nbgl_containerPoolGet(uint8_t nbObjs, uint8_t layer); +uint8_t nbgl_containerPoolGetNbUsed(uint8_t layer); + +#ifdef HAVE_SE_TOUCH +nbgl_container_t *nbgl_navigationPopulate(uint8_t nbPages, + uint8_t activePage, + bool withExitKey, + uint8_t layer); +bool nbgl_navigationCallback(nbgl_obj_t *obj, + nbgl_touchType_t eventType, + uint8_t nbPages, + uint8_t *activePage); +#endif // HAVE_SE_TOUCH + +// for internal use +void nbgl_objDrawKeyboard(nbgl_keyboard_t *kbd); +void nbgl_objDrawKeypad(nbgl_keypad_t *kbd); +#ifdef HAVE_SE_TOUCH +void nbgl_keyboardTouchCallback(nbgl_obj_t *obj, nbgl_touchType_t eventType); +void nbgl_keypadTouchCallback(nbgl_obj_t *obj, nbgl_touchType_t eventType); + +bool nbgl_keyboardGetPosition(nbgl_keyboard_t *kbd, char index, uint16_t *x, uint16_t *y); +bool nbgl_keypadGetPosition(nbgl_keypad_t *kbd, char index, uint16_t *x, uint16_t *y); +#else // HAVE_SE_TOUCH +void nbgl_keyboardCallback(nbgl_obj_t *obj, nbgl_buttonEvent_t buttonEvent); +void nbgl_keypadCallback(nbgl_obj_t *obj, nbgl_buttonEvent_t buttonEvent); +#endif // HAVE_SE_TOUCH + +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_OBJ_H */ diff --git a/sdk_lib_nbgl/include/nbgl_page.h b/sdk_lib_nbgl/include/nbgl_page.h new file mode 100644 index 00000000..b14a8bbe --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_page.h @@ -0,0 +1,323 @@ +/** + * @file nbgl_page.h + * @brief API of the Advanced BOLOS Graphical Library, for predefined pages + * + */ + +#ifndef NBGL_PAGE_H +#define NBGL_PAGE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ + +#include "nbgl_layout.h" +#include "nbgl_obj.h" +#include "nbgl_types.h" + +/********************* + * DEFINES + *********************/ + +/********************** + * TYPEDEFS + **********************/ + +/** + * @brief The different types of top-right / bottom button + * + */ +typedef enum { + NO_BUTTON_STYLE = 0, ///< no button. + SETTINGS_ICON, ///< settings (wheel) icon in the button. + QUIT_ICON, ///< quit (X) icon in the button. + INFO_ICON, ///< info (i) icon in the button. + QUIT_APP_TEXT ///< A full width button with "Quit app" text (only for bottom button) +} nbgl_pageButtonStyle_t; + +/** + * @brief The different types of predefined page contents + * + */ +typedef enum { + CENTERED_INFO = 0, ///< a centered info + INFO_LONG_PRESS, ///< a centered info and a long press button + INFO_BUTTON, ///< a centered info and a simple black button + TAG_VALUE_LIST, ///< list of tag/value pairs + TAG_VALUE_DETAILS, ///< a tag/value pair and a small button to get details. + TAG_VALUE_CONFIRM, ///< tag/value pairs and a black button/footer to confirm/cancel. + SWITCHES_LIST, ///< list of switches with descriptions + INFOS_LIST, ///< list of infos with titles + CHOICES_LIST, ///< list of choices through radio buttons + BARS_LIST ///< list of touchable bars (with > on the right to go to sub-pages) +} nbgl_pageContentType_t; + +/** + * @brief This structure contains a [item,value] pair and info about "details" button + */ +typedef struct nbgl_pageTagValueDetails_s { + nbgl_layoutTagValueList_t tagValueList; ///< list of tag/value pairs + const nbgl_icon_details_t *detailsButtonIcon; ///< icon to use in details button + const char *detailsButtonText; ///< this text is used for "details" button + uint8_t detailsButtonToken; ///< the token used as argument of the actionCallback when the + ///< "details" button is touched + tune_index_e + tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when details button is touched +} nbgl_pageTagValueDetails_t; + +/** + * @brief This structure contains [item,value] pair(s) and info about a potential "details" button, + * but also a black button + footer to confirm/cancel + */ +typedef struct nbgl_pageTagValueConfirm_s { + nbgl_layoutTagValueList_t tagValueList; ///< list of tag/value pairs + const nbgl_icon_details_t *detailsButtonIcon; ///< icon to use in details button + const char *detailsButtonText; ///< this text is used for "details" button (if NULL, no button) + uint8_t detailsButtonToken; ///< the token used as argument of the actionCallback when the + ///< "details" button is touched + tune_index_e + tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when details button is touched + const char + *confirmationText; ///< text of the confirmation button, if NULL "It matches" is used + const char + *cancelText; ///< the text used for cancel action, if NULL "It doesn't matches" is used + uint8_t confirmationToken; ///< the token used as argument of the onActionCallback + uint8_t cancelToken; ///< the token used as argument of the onActionCallback when the cancel + ///< button is pressed +} nbgl_pageTagValueConfirm_t; + +/** + * @brief This structure contains data to build a centered info + long press button page content + */ +typedef struct nbgl_pageInfoLongPress_s { + const char *text; ///< centered text in large case + const nbgl_icon_details_t *icon; ///< a buffer containing the 1BPP icon + const char *longPressText; ///< text of the long press button + uint8_t longPressToken; ///< the token used as argument of the onActionCallback when button is + ///< long pressed + tune_index_e + tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when button is touched +} nbgl_pageInfoLongPress_t; + +/** + * @brief This structure contains data to build a centered info + simple black button page content + */ +typedef struct nbgl_pageInfoButton_s { + const char *text; ///< centered text in large case + const nbgl_icon_details_t *icon; ///< a buffer containing the 1BPP icon + const char *buttonText; ///< text of the long press button + uint8_t buttonToken; ///< the token used as argument of the onActionCallback when button is + ///< long pressed + tune_index_e + tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when button is touched +} nbgl_pageInfoButton_t; + +/** + * @brief This structure contains data to build a @ref SWITCHES_LIST page content + */ +typedef struct nbgl_pageSwitchesList_s { + const nbgl_layoutSwitch_t *switches; ///< array of switches (nbSwitches items) + uint8_t nbSwitches; ///< number of elements in switches and tokens array +} nbgl_pageSwitchesList_t; + +/** + * @brief This structure contains data to build a @ref INFOS_LIST page content + */ +typedef struct nbgl_pageInfoList_s { + const char *const *infoTypes; ///< array of types of infos (in black/bold) + const char *const *infoContents; ///< array of contents of infos (in black) + uint8_t nbInfos; ///< number of elements in infoTypes and infoContents array +} nbgl_pageInfoList_t; + +/** + * @brief This structure contains data to build a @ref BARS_LIST page content + */ +typedef struct nbgl_pageBarsList_s { + const char *const *barTexts; ///< array of texts for each bar (nbBars items, in black/bold) + const uint8_t *tokens; ///< array of tokens, one for each bar (nbBars items) + uint8_t nbBars; ///< number of elements in barTexts and tokens array + tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when a bar is touched +} nbgl_pageBarsList_t; + +/** + * @brief This structure contains data to build a page in multi-pages mode (@ref + * nbgl_pageDrawGenericContent) + */ +typedef struct nbgl_pageContent_s { + const char *title; ///< text for the title of the page (if NULL, no title) + bool isTouchableTitle; ///< if set to true, the title is preceded by <- arrow to go back + uint8_t titleToken; ///< if isTouchableTitle set to true, this is the token used when touching + ///< title + tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when title is touched + nbgl_pageContentType_t type; ///< type of page content in the following union + union { + nbgl_layoutTagValueList_t tagValueList; ///< @ref TAG_VALUE_LIST type + nbgl_layoutCenteredInfo_t centeredInfo; ///< @ref CENTERED_INFO type + nbgl_pageInfoLongPress_t infoLongPress; ///< @ref INFO_LONG_PRESS type + nbgl_pageInfoButton_t infoButton; ///< @ref INFO_BUTTON type + nbgl_pageTagValueDetails_t tagValueDetails; ///< @ref TAG_VALUE_DETAILS type + nbgl_pageTagValueConfirm_t tagValueConfirm; ///< @ref TAG_VALUE_CONFIRM type + nbgl_pageSwitchesList_t switchesList; ///< @ref SWITCHES_LIST type + nbgl_pageInfoList_t infosList; ///< @ref INFOS_LIST type + nbgl_layoutRadioChoice_t choicesList; ///< @ref CHOICES_LIST type + nbgl_pageBarsList_t barsList; ///< @ref BARS_LIST type + }; +} nbgl_pageContent_t; + +/** + * @brief type shared externally + * + */ +typedef void *nbgl_page_t; + +/** + * @brief The different types of navigation in a multi-screens page + * + */ +typedef enum { + NAV_WITH_TAP, ///< move forward with "tap" and possibly backward with top left arrow + NAV_WITH_BUTTONS, ///< move forward and backward with buttons in bottom nav bar +} nbgl_pageNavigationType_t; + +/** + * @brief Structure containing all information to create a navigation with "tap" + * + */ +typedef struct nbgl_pageNavWithTap_s { + bool backButton; ///< if set to true, a back button (left arrow) is displayed in the top left + ///< corner (if page >=1) + uint8_t backToken; ///< the token used as argument of the actionCallback when the back button + ///< is touched + uint8_t nextPageToken; ///< the token used as argument of the actionCallback when the main + ///< panel is "tapped" + const char *nextPageText; ///< this text is used as indication for "Tap to continue", to + ///< navigate forward + const char *quitText; ///< the text displayed in footer, used to quit + const char *skipText; ///< if not NULL the text displayed in right part of footer, used for + ///< example to skip pages + uint8_t skipToken; ///< if skipText is NULL the token used when right part of footer is touched +} nbgl_pageNavWithTap_t; + +/** + * @brief Structure containing all information to create a navigation with buttons in bottom nav bar + * + */ +typedef struct nbgl_pageNavWithButtons_s { + bool quitButton; ///< if set to true, a quit button (X) is displayed in the nav bar + uint8_t navToken; ///< the token used as argument of the actionCallback when the nav buttons + ///< are pressed (index param gives the page) +} nbgl_pageNavWithButtons_t; + +/** + * @brief Structure containing all specific information when creating a multi-screens page. + * + */ +typedef struct nbgl_pageMultiScreensDescription_s { + uint8_t activePage; ///< the index of the page to display at start-up + uint8_t nbPages; ///< the number of pages to display (if <2, no navigation bar) + uint8_t + quitToken; ///< the token used as argument of the actionCallback when the footer is touched + nbgl_pageNavigationType_t navType; ///< type of navigation, it will tell which structure in the + ///< following union will be used + bool progressIndicator; ///< if set to true, display a progress indicator on top of the page + tune_index_e + tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when next or back is pressed + union { + nbgl_pageNavWithTap_t navWithTap; ///< structure used when navigation with "tap" + nbgl_pageNavWithButtons_t navWithButtons; ///< structure used when navigation with buttons + }; +} nbgl_pageNavigationInfo_t; + +/** + * @brief Structure containing all specific information when creating a confirmation page. + */ +typedef struct nbgl_pageConfirmationDescription_s { + nbgl_layoutCenteredInfo_t centeredInfo; ///< description of the centered info to be used + const char *confirmationText; ///< text of the confirmation button + const char *cancelText; ///< the text used for cancel action, if NULL a simple X button is used + uint8_t confirmationToken; ///< the token used as argument of the onActionCallback + uint8_t cancelToken; ///< the token used as argument of the onActionCallback when the cancel + ///< button is pressed + tune_index_e + tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when button is pressed + bool modal; ///< if true, page is open as a modal +} nbgl_pageConfirmationDescription_t; + +/** + * @brief Structure containing all specific information when creating a address confirmation page. + * This page contains the address in a tag/value format, and a white button to dipsplay the same + * address as a QRCode in a modal window. At the bottom 2 buttons allow to confirm or invalidate the + * address + */ +typedef struct nbgl_pageAddressConfirmationDescription_s { + const char *address; ///< address to confirm + const char + *qrCodeButtonText; ///< text to display in "QR code button", if NULL "Show as QR is used" + const char + *confirmationText; ///< text of the confirmation button, if NULL "It matches" is used + const char + *cancelText; ///< the text used for cancel action, if NULL "It doesn't matches" is used + uint8_t confirmationToken; ///< the token used as argument of the onActionCallback + uint8_t cancelToken; ///< the token used as argument of the onActionCallback when the cancel + ///< button is pressed + tune_index_e + tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when button is pressed +} nbgl_pageAddressConfirmationDescription_t; + +/** + * @brief Structure containing all specific information when creating an information page. + */ +typedef struct nbgl_pageInfoDescription_s { + nbgl_layoutCenteredInfo_t centeredInfo; ///< description of the centered info to be used + nbgl_pageButtonStyle_t topRightStyle; ///< style to apply to the Top-Right button + nbgl_pageButtonStyle_t bottomButtonStyle; ///< style to apply to the Bottom button + uint8_t topRightToken; ///< the token that will be used as argument of the onActionCallback + uint8_t bottomButtonsToken; ///< the token that will be used as argument of the + ///< onActionCallback if action/bottom button is touched + const char *footerText; ///< if not NULL, add a touchable footer + uint8_t footerToken; ///< the token that will be used as argument of the onActionCallback + const char + *tapActionText; ///< if set to true, main area is "tapable", with this text as indication + uint8_t tapActionToken; ///< the token that will be used as argument of the onActionCallback + const char + *actionButtonText; ///< if not NULL a black "action" button is set under the centered info + tune_index_e + tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played when button/footer is pressed +} nbgl_pageInfoDescription_t; + +/********************** + * GLOBAL PROTOTYPES + **********************/ + +nbgl_page_t *nbgl_pageDrawLedgerInfo(nbgl_layoutTouchCallback_t onActionCallback, + const nbgl_screenTickerConfiguration_t *ticker, + const char *text, + int tapActionToken); +nbgl_page_t *nbgl_pageDrawSpinner(nbgl_layoutTouchCallback_t onActionCallback, const char *text); +nbgl_page_t *nbgl_pageDrawInfo(nbgl_layoutTouchCallback_t onActionCallback, + const nbgl_screenTickerConfiguration_t *ticker, + const nbgl_pageInfoDescription_t *info); +nbgl_page_t *nbgl_pageDrawConfirmation(nbgl_layoutTouchCallback_t onActionCallback, + const nbgl_pageConfirmationDescription_t *info); +nbgl_page_t *nbgl_pageDrawGenericContentExt(nbgl_layoutTouchCallback_t onActionCallback, + const nbgl_pageNavigationInfo_t *nav, + nbgl_pageContent_t *content, + bool modal); +nbgl_page_t *nbgl_pageDrawGenericContent(nbgl_layoutTouchCallback_t onActionCallback, + const nbgl_pageNavigationInfo_t *nav, + nbgl_pageContent_t *content); +int nbgl_pageRelease(nbgl_page_t *); + +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_PAGE_H */ diff --git a/sdk_lib_nbgl/include/nbgl_screen.h b/sdk_lib_nbgl/include/nbgl_screen.h new file mode 100644 index 00000000..df76fe83 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_screen.h @@ -0,0 +1,127 @@ +/** + * @file nbgl_screen.h + * @brief API to manage screens + * + */ + +#ifndef NBGL_SCREEN_H +#define NBGL_SCREEN_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ + +#include "nbgl_types.h" +#include "nbgl_obj.h" + +/********************* + * DEFINES + *********************/ + +/********************** + * TYPEDEFS + **********************/ + +/** + * @brief prototype of function to be called when a timer on screen is fired + */ +typedef void (*nbgl_tickerCallback_t)(void); + +/** + * @brief struct to configure a screen layer + * + */ +typedef struct PACKED__ nbgl_screenTickerConfiguration_s { + nbgl_tickerCallback_t + tickerCallback; ///< callback called when ticker timer is fired. Set to NULL for no ticker + uint32_t tickerValue; ///< timer initial value, in ms (should be multiple of 100 ms). Set to 0 + ///< for no ticker + uint32_t tickerIntervale; ///< for periodic timers, the intervale in ms to rearm the timer + ///< (should be multiple of 100 ms). Set to 0 for one-shot timers +} nbgl_screenTickerConfiguration_t; + +/** + * @brief struct to represent a screen (@ref SCREEN type) + * @note Only for internal usage + * @note inherits from container + * + */ +typedef struct PACKED__ nbgl_screen_s { + nbgl_container_t container; ///< common part + nbgl_screenTickerConfiguration_t ticker; ///< ticker configuration +#ifdef HAVE_SE_TOUCH + nbgl_touchCallback_t + touchCallback; ///< function to be called on events defined in touchMask of each objects +#else // HAVE_SE_TOUCH + nbgl_buttonCallback_t buttonCallback; +#endif // HAVE_SE_TOUCH + struct nbgl_screen_s + *next; ///< pointer to screen on top of this one (or NULL is this screen is top of stack) + struct nbgl_screen_s *previous; ///< pointer to screen on bottom of this one (or NULL is this + ///< screen is bottom of stack) + uint8_t index; ///< index in screenStack array +} nbgl_screen_t; + +/********************** + * GLOBAL PROTOTYPES + **********************/ +void nbgl_screen_reinit(void); + +#ifdef HAVE_DISPLAY_FAST_MODE +void nbgl_screen_update_temperature(uint8_t temp_degrees); +#endif // HAVE_DISPLAY_FAST_MODE + +#ifdef HAVE_CONFIGURABLE_DISPLAY_FAST_MODE +void nbgl_screen_config_fast_mode(uint8_t fast_mode_setting); +#endif // HAVE_CONFIGURABLE_DISPLAY_FAST_MODE + +void nbgl_screenRedraw(void); +nbgl_obj_t *nbgl_screenGetTop(void); +uint8_t nbgl_screenGetCurrentStackSize(void); +bool nbgl_screenContainsObj(nbgl_obj_t *obj); +nbgl_obj_t *nbgl_screenContainsObjType(nbgl_screen_t *screen, nbgl_obj_type_t type); + +#ifdef HAVE_SE_TOUCH +int nbgl_screenSet(nbgl_obj_t ***elements, + uint8_t nbElements, + const nbgl_screenTickerConfiguration_t *ticker, + nbgl_touchCallback_t touchCallback); +#else // HAVE_SE_TOUCH +int nbgl_screenSet(nbgl_obj_t ***elements, + uint8_t nbElements, + const nbgl_screenTickerConfiguration_t *ticker, + nbgl_buttonCallback_t buttonCallback); +#endif // HAVE_SE_TOUCH +int nbgl_screenUpdateNbElements(uint8_t screenIndex, uint8_t nbElements); +int nbgl_screenUpdateBackgroundColor(uint8_t screenIndex, color_t color); +int nbgl_screenUpdateTicker(uint8_t screenIndex, const nbgl_screenTickerConfiguration_t *ticker); +nbgl_obj_t **nbgl_screenGetElements(uint8_t screenIndex); +int nbgl_screenRelease(void); +#ifdef HAVE_SE_TOUCH +int nbgl_screenPush(nbgl_obj_t ***elements, + uint8_t nbElements, + const nbgl_screenTickerConfiguration_t *ticker, + nbgl_touchCallback_t touchCallback); +#else // HAVE_SE_TOUCH +int nbgl_screenPush(nbgl_obj_t ***elements, + uint8_t nbElements, + const nbgl_screenTickerConfiguration_t *ticker, + nbgl_buttonCallback_t buttonCallback); +#endif // HAVE_SE_TOUCH +int nbgl_screenPop(uint8_t screenIndex); +int nbgl_screenReset(void); +void nbgl_screenHandler(uint32_t intervaleMs); + +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_SCREEN_H */ diff --git a/sdk_lib_nbgl/include/nbgl_serialize.h b/sdk_lib_nbgl/include/nbgl_serialize.h new file mode 100644 index 00000000..537b9f88 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_serialize.h @@ -0,0 +1,17 @@ +#pragma once + +#include "nbgl_obj.h" + +#define NBGL_SERIALIZE_OK 0 +#define NBGL_SERIALIZE_ERROR 1 + +typedef enum nbgl_serialized_event_type_e { + NBGL_DRAW_OBJ = 0, + NBGL_REFRESH_AREA +} nbgl_serialized_event_type_e; + +uint8_t nbgl_serializeNbglEvent(nbgl_serialized_event_type_e type, + nbgl_obj_t *obj, + uint8_t *out, + size_t *w_cnt, + size_t max_len); diff --git a/sdk_lib_nbgl/include/nbgl_side.h b/sdk_lib_nbgl/include/nbgl_side.h new file mode 100644 index 00000000..c2007110 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_side.h @@ -0,0 +1,53 @@ + +/** + * @file nbgl_side.h + * @brief Side screen low-Level driver API, to draw elementary forms + * + */ + +#ifndef NBGL_SIDE_H +#define NBGL_SIDE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "nbgl_types.h" + +/********************* + * DEFINES + *********************/ +/** + * Width of the side screen in pixels + */ +#define SIDE_SCREEN_WIDTH 96 + +/********************** + * TYPEDEFS + **********************/ + +/********************** + * GLOBAL PROTOTYPES + **********************/ +void nbgl_sideDrawRect(nbgl_area_t *area); +void nbgl_sideDrawHorizontalLine(nbgl_area_t *area, uint8_t mask, color_t lineColor); +void nbgl_sideDrawImage(nbgl_area_t *area, + uint8_t *buffer, + nbgl_transformation_t transformation, + nbgl_color_map_t colorMap); +void nbgl_sideRefreshArea(nbgl_area_t *area, nbgl_post_refresh_t post_refresh); + +/********************** + * VARIABLES + **********************/ + +extern uint8_t ramBuffer[]; + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_SIDE_H */ diff --git a/sdk_lib_nbgl/include/nbgl_step.h b/sdk_lib_nbgl/include/nbgl_step.h new file mode 100644 index 00000000..4edde5c8 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_step.h @@ -0,0 +1,125 @@ +/** + * @file nbgl_step.h + * @brief Step construction API of NBGL + * + */ + +#ifndef NBGL_STEP_H +#define NBGL_STEP_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ + +#include "nbgl_layout.h" +#include "nbgl_obj.h" +#include "nbgl_types.h" +#ifdef HAVE_LANGUAGE_PACK +#include "bolos_ux_loc_strings.h" +#endif // HAVE_LANGUAGE_PACK + +/********************* + * DEFINES + *********************/ +/** + * get the "position" of a step within a flow of several steps + * @param _step step index from which to get the position + * @param _nb_steps number of steps in the flow + */ +#define GET_POS_OF_STEP(_step, _nb_steps) \ + (_nb_steps < 2) \ + ? SINGLE_STEP \ + : ((_step == 0) ? FIRST_STEP \ + : ((_step == (_nb_steps - 1)) ? LAST_STEP : NEITHER_FIRST_NOR_LAST_STEP)) + +/********************** + * TYPEDEFS + **********************/ + +/** + * @brief type shared externally + * + */ +typedef void *nbgl_step_t; + +/** + * @brief prototype of chosen menu list item callback + * @param choiceIndex index of the menu list item + */ +typedef void (*nbgl_stepMenuListCallback_t)(uint8_t choiceIndex); + +/** + * @brief prototype of function to be called when buttons are touched on a screen + * @param event type of button event + */ +typedef void (*nbgl_stepButtonCallback_t)(nbgl_step_t stepCtx, nbgl_buttonEvent_t event); + +/** + * @brief possible position for a step in a flow + * + */ +enum { + SINGLE_STEP, ///< single step flow + FIRST_STEP, ///< first in a multiple steps flow + LAST_STEP, ///< last in a multiple steps flow + NEITHER_FIRST_NOR_LAST_STEP, ///< neither first nor last in a multiple steps flow +}; + +///< When the flow is navigated from first to last step +#define FORWARD_DIRECTION 0x00 +///< When the flow is navigated from last to first step +#define BACKWARD_DIRECTION 0x08 + +/** + * @brief this type contains nbgl_layoutNavIndication_t in its LSBs + * and direction in its MSB (using @ref FORWARD_DIRECTION and @ref BACKWARD_DIRECTION) + * + */ +typedef uint8_t nbgl_stepPosition_t; + +/********************** + * GLOBAL PROTOTYPES + **********************/ +#ifndef TARGET_NANOS +nbgl_step_t nbgl_stepDrawText(nbgl_stepPosition_t pos, + nbgl_stepButtonCallback_t onActionCallback, + nbgl_screenTickerConfiguration_t *ticker, + const char *text, + const char *subText, + nbgl_centeredInfoStyle_t style, + bool modal); +nbgl_step_t nbgl_stepDrawCenteredInfo(nbgl_stepPosition_t pos, + nbgl_stepButtonCallback_t onActionCallback, + nbgl_screenTickerConfiguration_t *ticker, + nbgl_layoutCenteredInfo_t *info, + bool modal); +nbgl_step_t nbgl_stepDrawMenuList(nbgl_stepMenuListCallback_t onActionCallback, + nbgl_screenTickerConfiguration_t *ticker, + nbgl_layoutMenuList_t *list, + bool modal); +uint8_t nbgl_stepGetMenuListCurrent(nbgl_step_t step); +int nbgl_stepRelease(nbgl_step_t step); +#else +void nbgl_screenDraw(nbgl_stepPosition_t pos, + nbgl_stepButtonCallback_t onActionCallback, + nbgl_screenTickerConfiguration_t *ticker, + const char *text, + const char *subText, + const nbgl_icon_details_t *icon, // TODO change for bagl_icon_details_t + bool centered, + bool text_bold, + bool horizontal_nav); +#endif +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_STEP_H */ diff --git a/sdk_lib_nbgl/include/nbgl_touch.h b/sdk_lib_nbgl/include/nbgl_touch.h new file mode 100644 index 00000000..03e95df8 --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_touch.h @@ -0,0 +1,49 @@ +/** + * @file nbgl_touch.h + * TouchScreen management of the new BOLOS Graphical Library + * + */ + +#ifndef NBGL_TOUCH_H +#define NBGL_TOUCH_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "nbgl_types.h" +#include "nbgl_obj.h" + +/********************* + * DEFINES + *********************/ +// duration of a short touch on touch panel (in ms) +#define SHORT_TOUCH_DURATION 0 +// duration of a long touch on touch panel (in ms) +#define LONG_TOUCH_DURATION 1500 +/********************** + * TYPEDEFS + **********************/ + +/********************** + * GLOBAL PROTOTYPES + **********************/ +void nbgl_touchHandler(nbgl_touchStatePosition_t *touchEvent, uint32_t currentTimeMs); +bool nbgl_touchGetTouchedPosition(nbgl_obj_t *obj, + nbgl_touchStatePosition_t **firstPos, + nbgl_touchStatePosition_t **lastPos); +uint32_t nbgl_touchGetTouchDuration(nbgl_obj_t *obj); +nbgl_obj_t *nbgl_touchGetObjectFromId(nbgl_obj_t *obj, uint8_t id); + +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_TOUCH_H */ diff --git a/sdk_lib_nbgl/include/nbgl_types.h b/sdk_lib_nbgl/include/nbgl_types.h new file mode 100644 index 00000000..2f4777bd --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_types.h @@ -0,0 +1,233 @@ +/** + * @file nbgl_types.h + * @brief common types for Graphical Library + * + */ + +#ifndef NBGL_TYPES_H +#define NBGL_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include +#include +#include +#include "bolos_target.h" + +/********************* + * DEFINES + *********************/ +/** + * Width of the front screen in pixels + */ +#ifdef SCREEN_SIZE_WALLET +#ifdef TARGET_STAX +#define SCREEN_WIDTH 400 +#endif // TARGET_STAX +#else // SCREEN_SIZE_WALLET +#define SCREEN_WIDTH 128 +#endif // SCREEN_SIZE_WALLET + +/** + * Height of the front screen in pixels + */ +#ifdef SCREEN_SIZE_WALLET +#ifdef TARGET_STAX +#define SCREEN_HEIGHT 672 +#endif // TARGET_STAX +#else // SCREEN_SIZE_WALLET +#define SCREEN_HEIGHT 64 +#endif // SCREEN_SIZE_WALLET + +/** + * No transformation + * + */ +#define NO_TRANSFORMATION 0 +/** + * Horizontal mirroring when rendering bitmap + * + */ +#define HORIZONTAL_MIRROR 0x1 +/** + * Vertical mirroring when rendering bitmap + * + */ +#define VERTICAL_MIRROR 0x2 + +/** + * Both directions mirroring when rendering bitmap + * + */ +#define BOTH_MIRRORS (HORIZONTAL_MIRROR | VERTICAL_MIRROR) + +/** + * Rotation 90 degrees clockwise when rendering bitmap + * + */ +#define ROTATE_90_CLOCKWISE 0x4 + +/** + * Code to be used for color map when not used + * + */ +#define INVALID_COLOR_MAP 0x0 + +/********************** + * TYPEDEFS + **********************/ +#ifndef MIN +#define MIN(x, y) ((x) < (y) ? (x) : (y)) +#endif +#ifndef MAX +#define MAX(x, y) ((x) > (y) ? (x) : (y)) +#endif + +#ifdef LINUX_SIMU +#define PACKED__ +#else // LINUX_SIMU +#define PACKED__ __attribute__((packed)) +#endif // LINUX_SIMU +#ifdef BICOLOR_MODE + +typedef enum { + BLACK = 0, + DARK_GRAY = 0, + LIGHT_GRAY = 0, + WHITE = 3 +} color_t; + +#else + +typedef enum { + BLACK = 0, + DARK_GRAY, + LIGHT_GRAY, + WHITE, + NB_NBGL_COLOR +} color_t; + +#endif + +/** + * @brief Enum to represent the number of bits per pixel (BPP) + * + */ +typedef enum { + NBGL_BPP_1 = 0, ///< 1 bit per pixel + NBGL_BPP_2, ///< 2 bits per pixel + NBGL_BPP_4, ///< 4 bits per pixel + NB_NBGL_BPP, ///< Number of NBGL_BPP enums +} nbgl_bpp_t; + +/** + * @brief Enum to represent the compression + * + */ +typedef enum { + NBGL_NO_COMPRESSION = 0, ///< no compression, raw data + NBGL_GZLIB_COMPRESSION, ///< gzlib compression + NBGL_RLE_COMPRESSION, ///< RLE compression + NB_NBGL_COMPRESSION ///< Number of NBGL_COMPRESSION enums +} nbgl_compression_t; + +/** + * @brief size of gzlib uncompression buffer in bytes + * + */ +#define GZLIB_UNCOMPRESSED_CHUNK 2049 + +/** + * @brief Represents a rectangle area of the screen + * + */ +typedef struct PACKED__ nbgl_area_s { + int16_t x0; ///< horizontal position of the upper left point of the area (signed int allow for + ///< out of screen rendering) + int16_t y0; ///< vertical position of the upper left point of the area (signed int allow for + ///< out of screen rendering) + uint16_t width; ///< width of the area, in pixels + uint16_t height; ///< height of the area, in pixels + color_t backgroundColor; ///< color (usually background) to be applied + nbgl_bpp_t bpp; ///< bits per pixel for this area +} nbgl_area_t; + +/** + * @brief different modes of refresh for @ref nbgl_refreshSpecial() + * + */ +typedef enum { + FULL_COLOR_REFRESH, ///< to be used for normal refresh + FULL_COLOR_PARTIAL_REFRESH, ///< to be used for small partial refresh (radio buttons, switches) + FULL_COLOR_CLEAN_REFRESH, ///< to be used for lock screen display (cleaner but longer refresh) + BLACK_AND_WHITE_REFRESH, ///< to be used for pure B&W area, when contrast is important + BLACK_AND_WHITE_FAST_REFRESH, ///< to be used for pure B&W area, when contrast is not priority + NB_REFRESH_MODES +} nbgl_refresh_mode_t; + +/** + * @brief Available post-refresh power modes + * + * - Power off after a refresh allows to save power + * - Keep the screen powered on after a refresh allows to + * achieve a faster following refresh. + */ +typedef enum nbgl_post_refresh_t { + POST_REFRESH_FORCE_POWER_OFF, ///< Force screen power off after refresh + POST_REFRESH_FORCE_POWER_ON, ///< Force screen power on after refresh + POST_REFRESH_KEEP_POWER_STATE, ///< Keep screen power state after refresh +} nbgl_post_refresh_t; + +/** + * @brief possible radius for objects + * + */ +typedef enum { + RADIUS_3_PIXELS = 0, ///< 3 pixels (not on Stax) + RADIUS_4_PIXELS, ///< 4 pixels + RADIUS_8_PIXELS, ///< 8 pixels + RADIUS_16_PIXELS, ///< 16 pixels + RADIUS_20_PIXELS, ///< 20 pixels + RADIUS_24_PIXELS, ///< 24 pixels + RADIUS_32_PIXELS, ///< 32 pixels + RADIUS_40_PIXELS, ///< 40 pixels + RADIUS_48_PIXELS, ///< 48 pixels + RADIUS_1_PIXEL, ///< 1 pixel (not on Stax) + RADIUS_0_PIXELS = 0xFF, ///< no radius (square angle) +} nbgl_radius_t; + +/** + * @brief Represents the transformation to be applied on the bitmap before rendering + * This is a bitfield using masks as @ref HORIZONTAL_MIRROR + */ +typedef uint8_t nbgl_transformation_t; + +/** + * @brief Represents the color_map to be used for 2BPP image, or the foreground color for 1BPP image + * @note colors are encoded on 2 bits, so a 8 bits stores the 4 colors for a 2BPP image + * @note not used for 4BPP image + */ +typedef uint8_t nbgl_color_map_t; + +/** + * @brief Represents all information about an icon + * + */ +typedef struct PACKED__ nbgl_icon_details_s { + uint16_t width; ///< width of the icon, in pixels + uint16_t height; ///< height of the icon, in pixels + nbgl_bpp_t bpp; ///< bits per pixel for this area + bool isFile; ///< if true, the bitmap buffer contains an image file + const uint8_t *bitmap; ///< buffer containing pixel values +} nbgl_icon_details_t; + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_TYPES_H */ diff --git a/sdk_lib_nbgl/include/nbgl_use_case.h b/sdk_lib_nbgl/include/nbgl_use_case.h new file mode 100644 index 00000000..57ba20bf --- /dev/null +++ b/sdk_lib_nbgl/include/nbgl_use_case.h @@ -0,0 +1,232 @@ +/** + * @file nbgl_use_case.h + * @brief API of the Advanced BOLOS Graphical Library, for typical application use-cases + * + */ + +#ifndef NBGL_USE_CASE_H +#define NBGL_USE_CASE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ + +#ifdef NBGL_PAGE +#include "nbgl_page.h" +#else // NBGL_PAGE +#include "nbgl_flow.h" +#endif // NBGL_PAGE + +/********************* + * DEFINES + *********************/ +/** + * @brief when using controls in page content (@ref nbgl_pageContent_t), this is the first token + * value usable for these controls + */ +#define FIRST_USER_TOKEN 20 + +/** + * @brief value of page parameter used with navigation callback when "skip" button is touched, to + * display the long press button to confirm review. + */ +#define LAST_PAGE_FOR_REVIEW 0xFF + +/** + * @brief maximum number of lines for value field in details pages + */ +#define NB_MAX_LINES_IN_DETAILS 12 + +/** + * @brief maximum number of lines for value field in review pages + */ +#define NB_MAX_LINES_IN_REVIEW 9 + +/** + * @brief maximum number of simultaneously displayed pairs in review pages. + * Can be useful when using nbgl_useCaseStaticReview() with the + * callback mechanism to retrieve the item/value pairs. + */ +#define NB_MAX_DISPLAYED_PAIRS_IN_REVIEW 4 + +/** + * @brief height available for tag/value pairs display + */ +#define TAG_VALUE_AREA_HEIGHT 400 + +/** + * @brief Default strings used in the Home tagline + */ +#define TAGLINE_PART1 "This app enables signing\ntransactions on the" +#define TAGLINE_PART2 "network." + +/** + * @brief Length of buffer used for the default Home tagline + */ +#define APP_DESCRIPTION_MAX_LEN 74 + +/** + * @brief Max supported length of appName used for the default Home tagline + */ +#define MAX_APP_NAME_FOR_SDK_TAGLINE \ + (APP_DESCRIPTION_MAX_LEN - 1 - (sizeof(TAGLINE_PART1) + sizeof(TAGLINE_PART2))) + +/********************** + * MACROS + **********************/ + +/********************** + * TYPEDEFS + **********************/ +/** + * @brief prototype of generic callback function + */ +typedef void (*nbgl_callback_t)(void); + +/** + * @brief prototype of navigation callback function + * @param page page index (0->(nb_pages-1)) on which we go + * @param content content to fill (only type and union) + * @return true if the page content is valid, false if no more page + */ +typedef bool (*nbgl_navCallback_t)(uint8_t page, nbgl_pageContent_t *content); + +/** + * @brief prototype of choice callback function + * @param confirm if true, means that the confirmation button has been pressed + */ +typedef void (*nbgl_choiceCallback_t)(bool confirm); + +/** + * @brief prototype of function to be called when an page of settings is double-pressed + * @param page page index (0->(nb_pages-1)) + */ +typedef void (*nbgl_actionCallback_t)(uint8_t page); + +/********************** + * GLOBAL PROTOTYPES + **********************/ + +#ifdef HAVE_SE_TOUCH +// utils +uint8_t nbgl_useCaseGetNbTagValuesInPage(uint8_t nbPairs, + const nbgl_layoutTagValueList_t *tagValueList, + uint8_t startIndex, + bool *tooLongToFit); +uint8_t nbgl_useCaseGetNbPagesForTagValueList(const nbgl_layoutTagValueList_t *tagValueList); + +// use case drawing +void nbgl_useCaseHome(const char *appName, + const nbgl_icon_details_t *appIcon, + const char *tagline, + bool withSettings, + nbgl_callback_t topRightCallback, + nbgl_callback_t quitCallback); +void nbgl_useCaseHomeExt(const char *appName, + const nbgl_icon_details_t *appIcon, + const char *tagline, + bool withSettings, + const char *actionButtonText, + nbgl_callback_t actionCallback, + nbgl_callback_t topRightCallback, + nbgl_callback_t quitCallback); +void nbgl_useCasePlugInHome(const char *plugInName, + const char *appName, + const nbgl_icon_details_t *appIcon, + const char *tagline, + const char *subTagline, + bool withSettings, + nbgl_callback_t topRightCallback, + nbgl_callback_t quitCallback); +void nbgl_useCaseSettings(const char *settingsTitle, + uint8_t initPage, + uint8_t nbPages, + bool touchableTitle, + nbgl_callback_t quitCallback, + nbgl_navCallback_t navCallback, + nbgl_layoutTouchCallback_t controlsCallback); +void nbgl_useCaseChoice(const nbgl_icon_details_t *icon, + const char *message, + const char *subMessage, + const char *confirmText, + const char *rejectString, + nbgl_choiceCallback_t callback); +void nbgl_useCaseConfirm(const char *message, + const char *subMessage, + const char *confirmText, + const char *rejectText, + nbgl_callback_t callback); +void nbgl_useCaseStatus(const char *message, bool isSuccess, nbgl_callback_t quitCallback); +void nbgl_useCaseReviewStart(const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const char *rejectText, + nbgl_callback_t continueCallback, + nbgl_callback_t rejectCallback); +void nbgl_useCaseRegularReview(uint8_t initPage, + uint8_t nbPages, + const char *rejectText, + nbgl_layoutTouchCallback_t buttonCallback, + nbgl_navCallback_t navCallback, + nbgl_choiceCallback_t choiceCallback); +void nbgl_useCaseForwardOnlyReview(const char *rejectText, + nbgl_layoutTouchCallback_t buttonCallback, + nbgl_navCallback_t navCallback, + nbgl_choiceCallback_t choiceCallback); +void nbgl_useCaseForwardOnlyReviewNoSkip(const char *rejectText, + nbgl_layoutTouchCallback_t buttonCallback, + nbgl_navCallback_t navCallback, + nbgl_choiceCallback_t choiceCallback); +void nbgl_useCaseStaticReview(const nbgl_layoutTagValueList_t *tagValueList, + const nbgl_pageInfoLongPress_t *infoLongPress, + const char *rejectText, + nbgl_choiceCallback_t callback); +void nbgl_useCaseStaticReviewLight(const nbgl_layoutTagValueList_t *tagValueList, + const nbgl_pageInfoLongPress_t *infoLongPress, + const char *rejectText, + nbgl_choiceCallback_t callback); +void nbgl_useCaseViewDetails(const char *tag, const char *value, bool wrapping); +void nbgl_useCaseAddressConfirmation(const char *address, nbgl_choiceCallback_t callback); +void nbgl_useCaseAddressConfirmationExt(const char *address, + nbgl_choiceCallback_t callback, + const nbgl_layoutTagValueList_t *tagValueList); +#else // HAVE_SE_TOUCH +void nbgl_useCaseHome(const char *appName, + const nbgl_icon_details_t *appIcon, + const char *appVersion, + const char *tagline, + nbgl_callback_t aboutCallback, + nbgl_callback_t quitCallback); +void nbgl_useCaseSettings(uint8_t initPage, + uint8_t nbPages, + nbgl_callback_t quitCallback, + nbgl_navCallback_t navCallback, + nbgl_actionCallback_t actionCallback); +void nbgl_useCaseRegularReview(uint8_t initPage, uint8_t nbPages, nbgl_navCallback_t navCallback); +void nbgl_useCaseForwardOnlyReview(nbgl_navCallback_t navCallback); +void nbgl_useCaseStaticReview(nbgl_layoutTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *acceptText, + const char *rejectText, + nbgl_choiceCallback_t callback); +void nbgl_useCaseAddressConfirmation(const nbgl_icon_details_t *icon, + const char *title, + const char *address, + nbgl_choiceCallback_t callback); +void nbgl_useCaseAddressConfirmationExt(const nbgl_icon_details_t *icon, + const char *title, + const char *address, + nbgl_choiceCallback_t callback, + const nbgl_layoutTagValueList_t *tagValueList); +#endif // HAVE_SE_TOUCH +void nbgl_useCaseSpinner(const char *text); +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* NBGL_USE_CASE_H */ diff --git a/sdk_lib_nbgl/serialization/Makefile b/sdk_lib_nbgl/serialization/Makefile new file mode 100644 index 00000000..6ce375b3 --- /dev/null +++ b/sdk_lib_nbgl/serialization/Makefile @@ -0,0 +1,19 @@ + +INC := -I . -I ../include +DEFINES := -DLINUX_SIMU -DHAVE_LANGUAGE_PACK -DNBGL_GENERATE_DATA_TEST + +ifdef TARGET_STAX +DEFINES += -DHAVE_SE_TOUCH +DEFINES += -DSCREEN_SIZE_WALLET +endif + +default: generate_data_test.c + gcc $(INC) generate_data_test.c $(DEFINES) ../src/nbgl_serialize.c -o generate_data_test + +run_test: default + ./generate_data_test > data_test.txt + pytest -s -vv + rm data_test.txt + +clean: + rm generate_data_test diff --git a/sdk_lib_nbgl/serialization/README.md b/sdk_lib_nbgl/serialization/README.md new file mode 100644 index 00000000..01df40dd --- /dev/null +++ b/sdk_lib_nbgl/serialization/README.md @@ -0,0 +1,38 @@ +## NBGL serialization/deserialization library + +### Usage + +Deserialize raw bytes into an Nbgl event: +```python +>>> from nbgl_lib import deserialize_nbgl_bytes +>>> data = bytes.fromhex("00010501f403e800ff003201000301020403015465737420627574746f6e00") +>>> nbgl_event = deserialize_nbgl_bytes(data) +>>> nbgl_event +NbglDrawObjectEvent(obj=NbglButton(area=NbglArea(width=255, height=50, x0=500, y0=1000, background_color=, bpp=), inner_color=, border_color=, foreground_color=, radius=, font_id=, localized=True, text='Test button')) +``` + +Serialize a Nbgl event into a json-like dict: +```python +>>> from nbgl_lib import serialize_nbgl_json +>>> serialize_nbgl_json(nbgl_event) +{'event': 'NBGL_DRAW_OBJ', 'obj': {'type': 'BUTTON', 'content': {'area': {'width': 255, 'height': 50, 'x0': 500, 'y0': 1000, 'background_color': 'DARK_GRAY', 'bpp': 'BPP_1'}, 'inner_color': 'WHITE', 'border_color': 'DARK_GRAY', 'foreground_color': 'LIGHT_GRAY', 'radius': 'RADIUS_24_PIXELS', 'font_id': 'BAGL_FONT_HM_ALPHA_MONO_MEDIUM_32px', 'localized': True, 'text': 'Test button'}}} +``` + +Deserialize json-like dict into an Nbgl event: + +```python +>>> from nbgl_lib import deserialize_nbgl_json +>>> deserialize_nbgl_json({'event': 'NBGL_DRAW_OBJ', 'obj': {'type': 'BUTTON', 'content': {'area': {'width': 255, 'height': 50, 'x0': 500, 'y0': 1000, 'background_color': 'DARK_GRAY', 'bpp': 'BPP_1'}, 'inner_color': 'WHITE', 'border_color': 'DARK_GRAY', 'foreground_color': 'LIGHT_GRAY', 'radius': 'RADIUS_24_PIXELS', 'font_id': 'BAGL_FONT_HM_ALPHA_MONO_MEDIUM_32px', 'localized': True, 'text': 'Test button'}}}) +NbglDrawObjectEvent(obj=NbglButton(area=NbglArea(width=255, height=50, x0=500, y0=1000, background_color=, bpp=), inner_color=, border_color=, foreground_color=, radius=, font_id=, localized=True, text='Test button')) +``` +### Tests + +- ```generate_data_test.c``` generates reference data for deserialization testing. +- ```test_bytes_deserialize.py``` deserializes the reference data and inspect the result. +- ```test_json_ser_deser.py``` tests json serialization and deserialization. + +To generate reference data and run tests: + +``` +make run_test +``` diff --git a/sdk_lib_nbgl/serialization/generate_data_test.c b/sdk_lib_nbgl/serialization/generate_data_test.c new file mode 100644 index 00000000..ae6a9b7a --- /dev/null +++ b/sdk_lib_nbgl/serialization/generate_data_test.c @@ -0,0 +1,377 @@ +#ifdef NBGL_GENERATE_DATA_TEST + +#include +#include "nbgl_types.h" +#include "nbgl_serialize.h" +#include "nbgl_obj.h" +#include "nbgl_screen.h" + +static uint8_t const C_leftArrow32px_bitmap[] = { + 0x20, 0x00, 0x20, 0x00, 0x02, 0x4d, 0x00, 0x00, 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, + 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, + 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, 0xe3, 0xf0, 0x32, 0x93, 0x92, + 0x64, 0x83, 0x84, 0x64, 0x73, 0x74, 0x84, 0x63, 0x64, 0xa4, 0x53, 0x54, 0xc4, 0x43, 0x44, + 0xe4, 0x33, 0x34, 0xf0, 0x14, 0x23, 0x24, 0xf0, 0x34, 0x13, 0x14, 0xf0, 0x5b, 0xf0, 0x79, + 0xf0, 0x97, 0xf0, 0xb5, 0xf0, 0xd3, 0xf0, 0xf1, 0xf0, 0x10, +}; +const nbgl_icon_details_t C_leftArrow32px = {32, 32, NBGL_BPP_1, true, C_leftArrow32px_bitmap}; + +uint8_t nbgl_objPoolGetId(nbgl_obj_t *obj) +{ + return 0; +} + +void print_hex(const char *name, uint8_t *buffer, size_t len) +{ + printf("%s,", name); + for (size_t i = 0; i < len; i++) { + printf("%02x", buffer[i]); + } + printf("\n"); +} + +void run_serialize_and_print(const char *name, nbgl_serialized_event_type_e event, nbgl_obj_t *obj) +{ + uint8_t buf[192]; + size_t len = 0; + nbgl_serializeNbglEvent(event, obj, buf, &len, sizeof(buf)); + print_hex(name, buf, len); +} + +#define SERIALIZE_AND_PRINT(obj, event) \ + run_serialize_and_print(__FUNCTION__, event, (nbgl_obj_t *) obj) + +void test_draw_nbgl_screen() +{ + nbgl_screen_t screen = { + .container.obj.type = SCREEN, + + .container.obj.area.backgroundColor = WHITE, + .container.obj.area.bpp = NBGL_BPP_4, + .container.obj.area.height = 670, + .container.obj.area.width = 400, + .container.obj.area.x0 = 0, + .container.obj.area.y0 = 0, + }; + + SERIALIZE_AND_PRINT(&screen, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_container() +{ + nbgl_container_t container = {.obj.type = CONTAINER, + + .obj.area.backgroundColor = DARK_GRAY, + .obj.area.bpp = NBGL_BPP_4, + .obj.area.height = 450, + .obj.area.width = 460, + .obj.area.x0 = 56, + .obj.area.y0 = 12, + + .layout = VERTICAL, + .nbChildren = 4, + .forceClean = true}; + + SERIALIZE_AND_PRINT(&container, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_text_area() +{ + nbgl_text_area_t text = {.obj.type = TEXT_AREA, + + .obj.area.backgroundColor = DARK_GRAY, + .obj.area.bpp = NBGL_BPP_4, + .obj.area.height = 400, + .obj.area.width = 360, + .obj.area.x0 = 12, + .obj.area.y0 = 256, + + .textColor = BLACK, + .textAlignment = BOTTOM_RIGHT, + .style = NO_STYLE, + .fontId = BAGL_FONT_INTER_MEDIUM_32px, + .localized = false, + .autoHideLongLine = true, + .text = "arthur"}; + + SERIALIZE_AND_PRINT(&text, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_line() +{ + nbgl_line_t line = {.obj.type = LINE, + .obj.area.backgroundColor = WHITE, + .obj.area.bpp = NBGL_BPP_1, + .obj.area.height = 267, + .obj.area.width = 36, + .obj.area.x0 = 0, + .obj.area.y0 = 42, + .direction = HORIZONTAL, + .lineColor = DARK_GRAY, + .thickness = 4, + .offset = 2}; + + SERIALIZE_AND_PRINT(&line, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_qr_code() +{ + nbgl_qrcode_t qr_code = {.obj.type = QR_CODE, + + .obj.area.backgroundColor = DARK_GRAY, + .obj.area.bpp = NBGL_BPP_2, + .obj.area.height = 55, + .obj.area.width = 66, + .obj.area.x0 = 400, + .obj.area.y0 = 300, + + .foregroundColor = DARK_GRAY, + .text = "fatstacks", + .version = QRCODE_V10}; + + SERIALIZE_AND_PRINT(&qr_code, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_radio() +{ + nbgl_radio_t radio = {.obj.type = RADIO_BUTTON, + + .obj.area.backgroundColor = BLACK, + .obj.area.bpp = NBGL_BPP_4, + .obj.area.height = 100, + .obj.area.width = 200, + .obj.area.x0 = 123, + .obj.area.y0 = 234, + + .activeColor = BLACK, + .borderColor = DARK_GRAY, + .state = ON_STATE}; + + SERIALIZE_AND_PRINT(&radio, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_switch() +{ + nbgl_switch_t switch_obj = {.obj.type = SWITCH, + + .obj.area.backgroundColor = LIGHT_GRAY, + .obj.area.bpp = NBGL_BPP_1, + .obj.area.height = 333, + .obj.area.width = 89, + .obj.area.x0 = 1, + .obj.area.y0 = 10000, + + .offColor = WHITE, + .onColor = BLACK, + .state = OFF_STATE}; + + SERIALIZE_AND_PRINT(&switch_obj, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_progress_bar() +{ + nbgl_progress_bar_t progress_bar = {.obj.type = PROGRESS_BAR, + + .obj.area.backgroundColor = BLACK, + .obj.area.bpp = NBGL_BPP_1, + .obj.area.height = 10000, + .obj.area.width = 11000, + .obj.area.x0 = 12000, + .obj.area.y0 = 13000, + + .withBorder = true, + .state = 91}; + + SERIALIZE_AND_PRINT(&progress_bar, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_page_indicator() +{ + nbgl_page_indicator_t page_indicator = {.obj.type = PAGE_INDICATOR, + + .obj.area.backgroundColor = BLACK, + .obj.area.bpp = NBGL_BPP_2, + .obj.area.height = 11, + .obj.area.width = 22, + .obj.area.x0 = 33, + .obj.area.y0 = 44, + + .activePage = 2, + .nbPages = 10}; + + SERIALIZE_AND_PRINT(&page_indicator, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_button() +{ + nbgl_button_t button = { + .obj.type = BUTTON, + + .obj.area.backgroundColor = DARK_GRAY, + .obj.area.bpp = NBGL_BPP_1, + .obj.area.height = 50, + .obj.area.width = 255, + .obj.area.x0 = 500, + .obj.area.y0 = 1000, + + .innerColor = WHITE, + .borderColor = DARK_GRAY, + .foregroundColor = LIGHT_GRAY, + .radius = RADIUS_24_PIXELS, + .fontId = BAGL_FONT_INTER_MEDIUM_32px, + .localized = true, + .text = "Test button", + }; + + SERIALIZE_AND_PRINT(&button, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_image() +{ + nbgl_image_t image = {.obj.type = IMAGE, + + .obj.area.backgroundColor = WHITE, + .obj.area.bpp = NBGL_BPP_1, + .obj.area.height = 32, + .obj.area.width = 32, + .obj.area.x0 = 124, + .obj.area.y0 = 235, + + .foregroundColor = DARK_GRAY, + .buffer = &C_leftArrow32px}; + + SERIALIZE_AND_PRINT(&image, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_keyboard() +{ + nbgl_keyboard_t keyboard = { + .obj.type = KEYBOARD, + + .obj.area.backgroundColor = LIGHT_GRAY, + .obj.area.bpp = NBGL_BPP_2, + .obj.area.height = 210, + .obj.area.width = 225, + .obj.area.x0 = 332, + .obj.area.y0 = 431, + + .textColor = WHITE, + .borderColor = BLACK, + .lettersOnly = true, +#ifdef HAVE_SE_TOUCH + .casing = 0, + .mode = MODE_DIGITS, +#else // HAVE_SE_TOUCH + .mode = MODE_UPPER_LETTERS, +#endif // HAVE_SE_TOUCH + .keyMask = 0x12345678, + }; + + SERIALIZE_AND_PRINT(&keyboard, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_keypad() +{ + nbgl_keypad_t keypad = {.obj.type = KEYPAD, + + .obj.area.backgroundColor = WHITE, + .obj.area.bpp = NBGL_BPP_4, + .obj.area.height = 4, + .obj.area.width = 4, + .obj.area.x0 = 3, + .obj.area.y0 = 4, + +#ifdef HAVE_SE_TOUCH + .textColor = WHITE, + .borderColor = BLACK, +#endif // HAVE_SE_TOUCH + .enableBackspace = true, + .enableValidate = false, +#ifdef HAVE_SE_TOUCH + .enableDigits = true, +#endif // HAVE_SE_TOUCH + .shuffled = false}; + + SERIALIZE_AND_PRINT(&keypad, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_spinner() +{ + nbgl_spinner_t spinner = {.obj.type = SPINNER, + + .obj.area.backgroundColor = LIGHT_GRAY, + .obj.area.bpp = NBGL_BPP_1, + .obj.area.height = 14, + .obj.area.width = 25, + .obj.area.x0 = 12, + .obj.area.y0 = 10, + + .position = 2}; + + SERIALIZE_AND_PRINT(&spinner, NBGL_DRAW_OBJ); +} + +void test_draw_nbgl_image_file() +{ + nbgl_image_file_t image_file = { + .obj.type = IMAGE_FILE, + + .obj.area.backgroundColor = DARK_GRAY, + .obj.area.bpp = NBGL_BPP_4, + .obj.area.height = 24, + .obj.area.width = 35, + .obj.area.x0 = 22, + .obj.area.y0 = 20, + }; + + SERIALIZE_AND_PRINT(&image_file, NBGL_DRAW_OBJ); +} + +void test_refresh_area() +{ + nbgl_area_t area = { + .backgroundColor = WHITE, + .bpp = NBGL_BPP_4, + .height = 4, + .width = 4, + .x0 = 3, + .y0 = 4, + }; + + SERIALIZE_AND_PRINT(&area, NBGL_REFRESH_AREA); +} + +int main() +{ +#ifdef HAVE_SE_TOUCH + printf("stax\n"); +#else // HAVE_SE_TOUCH + printf("nano\n"); +#endif // HAVE_SE_TOUCH + test_draw_nbgl_screen(); + test_draw_nbgl_container(); +#ifdef HAVE_SE_TOUCH + test_draw_nbgl_line(); +#endif // HAVE_SE_TOUCH + test_draw_nbgl_text_area(); +#ifdef HAVE_SE_TOUCH + test_draw_nbgl_qr_code(); + test_draw_nbgl_radio(); + test_draw_nbgl_switch(); +#endif // HAVE_SE_TOUCH + test_draw_nbgl_progress_bar(); +#ifdef HAVE_SE_TOUCH + test_draw_nbgl_page_indicator(); + test_draw_nbgl_button(); +#endif // HAVE_SE_TOUCH + test_draw_nbgl_image(); + test_draw_nbgl_keyboard(); + test_draw_nbgl_keypad(); +#ifdef HAVE_SE_TOUCH + test_draw_nbgl_spinner(); +#endif // HAVE_SE_TOUCH + test_draw_nbgl_image_file(); + test_refresh_area(); +} + +#endif diff --git a/sdk_lib_nbgl/serialization/nbgl_lib.py b/sdk_lib_nbgl/serialization/nbgl_lib.py new file mode 100644 index 00000000..ab10d941 --- /dev/null +++ b/sdk_lib_nbgl/serialization/nbgl_lib.py @@ -0,0 +1,695 @@ +from dataclasses import dataclass +from enum import IntEnum, Enum +from typing import Tuple, Dict, Union +from abc import ABC, abstractclassmethod +import struct + +# Common types + + +class NbglColor(IntEnum): + BLACK = 0, + DARK_GRAY = 1, + LIGHT_GRAY = 2, + WHITE = 3 + + +class NbglBpp(IntEnum): + BPP_1 = 0, + BPP_2 = 1 + BPP_4 = 2 + + +class NbglDirection(IntEnum): + VERTICAL = 0, + HORIZONTAL = 1 + + +class NbglState(IntEnum): + OFF_STATE = 0, + ON_STATE = 1 + + +class NbglQrCodeVersion(IntEnum): + QRCODE_V4 = 0, + QRCODE_V10 = 1 + + +class NbglRadius(IntEnum): + RADIUS_4_PIXELS = 0, + RADIUS_8_PIXELS = 1, + RADIUS_16_PIXELS = 2, + RADIUS_20_PIXELS = 3, + RADIUS_24_PIXELS = 4, + RADIUS_32_PIXELS = 5, + RADIUS_40_PIXELS = 6, + RADIUS_48_PIXELS = 7, + RADIUS_0_PIXELS = 0xFF, + + +class NbglKeyboardMode(IntEnum): + MODE_LETTERS = 0, + MODE_DIGITS = 1, + MODE_SPECIAL = 2, + MODE_NONE = 3 + + +class NbglObjType(IntEnum): + SCREEN, = 0, # Main screen + CONTAINER, = 1, # Empty container + IMAGE, = 2, # Bitmap (x and width must be multiple of 4) + LINE, = 3, # Vertical or Horizontal line + TEXT_AREA, = 4, # Area to contain text line(s) + BUTTON, = 5, # Rounded rectangle button with icon and/or text + SWITCH, = 6, # Switch to turn on/off something + PAGE_INDICATOR, = 7, # horizontal bar to indicate navigation across pages + + PROGRESS_BAR, = 8, # horizontal bar to indicate progression of something (between 0% and 100%) + RADIO_BUTTON, = 9, # Indicator to inform whether something is on or off + QR_CODE, = 10, # QR Code + KEYBOARD, = 11, # Keyboard + KEYPAD, = 12, # Keypad + SPINNER, = 13, # Spinner + IMAGE_FILE = 14, # Image file (with Ledger compression) + + +class NbglAlignment(IntEnum): + NO_ALIGNMENT = 0, + TOP_LEFT = 1, + TOP_MIDDLE = 2, + TOP_RIGHT = 3, + MID_LEFT = 4, + CENTER = 5, + MID_RIGHT = 6, + BOTTOM_LEFT = 7, + BOTTOM_MIDDLE = 8, + BOTTOM_RIGHT = 9, + LEFT_TOP = 10, + LEFT_BOTTOM = 11, + RIGHT_TOP = 12, + RIGHT_BOTTOM = 13, + + +class NbglFontId(IntEnum): + BAGL_FONT_INTER_REGULAR_24px = 0 + BAGL_FONT_INTER_SEMIBOLD_24px = 1 + BAGL_FONT_INTER_MEDIUM_32px = 2 + BAGL_FONT_INTER_REGULAR_24px_1bpp = 3 + BAGL_FONT_INTER_SEMIBOLD_24px_1bpp = 4 + BAGL_FONT_INTER_MEDIUM_32px_1bpp = 5 + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px_1bpp = 8 + BAGL_FONT_OPEN_SANS_LIGHT_16px_1bpp = 9 + BAGL_FONT_OPEN_SANS_REGULAR_11px_1bpp = 10 + BAGL_FONT_INTER_REGULAR_28px = 11 + BAGL_FONT_INTER_SEMIBOLD_28px = 12 + BAGL_FONT_INTER_MEDIUM_36px = 13 + BAGL_FONT_INTER_REGULAR_28px_1bpp = 14 + BAGL_FONT_INTER_SEMIBOLD_28px_1bpp = 15 + BAGL_FONT_INTER_MEDIUM_36px_1bpp = 16 + + +class NbglStyle(IntEnum): + NO_STYLE = 0, + INVERTED_COLORS = 1 + + +def parse_str(data: bytes) -> str: + """ + Utility function to parse a NULL terminated string + from input bytes. If the string is not terminated by NULL, + take the truncated string instead. + + Returns the string + """ + size = 0 + for b in data: + size += 1 + if b == 0: + break + + if size > 0: + return data[:size-1].decode() + + return "" + + +class NbglGenericJsonSerializable(ABC): + """ + Base type for all NBGL objects. + Implements generic json dict serialization / deserialization functions. + """ + + def to_json_dict(self) -> Dict: + """ + Returns the json dict version of the object. + """ + result = {} + for name, val in self.__dict__.items(): + if issubclass(type(val), NbglObj): + result[name] = val.to_json_dict() + elif issubclass(type(val), Enum): + result[name] = val.name + else: + result[name] = val + return result + + @classmethod + def from_json_dict(cls, data: Dict): + """ + Get an instance of the class, from its json dict version. + """ + fields = {} + for field_name, field_obj in cls.__dataclass_fields__.items(): + field_type = field_obj.type + if issubclass(field_type, Enum): + fields[field_name] = field_type[data[field_name]] + elif issubclass(field_type, NbglObj): + fields[field_name] = NbglArea.from_json_dict(data[field_name]) + else: + fields[field_name] = data[field_name] + return cls(**fields) + + @abstractclassmethod + def from_bytes(cls, is_stax : bool, data: bytes): + """ + Get an instance of the class, from raw bytes. + """ + pass + + +class NbglObj(NbglGenericJsonSerializable): + pass + +# Nbgl objects + + +@dataclass +class NbglArea(NbglObj): + width: int + height: int + x0: int + y0: int + background_color: NbglColor + bpp: NbglBpp + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + x0, y0, width, height, color_n, bpp_n = struct.unpack('>HHHHBB', data[:10]) + color = NbglColor(color_n) + bpp = NbglBpp(bpp_n) + return cls(width, height, x0, y0, color, bpp) + + @staticmethod + def size(): + return struct.calcsize('>HHHHBB') + + +@dataclass +class NbglScreen(NbglObj): + area: NbglArea + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + return cls( + area=area + ) + + +@dataclass +class NbglContainer(NbglObj): + area: NbglArea + layout: NbglDirection + nb_children: int + force_clean: bool + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + layout, nb_children, force_clean = struct.unpack( + '>BB?', data[NbglArea.size():]) + return cls( + area=area, + layout=NbglDirection(layout), + nb_children=nb_children, + force_clean=force_clean + ) + + +@dataclass +class NbglLine(NbglObj): + area: NbglArea + direction: NbglDirection + line_color: NbglColor + thickness: int + offset: int + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + direction, line_color, thickness, offset = struct.unpack( + '>BBBB', data[NbglArea.size():]) + return cls( + area=area, + direction=NbglDirection(direction), + line_color=NbglColor(line_color), + thickness=thickness, + offset=offset) + + +@dataclass +class NbglRadioButton(NbglObj): + area: NbglArea + active_color: NbglColor + border_color: NbglColor + state: NbglState + + @classmethod + def from_bytes(cls, is_stax : bool, data): + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + active_color, border_color, state = struct.unpack( + '>BBB', data[NbglArea.size():]) + return cls( + area=area, + active_color=NbglColor(active_color), + border_color=NbglColor(border_color), + state=NbglState(state) + ) + + +@dataclass +class NbglSwitch(NbglObj): + area: NbglArea + on_color: NbglColor + off_color: NbglColor + state: NbglState + + @classmethod + def from_bytes(cls, is_stax : bool, data): + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + on_color, off_color, state = struct.unpack( + '>BBB', data[NbglArea.size():]) + return cls( + area=area, + on_color=NbglColor(on_color), + off_color=NbglColor(off_color), + state=NbglState(state) + ) + + +@dataclass +class NbglProgressBar(NbglObj): + area: NbglArea + with_border: bool + state: int + + @classmethod + def from_bytes(cls, is_stax : bool, data): + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + with_border, state = struct.unpack( + '>?B', data[NbglArea.size():]) + return cls( + area=area, + with_border=with_border, + state=state + ) + + +@dataclass +class NbglPageIndicator(NbglObj): + area: NbglArea + active_page: int + nb_pages: int + + @classmethod + def from_bytes(cls, is_stax : bool, data): + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + active_page, nb_pages = \ + struct.unpack('>BB', data[NbglArea.size():]) + return cls( + area=area, + active_page=active_page, + nb_pages=nb_pages + ) + + +@dataclass +class NbglButton(NbglObj): + area: NbglArea + inner_color: NbglColor + border_color: NbglColor + foreground_color: NbglColor + radius: NbglRadius + font_id: NbglFontId + localized: bool + text: str + + @classmethod + def from_bytes(cls, is_stax : bool, data): + cnt = NbglArea.size() + area = NbglArea.from_bytes(is_stax, data[0:cnt]) + params_template = '>BBBBB?' + params_size = struct.calcsize(params_template) + inner_color, border_color, \ + foreground_color, \ + radius, font_id, localized = struct.unpack( + params_template, data[cnt:cnt+params_size]) + + cnt += params_size + text = parse_str(data[cnt:]) + + return cls( + area=area, + inner_color=NbglColor(inner_color), + border_color=NbglColor(border_color), + foreground_color=NbglColor(foreground_color), + radius=NbglRadius(radius), + font_id=NbglFontId(font_id), + localized=localized, + text=text) + + +@dataclass +class NbglTextArea(NbglObj): + area: NbglArea + text_color: NbglColor + text_alignment: NbglAlignment + style: NbglStyle + font_id: NbglFontId + localized: bool + auto_hide_long_line: bool + len: int + text: str + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + # Parse area + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + cnt = NbglArea.size() + + # Parse pattern + params_pattern = '>BBBB??H' + params_size = struct.calcsize(params_pattern) + text_color, alignment, style, font_id, localized, auto_hide_long_line, len = struct.unpack( + params_pattern, data[cnt:cnt+params_size] + ) + cnt += params_size + + # Parse text + text = parse_str(data[cnt:]) + return cls( + area=area, + text_color=NbglColor(text_color), + text_alignment=NbglAlignment(alignment), + style=NbglStyle(style), + font_id=NbglFontId(font_id), + localized=localized, + auto_hide_long_line=auto_hide_long_line, + len=len, + text=text + ) + + +@dataclass +class NbglSpinner(NbglObj): + area: NbglArea + position: int + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + position, = struct.unpack( + '>B', data[NbglArea.size():] + ) + return cls( + area=area, + position=position + ) + + +@dataclass +class NbglImage(NbglObj): + area: NbglArea + width: int + height: int + bpp: int + isFile: int + foreground_color: NbglColor + + @classmethod + def from_bytes(cls, is_stax : bool, data): + + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + + width,height,bpp,isFile,size, = struct.unpack('>HHBBI', data[NbglArea.size():NbglArea.size()+10]) + foreground_color, = struct.unpack('>B', data[NbglArea.size()+10:]) + return cls( + area=area, + width=width, + height=height, + bpp=bpp, + isFile=isFile, + foreground_color=NbglColor(foreground_color) + ) + + +@dataclass +class NbglImageFile(NbglObj): + area: NbglArea + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + area = NbglArea.from_bytes(is_stax, data[0:]) + return cls(area) + + +@dataclass +class NbglQrCode(NbglObj): + area: NbglArea + foreground_color: NbglColor + version: NbglQrCodeVersion + text: str + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + # Parse area + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + cnt = NbglArea.size() + + # Parse QR code color and version + foreground_color, version = struct.unpack('>BB', data[cnt: cnt + 2]) + cnt += 2 + + # Parse text + text = parse_str(data[cnt:]) + + # Return + return cls( + area=area, + foreground_color=NbglColor(foreground_color), + version=NbglQrCodeVersion(version), + text=text) + + +@dataclass +class NbglKeyboard(NbglObj): + area: NbglArea + text_color: NbglColor + border_color: NbglColor + letters_only: bool + upper_case: bool + mode: NbglKeyboardMode + key_mask: int + selected_char_index: int + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + if is_stax: + selected_char_index = 0 + text_color, border_color, letters_only, upper_case, mode, key_mask = struct.unpack( + '>BBBBBI', data[NbglArea.size():]) + else: + upper_case = False + text_color, border_color, letters_only, selected_char_index, mode, key_mask = struct.unpack( + '>BBBBBI', data[NbglArea.size():]) + + return cls( + area=area, + text_color=NbglColor(text_color), + border_color=NbglColor(border_color), + letters_only=letters_only, + upper_case=upper_case, + mode=NbglKeyboardMode(mode), + key_mask=key_mask, + selected_char_index=selected_char_index + ) + + +@dataclass +class NbglKeypad(NbglObj): + area: NbglArea + text_color: NbglColor + border_color: NbglColor + enable_backspace: bool + enable_validate: bool + enable_digits: bool + shuffled:bool + selectedKey: int + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + area = NbglArea.from_bytes(is_stax, data[0:NbglArea.size()]) + if is_stax: + text_color, border_color, enable_backspace, enable_validate, enable_digits, shuffled = \ + struct.unpack('>BBBBBB', data[NbglArea.size():NbglArea.size()+6]) + selectedKey = 0 # unused on Stax + else: + text_color = NbglColor.WHITE + border_color = NbglColor.BLACK + enable_digits = True + enable_backspace, enable_validate, shuffled, selectedKey = \ + struct.unpack('>BBBB', data[NbglArea.size():NbglArea.size()+4]) + return cls( + area=area, + text_color=NbglColor(text_color), + border_color=NbglColor(border_color), + enable_backspace=enable_backspace, + enable_validate=enable_validate, + enable_digits=enable_digits, + shuffled=shuffled, + selectedKey=selectedKey + ) + + +# Mapping of NbglObjType and their associated class. +NBGL_OBJ_TYPES = { + NbglObjType.SCREEN: NbglScreen, + NbglObjType.CONTAINER: NbglContainer, + NbglObjType.LINE: NbglLine, + NbglObjType.IMAGE: NbglImage, + NbglObjType.IMAGE_FILE: NbglImageFile, + NbglObjType.QR_CODE: NbglQrCode, + NbglObjType.RADIO_BUTTON: NbglRadioButton, + NbglObjType.TEXT_AREA: NbglTextArea, + NbglObjType.BUTTON: NbglButton, + NbglObjType.SWITCH: NbglSwitch, + NbglObjType.PAGE_INDICATOR: NbglPageIndicator, + NbglObjType.PROGRESS_BAR: NbglProgressBar, + NbglObjType.KEYBOARD: NbglKeyboard, + NbglObjType.KEYPAD: NbglKeypad, + NbglObjType.SPINNER: NbglSpinner, +} + +# Nbgl events + + +class NbglEventType(IntEnum): + """ + Available serialized Nbgl events + """ + NBGL_DRAW_OBJ = 0, + NBGL_REFRESH_AREA = 1 + + +@dataclass +class NbglRefreshAreaEvent(NbglGenericJsonSerializable): + area: NbglArea + + @classmethod + def from_bytes(cls, is_stax : bool, data: bytes): + return cls( + area=NbglArea.from_bytes(is_stax, data) + ) + + +@dataclass +class NbglDrawObjectEvent(NbglGenericJsonSerializable): + obj: NbglObj + id: int + + @classmethod + def from_bytes(cls, is_stax: bool, data: bytes): + # the first byte is the object id + # the second one is the object type + id = int(data[0]) + obj_type = NbglObjType(data[1]) + class_type = NBGL_OBJ_TYPES[obj_type] + + return cls( + obj=class_type.from_bytes(is_stax, data[2:]), + id=id + ) + + def to_json_dict(self) -> Dict: + for obj_type, obj_class in NBGL_OBJ_TYPES.items(): + if obj_class == type(self.obj): + return { + 'obj': { + 'type': obj_type.name, + 'content': self.obj.to_json_dict() + }, + 'id': self.id + } + # Object not serializable + return None + + @classmethod + def from_json_dict(cls, data: Dict): + obj_id = data['id'] + obj_data = data['obj'] + obj_type = NBGL_OBJ_TYPES[NbglObjType[obj_data['type']]] + + return cls( + obj=obj_type.from_json_dict(obj_data['content']), + id=obj_id + ) + +# Public functions + + +NbglEvent = Union[ + NbglRefreshAreaEvent, + NbglDrawObjectEvent +] + + +def deserialize_nbgl_bytes(is_stax: bool, data: bytes) -> NbglEvent: + """ + Return a NbglRefreshAreaEvent or a NbglDrawObjectEvent, + from input bytes. + """ + event_type = NbglEventType(int(data[0])) + + if event_type == NbglEventType.NBGL_DRAW_OBJ: + return NbglDrawObjectEvent.from_bytes(is_stax, data[1:]) + elif event_type == NbglEventType.NBGL_REFRESH_AREA: + return NbglRefreshAreaEvent.from_bytes(is_stax, data[1:]) + + +def deserialize_nbgl_json(data: Dict) -> NbglEvent: + """ + Return a NbglRefreshAreaEvent or a NbglDrawObjectEvent, + from input json-like dictionary. + """ + event_type = NbglEventType[data['event']] + + if event_type == NbglEventType.NBGL_DRAW_OBJ: + return NbglDrawObjectEvent.from_json_dict(data) + elif event_type == NbglEventType.NBGL_REFRESH_AREA: + return NbglRefreshAreaEvent.from_json_dict(data) + + +def serialize_nbgl_json(data: NbglEvent) -> Dict: + """ + Return a json-like dictionary from + input NbglRefreshAreaEvent / NbglDrawObjectEvent + """ + EVENT_TYPES = { + NbglRefreshAreaEvent: NbglEventType.NBGL_REFRESH_AREA, + NbglDrawObjectEvent: NbglEventType.NBGL_DRAW_OBJ + } + + result = {'event': EVENT_TYPES[type(data)].name} + result.update(data.to_json_dict()) + return result diff --git a/sdk_lib_nbgl/serialization/test_bytes_deserialize.py b/sdk_lib_nbgl/serialization/test_bytes_deserialize.py new file mode 100644 index 00000000..ec906570 --- /dev/null +++ b/sdk_lib_nbgl/serialization/test_bytes_deserialize.py @@ -0,0 +1,426 @@ +from nbgl_lib import * +import pytest + + +@pytest.fixture +def data_test(): + with open("data_test.txt", "r") as data_test_file: + data_test = data_test_file.readlines() + # first line is giving the HW: stax or nano + if data_test[0] == 'stax\n': + is_stax = True + else: + is_stax = False + data_test = list(map(lambda s: s.rstrip().split(','), data_test[1:])) + data_test = {el[0]: el[1] for el in data_test} + return (is_stax,data_test) + + +def run_deserialize_nbgl(is_stax: bool, hex_str: str): + bytes_in = bytes.fromhex(hex_str) + return deserialize_nbgl_bytes(is_stax, bytes_in) + + +def test_draw_nbgl_screen(data_test): + is_stax = data_test[0] + serialized = data_test[1]["test_draw_nbgl_screen"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglScreen( + area=NbglArea( + background_color=NbglColor.WHITE, + bpp=NbglBpp.BPP_4, + height=670, + width=400, + x0=0, + y0=0 + ) + ), + id=0 + ) + + assert deserialized == expected + + +def test_draw_nbgl_container(data_test): + is_stax = data_test[0] + serialized = data_test[1]["test_draw_nbgl_container"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglContainer( + area=NbglArea( + background_color=NbglColor.DARK_GRAY, + bpp=NbglBpp.BPP_4, + height=450, + width=460, + x0=56, + y0=12 + ), + layout=NbglDirection.VERTICAL, + nb_children=4, + force_clean=True + ), + id=1 + ) + + assert deserialized == expected + + +def test_draw_nbgl_text_area(data_test): + is_stax = data_test[0] + serialized = data_test[1]["test_draw_nbgl_text_area"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + excepted = \ + NbglDrawObjectEvent( + obj=NbglTextArea( + area=NbglArea( + bpp=NbglBpp.BPP_4, + width=360, + height=400, + x0=12, + y0=256, + background_color=NbglColor.DARK_GRAY, + ), + + text_color=NbglColor.BLACK, + text_alignment=NbglAlignment.BOTTOM_RIGHT, + style=NbglStyle.NO_STYLE, + font_id=NbglFontId.BAGL_FONT_INTER_MEDIUM_32px, + localized=False, + auto_hide_long_line=True, + len=0, + text="arthur" + ), + id=1 + ) + assert deserialized == excepted + + +def test_draw_nbgl_line(data_test): + is_stax = data_test[0] + if is_stax: + serialized = data_test[1]["test_draw_nbgl_line"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglLine( + area=NbglArea( + bpp=NbglBpp.BPP_1, + width=36, + height=267, + x0=0, + y0=42, + background_color=NbglColor.WHITE, + ), + direction=NbglDirection.HORIZONTAL, + line_color=NbglColor.DARK_GRAY, + thickness=4, + offset=2 + ), + id=1 + ) + assert deserialized == expected + assert True + + +def test_draw_nbgl_qr_code(data_test): + is_stax = data_test[0] + if is_stax: + serialized = data_test[1]["test_draw_nbgl_qr_code"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglQrCode( + area=NbglArea( + background_color=NbglColor.DARK_GRAY, + bpp=NbglBpp.BPP_2, + height=55, + width=66, + x0=400, + y0=300 + ), + foreground_color=NbglColor.DARK_GRAY, + text="fatstacks", + version=NbglQrCodeVersion.QRCODE_V10 + ), + id=1 + ) + assert deserialized == expected + assert True + + +def test_draw_nbgl_radio(data_test): + is_stax = data_test[0] + if is_stax: + serialized = data_test[1]["test_draw_nbgl_radio"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglRadioButton( + area=NbglArea( + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_4, + height=100, + width=200, + x0=123, + y0=234 + ), + active_color=NbglColor.BLACK, + border_color=NbglColor.DARK_GRAY, + state=NbglState.ON_STATE + ), + id=1 + ) + assert deserialized == expected + assert True + + +def test_draw_nbgl_switch(data_test): + is_stax = data_test[0] + if is_stax: + serialized = data_test[1]["test_draw_nbgl_switch"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglSwitch( + area=NbglArea( + background_color=NbglColor.LIGHT_GRAY, + bpp=NbglBpp.BPP_1, + height=333, + width=89, + x0=1, + y0=10000 + ), + off_color=NbglColor.WHITE, + on_color=NbglColor.BLACK, + state=NbglState.OFF_STATE + ), + id=1 + ) + assert deserialized == expected + assert True + + +def test_draw_nbgl_progress_bar(data_test): + is_stax = data_test[0] + serialized = data_test[1]["test_draw_nbgl_progress_bar"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglProgressBar( + area=NbglArea( + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_1, + height=10000, + width=11000, + x0=12000, + y0=13000, + ), + with_border=True, + state=91 + ), + id=1 + ) + assert deserialized == expected + + +def test_draw_nbgl_page_indicator(data_test): + is_stax = data_test[0] + if is_stax: + serialized = data_test[1]["test_draw_nbgl_page_indicator"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglPageIndicator( + area=NbglArea( + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2, + height=11, + width=22, + x0=33, + y0=44 + ), + active_page=2, + nb_pages=10 + ), + id=1 + ) + assert deserialized == expected + assert True + + +def test_draw_nbgl_button(data_test): + is_stax = data_test[0] + if is_stax: + serialized = data_test[1]["test_draw_nbgl_button"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglButton( + area=NbglArea( + background_color=NbglColor.DARK_GRAY, + bpp=NbglBpp.BPP_1, + height=50, + width=255, + x0=500, + y0=1000 + ), + inner_color=NbglColor.WHITE, + border_color=NbglColor.DARK_GRAY, + foreground_color=NbglColor.LIGHT_GRAY, + radius=NbglRadius.RADIUS_24_PIXELS, + font_id=NbglFontId.BAGL_FONT_INTER_MEDIUM_32px, + text="Test button", + localized=True + ), + id=1 + ) + assert deserialized == expected + assert True + + +def test_draw_nbgl_image(data_test): + is_stax = data_test[0] + serialized = data_test[1]["test_draw_nbgl_image"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglImage( + area=NbglArea( + background_color=NbglColor.WHITE, + bpp=NbglBpp.BPP_1, + height=32, + width=32, + x0=124, + y0=235 + ), + height=32, + width=32, + bpp=0, + isFile=1, + foreground_color=NbglColor.DARK_GRAY + ), + id=1 + ) + assert deserialized == expected + + +def test_draw_nbgl_keyboard(data_test): + is_stax = data_test[0] + serialized = data_test[1]["test_draw_nbgl_keyboard"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglKeyboard( + area=NbglArea( + width=225, + height=210, + x0=332, + y0=431, + background_color=NbglColor.LIGHT_GRAY, + bpp=NbglBpp.BPP_2 + ), + text_color=NbglColor.WHITE, + border_color=NbglColor.BLACK, + letters_only=True, + upper_case=False, + mode=NbglKeyboardMode.MODE_DIGITS, + key_mask=0x12345678, + selected_char_index=0 + ), + id=1 + ) + assert deserialized == expected + + +def test_draw_nbgl_keypad(data_test): + is_stax = data_test[0] + serialized = data_test[1]["test_draw_nbgl_keypad"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglKeypad( + area=NbglArea( + width=4, + height=4, + x0=3, + y0=4, + background_color=NbglColor.WHITE, + bpp=NbglBpp.BPP_4 + ), + text_color=NbglColor.WHITE, + border_color=NbglColor.BLACK, + enable_backspace=True, + enable_validate=False, + enable_digits=True, + shuffled=False, + selectedKey=0 + ), + id=1 + ) + assert deserialized == expected + + +def test_draw_nbgl_spinner(data_test): + is_stax = data_test[0] + if is_stax: + serialized = data_test[1]["test_draw_nbgl_spinner"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglSpinner( + area=NbglArea( + background_color=NbglColor.LIGHT_GRAY, + bpp=NbglBpp.BPP_1, + height=14, + width=25, + x0=12, + y0=10, + ), + position=2 + ), + id=1 + ) + assert deserialized == expected + assert True + + +def test_draw_nbgl_image_file(data_test): + is_stax = data_test[0] + serialized = data_test[1]["test_draw_nbgl_image_file"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglDrawObjectEvent( + obj=NbglImageFile( + area=NbglArea( + background_color=NbglColor.DARK_GRAY, + bpp=NbglBpp.BPP_4, + height=24, + width=35, + x0=22, + y0=20, + ), + ), + id=1 + ) + assert deserialized == expected + + +def test_refresh_area(data_test): + is_stax = data_test[0] + serialized = data_test[1]["test_refresh_area"] + deserialized = run_deserialize_nbgl(is_stax, serialized) + expected = \ + NbglRefreshAreaEvent( + area=NbglArea( + background_color=NbglColor.WHITE, + bpp=NbglBpp.BPP_4, + height=4, + width=4, + x0=3, + y0=4, + ), + ) + assert deserialized == expected diff --git a/sdk_lib_nbgl/serialization/test_json_ser_deser.py b/sdk_lib_nbgl/serialization/test_json_ser_deser.py new file mode 100644 index 00000000..fbfafdf8 --- /dev/null +++ b/sdk_lib_nbgl/serialization/test_json_ser_deser.py @@ -0,0 +1,694 @@ +from threading import local +from nbgl_lib import * +import pytest + + +def test_json_deserialize_screen(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'SCREEN', + 'content': { + 'area': { + 'width': 400, + 'height': 670, + 'x0': 0, + 'y0': 0, + 'background_color': 'WHITE', + 'bpp': 'BPP_4' + } + } + }, + 'id':0 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglScreen( + area=NbglArea( + width=400, + height=670, + x0=0, + y0=0, + background_color=NbglColor.WHITE, + bpp=NbglBpp.BPP_4 + ) + ), + id=0 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + +def test_json_deserialize_container(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'CONTAINER', + 'content': { + 'area': { + 'width': 1, + 'height': 2, + 'x0': 3, + 'y0': 4, + 'background_color': 'DARK_GRAY', + 'bpp': 'BPP_4' + }, + 'layout': 'VERTICAL', + 'nb_children': 4, + 'force_clean': True + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglContainer( + area=NbglArea( + width=1, + height=2, + x0=3, + y0=4, + background_color=NbglColor.DARK_GRAY, + bpp=NbglBpp.BPP_4 + ), + layout=NbglDirection.VERTICAL, + nb_children=4, + force_clean=True + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_line(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'LINE', + 'content': { + 'area': { + 'width': 10, + 'height': 20, + 'x0': 30, + 'y0': 40, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + 'direction': 'VERTICAL', + 'line_color': 'WHITE', + 'thickness': 4, + 'offset': 1 + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglLine( + area=NbglArea( + width=10, + height=20, + x0=30, + y0=40, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + direction=NbglDirection.VERTICAL, + line_color=NbglColor.WHITE, + thickness=4, + offset=1 + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_radio_button(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'RADIO_BUTTON', + 'content': { + 'area': { + 'width': 100, + 'height': 200, + 'x0': 300, + 'y0': 400, + 'background_color': 'LIGHT_GRAY', + 'bpp': 'BPP_1' + }, + 'active_color': 'WHITE', + 'border_color': 'BLACK', + 'state': 'OFF_STATE' + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglRadioButton( + area=NbglArea( + width=100, + height=200, + x0=300, + y0=400, + background_color=NbglColor.LIGHT_GRAY, + bpp=NbglBpp.BPP_1 + ), + active_color=NbglColor.WHITE, + border_color=NbglColor.BLACK, + state=NbglState.OFF_STATE + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_switch(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'SWITCH', + 'content': { + 'area': { + 'width': 50, + 'height': 60, + 'x0': 150, + 'y0': 300, + 'background_color': 'DARK_GRAY', + 'bpp': 'BPP_2' + }, + 'on_color': 'BLACK', + 'off_color': 'DARK_GRAY', + 'state': 'ON_STATE' + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglSwitch( + area=NbglArea( + width=50, + height=60, + x0=150, + y0=300, + background_color=NbglColor.DARK_GRAY, + bpp=NbglBpp.BPP_2 + ), + on_color=NbglColor.BLACK, + off_color=NbglColor.DARK_GRAY, + state=NbglState.ON_STATE + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_progress_bar(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'PROGRESS_BAR', + 'content': { + 'area': { + 'width': 20, + 'height': 30, + 'x0': 70, + 'y0': 80, + 'background_color': 'WHITE', + 'bpp': 'BPP_1' + }, + 'with_border': True, + 'state': 42 + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglProgressBar( + area=NbglArea( + width=20, + height=30, + x0=70, + y0=80, + background_color=NbglColor.WHITE, + bpp=NbglBpp.BPP_1 + ), + with_border=True, + state=42 + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_page_indicator(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'PAGE_INDICATOR', + 'content': { + 'area': { + 'width': 35, + 'height': 36, + 'x0': 37, + 'y0': 38, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + 'active_page': 2, + 'nb_pages': 10 + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglPageIndicator( + area=NbglArea( + width=35, + height=36, + x0=37, + y0=38, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + active_page=2, + nb_pages=10 + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_button(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'BUTTON', + 'content': { + 'area': { + 'width': 35, + 'height': 36, + 'x0': 37, + 'y0': 38, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + 'inner_color': 'BLACK', + 'border_color': 'WHITE', + 'foreground_color': 'DARK_GRAY', + 'radius': 'RADIUS_40_PIXELS', + 'font_id': 'BAGL_FONT_INTER_REGULAR_24px', + 'localized': True, + 'text': 'Hello world' + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglButton( + area=NbglArea( + width=35, + height=36, + x0=37, + y0=38, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + inner_color=NbglColor.BLACK, + border_color=NbglColor.WHITE, + foreground_color=NbglColor.DARK_GRAY, + radius=NbglRadius.RADIUS_40_PIXELS, + font_id=NbglFontId.BAGL_FONT_INTER_REGULAR_24px, + localized=True, + text="Hello world" + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_text_area(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'TEXT_AREA', + 'content': { + 'area': { + 'width': 35, + 'height': 36, + 'x0': 37, + 'y0': 38, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + 'text_color': 'BLACK', + 'text_alignment': 'RIGHT_BOTTOM', + 'style': 'NO_STYLE', + 'font_id': 'BAGL_FONT_INTER_SEMIBOLD_24px', + 'localized': True, + 'auto_hide_long_line': True, + 'len': 0, + 'text': 'Hello fatstacks' + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglTextArea( + area=NbglArea( + width=35, + height=36, + x0=37, + y0=38, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + text_color=NbglColor.BLACK, + text_alignment=NbglAlignment.RIGHT_BOTTOM, + style=NbglStyle.NO_STYLE, + font_id=NbglFontId.BAGL_FONT_INTER_SEMIBOLD_24px, + localized=True, + auto_hide_long_line=True, + len=0, + text='Hello fatstacks' + ), + id=1 + ) + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_spinner(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'SPINNER', + 'content': { + 'area': { + 'width': 35, + 'height': 36, + 'x0': 37, + 'y0': 38, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + 'position': 3 + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglSpinner( + area=NbglArea( + width=35, + height=36, + x0=37, + y0=38, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + position=3 + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_image(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'IMAGE', + 'content': { + 'area': { + 'width': 35, + 'height': 36, + 'x0': 37, + 'y0': 38, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + 'height':35, + 'width':36, + 'bpp':2, + 'isFile':1, + 'foreground_color': 'WHITE' + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + NbglImage( + area=NbglArea( + width=35, + height=36, + x0=37, + y0=38, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + height=35, + width=36, + bpp=2, + isFile=1, + foreground_color=NbglColor.WHITE + ), + id=1 + ) + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_image_file(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'IMAGE_FILE', + 'content': { + 'area': { + 'width': 35, + 'height': 36, + 'x0': 37, + 'y0': 38, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglImageFile( + area=NbglArea( + width=35, + height=36, + x0=37, + y0=38, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_qr_code(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'QR_CODE', + 'content': { + 'area': { + 'width': 35, + 'height': 36, + 'x0': 37, + 'y0': 38, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + 'foreground_color': 'LIGHT_GRAY', + 'version': 'QRCODE_V10', + 'text': 'Qr code text qr code' + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + NbglQrCode( + area=NbglArea( + width=35, + height=36, + x0=37, + y0=38, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + foreground_color=NbglColor.LIGHT_GRAY, + version=NbglQrCodeVersion.QRCODE_V10, + text='Qr code text qr code' + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_keyboard(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'KEYBOARD', + 'content': { + 'area': { + 'width': 35, + 'height': 36, + 'x0': 37, + 'y0': 38, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + 'text_color': 'WHITE', + 'border_color': 'LIGHT_GRAY', + 'letters_only': False, + 'upper_case': True, + 'mode': 'MODE_SPECIAL', + 'key_mask': 255, + 'selected_char_index':2 + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglKeyboard( + area=NbglArea( + width=35, + height=36, + x0=37, + y0=38, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + text_color=NbglColor.WHITE, + border_color=NbglColor.LIGHT_GRAY, + letters_only=False, + upper_case=True, + mode=NbglKeyboardMode.MODE_SPECIAL, + key_mask=255, + selected_char_index=2 + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_keypad(): + serialized = { + 'event': 'NBGL_DRAW_OBJ', + 'obj': { + 'type': 'KEYPAD', + 'content': { + 'area': { + 'width': 35, + 'height': 36, + 'x0': 37, + 'y0': 38, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + 'text_color': 'WHITE', + 'border_color': 'LIGHT_GRAY', + 'enable_backspace': True, + 'enable_validate': False, + 'enable_digits': True, + 'shuffled': False, + 'selectedKey': 0 + } + }, + 'id':1 + } + deserialized = \ + NbglDrawObjectEvent( + obj=NbglKeypad( + area=NbglArea( + width=35, + height=36, + x0=37, + y0=38, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + text_color=NbglColor.WHITE, + border_color=NbglColor.LIGHT_GRAY, + enable_backspace=True, + enable_validate=False, + enable_digits=True, + shuffled=False, + selectedKey=0 + ), + id=1 + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) + + +def test_json_deserialize_refresh(): + serialized = { + 'event': 'NBGL_REFRESH_AREA', + 'area': { + 'width': 35, + 'height': 36, + 'x0': 37, + 'y0': 38, + 'background_color': 'BLACK', + 'bpp': 'BPP_2' + }, + } + + deserialized = \ + NbglRefreshAreaEvent( + area=NbglArea( + width=35, + height=36, + x0=37, + y0=38, + background_color=NbglColor.BLACK, + bpp=NbglBpp.BPP_2 + ), + ) + + assert serialize_nbgl_json(deserialized) == serialized + assert deserialized == deserialize_nbgl_json(serialized) diff --git a/sdk_lib_nbgl/serialization/ux_loc.h b/sdk_lib_nbgl/serialization/ux_loc.h new file mode 100644 index 00000000..010970af --- /dev/null +++ b/sdk_lib_nbgl/serialization/ux_loc.h @@ -0,0 +1,3 @@ +#pragma once + +typedef unsigned short UX_LOC_STRINGS_INDEX; diff --git a/sdk_lib_nbgl/src/nbgl_buttons.c b/sdk_lib_nbgl/src/nbgl_buttons.c new file mode 100644 index 00000000..29e737a1 --- /dev/null +++ b/sdk_lib_nbgl/src/nbgl_buttons.c @@ -0,0 +1,155 @@ +/** + * @file nbgl_buttons.c + * Implementation of buttons management in NBGL + */ + +#ifndef HAVE_SE_TOUCH +/********************* + * INCLUDES + *********************/ +#include +#include "nbgl_obj.h" +#include "nbgl_debug.h" +#include "nbgl_buttons.h" +#include "nbgl_screen.h" +#include "os_pic.h" + +/********************* + * DEFINES + *********************/ + +/********************** + * TYPEDEFS + **********************/ + +/********************** + * STATIC PROTOTYPES + **********************/ + +/********************** + * STATIC VARIABLES + **********************/ +static uint8_t gButtonMask = 0; +static uint32_t gButtonSameMaskCounter = 0; + +/********************** + * VARIABLES + **********************/ + +/********************** + * STATIC PROTOTYPES + **********************/ +static nbgl_buttonEvent_t maskToEvent(uint8_t mask) +{ + nbgl_buttonEvent_t event = INVALID_BUTTON_EVENT; + switch (mask) { + case RELEASED_MASK | LEFT_BUTTON | RIGHT_BUTTON: + event = BUTTON_BOTH_PRESSED; + break; + + case LEFT_BUTTON | RIGHT_BUTTON: + event = BUTTON_BOTH_TOUCHED; + break; + + case CONTINUOUS_MASK | LEFT_BUTTON: + event = BUTTON_LEFT_CONTINUOUS_PRESSED; + break; + + case RELEASED_MASK | LEFT_BUTTON: + event = BUTTON_LEFT_PRESSED; + break; + + case CONTINUOUS_MASK | RIGHT_BUTTON: + event = BUTTON_RIGHT_CONTINUOUS_PRESSED; + break; + + case RELEASED_MASK | RIGHT_BUTTON: + event = BUTTON_RIGHT_PRESSED; + break; + } + + return event; +} +/********************** + * GLOBAL FUNCTIONS + **********************/ + +/** + * @brief Function to be called periodically to check touchscreen state + * and coordinates + * @param buttonState state of both buttons (only 2 LSB are used) + * @param currentTimeMs current time in ms + */ +void nbgl_buttonsHandler(uint8_t buttonState, uint32_t currentTimeMs) +{ + uint8_t button_mask; + uint32_t button_same_mask_counter; + + (void) currentTimeMs; + // enable speeded up long push (continuous) + if (buttonState == gButtonMask) { + if (buttonState == 0) { + // nothing to be done when both buttons released twice in a row + return; + } + // each 100ms ~ + gButtonSameMaskCounter++; + } + + // append the button mask + button_mask = gButtonMask | buttonState; + + // pre reset variable due to os_sched_exit + button_same_mask_counter = gButtonSameMaskCounter; + + if (buttonState == 0) { + // reset next state when both buttons are released + gButtonMask = 0; + gButtonSameMaskCounter = 0; + + // notify button released event + button_mask |= RELEASED_MASK; + } + else { + gButtonMask = button_mask; + } + + // reset counter when button mask changes + if (buttonState != gButtonMask) { + gButtonSameMaskCounter = 0; + } + + // if the same button(s) is pressed more than 800 ms + if (button_same_mask_counter >= CONTINOUS_PRESS_THRESHOLD) { + // fast bit when pressing and timing is right (tag the event every 300ms) + if ((button_same_mask_counter % CONTINUOUS_PRESS_PERIOD) == 0) { + button_mask |= CONTINUOUS_MASK; + } + + // discard the release event after a fastskip has been detected, to avoid strange at release + // behavior and also to enable user to cancel an operation by starting triggering the fast + // skip + button_mask &= ~RELEASED_MASK; + } + +#ifndef TARGET_NANOS + nbgl_screen_t *topScreen = (nbgl_screen_t *) nbgl_screenGetTop(); + if ((topScreen != NULL) && (topScreen->buttonCallback != NULL)) { + nbgl_buttonEvent_t event = maskToEvent(button_mask); + if (event != INVALID_BUTTON_EVENT) { + topScreen->buttonCallback(topScreen, event); + } + } +#else + nbgl_buttonEvent_t event = maskToEvent(button_mask); + layout_buttonCallback(event); +#endif +} + +void nbgl_buttonsReset(void) +{ + // no button push so far + gButtonMask = 0; + gButtonSameMaskCounter = 0; +} +#endif // HAVE_SE_TOUCH diff --git a/sdk_lib_nbgl/src/nbgl_fonts_lns.c b/sdk_lib_nbgl/src/nbgl_fonts_lns.c new file mode 100644 index 00000000..bad58ca0 --- /dev/null +++ b/sdk_lib_nbgl/src/nbgl_fonts_lns.c @@ -0,0 +1,289 @@ + +/** + * @file nbgl_fonts.c + * Implementation of fonts array + */ +#include +#include +#include +#include "nbgl_fonts.h" + +#define PAGING_FORMAT_NB 1 + +// We implement a light mecanism in order to be able to retrieve the width of +// nano S characters, in the two possible fonts: +// - BAGL_FONT_OPEN_SANS_EXTRABOLD_11px, +// - BAGL_FONT_OPEN_SANS_REGULAR_11px. +#define NANOS_FIRST_CHAR 0x20 +#define NANOS_LAST_CHAR 0x7F + +// OPEN_SANS_REGULAR_11PX << 4 | OPEN_SANS_EXTRABOLD_11PX +const char nanos_characters_width[96] = { + 3 << 4 | 3, /* code 0020 */ + 3 << 4 | 3, /* code 0021 */ + 4 << 4 | 6, /* code 0022 */ + 7 << 4 | 7, /* code 0023 */ + 6 << 4 | 6, /* code 0024 */ + 9 << 4 | 10, /* code 0025 */ + 8 << 4 | 9, /* code 0026 */ + 2 << 4 | 3, /* code 0027 */ + 3 << 4 | 4, /* code 0028 */ + 3 << 4 | 4, /* code 0029 */ + 6 << 4 | 6, /* code 002A */ + 6 << 4 | 6, /* code 002B */ + 3 << 4 | 3, /* code 002C */ + 4 << 4 | 4, /* code 002D */ + 3 << 4 | 3, /* code 002E */ + 4 << 4 | 5, /* code 002F */ + 6 << 4 | 8, /* code 0030 */ + 6 << 4 | 6, /* code 0031 */ + 6 << 4 | 7, /* code 0032 */ + 6 << 4 | 7, /* code 0033 */ + 8 << 4 | 8, /* code 0034 */ + 6 << 4 | 6, /* code 0035 */ + 6 << 4 | 8, /* code 0036 */ + 6 << 4 | 7, /* code 0037 */ + 6 << 4 | 8, /* code 0038 */ + 6 << 4 | 8, /* code 0039 */ + 3 << 4 | 3, /* code 003A */ + 3 << 4 | 3, /* code 003B */ + 6 << 4 | 5, /* code 003C */ + 6 << 4 | 6, /* code 003D */ + 6 << 4 | 5, /* code 003E */ + 5 << 4 | 6, /* code 003F */ + 10 << 4 | 10, /* code 0040 */ + 7 << 4 | 8, /* code 0041 */ + 7 << 4 | 7, /* code 0042 */ + 7 << 4 | 7, /* code 0043 */ + 8 << 4 | 8, /* code 0044 */ + 6 << 4 | 6, /* code 0045 */ + 6 << 4 | 6, /* code 0046 */ + 8 << 4 | 8, /* code 0047 */ + 8 << 4 | 8, /* code 0048 */ + 3 << 4 | 4, /* code 0049 */ + 4 << 4 | 5, /* code 004A */ + 7 << 4 | 8, /* code 004B */ + 6 << 4 | 6, /* code 004C */ + 10 << 4 | 11, /* code 004D */ + 8 << 4 | 9, /* code 004E */ + 9 << 4 | 9, /* code 004F */ + 7 << 4 | 7, /* code 0050 */ + 9 << 4 | 9, /* code 0051 */ + 7 << 4 | 8, /* code 0052 */ + 6 << 4 | 6, /* code 0053 */ + 7 << 4 | 6, /* code 0054 */ + 8 << 4 | 8, /* code 0055 */ + 7 << 4 | 6, /* code 0056 */ + 10 << 4 | 11, /* code 0057 */ + 6 << 4 | 8, /* code 0058 */ + 6 << 4 | 7, /* code 0059 */ + 6 << 4 | 7, /* code 005A */ + 4 << 4 | 5, /* code 005B */ + 4 << 4 | 5, /* code 005C */ + 4 << 4 | 5, /* code 005D */ + 6 << 4 | 7, /* code 005E */ + 5 << 4 | 6, /* code 005F */ + 6 << 4 | 7, /* code 0060 */ + 6 << 4 | 7, /* code 0061 */ + 7 << 4 | 7, /* code 0062 */ + 5 << 4 | 6, /* code 0063 */ + 7 << 4 | 7, /* code 0064 */ + 6 << 4 | 7, /* code 0065 */ + 5 << 4 | 6, /* code 0066 */ + 6 << 4 | 7, /* code 0067 */ + 7 << 4 | 7, /* code 0068 */ + 3 << 4 | 4, /* code 0069 */ + 4 << 4 | 5, /* code 006A */ + 6 << 4 | 7, /* code 006B */ + 3 << 4 | 4, /* code 006C */ + 10 << 4 | 10, /* code 006D */ + 7 << 4 | 7, /* code 006E */ + 7 << 4 | 7, /* code 006F */ + 7 << 4 | 7, /* code 0070 */ + 7 << 4 | 7, /* code 0071 */ + 4 << 4 | 5, /* code 0072 */ + 5 << 4 | 6, /* code 0073 */ + 4 << 4 | 5, /* code 0074 */ + 7 << 4 | 7, /* code 0075 */ + 6 << 4 | 7, /* code 0076 */ + 9 << 4 | 10, /* code 0077 */ + 6 << 4 | 7, /* code 0078 */ + 6 << 4 | 7, /* code 0079 */ + 5 << 4 | 6, /* code 007A */ + 4 << 4 | 5, /* code 007B */ + 6 << 4 | 6, /* code 007C */ + 4 << 4 | 5, /* code 007D */ + 6 << 4 | 6, /* code 007E */ + 7 << 4 | 6, /* code 007F */ +}; + +// This function is used to retrieve the length of a string (expressed in bytes) delimited with a +// boundary width (expressed in pixels). +uint8_t se_get_cropped_length(const char *text, + uint8_t text_length, + uint32_t width_limit_in_pixels, + uint8_t text_format) +{ + char current_char; + uint8_t length; + uint32_t current_width_in_pixels = 0; + + for (length = 0; length < text_length; length++) { + current_char = text[length]; + + if ((text_format & PAGING_FORMAT_NB) == PAGING_FORMAT_NB) { + // Bold. + current_width_in_pixels + += nanos_characters_width[current_char - NANOS_FIRST_CHAR] & 0x0F; + } + else { + // Regular. + current_width_in_pixels + += (nanos_characters_width[current_char - NANOS_FIRST_CHAR] >> 0x04) & 0x0F; + } + + // We stop the processing when we reached the limit. + if (current_width_in_pixels > width_limit_in_pixels) { + break; + } + } + + return length; +} + +// This function is used to retrieve the width of a line of text. +static uint32_t se_compute_line_width_light(const char *text, + uint8_t text_length, + uint8_t text_format) +{ + char current_char; + uint32_t line_width = 0; + + // We parse the characters of the input text on all the input length. + while (text_length--) { + current_char = *text; + + if (current_char < NANOS_FIRST_CHAR || current_char > NANOS_LAST_CHAR) { + if (current_char == '\n' || current_char == '\r') { + break; + } + } + else { + // We retrieve the character width, and the paging format indicates whether we are + // processing bold characters or not. + if ((text_format & PAGING_FORMAT_NB) == PAGING_FORMAT_NB) { + // Bold. + line_width += nanos_characters_width[current_char - NANOS_FIRST_CHAR] & 0x0F; + } + else { + // Regular. + line_width + += (nanos_characters_width[current_char - NANOS_FIRST_CHAR] >> 0x04) & 0x0F; + } + } + text++; + } + return line_width; +} + +/** + * @brief compute the len of the given text (in bytes) fitting in the given maximum nb lines, with + * the given maximum width + * + * @param fontId font ID + * @param text input UTF-8 string, possibly multi-line + * @param maxWidth maximum width in bytes, if text is greater than that the parsing is escaped + * @param maxNbLines maximum number of lines, if text is greater than that the parsing is escaped + * @param len (output) consumed bytes in text fitting in maxWidth + * @param wrapping if true, lines are split on separators like spaces, \n... + * + * @return true if maxNbLines is reached, false otherwise + * + */ +bool nbgl_getTextMaxLenInNbLines(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + uint16_t maxNbLines, + uint16_t *len, + bool wrapping) +{ + uint16_t textLen = strlen(text); + const char *origText = text; + + while ((textLen) && (maxNbLines > 0)) { + + uint8_t line_len = se_get_cropped_length(text, textLen, maxWidth, 0); + + text += line_len; + textLen -= line_len; + maxNbLines--; + } + *len = text - origText; + return (maxNbLines == 0); +} + +/** + * @brief compute the number of lines of the given text fitting in the given maxWidth + * + * @param fontId font ID + * @param text UTF-8 text to get the number of lines from + * @param maxWidth maximum width in which the text must fit + * @param wrapping if true, lines are split on separators like spaces, \n... + * @return the number of lines in the given text + */ +uint16_t nbgl_getTextNbLinesInWidth(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + bool wrapping) +{ + uint16_t textLen = strlen(text); + const char *origText = text; + uint16_t nbLines = 0; + + while (textLen) { + uint8_t char_width; + + uint8_t line_len = se_get_cropped_length(text, textLen, maxWidth, 0); + + text += line_len; + textLen -= line_len; + nbLines++; + } + return nbLines; +} + +/** + * @brief compute the number of pages of nbLinesPerPage lines per page of the given text fitting in + * the given maxWidth + * + * @param fontId font ID + * @param text UTF-8 text to get the number of pages from + * @param nbLinesPerPage number of lines in a page + * @param maxWidth maximum width in which the text must fit + * @return the number of pages in the given text + */ +uint8_t nbgl_getTextNbPagesInWidth(nbgl_font_id_e fontId, + const char *text, + uint8_t nbLinesPerPage, + uint16_t maxWidth) +{ + return (nbgl_getTextNbLinesInWidth(fontId, text, maxWidth, false) + nbLinesPerPage - 1 )/ nbLinesPerPage; +} + +/** + * @brief return the height of the given multiline text, with the given font. + * + * @param fontId font ID + * @param text text to get the height from + * @param maxWidth maximum width in which the text must fit + * @param wrapping if true, lines are split on separators like spaces, \n... + * @return the height in pixels + */ +uint16_t nbgl_getTextHeightInWidth(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + bool wrapping) +{ + const nbgl_font_t *font = nbgl_getFont(fontId); + return (nbgl_getTextNbLinesInWidth(fontId, text, maxWidth, wrapping) * font->line_height); +} diff --git a/sdk_lib_nbgl/src/nbgl_step.c b/sdk_lib_nbgl/src/nbgl_step.c new file mode 100644 index 00000000..9f13c239 --- /dev/null +++ b/sdk_lib_nbgl/src/nbgl_step.c @@ -0,0 +1,654 @@ +/** + * @file nbgl_step.c + * @brief Implementation of predefined pages management for Applications + */ + +#ifdef NBGL_STEP2 +/********************* + * INCLUDES + *********************/ +#include +#include "nbgl_debug.h" +#include "nbgl_step.h" +#include "glyphs.h" +#include "os_pic.h" +#include "os_print.h" + +/********************* + * DEFINES + *********************/ +// string to store the title for a multi-pages text+subtext +#define TMP_STRING_MAX_LEN 24 + +///< Maximum number of layers for steps, cannot be greater than max number of layout layers +#ifdef TARGET_NANOS +#undef NB_MAX_LINES +#define NB_MAX_LINES 2 +#define NB_MAX_LAYERS 1 +#else +#define NB_MAX_LAYERS 3 +#endif + +/********************** + * TYPEDEFS + **********************/ +/** + * type of step + */ +typedef enum { + TEXT_STEP = 0, ///< for a simple text step + CENTERED_INFO_STEP, ///< for a centered info step + MENU_LIST_STEP ///< for a menu list step +} StepStype_t; + +/** + * definition of context for a @ref TEXT_STEP or @ref CENTERED_INFO_STEP step + */ +typedef struct TextContext_s { + uint8_t nbPages; ///< number of pages for this text step + uint8_t currentPage; ///< current page for this text step + const char *txtStart; ///< pointer on the start point of text (first page) + const char *nextPageStart; ///< pointer on the start point of text at the next page + const char *subTxtStart; ///< pointer on the start point of sub-text (first page) + nbgl_stepPosition_t pos; ///< position of the step within a flow (used for navigation arrows) + nbgl_stepButtonCallback_t onActionCallback; ///< function called when key actions done on this + ///< step (other than internal navigation) + char tmpString[TMP_STRING_MAX_LEN]; ///< temporary string used for title when text + + ///< multi-pages subText + nbgl_centeredInfoStyle_t style; ///< style to be used with a @ref CENTERED_INFO_STEP step +} TextContext_t; + +/** + * definition of context for a @ref MENU_LIST_STEP step + */ +typedef struct MenuListContext_s { + nbgl_stepMenuListCallback_t + selectedCallback; ///< function to call when a menu list item is selected + nbgl_layoutMenuList_t + list; ///< structure to store the number of menu items and the way to select them +} MenuListContext_t; + +typedef struct StepContext_s { + union { + TextContext_t textContext; ///< if type is @ref TEXT_STEP or @ref CENTERED_INFO_STEP + MenuListContext_t menuListContext; ///< if type is @ref MENU_LIST_STEP + }; + nbgl_screenTickerConfiguration_t ticker; ///< structure containing information about ticker + nbgl_layout_t *layout; ///< handler of the used layout + StepStype_t type; ///< type of step + bool modal; ///< true if modal +} StepContext_t; + +/********************** + * STATIC VARIABLES + **********************/ +///< array of step contexts. Index 0 is reserved for background +static StepContext_t contexts[NB_MAX_LAYERS]; + +/********************** + * STATIC PROTOTYPES + **********************/ + +#ifdef TARGET_NANOS +static void actionCallback(nbgl_buttonEvent_t event); +static void menuListActionCallback(nbgl_buttonEvent_t event); +#else +static void actionCallback(nbgl_layout_t *layout, nbgl_buttonEvent_t event); +static void menuListActionCallback(nbgl_layout_t *layout, nbgl_buttonEvent_t event); +#endif + +// returns a non-used step context from the contexts[] array, or NULL if not found +static StepContext_t *getFreeContext(StepStype_t type, bool modal) +{ + StepContext_t *ctx = NULL; + +#ifdef TARGET_NANOS + ctx = &contexts[0]; +#else + if (!modal) { + // Index 0 is reserved for background + ctx = &contexts[0]; + } + else { + uint32_t i = 1; + while (i < NB_MAX_LAYERS) { + if (contexts[i].layout == NULL) { + ctx = &contexts[i]; + break; + } + i++; + } + } +#endif + if (ctx == NULL) { + LOG_FATAL(STEP_LOGGER, "getFreeContext(): no available context\n"); + } + else { + memset(ctx, 0, sizeof(StepContext_t)); + ctx->type = type; + ctx->modal = modal; + } + return ctx; +} + +#ifndef TARGET_NANOS +// returns the step context from the contexts[] array matching with the given layout handler, or +// NULL if not found +static StepContext_t *getContextFromLayout(nbgl_layout_t layout) +{ + StepContext_t *ctx = NULL; + uint32_t i = 0; + while (i < NB_MAX_LAYERS) { + if (contexts[i].layout == layout) { + ctx = &contexts[i]; + break; + } + i++; + } + if (ctx == NULL) { + LOG_WARN(STEP_LOGGER, "getContextFromLayout(): no matching context\n"); + } + return ctx; +} +#endif + +// from the current details context, return a pointer on the details at the given page +static const char *getTextPageAt(StepContext_t *ctx, uint8_t textPage) +{ + uint8_t page = 0; + const char *currentChar = ctx->textContext.txtStart; + while (page < textPage) { + if (page < (ctx->textContext.nbPages - 1)) { + uint16_t len; + nbgl_getTextMaxLenInNbLines(BAGL_FONT_OPEN_SANS_REGULAR_11px_1bpp, + currentChar, + AVAILABLE_WIDTH, + NB_MAX_LINES, + &len, + true); + currentChar = currentChar + len; + } + page++; + } + return currentChar; +} + +// from the current details context, return a pointer on the details at the given page, for subText +static const char *getSubTextPageAt(StepContext_t *ctx, uint8_t textPage) +{ + uint8_t page = 0; + const char *currentChar = ctx->textContext.subTxtStart; + while (page < textPage) { + if (page < (ctx->textContext.nbPages - 1)) { + uint16_t len; + nbgl_getTextMaxLenInNbLines(BAGL_FONT_OPEN_SANS_REGULAR_11px_1bpp, + currentChar, + AVAILABLE_WIDTH, + NB_MAX_LINES - 1, + &len, + true); + currentChar = currentChar + len; + } + page++; + } + return currentChar; +} + +// utility function to compute navigation arrows +static nbgl_layoutNavIndication_t getNavigationInfo(nbgl_stepPosition_t pos, + uint8_t nbPages, + uint8_t currentPage) +{ + nbgl_layoutNavIndication_t indication = NO_ARROWS; + + if (nbPages > 1) { + if (currentPage > 0) { + indication |= LEFT_ARROW; + } + if (currentPage < (nbPages - 1)) { + indication |= RIGHT_ARROW; + } + } + if (pos == FIRST_STEP) { + indication |= RIGHT_ARROW; + } + else if (pos == LAST_STEP) { + indication |= LEFT_ARROW; + } + else if (pos == NEITHER_FIRST_NOR_LAST_STEP) { + indication |= RIGHT_ARROW | LEFT_ARROW; + } + return indication; +} + +// function used to display the current page in details review mode +static void displayTextPage(StepContext_t *ctx, uint8_t textPage) +{ + const char *txt; + + // if move backward or first page + if (textPage <= ctx->textContext.currentPage) { + // recompute current start from beginning + if (ctx->textContext.subTxtStart == NULL) { + txt = getTextPageAt(ctx, textPage); + } + else { + txt = getSubTextPageAt(ctx, textPage); + } + } + // else move forward + else { + txt = ctx->textContext.nextPageStart; + } + ctx->textContext.currentPage = textPage; + + if (ctx->textContext.currentPage < (ctx->textContext.nbPages - 1)) { + uint16_t len; + uint8_t nbLines + = (ctx->textContext.subTxtStart == NULL) ? NB_MAX_LINES : (NB_MAX_LINES - 1); + nbgl_getTextMaxLenInNbLines( + BAGL_FONT_OPEN_SANS_REGULAR_11px_1bpp, txt, AVAILABLE_WIDTH, nbLines, &len, true); + // memorize next position to save processing + ctx->textContext.nextPageStart = txt + len; + } + else { + ctx->textContext.nextPageStart = NULL; + } + nbgl_layoutDescription_t layoutDescription; + nbgl_layoutNavigation_t navInfo = { + .direction = HORIZONTAL_NAV, + }; + + layoutDescription.modal = ctx->modal; + layoutDescription.onActionCallback = actionCallback; + layoutDescription.ticker.tickerCallback = ctx->ticker.tickerCallback; + layoutDescription.ticker.tickerIntervale = ctx->ticker.tickerIntervale; + layoutDescription.ticker.tickerValue = ctx->ticker.tickerValue; + + navInfo.indication = getNavigationInfo( + ctx->textContext.pos, ctx->textContext.nbPages, ctx->textContext.currentPage); + +#ifndef TARGET_NANOS + ctx->layout = nbgl_layoutGet(&layoutDescription); + + if (ctx->textContext.subTxtStart == NULL) { + nbgl_layoutAddText(ctx->layout, txt, NULL, ctx->textContext.style); + } + else { + if (ctx->textContext.nbPages == 1) { + nbgl_layoutAddText(ctx->layout, ctx->textContext.txtStart, txt, ctx->textContext.style); + } + else { + SPRINTF(ctx->textContext.tmpString, + "%s (%d/%d)", + ctx->textContext.txtStart, + ctx->textContext.currentPage + 1, + ctx->textContext.nbPages); + nbgl_layoutAddText( + ctx->layout, ctx->textContext.tmpString, txt, ctx->textContext.style); + } + } + if (navInfo.indication != NO_ARROWS) { + nbgl_layoutAddNavigation(&navInfo); + } + nbgl_layoutDraw(ctx->layout); + nbgl_refresh(); +#else + nbgl_layoutGet(&layoutDescription); + + if (ctx->textContext.subTxtStart == NULL) { + // TODO handle multiples lines + nbgl_layoutAddText(txt, 1, ctx->textContext.style == BOLD_TEXT1_INFO, true); + } + else { + if (ctx->textContext.nbPages == 1) { + nbgl_layoutAddText(ctx->textContext.txtStart, 1, ctx->textContext.style == BOLD_TEXT1_INFO, true); + nbgl_layoutAddText(txt, 1, ctx->textContext.style == BOLD_TEXT1_INFO, true); + } + else { + SPRINTF(ctx->textContext.tmpString, + "%s (%d/%d)", + ctx->textContext.txtStart, + ctx->textContext.currentPage + 1, + ctx->textContext.nbPages); + nbgl_layoutAddText(ctx->textContext.tmpString, 1, ctx->textContext.style == BOLD_TEXT1_INFO, true); + nbgl_layoutAddText(txt, 1, ctx->textContext.style == BOLD_TEXT1_INFO, true); + } + } + if (navInfo.indication != NO_ARROWS) { + nbgl_layoutAddNavigation(&navInfo); + } + nbgl_layoutDraw(); + nbgl_refresh(); +#endif +} + +// callback on key touch +#ifdef TARGET_NANOS +static void actionCallback(nbgl_buttonEvent_t event) +#else +static void actionCallback(nbgl_layout_t *layout, nbgl_buttonEvent_t event) +#endif +{ + StepContext_t *ctx; + +#ifdef TARGET_NANOS + ctx = &contexts[0]; +#else + ctx = getContextFromLayout(layout); +#endif + + if (!ctx) { + return; + } + if (event == BUTTON_LEFT_PRESSED) { + if (ctx->textContext.currentPage > 0) { + displayTextPage(ctx, ctx->textContext.currentPage - 1); + return; + } + else if ((ctx->textContext.pos == LAST_STEP) + || (ctx->textContext.pos == NEITHER_FIRST_NOR_LAST_STEP)) { + ctx->textContext.onActionCallback((nbgl_step_t) ctx, event); + } + } + else if (event == BUTTON_RIGHT_PRESSED) { + if (ctx->textContext.currentPage < (ctx->textContext.nbPages - 1)) { + displayTextPage(ctx, ctx->textContext.currentPage + 1); + return; + } + else if ((ctx->textContext.pos == FIRST_STEP) + || (ctx->textContext.pos == NEITHER_FIRST_NOR_LAST_STEP)) { + ctx->textContext.onActionCallback((nbgl_step_t) ctx, event); + } + } + else if (event == BUTTON_BOTH_PRESSED) { + ctx->textContext.onActionCallback((nbgl_step_t) ctx, event); + } +} + +static void displayMenuList(StepContext_t *ctx) +{ + nbgl_layoutDescription_t layoutDescription + = {.modal = ctx->modal, .onActionCallback = menuListActionCallback}; + nbgl_layoutMenuList_t *list = &ctx->menuListContext.list; + + layoutDescription.ticker.tickerCallback = ctx->ticker.tickerCallback; + layoutDescription.ticker.tickerIntervale = ctx->ticker.tickerIntervale; + layoutDescription.ticker.tickerValue = ctx->ticker.tickerValue; + +#ifdef TARGET_NANOS + nbgl_layoutGet(&layoutDescription); +#else + ctx->layout = nbgl_layoutGet(&layoutDescription); +#endif + +#if 0 + nbgl_layoutAddMenuList(ctx->layout, list); + if (list->nbChoices > 1) { + nbgl_layoutNavigation_t navInfo = {.direction = VERTICAL_NAV}; + navInfo.indication = 0; + if (list->selectedChoice > 0) { + navInfo.indication |= LEFT_ARROW; + } + if (list->selectedChoice < (list->nbChoices - 1)) { + navInfo.indication |= RIGHT_ARROW; + } + + if (navInfo.indication != NO_ARROWS) { + nbgl_layoutAddNavigation(ctx->layout, &navInfo); + } + } + nbgl_layoutDraw(ctx->layout); + nbgl_refresh(); +#endif +} + +// callback on key touch +#ifdef TARGET_NANOS +static void menuListActionCallback(nbgl_buttonEvent_t event) +#else +static void menuListActionCallback(nbgl_layout_t *layout, nbgl_buttonEvent_t event) +#endif +{ + StepContext_t *ctx; + +#ifdef TARGET_NANOS + ctx = &contexts[0]; +#else + ctx = getContextFromLayout(layout); +#endif + + if (!ctx) { + return; + } + + if (event == BUTTON_LEFT_PRESSED) { + if (ctx->menuListContext.list.selectedChoice > 0) { + ctx->menuListContext.list.selectedChoice--; + displayMenuList(ctx); + } + } + else if (event == BUTTON_RIGHT_PRESSED) { + if (ctx->menuListContext.list.selectedChoice < (ctx->menuListContext.list.nbChoices - 1)) { + ctx->menuListContext.list.selectedChoice++; + displayMenuList(ctx); + } + } + else if (event == BUTTON_BOTH_PRESSED) { + ctx->menuListContext.selectedCallback(ctx->menuListContext.list.selectedChoice); + } +} + +/********************** + * GLOBAL FUNCTIONS + **********************/ + + + +/** + * @brief draws a text type step, that can be multi-pages, depending of the length of text and + * subText. The navigation arrows are displayed depending of the given position of this step in a + * flow, and depending of the page in case of multi-pages + * + * @param pos position of this step in the flow (first, last, single, not_first_nor_last) + * @param onActionCallback common callback for all actions on this page + * @param ticker ticker configuration, set to NULL to disable it + * @param text text to display (depending of style) + * @param subText text to display under text (depending of style) + * @param style style to use for text and subText + * @param modal if true, means this step shall be displayed on top of existing one + * @return the step context (or NULL if error) + */ +nbgl_step_t nbgl_stepDrawText(nbgl_stepPosition_t pos, + nbgl_stepButtonCallback_t onActionCallback, + nbgl_screenTickerConfiguration_t *ticker, + const char *text, + const char *subText, + nbgl_centeredInfoStyle_t style, + bool modal) +{ + StepContext_t *ctx = getFreeContext(TEXT_STEP, modal); + if (!ctx) { + return NULL; + } + // initialize context (already set to 0 by getFreeContext()) + ctx->textContext.onActionCallback = onActionCallback; + if (ticker) { + ctx->ticker.tickerCallback = ticker->tickerCallback; + ctx->ticker.tickerIntervale = ticker->tickerIntervale; + ctx->ticker.tickerValue = ticker->tickerValue; + } + + // if no subText, get the number of pages for main text + if (subText == NULL) { + ctx->textContext.nbPages = nbgl_getTextNbPagesInWidth( + BAGL_FONT_OPEN_SANS_REGULAR_11px_1bpp, text, NB_MAX_LINES, AVAILABLE_WIDTH); + } + else { + // NB_MAX_LINES-1 because first line is for main text + ctx->textContext.nbPages = nbgl_getTextNbPagesInWidth( + BAGL_FONT_OPEN_SANS_REGULAR_11px_1bpp, subText, NB_MAX_LINES - 1, AVAILABLE_WIDTH); + } + LOG_DEBUG(STEP_LOGGER, + "nbgl_stepDrawText: ctx = %p, nbPages = %d, pos = 0x%X\n", + ctx, + ctx->textContext.nbPages, + pos); + if (pos & BACKWARD_DIRECTION) { + // start with last page + ctx->textContext.currentPage = ctx->textContext.nbPages - 1; + } + ctx->textContext.txtStart = text; + ctx->textContext.subTxtStart = subText; + // keep only direction part of position + ctx->textContext.pos = pos & (RIGHT_ARROW | LEFT_ARROW); + ctx->textContext.style = style; + displayTextPage(ctx, ctx->textContext.currentPage); + + return (nbgl_step_t) ctx; +} + +/** + * @brief draw a step with a centered info (icon + text). This is always a single page step + * + * @param pos position of this step in the flow (first, last, single, not_first_nor_last) + * @param onActionCallback common callback for all actions on this page + * @param ticker ticker configuration, set to NULL to disable it + * @param info all information about the centered info to be displayed + * @param modal if true, means this step shall be displayed on top of existing one + * @return the step context (or NULL if error) + */ +nbgl_step_t nbgl_stepDrawCenteredInfo(nbgl_stepPosition_t pos, + nbgl_stepButtonCallback_t onActionCallback, + nbgl_screenTickerConfiguration_t *ticker, + nbgl_layoutCenteredInfo_t *info, + bool modal) +{ + nbgl_layoutDescription_t layoutDescription + = {.modal = modal, .onActionCallback = (nbgl_layoutButtonCallback_t) actionCallback}; + nbgl_layoutNavigation_t navInfo = { + .direction = HORIZONTAL_NAV, + }; + StepContext_t *ctx = getFreeContext(CENTERED_INFO_STEP, modal); + if (!ctx) { + return NULL; + } + + // initialize context (already set to 0 by getFreeContext()) + ctx->textContext.onActionCallback = onActionCallback; + if (ticker) { + ctx->ticker.tickerCallback = ticker->tickerCallback; + ctx->ticker.tickerIntervale = ticker->tickerIntervale; + ctx->ticker.tickerValue = ticker->tickerValue; + } + + ctx->textContext.nbPages = 1; + // keep only direction part of position + ctx->textContext.pos = pos & (RIGHT_ARROW | LEFT_ARROW); + navInfo.indication = getNavigationInfo( + ctx->textContext.pos, ctx->textContext.nbPages, ctx->textContext.currentPage); + +#ifdef TARGET_NANOS + nbgl_layoutGet(&layoutDescription); + nbgl_layoutAddCenteredInfo(info); + if (navInfo.indication != NO_ARROWS) { + nbgl_layoutAddNavigation(&navInfo); + } + nbgl_layoutDraw(); + nbgl_refresh(); +#else + ctx->layout = nbgl_layoutGet(&layoutDescription); + nbgl_layoutAddCenteredInfo(ctx->layout, info); + if (navInfo.indication != NO_ARROWS) { + nbgl_layoutAddNavigation(ctx->layout, &navInfo); + } + nbgl_layoutDraw(ctx->layout); + nbgl_refresh(); +#endif + + LOG_DEBUG(STEP_LOGGER, "nbgl_stepDrawCenteredInfo(): step = %p\n", ctx); + return (nbgl_step_t) ctx; +} + +/** + * @brief draw a step page with a menu list and navigation arrows to parse it. This step must be + * alone + * + * @param onActionCallback common callback for all actions on this page + * @param ticker ticker configuration, set to NULL to disable it + * @param list configuration of the menu list + * @param modal if true, means this step shall be displayed on top of existing one + * @return the step context (or NULL if error) + */ +nbgl_step_t nbgl_stepDrawMenuList(nbgl_stepMenuListCallback_t onActionCallback, + nbgl_screenTickerConfiguration_t *ticker, + nbgl_layoutMenuList_t *list, + bool modal) +{ + StepContext_t *ctx = getFreeContext(MENU_LIST_STEP, modal); + if (!ctx) { + return NULL; + } + + // initialize context (already set to 0 by getFreeContext()) + if (ticker) { + ctx->ticker.tickerCallback = ticker->tickerCallback; + ctx->ticker.tickerIntervale = ticker->tickerIntervale; + ctx->ticker.tickerValue = ticker->tickerValue; + } + + ctx->menuListContext.list.nbChoices = list->nbChoices; + ctx->menuListContext.list.selectedChoice = list->selectedChoice; + ctx->menuListContext.list.callback = list->callback; + ctx->menuListContext.selectedCallback = onActionCallback; + + displayMenuList(ctx); + + LOG_DEBUG(STEP_LOGGER, "nbgl_stepDrawMenuList(): step = %p\n", ctx); + + return (nbgl_step_t) ctx; +} + +/** + * @brief Get the index of the currently selected item in the menulist + * + * @param step step from which to get the current menulist choice + * @return current menulist choice + */ +uint8_t nbgl_stepGetMenuListCurrent(nbgl_step_t step) +{ + StepContext_t *ctx = (StepContext_t *) step; + if (!ctx) { + return 0; + } + return (ctx->menuListContext.list.selectedChoice); +} + +/** + * @brief Release the step obtained with any of the nbgl_stepDrawXXX() functions + * + * @param step step to release + * @return >= 0 if OK + */ +int nbgl_stepRelease(nbgl_step_t step) +{ + StepContext_t *ctx = (StepContext_t *) step; + int ret; + + LOG_DEBUG(STEP_LOGGER, "nbgl_stepRelease(): ctx = %p\n", ctx); + if (!ctx) { + return -1; + } + +#ifdef TARGET_NANOS + nbgl_layoutRelease(); + ret = 0; +#else + ret = nbgl_layoutRelease((nbgl_layout_t *) ctx->layout); + ctx->layout = NULL; +#endif + + return ret; +} + +#endif // NBGL_STEP diff --git a/sdk_lib_nbgl/src/nbgl_step_lns.c b/sdk_lib_nbgl/src/nbgl_step_lns.c new file mode 100644 index 00000000..ce14398a --- /dev/null +++ b/sdk_lib_nbgl/src/nbgl_step_lns.c @@ -0,0 +1,288 @@ +/** + * @file nbgl_step.c + * @brief Implementation of predefined pages management for Applications + */ + +#ifdef NBGL_STEP +/********************* + * INCLUDES + *********************/ +#include +#include + +#include "glyphs.h" +#include "os_pic.h" +#include "lib_ux/include/ux.h" +#include "nbgl_step.h" +#include "nbgl_layout.h" + + +/********************* + * DEFINES + *********************/ +// string to store the title for a multi-pages text+subtext +#define TMP_STRING_MAX_LEN 24 + +///< Maximum number of layers for steps, cannot be greater than max number of layout layers +#ifdef TARGET_NANOS +#undef NB_MAX_LINES +#define NB_MAX_LINES 2 +#define NB_MAX_LAYERS 1 +#else +#define NB_MAX_LAYERS 3 +#endif + +static nbgl_layoutButtonCallback_t ctx_callback; +static nbgl_screenTickerConfiguration_t ctx_ticker; +static bool active; +static bool displayed; + + +void nbgl_process_ux_displayed_event(void) +{ + displayed = true; + PRINTF("Displayed\n"); +} + +static void wait_displayed(void) +{ + displayed = false; + while (!displayed) { + // - Looping on os_io_seph_recv_and_process(0); + // - This will send a general_status and then wait for an event. + // - Upon event reception this will call io_seproxyhal_handle_event() + // - On some case this will call io_event() which usually forward the + // event to the UX lib. + io_seproxyhal_spi_recv(G_io_seproxyhal_spi_buffer, sizeof(G_io_seproxyhal_spi_buffer), 0); \ + io_seproxyhal_handle_event(); + } +} + +static void display_clear(void) { + bagl_element_t element = { + .component = { + .type = BAGL_RECTANGLE, + .userid = 0, + .x = 0, + .y = 0, + .width = 128, + .height = 32, + .stroke = 0, + .radius = 0, + .fill = BAGL_FILL, + .fgcolor = 0x000000, + .bgcolor = 0xFFFFFF, + .icon_id = 0, + .font_id = 0, + }, + .text = NULL, + }; + + io_seproxyhal_display_default(&element); + wait_displayed(); + PRINTF("screen reset\n"); +} + +static void display_text_line(const char *text, bool bold, bool centered, uint8_t x, uint8_t y) { + bagl_element_t element = { + .component = { + .type = BAGL_LABELINE, + .userid = 0, + .x = x, + .y = y, + .height = 32, + .stroke = 0, + .radius = 0, + .fill = 0, + .fgcolor = 0xFFFFFF, + .bgcolor = 0x000000, + .icon_id = 0, + }, + .text = text + }; + + element.component.x = x; + element.component.y = y; + element.component.width = 128 - 2 * x; + + if (bold) { + element.component.font_id = BAGL_FONT_OPEN_SANS_EXTRABOLD_11px; + } else { + element.component.font_id = BAGL_FONT_OPEN_SANS_REGULAR_11px; + } + + if (centered) { + element.component.font_id |= BAGL_FONT_ALIGNMENT_CENTER; + } + io_seproxyhal_display_default(&element); + wait_displayed(); + PRINTF("text displayed\n"); + +} + +static void display_icon(const bagl_icon_details_t *icon_det, uint8_t x, uint8_t y, uint8_t width, uint8_t height) { + bagl_component_t component = { + .type = BAGL_ICON, + .userid = 0, + .stroke = 0, + .radius = 0, + .fill = 0, + .fgcolor = 0xFFFFFF, + .bgcolor = 0x000000, + .font_id = 0, + .icon_id = 0, + }; + + component.x = x; + component.y = y; + component.width = width; + component.height = height; + + io_seproxyhal_display_icon(&component, icon_det); + wait_displayed(); + PRINTF("icon displayed\n"); +} + +// utility function to compute navigation arrows +static void display_nav_info(nbgl_stepPosition_t pos, + uint8_t nbPages, + uint8_t currentPage, + bool horizontal_nav) +{ + bool right = false; + bool left = false; + + if (nbPages > 1) { + if (currentPage > 0) { + left = true; + } + if (currentPage < (nbPages - 1)) { + right = true; + } + } + if (pos == FIRST_STEP) { + right = true; + } + else if (pos == LAST_STEP) { + left = true; + } + else if (pos == NEITHER_FIRST_NOR_LAST_STEP) { + right = true; + left = true; + } + + if (horizontal_nav) { + if (left) { + display_icon(&C_icon_left, 2, 12, 4, 7); + } + if (right) { + display_icon(&C_icon_right, 122, 12, 4, 7); + } + } else { + if (left) { + display_icon(&C_icon_up, 2, 12, 7, 4); + } + if (right) { + display_icon(&C_icon_down, 122, 12, 7, 4); + } + } +} + +void layout_buttonCallback(nbgl_buttonEvent_t buttonEvent) +{ + if (!active) { + return; + } + + if (ctx_callback != NULL) { + ctx_callback(buttonEvent); + } +} + +void nbgl_screenHandler(uint32_t intervaleMs) +{ +#if 0 + // ensure a screen exists + if (nbScreensOnStack == 0) { + return; + } + // call ticker callback of top of stack if active and not expired yet (for a non periodic) + if ((topOfStack->ticker.tickerCallback != NULL) && (topOfStack->ticker.tickerValue != 0)) { + topOfStack->ticker.tickerValue -= MIN(topOfStack->ticker.tickerValue, intervaleMs); + if (topOfStack->ticker.tickerValue == 0) { + // rearm if intervale is not null, and call the registered function + topOfStack->ticker.tickerValue = topOfStack->ticker.tickerIntervale; + topOfStack->ticker.tickerCallback(); + } + } +#endif +} + +void nbgl_screenDraw(nbgl_stepPosition_t pos, + nbgl_stepButtonCallback_t onActionCallback, + nbgl_screenTickerConfiguration_t *ticker, + const char *text, + const char *subText, + const nbgl_icon_details_t *icon, + bool centered, + bool text_bold, + bool horizontal_nav) +{ + PRINTF("nbgl_screenDraw %d %d %d\n", centered, icon != NULL, subText != NULL); + if (icon != NULL) + PRINTF("icon <%p>\n", PIC(icon)); + if (text != NULL) + PRINTF("text <%s>\n", PIC(text)); + if (subText != NULL) + PRINTF("subText <%s>\n", PIC(subText)); + active = false; + + ctx_callback = (nbgl_layoutButtonCallback_t) PIC(onActionCallback); + if (ticker != NULL) { + memcpy(&ctx_ticker, ticker, sizeof(ctx_ticker)); + } else { + memset(&ctx_ticker, 0, sizeof(ctx_ticker)); + } + + display_clear(); + + uint8_t text_x = 6; + if (icon != NULL) { + if (centered) { + // Consider text is single line and subText is Null + display_icon(icon, 56, 2, 16, 16); + } else { + text_x = 41; + display_icon(icon, 16, 8, 16, 16); + } + } + + if ((icon != NULL) && (centered)) { + // Consider text is single line and subText is Null + display_text_line(text, text_bold, centered, text_x, 28); + } else { + if (subText != NULL) { + // Consider text exist and is single line + display_text_line(text, text_bold, centered, text_x, 12); + + // TODO handle paging + // For now simply display subText, paging is not handled + display_text_line(subText, false, centered, text_x, 26); + } else { + // TODO handle `\n` and paging? or align in middle if really single line? + // Consider text exist and is single line + display_text_line(text, text_bold, centered, text_x, 12); + } + } + + display_nav_info(pos, 1, 1, horizontal_nav); +} + +void nbgl_refresh(void) { + active = true; + if (!io_seproxyhal_spi_is_status_sent()) { + io_seproxyhal_general_status(); + } +} + +#endif // NBGL_STEP diff --git a/sdk_lib_nbgl/src/nbgl_use_case_nanos.c b/sdk_lib_nbgl/src/nbgl_use_case_nanos.c new file mode 100644 index 00000000..7ed2b70f --- /dev/null +++ b/sdk_lib_nbgl/src/nbgl_use_case_nanos.c @@ -0,0 +1,533 @@ +/** + * @file nbgl_use_case_nanos.c + * @brief Implementation of typical pages (or sets of pages) for Applications, for Nanos (X, SP) + */ + +#ifdef NBGL_USE_CASE +#ifndef HAVE_SE_TOUCH +/********************* + * INCLUDES + *********************/ +#include +#include +#include "nbgl_debug.h" +#include "nbgl_use_case.h" +#include "glyphs.h" +#include "os_pic.h" +#include "os_helpers.h" +#include "ux.h" + +/********************* + * DEFINES + *********************/ + +/********************** + * TYPEDEFS + **********************/ + +typedef struct ReviewContext_s { + nbgl_navCallback_t onNav; + nbgl_choiceCallback_t onChoice; + nbgl_layoutTagValueList_t tagValueList; + const nbgl_icon_details_t *icon; + const char *reviewTitle; + const char *address; // for address confirmation review + const char *acceptText; + const char *rejectText; + bool forwardNavOnly; +} ReviewContext_t; + +typedef struct HomeContext_s { + const char *appName; + const nbgl_icon_details_t *appIcon; + const char *appVersion; + const char *tagline; + nbgl_callback_t aboutCallback; + nbgl_callback_t quitCallback; +} HomeContext_t; + +typedef struct SettingsContext_s { + nbgl_navCallback_t onNav; + nbgl_callback_t quitCallback; + nbgl_actionCallback_t actionCallback; +} SettingsContext_t; + +typedef enum { + REVIEW_USE_CASE, + HOME_USE_CASE, + SETTINGS_USE_CASE +} ContextType_t; + +typedef struct UseCaseContext_s { + ContextType_t type; + uint8_t nbPages; + int8_t currentPage; + nbgl_step_t stepCtx; + nbgl_stepDesc_t step; + union { + ReviewContext_t review; + HomeContext_t home; + SettingsContext_t settings; + }; +} UseCaseContext_t; + +/********************** + * STATIC VARIABLES + **********************/ +// char buffers to build some strings +static char appDescription[APP_DESCRIPTION_MAX_LEN]; + +static UseCaseContext_t context; + +/********************** + * STATIC FUNCTIONS + **********************/ +#ifndef TARGET_NANOS +static void buttonCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event); +#else +static void buttonCallback(nbgl_buttonEvent_t event); +#endif +static void displayReviewPage(nbgl_stepPosition_t pos); +static void displayHomePage(nbgl_stepPosition_t pos); + +static void onAccept(void) +{ + if (context.review.onChoice) { + context.review.onChoice(true); + } +} + +static void onReject(void) +{ + if (context.review.onChoice) { + context.review.onChoice(false); + } +} + +static void onSettingsAction(void) +{ + if (context.settings.actionCallback) { + context.settings.actionCallback(context.currentPage); + } +} + +static void drawStep(nbgl_stepPosition_t pos, + const nbgl_icon_details_t *icon, + const char *txt, + const char *subTxt) +{ + if (context.nbPages > 1) { + pos |= NEITHER_FIRST_NOR_LAST_STEP; + } + else { + pos |= GET_POS_OF_STEP(context.currentPage, context.nbPages); + } + +#ifndef TARGET_NANOS + if (icon == NULL) { + context.stepCtx + = nbgl_stepDrawText(pos, buttonCallback, NULL, txt, subTxt, BOLD_TEXT1_INFO, false); + } + else { + nbgl_layoutCenteredInfo_t info; + info.icon = icon; + info.text1 = txt; + info.text2 = subTxt; + info.onTop = false; + info.style = BOLD_TEXT1_INFO; + context.stepCtx = nbgl_stepDrawCenteredInfo(pos, buttonCallback, NULL, &info, false); + } +#else + nbgl_screenDraw(pos, buttonCallback, NULL, txt, subTxt, icon, true, true, true); +#endif +} + +#ifndef TARGET_NANOS +static void buttonCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +#else +static void buttonCallback(nbgl_buttonEvent_t event) +#endif +{ + nbgl_stepPosition_t pos; + +#ifndef TARGET_NANOS + UNUSED(stepCtx); +#endif + // create text_area for main text + if (event == BUTTON_LEFT_PRESSED) { + if (context.currentPage > 0) { + context.currentPage--; + } + else { + context.currentPage = (context.nbPages - 1); + } + pos = BACKWARD_DIRECTION; + } + else if (event == BUTTON_RIGHT_PRESSED) { + if (context.currentPage < (int) (context.nbPages - 1)) { + context.currentPage++; + } + else { + context.currentPage = 0; + } + pos = FORWARD_DIRECTION; + } + else { + if ((event == BUTTON_BOTH_PRESSED) && (context.step.callback != NULL)) { + context.step.callback(); + } + return; + } + if ((context.type == REVIEW_USE_CASE) || (context.type == SETTINGS_USE_CASE)) { + displayReviewPage(pos); + } + else { + displayHomePage(pos); + } +} + +// function used to display the current page in review +static void displayReviewPage(nbgl_stepPosition_t pos) +{ + memset(&context.step, 0, sizeof(context.step)); + + if (context.type == REVIEW_USE_CASE) { + if (context.review.onNav != NULL) { + // try to get content for this page/step + if (context.review.onNav(context.currentPage, &context.step) == false) { + return; + } + } + else { + if (context.currentPage == 0) { // title page + context.step.icon = context.review.icon; + context.step.text = context.review.reviewTitle; + } + else if (context.currentPage == (context.nbPages - 2)) { // accept page + context.step.icon = &C_icon_validate_14; + context.step.text = context.review.acceptText; + context.step.callback = onAccept; + } + else if (context.currentPage == (context.nbPages - 1)) { // reject page + context.step.icon = &C_icon_crossmark; + context.step.text = context.review.rejectText; + context.step.callback = onReject; + } + else if ((context.review.address != NULL) + && (context.currentPage == 1)) { // address confirmation and 2nd page + context.step.text = "Address"; + context.step.subText = context.review.address; + } + else { + uint8_t pairIndex = (context.review.address != NULL) ? (context.currentPage - 2) + : (context.currentPage - 1); + context.step.text = context.review.tagValueList.pairs[pairIndex].item; + context.step.subText = context.review.tagValueList.pairs[pairIndex].value; + } + } + } + else if (context.type == SETTINGS_USE_CASE) { + if (context.currentPage < (context.nbPages - 1)) { + // try to get content for this page/step + if ((context.settings.onNav == NULL) + || (context.settings.onNav(context.currentPage, &context.step) == false)) { + return; + } + context.step.callback = onSettingsAction; + } + else { // last page is for quit + context.step.icon = &C_icon_back_x; + context.step.text = "Back"; + context.step.callback = context.settings.quitCallback; + } + } + + const char *txt = NULL; + if (context.step.text != NULL) { + txt = context.step.text; + } + if (context.step.init != NULL) { + context.step.init(); + } + drawStep(pos, context.step.icon, txt, context.step.subText); + nbgl_refresh(); +} + +// function used to display the current page in home +static void displayHomePage(nbgl_stepPosition_t pos) +{ + memset(&context.step, 0, sizeof(context.step)); + + switch (context.currentPage) { + case 0: + context.step.icon = context.home.appIcon; + context.step.text = context.home.tagline; + break; + case 1: + context.step.text = "Version"; + context.step.subText = context.home.appVersion; + break; + case 2: + context.step.icon = &C_icon_certificate; + context.step.text = "About"; + context.step.callback = context.home.aboutCallback; + break; + case 3: + context.step.icon = &C_icon_dashboard_x; + context.step.text = "Quit"; + context.step.callback = context.home.quitCallback; + break; + default: + break; + } + + const char *txt = NULL; + if (context.step.text != NULL) { + txt = context.step.text; + } + if (context.step.init != NULL) { + context.step.init(); + } + drawStep(pos, context.step.icon, txt, context.step.subText); + nbgl_refresh(); +} + +/********************** + * GLOBAL FUNCTIONS + **********************/ + +/** + * @brief draws the home page of an app (page on which we land when launching it from dashboard) + * + * @param appName app name + * @param appIcon app icon + * @param appVersion app version + * @param tagline text under app name (if NULL, it will be "\nisready") + * @param aboutCallback callback called when the "about" step is selected (double key) + * @param quitCallback callback called when the "quit" step is selected (double key) + */ +void nbgl_useCaseHome(const char *appName, + const nbgl_icon_details_t *appIcon, + const char *appVersion, + const char *tagline, + nbgl_callback_t aboutCallback, + nbgl_callback_t quitCallback) +{ + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = HOME_USE_CASE; + context.home.aboutCallback = aboutCallback; + context.home.quitCallback = quitCallback; + + if (tagline == NULL) { + snprintf(appDescription, APP_DESCRIPTION_MAX_LEN, "%s", appName); + context.home.tagline = appDescription; + } + else { + context.home.tagline = tagline; + } + + context.home.appName = appName; + context.home.appIcon = appIcon; + context.home.appVersion = appVersion; + + context.nbPages = 4; + context.currentPage = 0; + + displayHomePage(FORWARD_DIRECTION); +} + +/** + * @brief Draws the settings pages of an app with as many pages as given + * For each page, the given navCallback will be called to get the content. Only 'type' and + * union has to be set in this content + * + * @param initPage page on which to start [0->(nbPages-1)] + * @param nbPages number of pages + * @param quitCallback callback called when "quit" step is selected (double button) + * @param navCallback callback called when pages are navigated with buttons + * @param actionCallback callback called when one of the navigations page is selected (double + * button) + */ +void nbgl_useCaseSettings(uint8_t initPage, + uint8_t nbPages, + nbgl_callback_t quitCallback, + nbgl_navCallback_t navCallback, + nbgl_actionCallback_t actionCallback) +{ + memset(&context, 0, sizeof(UseCaseContext_t)); + // memorize context + context.type = SETTINGS_USE_CASE; + context.settings.onNav = navCallback; + context.settings.quitCallback = quitCallback; + context.settings.actionCallback = actionCallback; + + context.nbPages = nbPages + 1; + context.currentPage = initPage; + displayReviewPage(FORWARD_DIRECTION); +} + +/** + * @brief Draws a flow of pages of a review. Navigation is available for all pages + * For each page, the given navCallback will be called to get the content. + * When navigating before the first page of after the last page, the page number will be -1 + * + * @param initPage page on which to start [0->(nbPages-1)] + * @param nbPages number of pages. + * @param navCallback callback called when navigation is touched + */ +void nbgl_useCaseRegularReview(uint8_t initPage, uint8_t nbPages, nbgl_navCallback_t navCallback) +{ + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = REVIEW_USE_CASE; + // memorize context + context.review.onNav = navCallback; + context.review.forwardNavOnly = false; + + context.currentPage = initPage; + context.nbPages = nbPages; + + displayReviewPage(FORWARD_DIRECTION); +} + +/** + * @brief Draws a flow of pages of a review, without back key. + * It is possible to go to next page thanks to "tap to continue". + * For each page, the given navCallback will be called to get the content. Only 'type' and + * union has to be set in this content + * + * @param navCallback callback called when navigation "tap to continue" is touched, to get the + * content of next page + */ +void nbgl_useCaseForwardOnlyReview(nbgl_navCallback_t navCallback) +{ + // memorize context + context.type = REVIEW_USE_CASE; + context.review.onNav = navCallback; + context.review.forwardNavOnly = true; +} + +/** + * @brief Draws a flow of pages of a review. + * @note All tag/value pairs are provided in the API and the number of pages is automatically + * computed, the last page being a long press one + * + * @param tagValueList list of tag/value pairs + * @param icon icon to use in first page + * @param reviewTitle text to use in title page of the transaction + * @param acceptText text to use in validation page + * @param rejectText text to use in rejection page + * @param callback callback called when transaction is accepted (param is true) or rejected (param + * is false) + */ +void nbgl_useCaseStaticReview(nbgl_layoutTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *acceptText, + const char *rejectText, + nbgl_choiceCallback_t callback) +{ + // memorize context + memset(&context, 0, sizeof(UseCaseContext_t)); + context.review.forwardNavOnly = false; + context.type = REVIEW_USE_CASE; + + memcpy(&context.review.tagValueList, tagValueList, sizeof(nbgl_layoutTagValueList_t)); + + context.review.reviewTitle = reviewTitle; + context.review.icon = icon; + context.review.acceptText = acceptText; + context.review.rejectText = rejectText; + context.review.onChoice = callback; + + context.currentPage = 0; + // + 3 because 1 page for title and 2 pages at the end for accept/reject + context.nbPages = tagValueList->nbPairs + 3; + + displayReviewPage(FORWARD_DIRECTION); +} + +/** + * @brief Draws an address confirmation use case. This page contains the given address in a + * tag/value layout + * + * @param icon icon to be used on first page of address review + * @param title text to be used on first page of address review (NULL terminated string) + * @param address address to confirm (NULL terminated string) + * @param callback callback called when either confirm or reject page is double pressed + */ +void nbgl_useCaseAddressConfirmation(const nbgl_icon_details_t *icon, + const char *title, + const char *address, + nbgl_choiceCallback_t callback) +{ + nbgl_useCaseAddressConfirmationExt(icon, title, address, callback, NULL); +} + +/** + * @brief draws an extended address verification page. This page contains the given address in a + * tag/value layout. + * + * @param icon icon to be used on first page of address review + * @param title text to be used on first page of address review (NULL terminated string) + * @param address address to confirm (NULL terminated string) + * @param callback callback called when either confirm or reject page is double pressed + * @param tagValueList list of tag/value pairs (must be persistent because no copy) + */ +void nbgl_useCaseAddressConfirmationExt(const nbgl_icon_details_t *icon, + const char *title, + const char *address, + nbgl_choiceCallback_t callback, + const nbgl_layoutTagValueList_t *tagValueList) +{ + // memorize context + memset(&context, 0, sizeof(UseCaseContext_t)); + context.review.forwardNavOnly = false; + context.type = REVIEW_USE_CASE; + + if (tagValueList) { + memcpy(&context.review.tagValueList, tagValueList, sizeof(nbgl_layoutTagValueList_t)); + } + + context.review.address = address; + context.review.reviewTitle = title; + context.review.icon = icon; + context.review.acceptText = "Approve"; + context.review.rejectText = "Reject"; + context.review.onChoice = callback; + + context.currentPage = 0; + // + 4 because 1 page for title, 1 for address and 2 pages at the end for approve/reject + context.nbPages = 4; + if (tagValueList) { + context.nbPages += tagValueList->nbPairs; + } + + displayReviewPage(FORWARD_DIRECTION); +} + +/** + * @brief draw a spinner page with the given parameters. The spinner will "turn" automatically every + * 800 ms + * + * @param text text to use under spinner + */ +void nbgl_useCaseSpinner(const char *text) +{ + // pageContext = nbgl_pageDrawSpinner(NULL, (const char*)text); + UNUSED(text); + nbgl_refresh(); + // TODO +} + +void nbgl_useCaseStatus(const char *message, bool isSuccess, nbgl_callback_t quitCallback) +{ + UNUSED(message); + UNUSED(isSuccess); + UNUSED(quitCallback); + quitCallback(); + // TODO +} + + +#endif // HAVE_SE_TOUCH +#endif // NBGL_USE_CASE diff --git a/sdk_lib_nbgl/tools/bmp2display.py b/sdk_lib_nbgl/tools/bmp2display.py new file mode 100644 index 00000000..9e188553 --- /dev/null +++ b/sdk_lib_nbgl/tools/bmp2display.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +# coding: utf-8 +""" +Converts input bmp file in 24 bits format to Stax bitmap, as buffer in stdout +""" +# ----------------------------------------------------------------------------- +import argparse +import gzip + +def get4BPPval(buffer,i): + pixel_val = buffer[i]+(buffer[i+1]<<8)+(buffer[i+2]<<16) + + if (pixel_val > 0xF0F0F0): + return 0xF + elif (pixel_val > 0xE0E0E0): + return 0xE + elif (pixel_val > 0xD0D0D0): + return 0xD + elif (pixel_val > 0xC0C0C0): + return 0xC + elif (pixel_val > 0xB0B0B0): + return 0xB + elif (pixel_val > 0xA0A0A0): + return 0xA + elif (pixel_val > 0x909090): + return 0x9 + elif (pixel_val > 0x808080): + return 0x8 + elif (pixel_val > 0x707070): + return 0x7 + elif (pixel_val > 0x606060): + return 0x6 + elif (pixel_val > 0x505050): + return 0x5 + elif (pixel_val > 0x404040): + return 0x4 + elif (pixel_val > 0x303030): + return 0x3 + elif (pixel_val > 0x202020): + return 0x2 + elif (pixel_val > 0x101010): + return 0x1 + else: + return 0 + +def get2BPPval(color_index): + pixel_val = color_index[0]<<16+color_index[1]<<8+color_index[0] + if (pixel_val == 0xFFFFFF): + return 3 + elif (pixel_val > 0x808080): + return 2 + elif (pixel_val > 0x404080): + return 1 + else: + return 0 + +def get1BPPval(color_index): + pixel_val = color_index[0]<<16+color_index[1]<<8+color_index[0] + if (pixel_val > 0xF0F0F0): + return 0 + else: + return 1 + +def parse_bmp_file(bmp_file_name: str, bpp: int): + pixels_buffer = [] + with open(bmp_file_name, 'rb') as f: + buffer = f.read() + offset = buffer[0xA] + (buffer[0xB]<<8) + (buffer[0xC]<<16) + (buffer[0xD]<<24) + width = buffer[0x12] + (buffer[0x13]<<8) + height = buffer[0x16] + (buffer[0x17]<<8) + bmp_bpp = buffer[0x1C] + (buffer[0x1D]<<8) + row_size = int(((int(bmp_bpp)*int(width)+31)/32))<<2 + + #BMP header is bytes long in 24BPP mode + #lines are given from bottom to top, from left to right + nb_bytes = 0 + cur_byte = 0 + pixel_idx = 0 + line = ' ' + for x in range(width): + for y in range (height-1,-1,-1): + i = int(offset+(y*row_size+x*bmp_bpp/8)) + if (bpp == 1): + bpp_format = 0 + if (buffer[i] != 0xFF): + cur_byte |= (1<<(7-pixel_idx)) + elif (bpp == 2): + bpp_format = 1 + pixel_val = buffer[i]+(buffer[i+1]<<8)+(buffer[i+2]<<16) + if (pixel_val == 0xFFFFFF): + cur_byte |= (0x3<<(6-pixel_idx)) + + elif (pixel_val > 0): + cur_byte |= (0x2<<(6-pixel_idx)) + elif (bpp == 4): + bpp_format = 2 + cur_byte |= get4BPPval(buffer, i)<<(4-pixel_idx) + + pixel_idx+=bpp + # the byte is fully filled, let's save its content + if (pixel_idx%8 == 0): + pixels_buffer.append(cur_byte) + nb_bytes+=1 + cur_byte = 0 + pixel_idx = 0 + + return (bytes(pixels_buffer), width, height) + +def apply_compression(pixels: bytearray) -> bytes: + output_buffer = [] + # cut into chunks of 2048 bytes max of uncompressed data (because decompression needs the full buffer) + full_uncompressed_size = len(pixels) + i = 0 + while full_uncompressed_size>0: + chunk_size = min(2048,full_uncompressed_size) + tmp = bytes(pixels_buffer[i:i+chunk_size]) + #print("len = %d"%len(tmp)) + #print("0x%X"%tmp[chunk_size-1]) + compressed_buffer = gzip.compress(tmp) + output_buffer += [len(compressed_buffer)&0xFF, (len(compressed_buffer)>>8)&0xFF] + output_buffer += compressed_buffer + full_uncompressed_size -= chunk_size + i+=chunk_size + + return bytearray(output_buffer) + +def format_image(img: bytearray, width: int, height: int, compression: bool) -> bytes: + BPP_FORMATS = { + 1: 0, + 2: 1, + 4: 2 + } + + result = [width&0xFF, width>>8, height&0xFF, height>>8, + (BPP_FORMATS[args.bpp]<<4) | compression, len(img)&0xFF, (len(img)>>8)&0xFF, + (len(img)>>16)&0xFF] + result.extend(output_buffer) + return bytearray(result) + + +# ----------------------------------------------------------------------------- +# Program entry point: +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + # ------------------------------------------------------------------------- + # Parse arguments: + parser = argparse.ArgumentParser( + description="Converts input bmp file in 24 bits format to Stax bitmap, as buffer in stdout") + + parser.add_argument( + "-i", "--input", + dest="input", type=str, + required=True, + help="bmp file") + + parser.add_argument( + "-b", "--bpp", + dest="bpp", type=int, + required=True, + help="number of bit per pixel (1, 2 or 4)") + + parser.add_argument( + '--compress', + action='store_true', + default=False, + help="compress data") + + parser.add_argument( + '--file', + action='store_true', + default=False, + help="store in Ledger image format") + + parser.add_argument( + '--outfile', + dest='outfile', type=str, + required=False, + help='Optional outfile name' + ) + + parser.add_argument( + '--check-size', + dest='check_size', type=str, + required=False, + help='Check size of the input bmp (example: 480x512)' + ) + + args = parser.parse_args() + + pixels_buffer, width, height = parse_bmp_file(args.input, args.bpp) + + # Check size + if args.check_size: + xref, yref = args.check_size.split('x') + assert ((int(xref), int(yref)) == (width, height)), \ + f" Input file does not match required size {args.check_size}" + + # Apply compression + if args.compress: + output_buffer = apply_compression(bytes(pixels_buffer)) + else: + output_buffer = pixels_buffer + + # Apply file formatting + if args.file: + result = format_image(output_buffer, width, height, args.compress) + else: + result = output_buffer + + if args.outfile is None: + # Print result + print(result.hex()) + else: + # Write to output file + with open(args.outfile, 'wb') as out_file: + print(f'Write {out_file.name}') + out_file.write(bytes(result)) diff --git a/sdk_lib_nbgl/tools/generate_ledgerimg.sh b/sdk_lib_nbgl/tools/generate_ledgerimg.sh new file mode 100755 index 00000000..644ee398 --- /dev/null +++ b/sdk_lib_nbgl/tools/generate_ledgerimg.sh @@ -0,0 +1,30 @@ +# Generate a ledgerimg from any input image file +# +# Example: +# ``` +# sh tools/generate_ledgerimg.sh ~/Downloads/cryptopunk.png +# Write /home/abonnaudet/Downloads/cryptopunk.bmp +# Write /home/abonnaudet/Downloads/cryptopunk.ledgerimg +# ``` + +## Configs + +# Front screen dimension +FRONT_SCREEN_WIDTH=400 +FRONT_SCREEN_HEIGHT=672 + +# Path to bmp2display python script +BMP2DISPLAY_PY=$BOLOS_NG/public_sdk/lib_nbgl/tools/bmp2display.py + +# Input file is the first argument +FILE_INPUT=$1 + +## Generate a 24 bits BMP file +FRONT_SCREEN_DIM=$FRONT_SCREEN_WIDTH"x"$FRONT_SCREEN_HEIGHT +FILE_OUTPUT_BMP=${FILE_INPUT%.*}.bmp +convert $FILE_INPUT -resize $FRONT_SCREEN_DIM -background white -compose Copy -gravity center -type truecolor -extent $FRONT_SCREEN_DIM $FILE_OUTPUT_BMP +echo "Write" $FILE_OUTPUT_BMP + +## Convert BMP file into ledgerimg +FILE_OUTPUT_LEDGERIMG=${FILE_INPUT%.*}.ledgerimg +python3 $BMP2DISPLAY_PY --file --compress --input $FILE_OUTPUT_BMP --bpp 4 --outfile $FILE_OUTPUT_LEDGERIMG diff --git a/sdk_lib_nbgl/tools/icon2glyph.py b/sdk_lib_nbgl/tools/icon2glyph.py new file mode 100755 index 00000000..98175952 --- /dev/null +++ b/sdk_lib_nbgl/tools/icon2glyph.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 + +""" +Converts the given icons, in bmp or gif format, to Stax compatible glyphs, as C arrays +""" + +import argparse +import math +import os +import binascii +import sys +import traceback +import gzip + +from enum import IntEnum +from PIL import Image as img, ImageOps +from PIL.Image import Image +from typing import Tuple, Optional + +from nbgl_rle import Rle1bpp, Rle4bpp + + +class NbglFileCompression(IntEnum): + NoCompression = 0 + Gzlib = 1, + Rle = 2 + + +def is_power2(n): + return n != 0 and ((n & (n - 1)) == 0) + + +def open_image(file_path) -> Optional[Tuple[Image, int]]: + """ + Open and prepare image for processing. + Returns None if image has too many colors or does not exist. + else returns tuple: Image, bpp + """ + # Check existente + if not os.path.exists(file_path): + sys.stderr.write("Error: {} does not exist!".format(file_path) + "\n") + + # Load Image in mode L + im = img.open(file_path) + im.load() + im = im.convert('L') + + # Do not open image with more than 16 colors + num_colors = len(im.getcolors()) + if num_colors > 16: + sys.stderr.write( + "Error: input file {} has too many colors".format(file_path) + "\n") + return None + + # Compute bits_per_pixel + # Round number of colors to a power of 2 + if not is_power2(num_colors): + num_colors = int(pow(2, math.ceil(math.log(num_colors, 2)))) + + bits_per_pixel = int(math.log(num_colors, 2)) + if bits_per_pixel == 3: + bits_per_pixel = 4 + + if bits_per_pixel == 0: + bits_per_pixel = 1 + + # Invert if bpp is 1 + if bits_per_pixel == 1: + im = ImageOps.invert(im) + + return im, bits_per_pixel + + +def image_to_packed_buffer(img, bpp: int, reverse_1bpp): + """ + Rotate and pack bitmap data of the character. + """ + width, height = img.size + + current_byte = 0 + current_bit = 0 + image_data = [] + nb_colors = pow(2, bpp) + base_threshold = int(256 / nb_colors) + half_threshold = int(base_threshold / 2) + + # col first + for col in reversed(range(width)): + for row in range(height): + # Return an index in the indexed colors list + # top to bottom + # Perform implicit rotation here (0,0) is left top in NBGL, + # and generally left bottom for various canvas + color_index = img.getpixel((col, row)) + color_index = int((color_index + half_threshold) / base_threshold) + + if color_index >= nb_colors: + color_index = nb_colors - 1 + + if bpp == 1 and reverse_1bpp: + color_index = (color_index+1)&0x1 + + # le encoded + current_byte += color_index << ((8-bpp)-current_bit) + current_bit += bpp + + if current_bit >= 8: + image_data.append(current_byte & 0xFF) + current_bit = 0 + current_byte = 0 + + # Handle last byte if any + if current_bit > 0: + image_data.append(current_byte & 0xFF) + + return bytes(image_data) + + +# Compressions functions + + +def rle_compress(im: Image, bpp, reverse) -> Optional[bytes]: + """ + Run RLE compression on input image + """ + if bpp == 1: + return Rle1bpp.rle_1bpp(im, reverse)[1] + elif bpp == 2: + # No compression supports BPP2 + return None + elif bpp == 4: + return Rle4bpp.rle_4bpp(im)[1] + + +def gzlib_compress(im: Image, bpp: int, reverse) -> bytes: + """ + Run gzlib compression on input image + """ + pixels_buffer = image_to_packed_buffer(im, bpp, reverse) + output_buffer = [] + # cut into chunks of 2048 bytes max of uncompressed data (because decompression needs the full buffer) + full_uncompressed_size = len(pixels_buffer) + i = 0 + while full_uncompressed_size > 0: + chunk_size = min(2048, full_uncompressed_size) + tmp = bytes(pixels_buffer[i:i+chunk_size]) + compressed_buffer = gzip.compress(tmp, mtime=0) + output_buffer += [len(compressed_buffer) & 0xFF, + (len(compressed_buffer) >> 8) & 0xFF] + output_buffer += compressed_buffer + full_uncompressed_size -= chunk_size + i += chunk_size + + return bytearray(output_buffer) + + +NBGL_IMAGE_FILE_HEADER_SIZE = 8 + + +def compress(im: Image, bpp, reverse) -> Tuple[NbglFileCompression, bytes]: + """ + Compute multiple compression methods on the input image, + and return a tuple containing: + - The best compression method achieved + - The associated compressed bytes + """ + # GZlib is not supported on Nanos + if not reverse: + compressed_bufs = { + NbglFileCompression.NoCompression: image_to_packed_buffer(im, bpp, reverse), + NbglFileCompression.Gzlib: gzlib_compress(im, bpp, reverse), + NbglFileCompression.Rle: rle_compress(im, bpp, reverse) + } + else: + compressed_bufs = { + NbglFileCompression.NoCompression: image_to_packed_buffer(im, bpp, reverse), + NbglFileCompression.Rle: rle_compress(im, bpp, reverse) + } + + min_len = len(compressed_bufs[NbglFileCompression.NoCompression]) + min_comp = NbglFileCompression.NoCompression + + for compression, buffer in compressed_bufs.items(): + if buffer is None: + continue + + final_length = len(buffer) + if compression != NbglFileCompression.NoCompression: + final_length += NBGL_IMAGE_FILE_HEADER_SIZE + + if min_len > final_length: + min_len = final_length + min_comp = compression + + return min_comp, compressed_bufs[min_comp] + + +def convert_to_image_file(image_data: bytes, width: int, height: int, + bpp: int, compression: NbglFileCompression) -> bytes: + """ + Returns an image file version of the input image data and parameters + """ + BPP_FORMATS = { + 1: 0, + 2: 1, + 4: 2 + } + + result = [width & 0xFF, width >> 8, + height & 0xFF, height >> 8, + (BPP_FORMATS[bpp] << 4) | compression, + len(image_data) & 0xFF, (len(image_data) >> 8) & 0xFF, (len(image_data) >> 16) & 0xFF] + result.extend(image_data) + return bytes(bytearray(result)) + + +def compute_app_icon_data(im: Image, bpp, reverse) -> Tuple[bool, bytes]: + """ + Process image as app icon: + - App icon are always image file + - Compression is not limited to 64x64 + """ + compression, image_data = compress(im, bpp, reverse) + is_file = True + width, height = im.size + image_data = convert_to_image_file( + image_data, width, height, bpp, compression) + return is_file, image_data + + +def compute_regular_icon_data(no_comp: bool, im: Image, bpp, reverse) -> Tuple[bool, bytes]: + """ + Process image as regular icon: + - Regular icon are image file only if compressed + - Compression is limited to images bigger than 64x64 + """ + width, height = im.size + + if not no_comp: + compression, image_data = compress(im, bpp, reverse) + if compression != NbglFileCompression.NoCompression: + is_file = True + image_data = convert_to_image_file( + image_data, width, height, bpp, compression) + else: + is_file = False + else: + is_file = False + image_data = image_to_packed_buffer(im, bpp, reverse) + return is_file, image_data + +# glyphs.c/.h chunk files generators + + +def print_glyphfile_top(add_include: bool): + if add_include: + print("#include \"glyphs.h\"") + print( + """\ +#ifdef HAVE_NBGL +#include \"nbgl_types.h\" +#else +#include +#endif +""") + + +def print_glyphcfile_data(image_name, bpp, image_data): + print("uint8_t const C_{0}_bitmap[] = {{".format(image_name)) + for i in range(0, len(image_data), 16): + print(" " + ", ".join("0x{0:02x}".format(c) + for c in image_data[i:i+16]) + ",") + print("};") + print("""#ifdef HAVE_NBGL +const nbgl_icon_details_t C_{0} = {{ GLYPH_{0}_WIDTH, GLYPH_{0}_HEIGHT, NBGL_BPP_{1}, GLYPH_{0}_ISFILE, C_{0}_bitmap }}; +#endif // HAVE_NBGL +""".format(image_name, bpp)) + + +def print_glyphcheader_data(image_name, bpp, width, height, is_file, image_data): + if is_file: + is_file = 'true' + else: + is_file = 'false' + + print("""#ifndef GLYPH_{0}_BPP + #define GLYPH_{0}_WIDTH {1} + #define GLYPH_{0}_HEIGHT {2} + #define GLYPH_{0}_ISFILE {3} + #define GLYPH_{0}_BPP {4}""".format(image_name, width, height, is_file, bpp)) + print(" extern uint8_t const C_{0}_bitmap[{1:d}];".format( + image_name, len(image_data))) + print(""" #ifdef HAVE_NBGL + extern const nbgl_icon_details_t C_{0}; + #endif // HAVE_NBGL +#endif // GLYPH_{0}_BPP +""".format(image_name)) + + +def main(): + parser = argparse.ArgumentParser( + description='Generate source code for NBGL icons.') + parser.add_argument('image_file', help=""" + Icons to process. + Images that will be transformed through rotation or symmetry + must be suffixed by '_nocomp' (example: image_nocomp.png) + """, nargs='+') + parser.add_argument('--hexbitmaponly', action='store_true') + parser.add_argument('--glyphcheader', action='store_true') + parser.add_argument('--glyphcfile', action='store_true') + parser.add_argument('--reverse', help="Reverse B&W for 1BPP icons", action='store_true') + args = parser.parse_args() + + # Print C header + if args.glyphcfile or args.glyphcheader: + print_glyphfile_top(args.glyphcfile) + + processed_image_names = [] + for file in args.image_file: + try: + # Get image name + image_name = os.path.splitext(os.path.basename(file))[0] + + # if image name has already been done, then don't do it twice + if image_name in processed_image_names: + continue + processed_image_names.append(image_name) + + # Open image in luminance format + opened = open_image(file) + if opened is not None: + im, bpp = opened + else: + continue + + if args.hexbitmaponly: + # Prepare and print app icon data + _, image_data = compute_app_icon_data(im, bpp, args.reverse) + print(binascii.hexlify(image_data).decode('utf-8')) + else: + # Prepare and print regular icon data + width, height = im.size + + # Forbid compression if the image name ends with nocomp. + if image_name.endswith('_nocomp'): + no_comp = True + image_name = image_name[:-7] # Remove nocomp suffix + else: + no_comp = False + + is_file, image_data = compute_regular_icon_data(no_comp, im, bpp, args.reverse) + + if args.glyphcfile: + print_glyphcfile_data(image_name, bpp, image_data) + + if args.glyphcheader: + print_glyphcheader_data( + image_name, bpp, width, height, is_file, image_data) + + except Exception as e: + sys.stderr.write( + "Exception while processing {}: {}\n".format(file, e)) + try: + traceback.print_tb() + except: + pass + + +if __name__ == "__main__": + main() diff --git a/sdk_lib_nbgl/tools/nbgl_rle.py b/sdk_lib_nbgl/tools/nbgl_rle.py new file mode 100644 index 00000000..86d8dd96 --- /dev/null +++ b/sdk_lib_nbgl/tools/nbgl_rle.py @@ -0,0 +1,337 @@ +from typing import List, Tuple + +NB_MIN_PACKED_PIXELS = 3 +NB_MAX_PACKED_PIXELS = 6 +class Rle4bpp(): + + @staticmethod + def image_to_pixels(img, bpp: int) -> List[Tuple]: + """ + Rotate and pack bitmap data of the character. + """ + width, height = img.size + + color_indexes = [] + nb_colors = pow(2, bpp) + base_threshold = int(256 / nb_colors) + half_threshold = int(base_threshold / 2) + + # col first + for col in reversed(range(width)): + for row in range(height): + # Return an index in the indexed colors list + # top to bottom + # Perform implicit rotation here (0,0) is left top in NBGL, + # and generally left bottom for various canvas + color_index = img.getpixel((col, row)) + color_index = int((color_index + half_threshold) / base_threshold) + + if color_index >= nb_colors: + color_index = nb_colors - 1 + + color_indexes.append(color_index) + + return color_indexes + + + @staticmethod + def pixels_to_occurrences(pixels: List[int]) -> List[Tuple]: + occurrences = [] + for pixel in pixels: + if len(occurrences) == 0: + occurrences.append((pixel, 1)) + else: + color, cnt = occurrences[-1] + if pixel == color: + occurrences[-1] = (pixel, cnt+1) + else: + occurrences.append((pixel, 1)) + return occurrences + + # Fetch next single pixels that can be packed + @classmethod + def fetch_next_single_pixels(cls, occurrences: List[Tuple[int, int]]) -> List[int]: + result = [] + for occurrence in occurrences: + color, cnt = occurrence + if cnt >= 2: + break + else: + result.append(color) + + # Ensure pixels can be packed by groups + nb_pixels = len(result) + if (nb_pixels % NB_MAX_PACKED_PIXELS < NB_MIN_PACKED_PIXELS): + return result[0:(nb_pixels - nb_pixels%NB_MIN_PACKED_PIXELS)] + return result + + # Generate bytes from a list of single pixels + def generate_packed_single_pixels_bytes(packed_occurences: List[int]) -> bytes: + assert len(packed_occurences) >= 3 + assert len(packed_occurences) <= 6 + header = (0b10 << 2) | (len(packed_occurences) - 3) + nibbles = [header] + for occurrence in packed_occurences: + nibbles.append(occurrence) + + result = [] + for i, nibble in enumerate(nibbles): + if (i % 2) == 0: + result.append(nibble << 4) + else: + result[-1] += nibble + return bytes(result) + + @classmethod + def handle_packed_pixels(cls, packed_occurences: List[int]) -> bytes: + assert len(packed_occurences) >= 3 + result = bytes() + for i in range(0, len(packed_occurences), NB_MAX_PACKED_PIXELS): + result += cls.generate_packed_single_pixels_bytes(packed_occurences[i:i+NB_MAX_PACKED_PIXELS]) + return result + + @staticmethod + def handle_white_occurrence(occurrence: Tuple[int, int]) -> bytes: + _, cnt = occurrence + unit_cnt_max = 64 + result = [] + + for i in range(0, cnt, unit_cnt_max): + diff_cnt = cnt - i + if diff_cnt > unit_cnt_max: + i_cnt = unit_cnt_max + else: + i_cnt = diff_cnt + + result.append((0b11 << 6) | (i_cnt-1)) + return bytes(result) + + @staticmethod + def handle_non_white_occurrence(occurrence: Tuple[int, int]) -> bytes: + color, cnt = occurrence + unit_cnt_max = 8 + result = [] + + for i in range(0, cnt, unit_cnt_max): + diff_cnt = cnt - i + if diff_cnt > unit_cnt_max: + i_cnt = unit_cnt_max + else: + i_cnt = diff_cnt + + result.append((0 << 7) | (i_cnt-1) << 4 | color) + + return bytes(result) + + @classmethod + def occurrences_to_rle(cls, occurrences: Tuple[int, int], bpp: int) -> bytes: + result = bytes() + WHITE_COLOR = pow(2, bpp) - 1 + i = 0 + while i < len(occurrences): + # Check if next occurrences are packable in single occurrences + single_pixels = cls.fetch_next_single_pixels(occurrences[i:]) + if len(single_pixels) > 0: + # Pack single occurrences + result += cls.handle_packed_pixels(single_pixels) + i += len(single_pixels) + else: + # Encode next occurrence + occurrence = occurrences[i] + color, _ = occurrence + if color == WHITE_COLOR: + result += cls.handle_white_occurrence(occurrence) + else: + result += cls.handle_non_white_occurrence(occurrence) + i += 1 + return result + + @classmethod + def rle_4bpp(cls, img) -> Tuple[int, bytes]: + bpp = 4 + pixels = cls.image_to_pixels(img, bpp) + occurrences = cls.pixels_to_occurrences(pixels) + return 1, cls.occurrences_to_rle(occurrences, bpp) + +# ----------------------------------------------------------------------------- +class Rle1bpp(): + """ + Class handling custom RLE encoding, optimized for 1PP. + The generated bytes will contain alternances counts ZZZZOOOO with + - ZZZZ: number of consecutives 0, from 0 to 15 + - OOOO: number of consecutives 1, from 0 to 15 + """ + # ------------------------------------------------------------------------- + @staticmethod + def image_to_pixels(img, reverse): + """ + Rotate and pack bitmap data of the character. + return an array of pixels values. + """ + width, height = img.size + + pixels = [] + # Intensity level value to be considered a white pixel + white_threshold = 128 + if reverse: + white_pixel = 0 + black_pixel = 1 + else: + white_pixel = 1 + black_pixel = 0 + + # col first + for col in reversed(range(width)): + for row in range(height): + # Return an index in the indexed colors list (0 or 1, here) + # top to bottom + # Perform implicit rotation here (0,0) is left top in NBGL, + # and generally left bottom for various canvas + if img.getpixel((col, row)) >= white_threshold: + pixels.append(white_pixel) + else: + pixels.append(black_pixel) + + return pixels + + # ------------------------------------------------------------------------- + @staticmethod + def encode_pass1(data): + """ + Encode array of values using 'normal' RLE. + Return an array of tuples containing (repeat, val) + """ + output = [] + previous_value = -1 + count = 0 + for value in data: + # Same value than before? + if value == previous_value: + count += 1 + else: + # Store previous result + if count: + pair = (count, previous_value) + output.append(pair) + # Start a new count + previous_value = value + count = 1 + + # Store previous result + if count: + pair = (count, previous_value) + output.append(pair) + + return output + + # ------------------------------------------------------------------------- + @staticmethod + def encode_pass2(pairs): + """ + Encode alternance counts between pixels (nb of 0, then nb of 1, etc) + The generated packed values will contain ZZZZOOOO ZZZZOOOO with + - ZZZZ: number of consecutives 0, from 0 to 15 + - OOOO: number of consecutives 1, from 0 to 15 + """ + max_count = 15 + # First step: store alternances, and split if count > 15 + next_pixel = 0 + alternances = [] + for repeat, value in pairs: + # Store a count of 0 pixel if next pixel is not the one expected + if value != next_pixel: + alternances.append(0) + next_pixel ^= 1 + + while repeat > max_count: + # Store 15 pixels of value next_pixel + alternances.append(max_count) + repeat -= max_count + # Store 0 pixel of alternate value + alternances.append(0) + + if repeat: + alternances.append(repeat) + next_pixel ^= 1 + + # Now read all those values and store them into quartets + output = bytes() + index = 0 + + while index < len(alternances): + zeros = alternances[index] + index += 1 + if index < len(alternances): + ones = alternances[index] + index += 1 + else: + ones = 0 + + byte = (zeros << 4) | ones + output += bytes([byte]) + + return output + + # ------------------------------------------------------------------------- + @staticmethod + def remove_duplicates(pairs): + """ + Check if there are some duplicated pairs (same values) and merge them. + """ + index = len(pairs) - 1 + while index >= 1: + repeat1, value1 = pairs[index-1] + repeat2, value2 = pairs[index] + # Do we have identical consecutives values? + if value1 == value2: + repeat1 += repeat2 + # Merge them and remove last entry + pairs[index-1] = (repeat1, value1) + pairs.pop(index) + index -= 1 + + return pairs + + # ------------------------------------------------------------------------- + @classmethod + def decode_pass2(cls, data): + """ + Decode packed bytes into array of tuples containing (repeat, value). + The provided packed values will contain ZZZZOOOO ZZZZOOOO with + - ZZZZ: number of consecutives 0, from 0 to 15 + - OOOO: number of consecutives 1, from 0 to 15 + """ + pairs = [] + for byte in data: + ones = byte & 0x0F + byte >>= 4 + zeros = byte & 0x0F + if zeros: + pairs.append((zeros, 0)) + if ones: + pairs.append((ones, 1)) + + # There was a limitation on repeat count => remove duplicates + pairs = cls.remove_duplicates(pairs) + + return pairs + + # ------------------------------------------------------------------------- + @classmethod + def rle_1bpp(cls, img, reverse) -> Tuple[int, bytes]: + """ + Input: image to compress + - convert the picture to an array of pixels + - convert the array of pixels to tuples of (repeat, value) + - encode using custom RLE + Output: array of compressed bytes + """ + pixels = cls.image_to_pixels(img, reverse) + pairs = cls.encode_pass1(pixels) + encoded_data = cls.encode_pass2(pairs) + + # Check if encoding/decoding is fine + pairs2 = cls.decode_pass2(encoded_data) + assert pairs == pairs2 + + return 1, encoded_data diff --git a/sdk_lib_nbgl/tools/zlibcompress.py b/sdk_lib_nbgl/tools/zlibcompress.py new file mode 100644 index 00000000..936f79a4 --- /dev/null +++ b/sdk_lib_nbgl/tools/zlibcompress.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# coding: utf-8 +""" +Just a simple script to compress a given file in gzip format +""" +# ----------------------------------------------------------------------------- +import argparse +import sys +import gzip + +# ----------------------------------------------------------------------------- +# Program entry point: +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + # ------------------------------------------------------------------------- + # Parse arguments: + parser = argparse.ArgumentParser( + description="Compress the given file in zlib") + + parser.add_argument( + "-i", "--input", + dest="input", type=str, + help="file to compress") + + parser.add_argument( + "-o", "--output", + dest="output", type=str, + help="Output file") + args = parser.parse_args() + + with open(args.input, 'rb') as f: + s = f.read() + z = gzip.compress(s) + with open(args.output, 'wb') as f: + f.write(z) diff --git a/sdk_lib_ux_stax/doc/mainpage.dox b/sdk_lib_ux_stax/doc/mainpage.dox new file mode 100644 index 00000000..84b54bf1 --- /dev/null +++ b/sdk_lib_ux_stax/doc/mainpage.dox @@ -0,0 +1,9 @@ +/** @page ux_stax_mainpage User Experience library for Stax + +@section mainpage_intro Introduction + +This page describes the UX (User eXperience) library, available on \b Stax product. + +@note TO BE COMPLETED + +*/ diff --git a/sdk_lib_ux_stax/ux.c b/sdk_lib_ux_stax/ux.c new file mode 100644 index 00000000..25816ddb --- /dev/null +++ b/sdk_lib_ux_stax/ux.c @@ -0,0 +1,157 @@ + +/******************************************************************************* + * Ledger Nano S - Secure firmware + * (c) 2022 Ledger + * + * 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. + ********************************************************************************/ + +#include "seproxyhal_protocol.h" +#include "ux.h" +#include "nbgl_touch.h" +#include "nbgl_buttons.h" +#include "os_io.h" +#ifndef HAVE_BOLOS +// number of 100ms ticks since the start-up of the app +static uint32_t nbTicks; + +/** + * @brief internal bolos ux event processing with callback in case event is to be processed by the + * application + * + * @param ignoring_app_if_ux_busy if set to false, function returns true if no REDRAW needed + * @return true if ignoring_app_if_ux_busy is false or UX is not busy + */ +static bool ux_forward_event(bool ignoring_app_if_ux_busy) +{ + G_ux_params.ux_id = BOLOS_UX_EVENT; + G_ux_params.len = 0; + os_ux(&G_ux_params); + G_ux_params.len = os_sched_last_status(TASK_BOLOS_UX); + if (G_ux_params.len == BOLOS_UX_REDRAW) { + // enable drawing according to UX decision + //nbgl_objAllowDrawing(true); + //nbgl_screenRedraw(); + nbgl_refresh(); + } + else if (!ignoring_app_if_ux_busy + || ((G_ux_params.len != BOLOS_UX_IGNORE) && (G_ux_params.len != BOLOS_UX_CONTINUE))) { + return true; + } + return false; +} + +#ifdef HAVE_SE_TOUCH +static nbgl_touchStatePosition_t pos; + +/** + * @brief Process finger event. + * @note Application's finger event handler is called only if the ux app does not deny it (finger + * event caught by BOLOS UX page). + * + * @param seph_packet received SEPH packet + */ +void ux_process_finger_event(uint8_t seph_packet[]) +{ + bool displayEnabled = ux_forward_event(true); + // enable/disable drawing according to UX decision + //nbgl_objAllowDrawing(displayEnabled); + + // if the event is not fully consumed by UX, use it for NBGL + if (displayEnabled) { + pos.state = (seph_packet[3] == SEPROXYHAL_TAG_FINGER_EVENT_TOUCH) ? PRESSED : RELEASED; + pos.x = (seph_packet[4] << 8) + seph_packet[5]; + pos.y = (seph_packet[6] << 8) + seph_packet[7]; + nbgl_touchHandler(&pos, nbTicks * 100); + nbgl_refresh(); + } +} +#else // HAVE_SE_TOUCH +/** + * @brief Process button push event. + * @note Application's button push/release event handler is called only if the ux app does not deny + * it (button event caught by BOLOS UX page). + * + * @param seph_packet received SEPH packet + */ +void ux_process_button_event(uint8_t seph_packet[]) +{ + bool displayEnabled = ux_forward_event(true); + // enable/disable drawing according to UX decision + //nbgl_objAllowDrawing(displayEnabled); + + // if the event is not fully consumed by UX, use it for NBGL + if (displayEnabled) { + uint8_t buttons_state = seph_packet[3] >> 1; + nbgl_buttonsHandler(buttons_state, nbTicks * 100); + nbgl_refresh(); + } +} +#endif // HAVE_SE_TOUCH + +/** + * @brief Process the ticker_event to the os ux handler. Ticker event callback is always called + * whatever the return code of the ux app. + * @note Ticker event interval is assumed to be 100 ms. + */ +void ux_process_ticker_event(void) +{ + return; + nbTicks++; + // forward to UX + bool displayEnabled = ux_forward_event(true); + + // enable/disable drawing according to UX decision + //nbgl_objAllowDrawing(displayEnabled); + // update ticker in NBGL + nbgl_screenHandler(100); + + if (!displayEnabled) { + return; + } + +#ifdef HAVE_SE_TOUCH + // handle touch only if detected as pressed in last touch message + if (pos.state == PRESSED) { + io_touch_info_t touch_info; + touch_get_last_info(&touch_info); + pos.state = (touch_info.state == SEPROXYHAL_TAG_FINGER_EVENT_TOUCH) ? PRESSED : RELEASED; + pos.x = touch_info.x; + pos.y = touch_info.y; + // Send current touch position to nbgl + nbgl_touchHandler(&pos, nbTicks * 100); + } +#endif // HAVE_SE_TOUCH + nbgl_refresh(); +} + +/** + * Forwards the event to UX + */ +void ux_process_default_event(void) +{ + // forward to UX + ux_forward_event(false); +} + +/** + * Forwards the event to UX + */ +void ux_process_displayed_event(void) +{ + // forward to UX + nbgl_process_ux_displayed_event(); + //ux_forward_event(false); +} + +#endif // HAVE_BOLOS diff --git a/sdk_lib_ux_stax/ux.h b/sdk_lib_ux_stax/ux.h new file mode 100644 index 00000000..6cdcf6bc --- /dev/null +++ b/sdk_lib_ux_stax/ux.h @@ -0,0 +1,229 @@ + +/******************************************************************************* + * Ledger Nano S - Secure firmware + * (c) 2022 Ledger + * + * 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. + ********************************************************************************/ + +#pragma once + +#if defined(HAVE_BOLOS) +#include "bolos_privileged_ux.h" +#endif // HAVE_BOLOS + +#include "os_math.h" +#include "os_ux.h" +#include "os_task.h" +#include "nbgl_screen.h" +#include "nbgl_touch.h" + +#include + +#ifdef TARGET_NANOS +#include "bagl.h" +typedef struct bagl_element_e bagl_element_t; + +// callback returns NULL when element must not be redrawn (with a changing color or what so ever) +typedef const bagl_element_t *(*bagl_element_callback_t)(const bagl_element_t *element); + +// a graphic element is an element with defined text and actions depending on user touches +struct bagl_element_e { + bagl_component_t component; + +#if defined(HAVE_INDEXED_STRINGS) + // Nameless union, to be able to access one member of the union or the other. + // No space won when using index with bagl_element_e, but headaches are avoided :) + union { + const char *text; + UX_LOC_STRINGS_INDEX index; + }; +#else // defined(HAVE_INDEXED_STRINGS) + const char *text; +#endif // defined(HAVE_INDEXED_STRINGS) +}; + +void io_seproxyhal_display_icon(bagl_component_t *icon_component, + bagl_icon_details_t *icon_details); + +// Helper function that give a realistic timing of scrolling for label with text larger than screen +unsigned int bagl_label_roundtrip_duration_ms(const bagl_element_t *e, + unsigned int average_char_width); +unsigned int bagl_label_roundtrip_duration_ms_buf(const bagl_element_t *e, + const char *str, + unsigned int average_char_width); + +#ifndef BUTTON_FAST_THRESHOLD_CS +#define BUTTON_FAST_THRESHOLD_CS 8 // x100MS +#endif // BUTTON_FAST_THRESHOLD_CS +#ifndef BUTTON_FAST_ACTION_CS +#define BUTTON_FAST_ACTION_CS 3 // x100MS +#endif // BUTTON_FAST_ACTION_CS + +typedef unsigned int (*button_push_callback_t)(unsigned int button_mask, + unsigned int button_mask_counter); +// flag set when fast threshold is reached and above +#define BUTTON_EVT_FAST 0x40000000UL +#define BUTTON_EVT_RELEASED 0x80000000UL +#endif + +#define BUTTON_LEFT 1 +#define BUTTON_RIGHT 2 + +typedef void (*asynchmodal_end_callback_t)(unsigned int ux_status); + +/** + * Common structure for applications to perform asynchronous UX aside IO operations + */ +typedef struct ux_state_s ux_state_t; + +struct ux_state_s { + bolos_task_status_t exit_code; + bool validate_pin_from_dashboard; // set to true when BOLOS_UX_VALIDATE_PIN is received from + // Dashboard task + + asynchmodal_end_callback_t asynchmodal_end_callback; + + char string_buffer[128]; +}; + +extern ux_state_t G_ux; +#if !defined(APP_UX) +extern bolos_ux_params_t G_ux_params; + +extern void ux_process_finger_event(uint8_t seph_packet[]); +extern void ux_process_button_event(uint8_t seph_packet[]); +extern void ux_process_ticker_event(void); +extern void ux_process_default_event(void); +extern void ux_process_displayed_event(void); +#endif // !defined(APP_UX) + +/** + * Initialize the user experience structure + */ +#ifdef TARGET_NANOS +#define UX_INIT() +#else +#define UX_INIT() nbgl_objInit(); +#endif + +#ifdef HAVE_BOLOS +// to be used only by hal_io.c in BOLOS, for compatibility +#define UX_FORWARD_EVENT_REDRAWCB(bypasspincheck, \ + ux_params, \ + ux, \ + os_ux, \ + os_sched_last_status, \ + callback, \ + redraw_cb, \ + ignoring_app_if_ux_busy) \ + ux_params.ux_id = BOLOS_UX_EVENT; \ + ux_params.len = 0; \ + os_ux(&ux_params); \ + ux_params.len = os_sched_last_status(TASK_BOLOS_UX); \ + if (ux.asynchmodal_end_callback \ + && os_ux_get_status(BOLOS_UX_ASYNCHMODAL_PAIRING_REQUEST) != 0) { \ + asynchmodal_end_callback_t cb = ux.asynchmodal_end_callback; \ + ux.asynchmodal_end_callback = NULL; \ + cb(os_ux_get_status(BOLOS_UX_ASYNCHMODAL_PAIRING_REQUEST)); \ + } +#endif // HAVE_BOLOS + +/** + * Request a wake up of the device (pin lock screen, ...) to display a new interface to the user. + * Wake up prevents power-off features. Therefore, security wise, this function shall only + * be called to request direct user interaction. + */ +#define UX_WAKE_UP() \ + G_ux_params.ux_id = BOLOS_UX_WAKE_UP; \ + G_ux_params.len = 0; \ + os_ux(&G_ux_params); \ + G_ux_params.len = os_sched_last_status(TASK_BOLOS_UX); + +/** + * forward the button push/release events to the os ux handler. if not used by it, it will + * be used by App controls + */ +#ifdef HAVE_SE_TOUCH +#define UX_BUTTON_PUSH_EVENT(seph_packet) +#else // HAVE_SE_TOUCH +#define UX_BUTTON_PUSH_EVENT(seph_packet) ux_process_button_event(seph_packet) +#endif // HAVE_SE_TOUCH + +/** + * forward the finger_event to the os ux handler. if not used by it, it will + * be used by App controls + */ +#ifdef HAVE_SE_TOUCH +#define UX_FINGER_EVENT(seph_packet) ux_process_finger_event(seph_packet) +#else // HAVE_SE_TOUCH +#define UX_FINGER_EVENT(seph_packet) +#endif // HAVE_SE_TOUCH + +/** + * forward the ticker_event to the os ux handler. Ticker event callback is always called whatever + * the return code of the ux app. Ticker event interval is assumed to be 100 ms. + */ +#define UX_TICKER_EVENT(seph_packet, callback) ux_process_ticker_event() + +/** + * Forward the event, ignoring the UX return code, the event must therefore be either not processed + * or processed with extreme care by the application afterwards + */ +#define UX_DEFAULT_EVENT() ux_process_default_event() +#define UX_DISPLAYED_EVENT(...) \ + ux_process_displayed_event(); \ + return 1 /* this one is really messy, sorry... */ + +// discriminated from io to allow for different memory placement +typedef struct ux_seph_s { + unsigned int button_mask; + unsigned int button_same_mask_counter; +#ifdef HAVE_BOLOS + unsigned int ux_id; + unsigned int ux_status; +#endif // HAVE_BOLOS +} ux_seph_os_and_app_t; + +#ifdef HAVE_BACKGROUND_IMG +SYSCALL PERMISSION(APPLICATION_FLAG_BOLOS_UX) +uint8_t *fetch_background_img(bool allow_candidate); +SYSCALL PERMISSION(APPLICATION_FLAG_BOLOS_UX) +bolos_err_t delete_background_img(void); +#endif + +extern ux_seph_os_and_app_t G_ux_os; + +void io_seproxyhal_request_mcu_status(void); +#ifndef TARGET_NANOS +void io_seproxyhal_power_off(bool criticalBattery); +#endif + +#if defined(HAVE_LANGUAGE_PACK) +const char *get_ux_loc_string(UX_LOC_STRINGS_INDEX index); +void bolos_ux_select_language(uint16_t language); +void bolos_ux_refresh_language(void); + +typedef struct ux_loc_language_pack_infos { + unsigned char available; + +} UX_LOC_LANGUAGE_PACK_INFO; + +// To populate infos about language packs +SYSCALL PERMISSION(APPLICATION_FLAG_BOLOS_UX) void list_language_packs( + UX_LOC_LANGUAGE_PACK_INFO *packs PLENGTH(NB_LANG * sizeof(UX_LOC_LANGUAGE_PACK_INFO))); +SYSCALL PERMISSION(APPLICATION_FLAG_BOLOS_UX) const LANGUAGE_PACK *get_language_pack( + unsigned int language); +#endif // defined(HAVE_LANGUAGE_PACK) + +#include "glyphs.h" diff --git a/sdk_lib_ux_stax/ux_loc.h b/sdk_lib_ux_stax/ux_loc.h new file mode 100644 index 00000000..c3cd93ed --- /dev/null +++ b/sdk_lib_ux_stax/ux_loc.h @@ -0,0 +1,62 @@ + +/******************************************************************************* + * Ledger Nano S - Secure firmware + * (c) 2022 Ledger + * + * 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. + ********************************************************************************/ + +#pragma once + +#include "nbgl_types.h" + +#if defined(HAVE_LANGUAGE_PACK) +// Structure used for language packs: +typedef struct language_pack { + uint32_t target_id; // 0x33000004:NanoX, 0x31100004:NanoS 1.5.x etc + uint16_t language; // Language contained in this pack (7000) => use a u_int16_t; +- we may store some CRC, to check language pack's integrity. +*/ + +typedef unsigned short UX_LOC_STRINGS_INDEX; + +#endif // defined(HAVE_LANGUAGE_PACK) diff --git a/src/apdu/dispatcher.c b/src/apdu/dispatcher.c index b33b19f8..f28287b2 100644 --- a/src/apdu/dispatcher.c +++ b/src/apdu/dispatcher.c @@ -23,14 +23,12 @@ #include "ledger_assert.h" #include "dispatcher.h" -#include "../constants.h" -#include "../globals.h" -#include "../types.h" -#include "../sw.h" -#include "../handler/get_version.h" -#include "../handler/get_app_name.h" -#include "../handler/get_public_key.h" -#include "../handler/sign_tx.h" +#include "constants.h" +#include "sw.h" +#include "handler/get_version.h" +#include "handler/get_app_name.h" +#include "handler/get_public_key.h" +#include "handler/sign_tx.h" int apdu_dispatcher(const command_t *cmd) { LEDGER_ASSERT(cmd != NULL, "NULL cmd"); @@ -69,12 +67,6 @@ int apdu_dispatcher(const command_t *cmd) { return handler_get_public_key(&buf, (bool) cmd->p1); case SIGN_TX: - if ((cmd->p1 == P1_START && cmd->p2 != P2_MORE) || // - cmd->p1 > P1_MAX || // - (cmd->p2 != P2_LAST && cmd->p2 != P2_MORE)) { - return io_send_sw(SW_WRONG_P1P2); - } - if (!cmd->data) { return io_send_sw(SW_WRONG_DATA_LENGTH); } @@ -83,7 +75,7 @@ int apdu_dispatcher(const command_t *cmd) { buf.size = cmd->lc; buf.offset = 0; - return handler_sign_tx(&buf, cmd->p1, (bool) (cmd->p2 & P2_MORE)); + return handler_sign_tx(&buf, cmd->p1, cmd->p2); default: return io_send_sw(SW_INS_NOT_SUPPORTED); } diff --git a/src/apdu/dispatcher.h b/src/apdu/dispatcher.h index 191ce3f8..36e1f60c 100644 --- a/src/apdu/dispatcher.h +++ b/src/apdu/dispatcher.h @@ -2,25 +2,6 @@ #include "parser.h" -#include "../types.h" - -/** - * Parameter 2 for last APDU to receive. - */ -#define P2_LAST 0x00 -/** - * Parameter 2 for more APDU to receive. - */ -#define P2_MORE 0x80 -/** - * Parameter 1 for first APDU number. - */ -#define P1_START 0x00 -/** - * Parameter 1 for maximum APDU number. - */ -#define P1_MAX 0x03 - /** * Dispatch APDU command received to the right handler. * diff --git a/src/app_main.c b/src/app_main.c index 30e877c7..98319441 100644 --- a/src/app_main.c +++ b/src/app_main.c @@ -21,15 +21,12 @@ #include "os.h" #include "ux.h" -#include "types.h" #include "globals.h" #include "io.h" #include "sw.h" #include "ui/menu.h" #include "apdu/dispatcher.h" -global_ctx_t G_context; - const internal_storage_t N_storage_real; /** @@ -43,10 +40,16 @@ void app_main() { io_init(); - ui_menu_main(); + //for (int i = 0; i < 3; i++) { + // // - Looping on os_io_seph_recv_and_process(0); + // // - This will send a general_status and then wait for an event. + // // - Upon event reception this will call io_seproxyhal_handle_event() + // // - On some case this will call io_event() which usually forward the + // // event to the UX lib. + // os_io_seph_recv_and_process(0); + //} - // Reset context - explicit_bzero(&G_context, sizeof(G_context)); + ui_menu_main(); // Initialize the NVM data if required if (N_storage.initialized != 0x01) { diff --git a/src/constants.h b/src/constants.h index 487da075..4ed83ede 100644 --- a/src/constants.h +++ b/src/constants.h @@ -5,6 +5,16 @@ */ #define CLA 0xE0 +/** + * Enumeration with expected INS of APDU commands. + */ +typedef enum { + GET_VERSION = 0x03, /// version of the application + GET_APP_NAME = 0x04, /// name of the application + GET_PUBLIC_KEY = 0x05, /// public key of corresponding BIP32 path + SIGN_TX = 0x06 /// sign transaction with BIP32 path +} command_e; + /** * Length of APPNAME variable in the Makefile. */ @@ -34,3 +44,7 @@ * Exponent used to convert mBOL to BOL unit (N BOL = N * 10^3 mBOL). */ #define EXPONENT_SMALLEST_UNIT 3 + +#define PUBKEY_LEN 65 +#define CHAINCODE_LEN 32 +#define ADDRESS_LEN 20 diff --git a/src/globals.h b/src/globals.h index c2fd830b..cd3fd69a 100644 --- a/src/globals.h +++ b/src/globals.h @@ -5,7 +5,6 @@ #include "ux.h" #include "io.h" -#include "types.h" #include "constants.h" /** @@ -23,11 +22,6 @@ extern ux_state_t G_ux; */ extern bolos_ux_params_t G_ux_params; -/** - * Global context for user requests. - */ -extern global_ctx_t G_context; - /** * Global structure for NVM data storage. */ diff --git a/src/handler/get_app_name.c b/src/handler/get_app_name.c index 1eabb8b1..2c5441e0 100644 --- a/src/handler/get_app_name.c +++ b/src/handler/get_app_name.c @@ -21,10 +21,8 @@ #include "buffer.h" #include "get_app_name.h" -#include "../constants.h" -#include "../globals.h" -#include "../sw.h" -#include "../types.h" +#include "constants.h" +#include "sw.h" int handler_get_app_name() { _Static_assert(APPNAME_LEN < MAX_APPNAME_LEN, "APPNAME must be at most 64 characters!"); diff --git a/src/handler/get_public_key.c b/src/handler/get_public_key.c index 9e1bc8c5..0239f3e5 100644 --- a/src/handler/get_public_key.c +++ b/src/handler/get_public_key.c @@ -25,38 +25,65 @@ #include "io.h" #include "buffer.h" #include "crypto_helpers.h" +#include "bip32.h" #include "get_public_key.h" -#include "../globals.h" -#include "../types.h" -#include "../sw.h" -#include "../ui/display.h" -#include "../helper/send_response.h" +#include "sw.h" +#include "ui/display.h" -int handler_get_public_key(buffer_t *cdata, bool display) { - explicit_bzero(&G_context, sizeof(G_context)); - G_context.req_type = CONFIRM_ADDRESS; - G_context.state = STATE_NONE; +static void send_response_pubkey(const uint8_t raw_public_key[static PUBKEY_LEN], const uint8_t chain_code[static CHAINCODE_LEN]) { + uint8_t pubkey_len[1] = {PUBKEY_LEN}; + uint8_t chain_code_len[1] = {CHAINCODE_LEN}; + + buffer_t buffers[4] = { + {.ptr = pubkey_len, .size = sizeof(pubkey_len), .offset = 0}, + {.ptr = raw_public_key, .size = PUBKEY_LEN, .offset = 0}, + {.ptr = chain_code_len, .size = sizeof(chain_code_len), .offset = 0}, + {.ptr = chain_code, .size = CHAINCODE_LEN, .offset = 0}, + }; + + io_send_response_buffers(buffers, 4, SW_OK); +} - if (!buffer_read_u8(cdata, &G_context.bip32_path_len) || - !buffer_read_bip32_path(cdata, G_context.bip32_path, (size_t) G_context.bip32_path_len)) { +int handler_get_public_key(buffer_t *cdata, bool display) { + uint8_t bip32_path_len; + uint32_t bip32_path[MAX_BIP32_PATH] = {0}; + if (!buffer_read_u8(cdata, &bip32_path_len) || + !buffer_read_bip32_path(cdata, bip32_path, (size_t) bip32_path_len)) { return io_send_sw(SW_WRONG_DATA_LENGTH); } + + uint8_t raw_public_key[PUBKEY_LEN] = {0}; + uint8_t chain_code[CHAINCODE_LEN] = {0}; cx_err_t error = bip32_derive_get_pubkey_256(CX_CURVE_256K1, - G_context.bip32_path, - G_context.bip32_path_len, - G_context.pk_info.raw_public_key, - G_context.pk_info.chain_code, + bip32_path, + bip32_path_len, + raw_public_key, + chain_code, CX_SHA512); if (error != CX_OK) { return io_send_sw(error); } + ui_ret_e ret = UI_RET_APPROVED; + if (display) { + ret = ui_display_address(raw_public_key); + } + + if (ret == UI_RET_APPROVED) { + send_response_pubkey(raw_public_key, chain_code); + } else if (ret == UI_RET_REJECTED) { + io_send_sw(SW_DENY); + } else { + io_send_sw(SW_DISPLAY_ADDRESS_FAIL); + } + + if (display) { - return ui_display_address(); + ui_display_address_status(ret); } - return helper_send_response_pubkey(); + return 0; } diff --git a/src/handler/get_public_key.h b/src/handler/get_public_key.h index 3cb538a6..119811bd 100644 --- a/src/handler/get_public_key.h +++ b/src/handler/get_public_key.h @@ -6,8 +6,6 @@ #include "buffer.h" -#include "../types.h" - /** * Handler for GET_PUBLIC_KEY command. If successfully parse BIP32 path, * derive public key/chain code and send APDU response. diff --git a/src/handler/get_version.c b/src/handler/get_version.c index bf37ec1f..a8ea911d 100644 --- a/src/handler/get_version.c +++ b/src/handler/get_version.c @@ -23,10 +23,8 @@ #include "buffer.h" #include "get_version.h" -#include "../globals.h" -#include "../constants.h" -#include "../sw.h" -#include "../types.h" +#include "constants.h" +#include "sw.h" int handler_get_version() { _Static_assert(APPVERSION_LEN == 3, "Length of (MAJOR || MINOR || PATCH) must be 3!"); diff --git a/src/handler/sign_tx.c b/src/handler/sign_tx.c index 60c89912..b8a47183 100644 --- a/src/handler/sign_tx.c +++ b/src/handler/sign_tx.c @@ -23,84 +23,234 @@ #include "os.h" #include "cx.h" #include "buffer.h" +#include "parser.h" +#include "bip32.h" +#include "io.h" +#include "crypto_helpers.h" #include "sign_tx.h" -#include "../sw.h" -#include "../globals.h" -#include "../ui/display.h" -#include "../transaction/types.h" -#include "../transaction/deserialize.h" - -int handler_sign_tx(buffer_t *cdata, uint8_t chunk, bool more) { - if (chunk == 0) { // first APDU, parse BIP32 path - explicit_bzero(&G_context, sizeof(G_context)); - G_context.req_type = CONFIRM_TRANSACTION; - G_context.state = STATE_NONE; - - if (!buffer_read_u8(cdata, &G_context.bip32_path_len) || - !buffer_read_bip32_path(cdata, - G_context.bip32_path, - (size_t) G_context.bip32_path_len)) { - return io_send_sw(SW_WRONG_DATA_LENGTH); - } +#include "sw.h" +#include "ui/display.h" +#include "transaction/types.h" +#include "transaction/deserialize.h" + +/** + * Parameter 2 for last APDU to receive. + */ +#define P2_LAST 0x00 +/** + * Parameter 2 for more APDU to receive. + */ +#define P2_MORE 0x80 +/** + * Parameter 1 for first APDU number. + */ +#define P1_START 0x00 +/** + * Parameter 1 for maximum APDU number. + */ +#define P1_MAX 0x03 + + +static uint16_t get_next_chunk(buffer_t *cdata, bool *more) { + int input_len = 0; + command_t cmd; + + if ((input_len = io_recv_command()) < 0) { + LEDGER_ASSERT(false, "=> io_recv_command failure\n"); + } + + // Parse APDU command from G_io_apdu_buffer + if (!apdu_parser(&cmd, G_io_apdu_buffer, input_len)) { + PRINTF("=> /!\\ BAD LENGTH: %.*H\n", input_len, G_io_apdu_buffer); + return SW_WRONG_DATA_LENGTH; + } - return io_send_sw(SW_OK); + PRINTF("=> CLA=%02X | INS=%02X | P1=%02X | P2=%02X | Lc=%02X | CData=%.*H\n", + cmd.cla, + cmd.ins, + cmd.p1, + cmd.p2, + cmd.lc, + cmd.lc, + cmd.data); - } else { // parse transaction + if (cmd.cla != CLA) { + return SW_BAD_STATE; + } + + if (cmd.ins != SIGN_TX) { + return SW_BAD_STATE; + } + + if (cmd.p1 == P1_START) { + return SW_BAD_STATE; + } + + if ((cmd.p2 != P2_MORE) && (cmd.p2 != P2_LAST)) { + return SW_WRONG_P1P2; + } - if (G_context.req_type != CONFIRM_TRANSACTION) { - return io_send_sw(SW_BAD_STATE); + if (!cmd.data) { + return SW_WRONG_DATA_LENGTH; + } + + cdata->ptr = cmd.data; + cdata->size = cmd.lc; + cdata->offset = 0; + + if (cmd.p2 == P2_MORE) { + *more = true; + } else { + *more = false; + } + + return SW_OK; +} + +static void hash_message(const uint8_t *raw_tx, size_t raw_tx_len, uint8_t m_hash[static 32]) { + cx_sha3_t keccak256; + + CX_ASSERT(cx_keccak_init_no_throw(&keccak256, 256)); + + CX_ASSERT(cx_hash_no_throw((cx_hash_t *) &keccak256, + CX_LAST, + raw_tx, + raw_tx_len, + m_hash, + 32)); + + PRINTF("Hash: %.*H\n", 32, m_hash); +} + +static cx_err_t crypto_sign_message(const uint32_t *bip32_path, + uint8_t bip32_path_len, + const uint8_t m_hash[static 32], + uint8_t signature[static MAX_DER_SIG_LEN], + uint8_t *signature_len, + uint8_t *v) { + uint32_t info = 0; + size_t sig_len = MAX_DER_SIG_LEN; + + cx_err_t error = bip32_derive_ecdsa_sign_hash_256(CX_CURVE_256K1, + bip32_path, + bip32_path_len, + CX_RND_RFC6979 | CX_LAST, + CX_SHA256, + m_hash, + 32, + signature, + &sig_len, + &info); + if (error != CX_OK) { + return error; + } + + PRINTF("Signature: %.*H\n", sig_len, signature); + + *signature_len = sig_len; + *v = (uint8_t)(info & CX_ECCINFO_PARITY_ODD); + + return CX_OK; +} + + +static void helper_send_response_sig(uint8_t signature_len, const uint8_t *signature, uint8_t v) { + uint8_t _signature_len[1] = {signature_len}; + uint8_t _v[1] = {v}; + + buffer_t buffers[3] = { + {.ptr = _signature_len, .size = sizeof(_signature_len), .offset = 0}, + {.ptr = signature, .size = signature_len, .offset = 0}, + {.ptr = _v, .size = sizeof(_v), .offset = 0}, + }; + + io_send_response_buffers(buffers, 3, SW_OK); +} + + + +int handler_sign_tx(buffer_t *cdata, uint8_t p1, uint8_t p2) { + if (p1 != P1_START) { + return io_send_sw(SW_BAD_STATE); + } + + if (p2 != P2_MORE) { + return io_send_sw(SW_BAD_STATE); + } + + uint8_t bip32_path_len; + uint32_t bip32_path[MAX_BIP32_PATH] = {0}; + if (!buffer_read_u8(cdata, &bip32_path_len) || + !buffer_read_bip32_path(cdata, bip32_path, (size_t) bip32_path_len)) { + return io_send_sw(SW_WRONG_DATA_LENGTH); + } + + uint8_t raw_tx[MAX_TRANSACTION_LEN]; /// raw transaction serialized + size_t raw_tx_len = 0; /// length of raw transaction + bool more = true; + while (more) { + io_send_sw(SW_OK); + + uint16_t sw = get_next_chunk(cdata, &more); + + if (sw != SW_OK) { + return io_send_sw(sw); } - if (G_context.tx_info.raw_tx_len + cdata->size > sizeof(G_context.tx_info.raw_tx)) { + + if (raw_tx_len + cdata->size > sizeof(raw_tx)) { return io_send_sw(SW_WRONG_TX_LENGTH); } + if (!buffer_move(cdata, - G_context.tx_info.raw_tx + G_context.tx_info.raw_tx_len, + raw_tx + raw_tx_len, cdata->size)) { return io_send_sw(SW_TX_PARSING_FAIL); } - G_context.tx_info.raw_tx_len += cdata->size; - - if (more) { - // more APDUs with transaction part are expected. - // Send a SW_OK to signal that we have received the chunk - return io_send_sw(SW_OK); - - } else { - // last APDU for this transaction, let's parse, display and request a sign confirmation - - buffer_t buf = {.ptr = G_context.tx_info.raw_tx, - .size = G_context.tx_info.raw_tx_len, - .offset = 0}; + raw_tx_len += cdata->size; + } - parser_status_e status = transaction_deserialize(&buf, &G_context.tx_info.transaction); - PRINTF("Parsing status: %d.\n", status); - if (status != PARSING_OK) { - return io_send_sw(SW_TX_PARSING_FAIL); - } + // last APDU for this transaction, let's parse, display and request a sign confirmation - G_context.state = STATE_PARSED; + buffer_t buf = {.ptr = raw_tx, + .size = raw_tx_len, + .offset = 0}; + transaction_t transaction = {0}; - cx_sha3_t keccak256; + parser_status_e status = transaction_deserialize(&buf, &transaction); + PRINTF("Parsing status: %d.\n", status); + if (status != PARSING_OK) { + return io_send_sw(SW_TX_PARSING_FAIL); + } - if (cx_keccak_init_no_throw(&keccak256, 256) != CX_OK) { - return io_send_sw(SW_TX_HASH_FAIL); - } + uint8_t m_hash[32]; + hash_message(raw_tx, raw_tx_len, m_hash); - if (cx_hash_no_throw((cx_hash_t *) &keccak256, - CX_LAST, - G_context.tx_info.raw_tx, - G_context.tx_info.raw_tx_len, - G_context.tx_info.m_hash, - sizeof(G_context.tx_info.m_hash)) != CX_OK) { - return io_send_sw(SW_TX_HASH_FAIL); - } + ui_ret_e ret = ui_display_transaction(&transaction); - PRINTF("Hash: %.*H\n", sizeof(G_context.tx_info.m_hash), G_context.tx_info.m_hash); + if (ret == UI_RET_APPROVED) { + uint8_t signature[MAX_DER_SIG_LEN]; + uint8_t signature_len; + uint8_t v; + cx_err_t err; - return ui_display_transaction(); + err = crypto_sign_message(bip32_path, + bip32_path_len, + m_hash, + signature, + &signature_len, + &v); + if (err != CX_OK) { + io_send_sw(SW_SIGNATURE_FAIL); } + helper_send_response_sig(signature_len, signature, v); + } else if (ret == UI_RET_REJECTED) { + io_send_sw(SW_DENY); + } else { + io_send_sw(SW_BAD_STATE); } + ui_display_transaction_status(ret); + return 0; } diff --git a/src/handler/sign_tx.h b/src/handler/sign_tx.h index 633ddf6d..a1bab52b 100644 --- a/src/handler/sign_tx.h +++ b/src/handler/sign_tx.h @@ -22,4 +22,4 @@ * @return zero or positive integer if success, negative integer otherwise. * */ -int handler_sign_tx(buffer_t *cdata, uint8_t chunk, bool more); +int handler_sign_tx(buffer_t *cdata, uint8_t p1, uint8_t p2); diff --git a/src/helper/send_reponse.c b/src/helper/send_reponse.c deleted file mode 100644 index 19f9c395..00000000 --- a/src/helper/send_reponse.c +++ /dev/null @@ -1,53 +0,0 @@ -/***************************************************************************** - * Ledger App Boilerplate. - * (c) 2020 Ledger SAS. - * - * 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. - *****************************************************************************/ - -#include // size_t -#include // uint*_t -#include // memmove - -#include "buffer.h" - -#include "send_response.h" -#include "../constants.h" -#include "../globals.h" -#include "../sw.h" - -int helper_send_response_pubkey() { - uint8_t resp[1 + PUBKEY_LEN + 1 + CHAINCODE_LEN] = {0}; - size_t offset = 0; - - resp[offset++] = PUBKEY_LEN; - memmove(resp + offset, G_context.pk_info.raw_public_key, PUBKEY_LEN); - offset += PUBKEY_LEN; - resp[offset++] = CHAINCODE_LEN; - memmove(resp + offset, G_context.pk_info.chain_code, CHAINCODE_LEN); - offset += CHAINCODE_LEN; - - return io_send_response_pointer(resp, offset, SW_OK); -} - -int helper_send_response_sig() { - uint8_t resp[1 + MAX_DER_SIG_LEN + 1] = {0}; - size_t offset = 0; - - resp[offset++] = G_context.tx_info.signature_len; - memmove(resp + offset, G_context.tx_info.signature, G_context.tx_info.signature_len); - offset += G_context.tx_info.signature_len; - resp[offset++] = (uint8_t) G_context.tx_info.v; - - return io_send_response_pointer(resp, offset, SW_OK); -} diff --git a/src/helper/send_response.h b/src/helper/send_response.h deleted file mode 100644 index 52fb40db..00000000 --- a/src/helper/send_response.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include "os.h" -#include "macros.h" - -/** - * Length of public key. - */ -#define PUBKEY_LEN (MEMBER_SIZE(pubkey_ctx_t, raw_public_key)) -/** - * Length of chain code. - */ -#define CHAINCODE_LEN (MEMBER_SIZE(pubkey_ctx_t, chain_code)) - -/** - * Helper to send APDU response with public key and chain code. - * - * response = PUBKEY_LEN (1) || - * G_context.pk_info.public_key (PUBKEY_LEN) || - * CHAINCODE_LEN (1) || - * G_context.pk_info.chain_code (CHAINCODE_LEN) - * - * @return zero or positive integer if success, -1 otherwise. - * - */ -int helper_send_response_pubkey(void); - -/** - * Helper to send APDU response with signature and v (parity of - * y-coordinate of R). - * - * response = G_context.tx_info.signature_len (1) || - * G_context.tx_info.signature (G_context.tx_info.signature_len) || - * G_context.tx_info.v (1) - * - * @return zero or positive integer if success, -1 otherwise. - * - */ -int helper_send_response_sig(void); diff --git a/src/types.h b/src/types.h deleted file mode 100644 index 39bb1dad..00000000 --- a/src/types.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once - -#include // size_t -#include // uint*_t - -#include "bip32.h" - -#include "constants.h" -#include "transaction/types.h" - -/** - * Enumeration with expected INS of APDU commands. - */ -typedef enum { - GET_VERSION = 0x03, /// version of the application - GET_APP_NAME = 0x04, /// name of the application - GET_PUBLIC_KEY = 0x05, /// public key of corresponding BIP32 path - SIGN_TX = 0x06 /// sign transaction with BIP32 path -} command_e; -/** - * Enumeration with parsing state. - */ -typedef enum { - STATE_NONE, /// No state - STATE_PARSED, /// Transaction data parsed - STATE_APPROVED /// Transaction data approved -} state_e; - -/** - * Enumeration with user request type. - */ -typedef enum { - CONFIRM_ADDRESS, /// confirm address derived from public key - CONFIRM_TRANSACTION /// confirm transaction information -} request_type_e; - -/** - * Structure for public key context information. - */ -typedef struct { - uint8_t raw_public_key[65]; /// format (1), x-coordinate (32), y-coodinate (32) - uint8_t chain_code[32]; /// for public key derivation -} pubkey_ctx_t; - -/** - * Structure for transaction information context. - */ -typedef struct { - uint8_t raw_tx[MAX_TRANSACTION_LEN]; /// raw transaction serialized - size_t raw_tx_len; /// length of raw transaction - transaction_t transaction; /// structured transaction - uint8_t m_hash[32]; /// message hash digest - uint8_t signature[MAX_DER_SIG_LEN]; /// transaction signature encoded in DER - uint8_t signature_len; /// length of transaction signature - uint8_t v; /// parity of y-coordinate of R in ECDSA signature -} transaction_ctx_t; - -/** - * Structure for global context. - */ -typedef struct { - state_e state; /// state of the context - union { - pubkey_ctx_t pk_info; /// public key context - transaction_ctx_t tx_info; /// transaction context - }; - request_type_e req_type; /// user request - uint32_t bip32_path[MAX_BIP32_PATH]; /// BIP32 path - uint8_t bip32_path_len; /// length of BIP32 path -} global_ctx_t; diff --git a/src/ui/action/validate.c b/src/ui/action/validate.c deleted file mode 100644 index a8a79018..00000000 --- a/src/ui/action/validate.c +++ /dev/null @@ -1,76 +0,0 @@ -/***************************************************************************** - * Ledger App Boilerplate. - * (c) 2020 Ledger SAS. - * - * 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. - *****************************************************************************/ - -#include // bool - -#include "crypto_helpers.h" - -#include "validate.h" -#include "../menu.h" -#include "../../sw.h" -#include "../../globals.h" -#include "../../helper/send_response.h" - -void validate_pubkey(bool choice) { - if (choice) { - helper_send_response_pubkey(); - } else { - io_send_sw(SW_DENY); - } -} - -static int crypto_sign_message(void) { - uint32_t info = 0; - size_t sig_len = sizeof(G_context.tx_info.signature); - - cx_err_t error = bip32_derive_ecdsa_sign_hash_256(CX_CURVE_256K1, - G_context.bip32_path, - G_context.bip32_path_len, - CX_RND_RFC6979 | CX_LAST, - CX_SHA256, - G_context.tx_info.m_hash, - sizeof(G_context.tx_info.m_hash), - G_context.tx_info.signature, - &sig_len, - &info); - if (error != CX_OK) { - return -1; - } - - PRINTF("Signature: %.*H\n", sig_len, G_context.tx_info.signature); - - G_context.tx_info.signature_len = sig_len; - G_context.tx_info.v = (uint8_t)(info & CX_ECCINFO_PARITY_ODD); - - return 0; -} - -void validate_transaction(bool choice) { - if (choice) { - G_context.state = STATE_APPROVED; - - if (crypto_sign_message() != 0) { - G_context.state = STATE_NONE; - io_send_sw(SW_SIGNATURE_FAIL); - } else { - helper_send_response_sig(); - } - } else { - G_context.state = STATE_NONE; - io_send_sw(SW_DENY); - } -} diff --git a/src/ui/action/validate.h b/src/ui/action/validate.h deleted file mode 100644 index ca1ba632..00000000 --- a/src/ui/action/validate.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include // bool - -/** - * Action for public key validation and export. - * - * @param[in] choice - * User choice (either approved or rejected). - * - */ -void validate_pubkey(bool choice); - -/** - * Action for transaction information validation. - * - * @param[in] choice - * User choice (either approved or rejectd). - * - */ -void validate_transaction(bool choice); diff --git a/src/ui/bagl_display.c b/src/ui/bagl_display.c deleted file mode 100644 index cf60c3b4..00000000 --- a/src/ui/bagl_display.c +++ /dev/null @@ -1,173 +0,0 @@ -/***************************************************************************** - * Ledger App Boilerplate. - * (c) 2020 Ledger SAS. - * - * 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. - *****************************************************************************/ - -#ifdef HAVE_BAGL - -#include // bool -#include // memset - -#include "os.h" -#include "ux.h" -#include "glyphs.h" -#include "io.h" -#include "bip32.h" -#include "format.h" - -#include "display.h" -#include "constants.h" -#include "../globals.h" -#include "../sw.h" -#include "../address.h" -#include "action/validate.h" -#include "../transaction/types.h" -#include "../menu.h" - -static action_validate_cb g_validate_callback; -static char g_amount[30]; -static char g_address[43]; - -// Validate/Invalidate public key and go back to home -static void ui_action_validate_pubkey(bool choice) { - validate_pubkey(choice); - ui_menu_main(); -} - -// Validate/Invalidate transaction and go back to home -static void ui_action_validate_transaction(bool choice) { - validate_transaction(choice); - ui_menu_main(); -} - -// Step with icon and text -UX_STEP_NOCB(ux_display_confirm_addr_step, pn, {&C_icon_eye, "Confirm Address"}); -// Step with title/text for address -UX_STEP_NOCB(ux_display_address_step, - bnnn_paging, - { - .title = "Address", - .text = g_address, - }); -// Step with approve button -UX_STEP_CB(ux_display_approve_step, - pb, - (*g_validate_callback)(true), - { - &C_icon_validate_14, - "Approve", - }); -// Step with reject button -UX_STEP_CB(ux_display_reject_step, - pb, - (*g_validate_callback)(false), - { - &C_icon_crossmark, - "Reject", - }); - -// FLOW to display address: -// #1 screen: eye icon + "Confirm Address" -// #2 screen: display address -// #3 screen: approve button -// #4 screen: reject button -UX_FLOW(ux_display_pubkey_flow, - &ux_display_confirm_addr_step, - &ux_display_address_step, - &ux_display_approve_step, - &ux_display_reject_step); - -int ui_display_address() { - if (G_context.req_type != CONFIRM_ADDRESS || G_context.state != STATE_NONE) { - G_context.state = STATE_NONE; - return io_send_sw(SW_BAD_STATE); - } - memset(g_address, 0, sizeof(g_address)); - uint8_t address[ADDRESS_LEN] = {0}; - if (!address_from_pubkey(G_context.pk_info.raw_public_key, address, sizeof(address))) { - return io_send_sw(SW_DISPLAY_ADDRESS_FAIL); - } - - if (format_hex(address, sizeof(address), g_address, sizeof(g_address)) == -1) { - return io_send_sw(SW_DISPLAY_ADDRESS_FAIL); - } - - g_validate_callback = &ui_action_validate_pubkey; - - ux_flow_init(0, ux_display_pubkey_flow, NULL); - return 0; -} - -// Step with icon and text -UX_STEP_NOCB(ux_display_review_step, - pnn, - { - &C_icon_eye, - "Review", - "Transaction", - }); -// Step with title/text for amount -UX_STEP_NOCB(ux_display_amount_step, - bnnn_paging, - { - .title = "Amount", - .text = g_amount, - }); - -// FLOW to display transaction information: -// #1 screen : eye icon + "Review Transaction" -// #2 screen : display amount -// #3 screen : display destination address -// #4 screen : approve button -// #5 screen : reject button -UX_FLOW(ux_display_transaction_flow, - &ux_display_review_step, - &ux_display_address_step, - &ux_display_amount_step, - &ux_display_approve_step, - &ux_display_reject_step); - -int ui_display_transaction() { - if (G_context.req_type != CONFIRM_TRANSACTION || G_context.state != STATE_PARSED) { - G_context.state = STATE_NONE; - return io_send_sw(SW_BAD_STATE); - } - - memset(g_amount, 0, sizeof(g_amount)); - char amount[30] = {0}; - if (!format_fpu64(amount, - sizeof(amount), - G_context.tx_info.transaction.value, - EXPONENT_SMALLEST_UNIT)) { - return io_send_sw(SW_DISPLAY_AMOUNT_FAIL); - } - snprintf(g_amount, sizeof(g_amount), "BOL %.*s", sizeof(amount), amount); - PRINTF("Amount: %s\n", g_amount); - - memset(g_address, 0, sizeof(g_address)); - - if (format_hex(G_context.tx_info.transaction.to, ADDRESS_LEN, g_address, sizeof(g_address)) == - -1) { - return io_send_sw(SW_DISPLAY_ADDRESS_FAIL); - } - - g_validate_callback = &ui_action_validate_transaction; - - ux_flow_init(0, ux_display_transaction_flow, NULL); - - return 0; -} - -#endif diff --git a/src/ui/display.h b/src/ui/display.h index 2168e700..8e974553 100644 --- a/src/ui/display.h +++ b/src/ui/display.h @@ -2,10 +2,17 @@ #include // bool +#include "constants.h" +#include "transaction/types.h" + /** - * Callback to reuse action with approve/reject in step FLOW. + * Enumeration with parsing state. */ -typedef void (*action_validate_cb)(bool); +typedef enum { + UI_RET_APPROVED, + UI_RET_REJECTED, + UI_RET_FAILURE +} ui_ret_e; /** * Display address on the device and ask confirmation to export. @@ -13,7 +20,9 @@ typedef void (*action_validate_cb)(bool); * @return 0 if success, negative integer otherwise. * */ -int ui_display_address(void); +ui_ret_e ui_display_address(const uint8_t raw_public_key[PUBKEY_LEN]); + +void ui_display_address_status(ui_ret_e ret); /** * Display transaction information on the device and ask confirmation to sign. @@ -21,4 +30,6 @@ int ui_display_address(void); * @return 0 if success, negative integer otherwise. * */ -int ui_display_transaction(void); +ui_ret_e ui_display_transaction(const transaction_t *transaction); + +void ui_display_transaction_status(ui_ret_e ret); diff --git a/src/ui/menu_bagl.c b/src/ui/menu_bagl.c deleted file mode 100644 index 91062a6b..00000000 --- a/src/ui/menu_bagl.c +++ /dev/null @@ -1,64 +0,0 @@ -/***************************************************************************** - * Ledger App Boilerplate. - * (c) 2020 Ledger SAS. - * - * 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. - *****************************************************************************/ - -#ifdef HAVE_BAGL - -#include "os.h" -#include "ux.h" -#include "glyphs.h" - -#include "../globals.h" -#include "menu.h" - -UX_STEP_NOCB(ux_menu_ready_step, pnn, {&C_app_boilerplate_16px, "Boilerplate", "is ready"}); -UX_STEP_NOCB(ux_menu_version_step, bn, {"Version", APPVERSION}); -UX_STEP_CB(ux_menu_about_step, pb, ui_menu_about(), {&C_icon_certificate, "About"}); -UX_STEP_VALID(ux_menu_exit_step, pb, os_sched_exit(-1), {&C_icon_dashboard_x, "Quit"}); - -// FLOW for the main menu: -// #1 screen: ready -// #2 screen: version of the app -// #3 screen: about submenu -// #4 screen: quit -UX_FLOW(ux_menu_main_flow, - &ux_menu_ready_step, - &ux_menu_version_step, - &ux_menu_about_step, - &ux_menu_exit_step, - FLOW_LOOP); - -void ui_menu_main() { - if (G_ux.stack_count == 0) { - ux_stack_push(); - } - - ux_flow_init(0, ux_menu_main_flow, NULL); -} - -UX_STEP_NOCB(ux_menu_info_step, bn, {"Boilerplate App", "(c) 2020 Ledger"}); -UX_STEP_CB(ux_menu_back_step, pb, ui_menu_main(), {&C_icon_back, "Back"}); - -// FLOW for the about submenu: -// #1 screen: app info -// #2 screen: back button to main menu -UX_FLOW(ux_menu_about_flow, &ux_menu_info_step, &ux_menu_back_step, FLOW_LOOP); - -void ui_menu_about() { - ux_flow_init(0, ux_menu_about_flow, NULL); -} - -#endif diff --git a/src/ui/menu_nbgl.c b/src/ui/menu_nbgl.c index 33109094..5e4a3c44 100644 --- a/src/ui/menu_nbgl.c +++ b/src/ui/menu_nbgl.c @@ -34,6 +34,7 @@ void app_quit(void) { os_sched_exit(-1); } +#ifndef TARGET_NANOS // home page definition void ui_menu_main(void) { // This parameter shall be set to false if the settings page contains only information @@ -157,4 +158,23 @@ void ui_menu_settings() { controls_callback); } +#else +// home page definition +void ui_menu_main(void) { +// This parameter shall be set to false if the settings page contains only information +// about the application (version , developer name, ...). It shall be set to +// true if the settings page also contains user configurable parameters related to the +// operation of the application. +#define SETTINGS_BUTTON_ENABLED (true) + + nbgl_useCaseHome(APPNAME, + &C_app_boilerplate_16px, + APPVERSION, + NULL, + NULL, + app_quit); +} + +#endif + #endif diff --git a/src/ui/nbgl_display_address.c b/src/ui/nbgl_display_address.c index 5f1bbfe8..b6e54d06 100644 --- a/src/ui/nbgl_display_address.c +++ b/src/ui/nbgl_display_address.c @@ -23,67 +23,54 @@ #include "os.h" #include "glyphs.h" #include "nbgl_use_case.h" -#include "io.h" -#include "bip32.h" +#include "nbgl_sync.h" #include "format.h" -#include "display.h" #include "constants.h" -#include "../globals.h" -#include "../sw.h" -#include "../address.h" -#include "action/validate.h" -#include "../transaction/types.h" -#include "../menu.h" - -static char g_address[43]; +#include "display.h" +#include "address.h" +#include "menu.h" -static void confirm_address_rejection(void) { - // display a status page and go back to main - validate_pubkey(false); - nbgl_useCaseStatus("Address verification\ncancelled", false, ui_menu_main); -} +ui_ret_e ui_display_address(const uint8_t raw_public_key[PUBKEY_LEN]) { + char address_str[43] = {0}; + uint8_t address_bin[ADDRESS_LEN] = {0}; -static void confirm_address_approval(void) { - // display a success status page and go back to main - validate_pubkey(true); - nbgl_useCaseStatus("ADDRESS\nVERIFIED", true, ui_menu_main); -} + if (!address_from_pubkey(raw_public_key, address_bin, sizeof(address_bin))) { + return UI_RET_FAILURE; + } -static void review_choice(bool confirm) { - if (confirm) { - confirm_address_approval(); - } else { - confirm_address_rejection(); + if (format_hex(address_bin, sizeof(address_bin), address_str, sizeof(address_str)) == -1) { + return UI_RET_FAILURE; } -} -static void continue_review(void) { - nbgl_useCaseAddressConfirmation(g_address, review_choice); -} + sync_nbgl_ret_t ret = sync_nbgl_useCaseAddressReview(address_str, + &C_app_boilerplate_64px, + "Verify BOL address", + NULL); -int ui_display_address() { - if (G_context.req_type != CONFIRM_ADDRESS || G_context.state != STATE_NONE) { - G_context.state = STATE_NONE; - return io_send_sw(SW_BAD_STATE); - } - memset(g_address, 0, sizeof(g_address)); - uint8_t address[ADDRESS_LEN] = {0}; - if (!address_from_pubkey(G_context.pk_info.raw_public_key, address, sizeof(address))) { - return io_send_sw(SW_DISPLAY_ADDRESS_FAIL); + if (ret == NBGL_SYNC_RET_SUCCESS) { + return UI_RET_APPROVED; + } else if (ret == NBGL_SYNC_RET_REJECTED) { + return UI_RET_REJECTED; + } else { + return UI_RET_FAILURE; } +} - if (format_hex(address, sizeof(address), g_address, sizeof(g_address)) == -1) { - return io_send_sw(SW_DISPLAY_ADDRESS_FAIL); +void ui_display_address_status(ui_ret_e ret) { + // Here we use async version of nbgl_useCaseStatus + // This means that upon end of timer or touch ui_menu_main will be called + // but in the meantime we return to app_main to process APDU. + // If an APDU is received before the timer ends, a new UX might be shown on + // the screen, and therefore the modal will be dismissed and its callback + // will never be called. + if (ret == UI_RET_APPROVED) { + nbgl_useCaseStatus("ADDRESS\nVERIFIED", true, ui_menu_main); + } else if (ret == UI_RET_REJECTED) { + nbgl_useCaseStatus("Address verification\ncancelled", false, ui_menu_main); + } else { + nbgl_useCaseStatus("Address verification\nissue", false, ui_menu_main); } - - nbgl_useCaseReviewStart(&C_app_boilerplate_64px, - "Verify BOL address", - NULL, - "Cancel", - continue_review, - confirm_address_rejection); - return 0; } #endif diff --git a/src/ui/nbgl_display_transaction.c b/src/ui/nbgl_display_transaction.c index 2396c8ce..1c8f24c2 100755 --- a/src/ui/nbgl_display_transaction.c +++ b/src/ui/nbgl_display_transaction.c @@ -22,111 +22,87 @@ #include "os.h" #include "glyphs.h" -#include "os_io_seproxyhal.h" #include "nbgl_use_case.h" -#include "io.h" -#include "bip32.h" +#include "nbgl_sync.h" #include "format.h" -#include "display.h" #include "constants.h" -#include "../globals.h" -#include "../sw.h" -#include "../address.h" -#include "action/validate.h" -#include "../transaction/types.h" -#include "../menu.h" - -// Buffer where the transaction amount string is written -static char g_amount[30]; -// Buffer where the transaction address string is written -static char g_address[43]; - -static nbgl_layoutTagValue_t pairs[2]; -static nbgl_layoutTagValueList_t pairList; -static nbgl_pageInfoLongPress_t infoLongPress; - -static void confirm_transaction_rejection(void) { - // display a status page and go back to main - validate_transaction(false); - nbgl_useCaseStatus("Transaction rejected", false, ui_menu_main); -} +#include "display.h" +#include "address.h" +#include "menu.h" -static void ask_transaction_rejection_confirmation(void) { - // display a choice to confirm/cancel rejection - nbgl_useCaseConfirm("Reject transaction?", - NULL, - "Yes, Reject", - "Go back to transaction", - confirm_transaction_rejection); -} +// Public function to start the transaction review +// - Check if the app is in the right state for transaction review +// - Format the amount and address strings in amount_str and address_str buffers +// - Display the first screen of the transaction review +ui_ret_e ui_display_transaction(const transaction_t *transaction) { + + // Buffer where the transaction amount string is written + char amount_str[30] = {0}; + // Buffer where the transaction address string is written + char address_str[43] = {0}; + + // Format amount and address to amount_str and address_str buffers + char amount_bin[30] = {0}; + if (!format_fpu64(amount_bin, + sizeof(amount_bin), + transaction->value, + EXPONENT_SMALLEST_UNIT)) { + return UI_RET_FAILURE; + } + snprintf(amount_str, sizeof(amount_str), "BOL %.*s", sizeof(amount_bin), amount_bin); -// called when long press button on 3rd page is long-touched or when reject footer is touched -static void review_choice(bool confirm) { - if (confirm) { - // display a status page and go back to main - validate_transaction(true); - nbgl_useCaseStatus("TRANSACTION\nSIGNED", true, ui_menu_main); - } else { - ask_transaction_rejection_confirmation(); + if (format_hex(transaction->to, ADDRESS_LEN, address_str, sizeof(address_str)) == + -1) { + return UI_RET_FAILURE; } -} -static void review_continue(void) { + nbgl_layoutTagValue_t pairs[2] = {0}; + nbgl_layoutTagValueList_t pairList = {0}; + // Setup data to display pairs[0].item = "Amount"; - pairs[0].value = g_amount; + pairs[0].value = amount_str; pairs[1].item = "Address"; - pairs[1].value = g_address; + pairs[1].value = address_str; // Setup list pairList.nbMaxLinesForValue = 0; pairList.nbPairs = 2; pairList.pairs = pairs; - // Info long press - infoLongPress.icon = &C_app_boilerplate_64px; - infoLongPress.text = "Sign transaction\nto send BOL"; - infoLongPress.longPressText = "Hold to sign"; - - nbgl_useCaseStaticReview(&pairList, &infoLongPress, "Reject transaction", review_choice); -} - -// Public function to start the transaction review -// - Check if the app is in the right state for transaction review -// - Format the amount and address strings in g_amount and g_address buffers -// - Display the first screen of the transaction review -int ui_display_transaction() { - if (G_context.req_type != CONFIRM_TRANSACTION || G_context.state != STATE_PARSED) { - G_context.state = STATE_NONE; - return io_send_sw(SW_BAD_STATE); + // Start review + sync_nbgl_ret_t ret = sync_nbgl_useCaseTransactionReview( + &pairList, + &C_app_boilerplate_64px, + "Review transaction\nto send BOL", + NULL, + "Sign transaction\nto send BOL"); + + if (ret == NBGL_SYNC_RET_SUCCESS) { + return UI_RET_APPROVED; + } else if (ret == NBGL_SYNC_RET_REJECTED) { + return UI_RET_REJECTED; + } else { + return UI_RET_FAILURE; } +} - // Format amount and address to g_amount and g_address buffers - memset(g_amount, 0, sizeof(g_amount)); - char amount[30] = {0}; - if (!format_fpu64(amount, - sizeof(amount), - G_context.tx_info.transaction.value, - EXPONENT_SMALLEST_UNIT)) { - return io_send_sw(SW_DISPLAY_AMOUNT_FAIL); - } - snprintf(g_amount, sizeof(g_amount), "BOL %.*s", sizeof(amount), amount); - memset(g_address, 0, sizeof(g_address)); +void ui_display_transaction_status(ui_ret_e ret) { + // Here we use sync version of nbgl_useCaseStatus + // This means that upon reception of any APDU during + // sync_nbgl_useCaseStatus, we will stop the status display even if the + // received APDU doesn't need an UX flow to be answered. - if (format_hex(G_context.tx_info.transaction.to, ADDRESS_LEN, g_address, sizeof(g_address)) == - -1) { - return io_send_sw(SW_DISPLAY_ADDRESS_FAIL); + if (ret == UI_RET_APPROVED) { + sync_nbgl_useCaseStatus("TRANSACTION\nSIGNED", true); + } else if (ret == UI_RET_REJECTED) { + sync_nbgl_useCaseStatus("Transaction rejected", false); + } else { + sync_nbgl_useCaseStatus("Transaction issue", false); } - // Start review - nbgl_useCaseReviewStart(&C_app_boilerplate_64px, - "Review transaction\nto send BOL", - NULL, - "Reject transaction", - review_continue, - ask_transaction_rejection_confirmation); - return 0; + ui_menu_main(); } #endif