What changed, and why it matters
This commit adds a desktop GUI mode for testing the Jade hardware wallet firmware on a regular computer. It is a development/testing feature, not a change to the production firmware that runs on real Jade devices. The code introduces a new event-dispatching shim and lets a Python/Tkinter window drive the firmware's screen and buttons. There is no indication this fixes a security vulnerability; it is a tooling enhancement.
No immediate security action required. Treat this as a normal feature commit. If using the new GUI for automated testing, review the esp_event.c shim for robustness (memory allocation failures, thread safety, and handler map limits) before relying on it for security-critical test coverage. Production firmware builds are not affected.
Security signals we found
New desktop-only GUI surface (Tkinter/Python) that calls into libjade via ctypes - only relevant to local test builds
New esp_event.c shim uses pthreads, mutexes, and a fixed-size wally_map (1000 entries) for event handler storage
Event queue items are heap-allocated and freed by the dispatcher; no size validation on event_data beyond copying the pointer
libjade_get_display_buffer returns a direct pointer to the emulated display buffer
libjade_handle_gui_event accepts integer event types and dispatches to gui_prev/gui_next/gui_front_click
CI mode (CONFIG_DEBUG_UNATTENDED_CI) remains ON by default, now explicitly controlled by CMake option
Evidence from the diff
The patch introduces CONFIG_LIBJADE_GUI, a Linux-hosted event loop (esp_event.c), a Tkinter GUI front-end (gui.py), and supporting hooks so the firmware’s GUI code can run outside the ESP32. It also adds coverage build options and CI flags. The changes are largely additive and conditional on the libjade (desktop emulation) build. Minor logic changes include making idletimer_register_activity return false in libjade, exposing _libjade_stop_requested, and adding re-entrancy guards for gui_init/display_init under CONFIG_LIBJADE. No cryptographic, authentication, or secure-storage code is modified.
Changed components
libjade (desktop emulation library)libjade/esp_event.clibjade/gui.pylibjade/libjade.cmain/display.cmain/display_hw.cmain/gui.cmain/utils/event.cGitLab CI libjade test jobsInspect captured patch +558 / −41
diff --git a/gitlab/test_libjade.yml b/gitlab/test_libjade.yml
index 36e79e1..715497a 100644
--- a/gitlab/test_libjade.yml
+++ b/gitlab/test_libjade.yml
@@ -10,7 +10,7 @@ test_libjade:
extends: .libjade_test_template
stage: pre_test
script:
- - ./libjade/make_libjade.sh Debug
+ - ./libjade/make_libjade.sh Debug --gui
- export LD_LIBRARY_PATH=$PWD/build_linux/libjade
- python ./test_jade.py --log CRITICAL --libjade
@@ -18,7 +18,7 @@ test_libjade_sanitize:
extends: .libjade_test_template
stage: test
script:
- - ./libjade/make_libjade.sh Sanitize
+ - ./libjade/make_libjade.sh Sanitize --gui
- export ASAN_OPTIONS=symbolize=1,detect_leaks=0
- export UBSAN_OPTIONS=print_stacktrace=1
- export ASAN_SO=/usr/lib/gcc/x86_64-linux-gnu/13/libasan.so
diff --git a/libjade/CMakeLists.txt b/libjade/CMakeLists.txt
index 89798fe..42bf6a4 100644
--- a/libjade/CMakeLists.txt
+++ b/libjade/CMakeLists.txt
@@ -54,6 +54,23 @@ if (NOT LOG)
add_compile_options(-DCONFIG_LOG_DEFAULT_LEVEL_NONE)
endif()
+option(COVERAGE "Enable libjade profile coverage" OFF)
+if (COVERAGE)
+ add_compile_options(-fprofile-arcs -ftest-coverage)
+ add_link_options(-lgcov --coverage)
+endif()
+
+option(GUI "Enable libjade gui" OFF)
+if (GUI)
+ add_compile_options(-DCONFIG_LIBJADE_GUI)
+endif()
+
+# CI mode defaults to ON since libjade is typically used for testing
+option(CI "Enable CI (auto-click) mode" ON)
+if (CI)
+ add_compile_options(-DCONFIG_DEBUG_UNATTENDED_CI)
+endif()
+
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN YES)
diff --git a/libjade/coverage.sh b/libjade/coverage.sh
new file mode 100755
index 0000000..e10a50f
--- /dev/null
+++ b/libjade/coverage.sh
@@ -0,0 +1,16 @@
+#! /usr/bin/env bash
+
+# Helper to generate coverage reports.
+# ./tools/coverage.sh clean : Sets coverage stats to 0.
+# ./tools/coverage.sh : Calculates coverage stats, produces
+# build_linux/lcov/index.html as output.
+lcov="lcov --directory=build_linux/ --base-directory build_linux/ --gcov-tool gcov"
+
+if [ "$1" = "clean" ]; then
+ $lcov --ignore-errors empty --zerocounters
+ $lcov --ignore-errors empty --output-file build_linux/lcov_base --capture --initial
+else
+ $lcov --output-file build_linux/lcov_result --capture --ignore-errors=gcov
+ $lcov --output-file build_linux/lcov_total --add-tracefile build_linux/lcov_base --add-tracefile build_linux/lcov_result --ignore-errors=gcov
+ genhtml --demangle-cpp -o build_linux/lcov/ build_linux/lcov_total
+fi
diff --git a/libjade/esp_event.c b/libjade/esp_event.c
new file mode 100644
index 0000000..1c75f6c
--- /dev/null
+++ b/libjade/esp_event.c
@@ -0,0 +1,246 @@
+#include "esp_event.h"
+#include "jade_assert.h"
+#include "jade_log.h"
+#include <pthread.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wally_core.h>
+#include <wally_crypto.h>
+#include <wally_map.h>
+
+#ifdef CONFIG_LIBJADE_GUI
+
+typedef struct {
+ esp_event_base_t event_base;
+ int32_t event_id;
+ void* event_data;
+ size_t event_data_size;
+} event_t;
+
+typedef struct {
+ esp_event_base_t event_base;
+ int32_t event_id;
+ esp_event_handler_t handler;
+ void* event_handler_arg;
+ esp_event_handler_instance_t instance;
+} event_handler_entry_t;
+
+struct queue_item_t {
+ struct queue_item_t* next;
+ struct queue_item_t* prev;
+ event_t payload;
+};
+
+static struct queue_item_t* _queue_head = NULL;
+static struct queue_item_t* _queue_tail = NULL;
+static pthread_mutex_t _queue_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+static struct wally_map _event_handlers;
+static pthread_mutex_t _event_handlers_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+static pthread_t _default_event_loop_task;
+static uint32_t next_entry_id = 0;
+
+void* _default_event_loop(void* params)
+{
+ while (true) {
+ // get next event from queue
+ if (pthread_mutex_lock(&_queue_mutex)) {
+ JADE_ABORT();
+ }
+ struct queue_item_t* item = _queue_head;
+ if (item) {
+ // remove from queue
+ _queue_head = item->next;
+ if (_queue_head) {
+ _queue_head->prev = NULL;
+ } else {
+ _queue_tail = NULL;
+ }
+ }
+ if (pthread_mutex_unlock(&_queue_mutex)) {
+ JADE_ABORT();
+ }
+ if (!item) {
+ // no event, sleep a bit
+ struct timespec ts = { 0, 1000000 }; // 1ms
+ nanosleep(&ts, NULL);
+ continue;
+ }
+ // dispatch event to handlers
+ if (pthread_mutex_lock(&_event_handlers_mutex)) {
+ JADE_ABORT();
+ }
+ for (size_t i = _event_handlers.num_items; i > 0; --i) {
+ event_handler_entry_t entry;
+ memcpy(&entry, _event_handlers.items[i - 1].value, sizeof(entry));
+ if ((entry.event_base == ESP_EVENT_ANY_BASE || entry.event_base == item->payload.event_base)
+ && (entry.event_id == ESP_EVENT_ANY_ID || entry.event_id == item->payload.event_id)) {
+ // call handler
+ entry.handler(entry.event_handler_arg, item->payload.event_base, item->payload.event_id,
+ item->payload.event_data);
+ }
+ }
+ if (pthread_mutex_unlock(&_event_handlers_mutex)) {
+ JADE_ABORT();
+ }
+ // free item
+ free(item);
+ }
+ return NULL;
+}
+
+esp_err_t esp_event_loop_create_default(void)
+{
+ if (_default_event_loop_task) {
+ JADE_LOGE("Default event loop already created");
+ return ESP_ERR_INVALID_STATE;
+ }
+ // init event handlers map
+ int ret = wally_map_init(1000, NULL, &_event_handlers);
+ if (ret != WALLY_OK) {
+ JADE_LOGE("Failed to initialize event handlers map");
+ return ESP_FAIL;
+ }
+ // init queue
+ JADE_ASSERT(!_queue_head);
+ JADE_ASSERT(!_queue_tail);
+ // init thread
+ esp_err_t result = ESP_FAIL;
+ if (pthread_create(&_default_event_loop_task, NULL, _default_event_loop, NULL)) {
+ goto cleanup;
+ }
+ // all succeeded
+ result = ESP_OK;
+cleanup:
+ if (result != ESP_OK) {
+ if (_default_event_loop_task) {
+ pthread_kill(_default_event_loop_task, SIGTERM);
+ _default_event_loop_task = 0;
+ }
+ }
+ return result;
+}
+
+esp_err_t esp_event_post(
+ esp_event_base_t event_base, int32_t event_id, void* event_data, size_t event_data_size, TickType_t ticks_to_wait)
+{
+ // make event_t
+ event_t event = {
+ .event_base = event_base,
+ .event_id = event_id,
+ .event_data = event_data,
+ .event_data_size = event_data_size,
+ };
+ // make queue item
+ struct queue_item_t* item = malloc(sizeof(struct queue_item_t));
+ if (!item) {
+ return ESP_ERR_NO_MEM;
+ }
+ item->next = NULL;
+ item->prev = NULL;
+ item->payload = event;
+ // add to queue
+ if (pthread_mutex_lock(&_queue_mutex)) {
+ JADE_ABORT();
+ }
+ if (!_queue_tail) {
+ _queue_head = item;
+ _queue_tail = item;
+ } else {
+ _queue_tail->next = item;
+ item->prev = _queue_tail;
+ _queue_tail = item;
+ }
+ if (pthread_mutex_unlock(&_queue_mutex)) {
+ JADE_ABORT();
+ }
+ return ESP_OK;
+}
+
+esp_err_t esp_event_handler_instance_register(esp_event_base_t event_base, int32_t event_id,
+ esp_event_handler_t event_handler, void* event_handler_arg, esp_event_handler_instance_t* instance)
+{
+ // get event handler mutex
+ if (pthread_mutex_lock(&_event_handlers_mutex)) {
+ return ESP_FAIL;
+ }
+ // create entry
+ event_handler_entry_t entry = {
+ .event_base = event_base,
+ .event_id = event_id,
+ .handler = event_handler,
+ .event_handler_arg = event_handler_arg,
+ .instance = NULL,
+ };
+ const uint32_t entry_id = ++next_entry_id;
+ JADE_LOGD("Registering event handler instance id %u (%s, %d)", entry_id, event_base, event_id);
+ entry.instance = (void*)(uintptr_t)entry_id;
+ *instance = entry.instance;
+ // add to map
+ int ret = wally_map_add_integer(&_event_handlers, entry_id, (unsigned char*)&entry, sizeof(entry));
+ JADE_ASSERT(ret == WALLY_OK);
+ // release event handler mutex
+ if (pthread_mutex_unlock(&_event_handlers_mutex)) {
+ JADE_ABORT();
+ }
+ return ESP_OK;
+}
+
+esp_err_t esp_event_handler_instance_unregister(
+ esp_event_base_t event_base, int32_t event_id, esp_event_handler_instance_t instance)
+{
+ // get event handler mutex
+ if (pthread_mutex_lock(&_event_handlers_mutex)) {
+ return ESP_FAIL;
+ }
+ // find entry
+ uint32_t entry_id = (uint32_t)(uintptr_t)instance;
+ JADE_LOGD("Unregistering event handler instance id %u (%s, %d)", entry_id, event_base, event_id);
+ const struct wally_map_item* item = wally_map_get_integer(&_event_handlers, entry_id);
+ JADE_ASSERT(item);
+ event_handler_entry_t entry;
+ memcpy(&entry, item->value, sizeof(entry));
+ JADE_ASSERT(entry.event_base == event_base);
+ JADE_ASSERT(entry.event_id == event_id);
+ JADE_ASSERT(entry.instance == instance);
+ // remove from map
+ int ret = wally_map_remove_integer(&_event_handlers, entry_id);
+ JADE_ASSERT(ret == WALLY_OK);
+ // release event handler mutex
+ if (pthread_mutex_unlock(&_event_handlers_mutex)) {
+ JADE_ABORT();
+ }
+ return ESP_OK;
+}
+
+#else
+
+esp_err_t esp_event_loop_create_default(void) { return ESP_OK; }
+
+esp_err_t esp_event_post(
+ esp_event_base_t event_base, int32_t event_id, void* event_data, size_t event_data_size, TickType_t ticks_to_wait)
+{
+ return ESP_OK;
+}
+
+esp_err_t esp_event_handler_instance_register(esp_event_base_t event_base, int32_t event_id,
+ esp_event_handler_t event_handler, void* event_handler_arg, esp_event_handler_instance_t* instance)
+{
+ return ESP_OK;
+}
+
+esp_err_t esp_event_handler_unregister(esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler)
+{
+ return ESP_OK;
+}
+
+esp_err_t esp_event_handler_instance_unregister(
+ esp_event_base_t event_base, int32_t event_id, esp_event_handler_instance_t instance)
+{
+ return ESP_OK;
+}
+
+#endif
diff --git a/libjade/gui.py b/libjade/gui.py
new file mode 100644
index 0000000..00f12f7
--- /dev/null
+++ b/libjade/gui.py
@@ -0,0 +1,100 @@
+from ctypes import POINTER, c_ubyte, c_size_t, byref
+import logging
+import tkinter as tk
+
+from jadepy.jade import JadeAPI, JadeError
+
+# Enable jade logging
+jadehandler = logging.StreamHandler()
+logger = logging.getLogger('jadepy.jade')
+logger.setLevel(logging.INFO)
+logger.addHandler(jadehandler)
+
+# set global logging level to info
+logging.basicConfig(level=logging.INFO)
+
+# Set when we connect to the software implementation
+libjade = None
+
+# enum for GUI event types (left, right, enter)
+TK_EVENT_KEY_LEFT = 1
+TK_EVENT_KEY_RIGHT = 2
+TK_EVENT_KEY_ENTER = 3
+
+_root = None
+_label = None
+
+def _window_close():
+ global _root
+ if _root:
+ _root.destroy()
+ _root = None
+
+def _key_press(event):
+ if event.keysym == 'Left' or event.keysym == 'Up':
+ libjade.libjade_handle_gui_event(TK_EVENT_KEY_LEFT)
+ elif event.keysym == 'Right' or event.keysym == 'Down':
+ libjade.libjade_handle_gui_event(TK_EVENT_KEY_RIGHT)
+ elif event.keysym == 'Return' or event.keysym == 'space':
+ libjade.libjade_handle_gui_event(TK_EVENT_KEY_ENTER)
+
+def _get_display_buffer():
+ # Fetch the raw bytes of the display buffer
+ buffer = POINTER(c_ubyte)()
+ buffer_len = c_size_t()
+ width = c_size_t()
+ height = c_size_t()
+ libjade.libjade_get_display_buffer(byref(buffer), byref(buffer_len), byref(width), byref(height))
+
+ # Convert to a binary PPM image
+ width = width.value
+ height = height.value
+ header = bytearray(f'P6\n{width} {height}\n255\n'.encode())
+ ppm = bytearray(header + bytearray(width * height * 3))
+ offset = len(header)
+ for i in range(width * height):
+ rgb565 = buffer[i*2] << 8 | buffer[i*2 + 1]
+ ppm[offset] = ((rgb565 >> 11) & 0x1F) << 3
+ offset += 1
+ ppm[offset] = ((rgb565 >> 5) & 0x3F) << 2
+ offset += 1
+ ppm[offset] = (rgb565 & 0x1F) << 3
+ offset += 1
+
+ # create PhotoImage from display PPM
+ img = tk.PhotoImage(data=bytes(ppm))
+
+ # update label
+ _label.config(image=img)
+ _label.image = img # hold on to reference
+ # if window is smaller than image, resize
+ if _root.winfo_width() < width or _root.winfo_height() < height:
+ _root.geometry(f'{width}x{height}')
+ _label.pack()
+ pass
+
+ _root.after(10, _get_display_buffer)
+
+def tk_basic_gui():
+ global _root, _label
+ _root = tk.Tk()
+ _root.title('libjade GUI')
+ _root.geometry('200x100')
+ _label = tk.Label(_root, text='Waiting for framebuffer updates...')
+ _label.pack()
+ _root.protocol("WM_DELETE_WINDOW", _window_close)
+ _root.bind('<Key>', _key_press)
+ _root.after(10, _get_display_buffer)
+ _root.mainloop()
+
+if __name__ == '__main__':
+ # Connect jade
+ jade = JadeAPI.create_libjade(timeout=0)
+ jade.connect()
+ libjade = jade.jade.impl.libjade
+ # start GUI
+ tk_basic_gui()
+ logger.debug('gui closed')
+ jade.disconnect()
+ logger.debug('jade disconnected')
+ exit(0)
diff --git a/libjade/include/esp_err.h b/libjade/include/esp_err.h
index 78bf174..4fe2dd7 100644
--- a/libjade/include/esp_err.h
+++ b/libjade/include/esp_err.h
@@ -9,5 +9,7 @@ typedef int esp_err_t;
#define ESP_ERR_NVS_NOT_FOUND 3
#define ESP_ERR_NVS_NO_FREE_PAGES 4
#define ESP_ERR_NVS_NEW_VERSION_FOUND 5
+#define ESP_ERR_NO_MEM 6
+#define ESP_ERR_INVALID_STATE 7
#endif // __LIBJADE_ESP_ERR__
diff --git a/libjade/include/esp_event.h b/libjade/include/esp_event.h
index 9b7012b..5a36ab1 100644
--- a/libjade/include/esp_event.h
+++ b/libjade/include/esp_event.h
@@ -5,7 +5,8 @@
#include <freertos/task.h>
typedef const char* esp_event_base_t;
-typedef void* esp_event_handler_t;
+typedef void (*esp_event_handler_t)(
+ void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
typedef void* esp_event_handler_instance_t;
typedef void* esp_event_loop_handle_t;
@@ -16,29 +17,15 @@ typedef void* esp_event_loop_handle_t;
#define ESP_EVENT_ANY_BASE NULL
#define ESP_EVENT_ANY_ID -1
-static inline esp_err_t esp_event_loop_create_default(void) { return ESP_OK; }
-
-static inline esp_err_t esp_event_post(
- esp_event_base_t event_base, int32_t event_id, void* event_data, size_t event_data_size, TickType_t ticks_to_wait)
-{
- return ESP_OK;
-}
-
-static inline esp_err_t esp_event_handler_instance_register(esp_event_base_t event_base, int32_t event_id,
- esp_event_handler_t event_handler, void* event_handler_arg, esp_event_handler_instance_t* instance)
-{
- return ESP_OK;
-}
-
-static inline esp_err_t esp_event_handler_unregister(
- esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler)
-{
- return ESP_OK;
-}
-
-static inline esp_err_t esp_event_handler_instance_unregister(
- esp_event_base_t event_base, int32_t event_id, esp_event_handler_instance_t instance)
-{
- return ESP_OK;
-}
+esp_err_t esp_event_loop_create_default(void);
+
+esp_err_t esp_event_post(
+ esp_event_base_t event_base, int32_t event_id, void* event_data, size_t event_data_size, TickType_t ticks_to_wait);
+
+esp_err_t esp_event_handler_instance_register(esp_event_base_t event_base, int32_t event_id,
+ esp_event_handler_t event_handler, void* event_handler_arg, esp_event_handler_instance_t* instance);
+
+esp_err_t esp_event_handler_instance_unregister(
+ esp_event_base_t event_base, int32_t event_id, esp_event_handler_instance_t instance);
+
#endif // _LIBJADE_ESP_EVENT_H_
diff --git a/libjade/include/sdkconfig.h b/libjade/include/sdkconfig.h
index b409d2c..aa1298e 100644
--- a/libjade/include/sdkconfig.h
+++ b/libjade/include/sdkconfig.h
@@ -6,15 +6,16 @@
// Export debug mode functions for testing
#define CONFIG_DEBUG_MODE 1
-// Auto "press" OK buttons when tasks are run (after 1ms)
-#define CONFIG_DEBUG_UNATTENDED_CI 1
+// In CI mode, auto "press" OK buttons after 1 millisecond
#define CONFIG_DEBUG_UNATTENDED_CI_TIMEOUT_MS 1
// Tell the firmware code we are building libjade
#define CONFIG_LIBJADE 1
// libjade currently has no GUI support
+#ifndef CONFIG_LIBJADE_GUI
#define CONFIG_LIBJADE_NO_GUI 1
+#endif
// Users can define CONFIG_LIBJADE_NO_SPIRAM to disable SPIRAM emulation
// (e.g. to allow testing DIY devices)
diff --git a/libjade/libjade.c b/libjade/libjade.c
index 9ef709c..00893c4 100644
--- a/libjade/libjade.c
+++ b/libjade/libjade.c
@@ -51,7 +51,8 @@
// https://github.com/richgel999/miniz with a couple of additional
// patches for memory safety.
#include "miniz.c"
-// Include the emulation of the o/s task functions
+// Include the emulation of the o/s task/event functions
+#include "esp_event.c"
#include "task.c"
// Include the esp32_deflate component
#define ESP_PLATFORM 1
@@ -106,7 +107,7 @@ const char* locale_lang_with_fallback(const locale_multilang_string_t* str, jloc
// main/idletimer.c
void idletimer_init(void) {}
-bool idletimer_register_activity(const bool is_ui) { return true; }
+bool idletimer_register_activity(const bool is_ui) { return false; }
void idletimer_set_min_timeout_secs(uint16_t min_timeout_secs) {};
// main/logging.c
@@ -184,7 +185,7 @@ const uint8_t _binary_pinserver_public_key_pub_start[33]
0xa2, 0x64, 0x28, 0x16, 0x0a, 0x27, 0xbd, 0xbf, 0xc3, 0x0b, 0x34, 0xec, 0x87, 0xc5, 0x47 };
// Events
-static volatile bool _libjade_stop_requested = false; // Used to stop the firmware
+volatile bool _libjade_stop_requested = false; // Used to stop the firmware
#ifdef CONFIG_LIBJADE_NO_GUI
void sync_wait_event_handler(void* handler_arg, esp_event_base_t base, int32_t id, void* event_data) {}
@@ -517,3 +518,39 @@ void libjade_set_log_level(int level)
}
#endif
}
+
+void libjade_get_display_buffer(uint8_t** out_buffer, size_t* out_size, size_t* out_width, size_t* out_height)
+{
+#ifndef CONFIG_LIBJADE_NO_GUI
+ *out_buffer = (uint8_t*)display_hw_get_buffer();
+ *out_size = CONFIG_DISPLAY_WIDTH * CONFIG_DISPLAY_HEIGHT * sizeof(color_t);
+ *out_width = CONFIG_DISPLAY_WIDTH;
+ *out_height = CONFIG_DISPLAY_HEIGHT;
+#else
+ *out_buffer = NULL;
+ *out_size = 0;
+ *out_width = 0;
+ *out_height = 0;
+#endif
+}
+
+void libjade_handle_gui_event(int event_type)
+{
+#ifndef CONFIG_LIBJADE_NO_GUI
+ JADE_LOGI("libjade_handle_gui_event: event_type=%d", event_type);
+ switch (event_type) {
+ case 1:
+ gui_prev();
+ break;
+ case 2:
+ gui_next();
+ break;
+ case 3:
+ gui_front_click();
+ break;
+ default:
+ JADE_LOGW("libjade_handle_gui_event: unknown event type %d", event_type);
+ break;
+ }
+#endif
+}
diff --git a/libjade/libjade.h b/libjade/libjade.h
index 3e80946..188ecf7 100644
--- a/libjade/libjade.h
+++ b/libjade/libjade.h
@@ -53,4 +53,16 @@ LIBJADE_API void libjade_release(uint8_t* data);
*/
LIBJADE_API void libjade_set_log_level(int level);
+/*
+ * Get the display buffer for the global libjade instance.
+ * A pointer to the raw pixel data in RGB565
+ */
+LIBJADE_API void libjade_get_display_buffer(
+ uint8_t** out_buffer, size_t* out_size, size_t* out_width, size_t* out_height);
+
+/*
+ * Handle a GUI event (1 = left button, 2 = right button, 3 = enter button)
+ */
+LIBJADE_API void libjade_handle_gui_event(int event_type);
+
#endif /* _LIBJADE_H_ */
diff --git a/libjade/make_libjade.sh b/libjade/make_libjade.sh
index fa89d77..6c3cdd0 100755
--- a/libjade/make_libjade.sh
+++ b/libjade/make_libjade.sh
@@ -2,12 +2,14 @@
#
# Build the Jade firmware into a shared library for in-process debugging
#
-# ./libjade/make_libjade.sh [Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize] [--log]
+# ./libjade/make_libjade.sh [Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize] [--log] [--gui] [--no-ci] [--coverage]
#
set -e
BUILD_TYPE="Debug"
LOG="0"
+GUI="0"
+CI="CI"
# iterate through optional arguments and set variables accordingly
for arg in "$@"; do
@@ -16,13 +18,25 @@ for arg in "$@"; do
BUILD_TYPE="$arg"
shift
;;
+ --coverage)
+ COVERAGE="COVERAGE"
+ shift
+ ;;
--log)
LOG="LOG"
shift
;;
+ --gui)
+ GUI="GUI"
+ shift
+ ;;
+ --no-ci)
+ CI="0"
+ shift
+ ;;
*)
echo "Unknown argument: $arg"
- echo "Usage: $0 [Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize] [--log]"
+ echo "Usage: $0 [Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize] [--log] [--gui] [--no-ci] [--coverage]"
exit 1
;;
esac
@@ -30,11 +44,11 @@ done
mkdir -p build_linux
cd build_linux
+EXTRA_ARGS=''
if [ "${BUILD_TYPE}" == "Sanitize" ]; then
- cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DCMAKE_C_FLAGS"-fsanitize=undefined" -DCMAKE_CXX_FLAGS"-fsanitize=undefined" -DLOG=${LOG} ..
-else
- cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DLOG=${LOG} ..
+ EXTRA_ARGS='-DCMAKE_C_FLAGS"-fsanitize=undefined" -DCMAKE_CXX_FLAGS"-fsanitize=undefined"'
fi
+cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${EXTRA_ARGS} -DLOG=${LOG} -DCOVERAGE=${COVERAGE} -DGUI=${GUI} -DCI=${CI} ..
make -j8
cd ..
diff --git a/libjade/run_libjade_gui.sh b/libjade/run_libjade_gui.sh
new file mode 100755
index 0000000..b6e4449
--- /dev/null
+++ b/libjade/run_libjade_gui.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+set -e
+
+BUILD_TYPE="${1:-Debug}"
+
+SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
+JADE_PATH=$(realpath $SCRIPT_DIR/..)
+if [ ! -d "$JADE_PATH" ]; then
+ echo "Error: Jade directory not found at $JADE_PATH"
+ exit 1
+fi
+
+echo "--------------------------------"
+echo "Building libjade..."
+echo "--------------------------------"
+$JADE_PATH/libjade/make_libjade.sh $BUILD_TYPE --log --gui --no-ci
+echo "--------------------------------"
+echo "Running Jade GUI..."
+echo "--------------------------------"
+export LD_LIBRARY_PATH=$JADE_PATH/build_linux/libjade:$LD_LIBRARY_PATH
+echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH"
+echo "--------------------------------"
+if [ "$BUILD_TYPE" == "Sanitize" ]; then
+ export ASAN_OPTIONS=symbolize=1:detect_leaks=0
+ echo "ASAN_OPTIONS=$ASAN_OPTIONS"
+ echo "--------------------------------"
+ export LD_PRELOAD=$(ls /usr/lib/gcc/x86_64-linux-gnu/*/libasan.so | tail -n1)
+ echo "LD_PRELOAD=$LD_PRELOAD"
+ echo "--------------------------------"
+ export UBSAN_OPTIONS=print_stacktrace=1
+ echo "UBSAN_OPTIONS=$UBSAN_OPTIONS"
+ echo "--------------------------------"
+fi
+python $JADE_PATH/libjade/gui.py
diff --git a/libjade/task.c b/libjade/task.c
index dc50d4c..d13778c 100644
--- a/libjade/task.c
+++ b/libjade/task.c
@@ -1,7 +1,6 @@
#define _GNU_SOURCE 1 // For extra pthread functions
#include "freertos/timecvt.h"
#include "jade_assert.h"
-#include "sdkconfig.h"
#include <limits.h>
#include <pthread.h>
#include <signal.h>
diff --git a/main/display.c b/main/display.c
index b85b3ae..07682f0 100644
--- a/main/display.c
+++ b/main/display.c
@@ -55,6 +55,8 @@ static inline void draw_bitmap(int x, int y, int w, int h, const uint16_t* color
#if defined(CONFIG_HAS_CAMERA)
qemu_draw_bitmap(x, y, w, h, color_data);
#endif
+#elif defined(CONFIG_LIBJADE)
+ display_libjade_draw_bitmap(x, y, w, h, color_data);
#else
display_hw_draw_bitmap(x, y, w, h, color_data);
#if BUF_N > 1
@@ -234,6 +236,11 @@ void display_init(TaskHandle_t* gui_h)
{
JADE_LOGI("display/screen init");
JADE_ASSERT(gui_h);
+#ifdef CONFIG_LIBJADE
+ if (*gui_h) {
+ return; // Already initialized
+ }
+#endif
JADE_ASSERT(!*gui_h);
power_screen_on();
diff --git a/main/display_hw.c b/main/display_hw.c
index f153983..2fbb4c1 100644
--- a/main/display_hw.c
+++ b/main/display_hw.c
@@ -235,6 +235,11 @@ bool display_hw_flip_orientation(const bool flipped_orientation)
void display_hw_init(TaskHandle_t* gui_handle)
{
JADE_ASSERT(gui_handle);
+#ifdef CONFIG_LIBJADE
+ if (*gui_handle) {
+ return; // Already initialized
+ }
+#endif
JADE_ASSERT(!*gui_handle);
JADE_ASSERT(!init_done);
init_done = xSemaphoreCreateBinary();
@@ -313,6 +318,26 @@ inline void display_hw_draw_bitmap(int x, int y, int w, int h, const uint16_t* c
#endif
}
+#ifdef CONFIG_LIBJADE
+inline void display_libjade_draw_bitmap(int x, int y, int w, int h, const uint16_t* color_data)
+{
+ JADE_ASSERT(color_data);
+ const int calculatedx = x - CONFIG_DISPLAY_OFFSET_X;
+ const int calculatedy = y - CONFIG_DISPLAY_OFFSET_Y;
+
+ // copy one line at the time
+ const int data_stride = w * sizeof(uint16_t);
+ uint16_t* screen_ptr = &disp_buf[calculatedx + calculatedy * CONFIG_DISPLAY_WIDTH];
+ const uint16_t* data_ptr = color_data;
+
+ for (int k = 0; k < h; ++k) {
+ jmemcpy(screen_ptr, data_ptr, data_stride);
+ screen_ptr += CONFIG_DISPLAY_WIDTH;
+ data_ptr += w;
+ }
+}
+#endif
+
#ifdef CONFIG_DISPLAY_FULL_FRAME_BUFFER
inline void display_hw_draw_rect(int x, int y, int w, int h, const uint16_t color)
{
diff --git a/main/display_hw.h b/main/display_hw.h
index 55d0d8a..bf86e8d 100644
--- a/main/display_hw.h
+++ b/main/display_hw.h
@@ -5,6 +5,9 @@
void display_hw_init(TaskHandle_t* gui_handle);
bool display_hw_flip_orientation(bool flipped_orientation);
void display_hw_draw_bitmap(int x, int y, int w, int h, const uint16_t* color_data);
+#ifdef CONFIG_LIBJADE
+void display_libjade_draw_bitmap(int x, int y, int w, int h, const uint16_t* color_data);
+#endif
#ifdef CONFIG_DISPLAY_FULL_FRAME_BUFFER
void display_hw_flush(void);
void display_hw_draw_rect(int x, int y, int w, int h, const uint16_t color_data);
diff --git a/main/gui.c b/main/gui.c
index cf55644..5860b7c 100644
--- a/main/gui.c
+++ b/main/gui.c
@@ -262,6 +262,11 @@ bool gui_set_flipped_orientation(const bool flipped_orientation)
void gui_init(TaskHandle_t* gui_h)
{
+#ifdef CONFIG_LIBJADE
+ if (gui_mutex) {
+ return; // Already initialized
+ }
+#endif
// Create mutex semaphore
gui_mutex = xSemaphoreCreateMutex();
JADE_ASSERT(gui_mutex);
@@ -294,13 +299,13 @@ void gui_init(TaskHandle_t* gui_h)
JADE_ASSERT_MSG(retval == pdPASS, "Failed to create GUI task, xTaskCreatePinnedToCore() returned %d", retval);
}
-#ifdef CONFIG_LIBJADE
+#if defined(CONFIG_LIBJADE) && !defined(CONFIG_LIBJADE_GUI)
bool gui_initialized(void) { return gui_task_handle; }
static bool gui_is_gui_task(void) { return true; }
#else
bool gui_initialized(void) { return gui_task_handle && *gui_task_handle; }
static bool gui_is_gui_task(void) { return gui_task_handle && xTaskGetCurrentTaskHandle() == *gui_task_handle; }
-#endif // ndef CONFIG_LIBJADE
+#endif
// Is this kind of node selectable?
static inline bool is_kind_selectable(enum view_node_kind kind) { return kind == BUTTON; }
diff --git a/main/utils/event.c b/main/utils/event.c
index 5271165..137a3ff 100644
--- a/main/utils/event.c
+++ b/main/utils/event.c
@@ -59,6 +59,10 @@ void sync_wait_event_handler(void* handler_arg, esp_event_base_t base, int32_t i
xSemaphoreGive(data->triggered);
}
+#ifdef CONFIG_LIBJADE
+extern volatile bool _libjade_stop_requested;
+#endif
+
// This function waits for a previously registered event to be triggered.
// NOTE: DOES NOT register any event handler - assumes one is already registered and the
// passed 'wait_event_data_t' instance should contain the relevant registration data.
@@ -69,6 +73,13 @@ esp_err_t sync_wait_event(wait_event_data_t* wait_event_data, esp_event_base_t*
{
JADE_ASSERT(wait_event_data);
+#ifdef CONFIG_LIBJADE
+ if (_libjade_stop_requested) {
+ // User requested the firmware to exit
+ pthread_exit(NULL);
+ }
+#endif
+
JADE_LOGD("Awaiting event %p (timeout = %lu)", wait_event_data, max_wait);
if (!max_wait) {
while (xSemaphoreTake(wait_event_data->triggered, portMAX_DELAY) != pdTRUE) {
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.