libjade: add camera/input/nvs support, refactor
What changed, and why it matters
This commit is a large feature/refactor patch for Blockstream Jade's libjade (a desktop emulator of the Jade hardware wallet). It adds RPC endpoints that let a host application read the emulated device's screen, push fake button presses, read/write all emulated flash storage (NVS), and feed camera frames. The README explicitly warns that these RPCs mean 'a libjade instance is NOT SECURE FROM EXTERNAL APPLICATIONS, and that SECRET DATA CAN BE READ FROM THE RUNNING FIRMWARE.' The change is intended for development/testing only, but it removes the option to build without GUI support and makes the GUI code always compiled in. The security relevance is therefore primarily about increased attack surface and accidental misuse of a testing tool, rather than a vulnerability in production hardware wallets.
Treat this commit as a deliberate expansion of libjade's insecure-by-design testing surface. Users should ensure libjade is only used in isolated development/test environments, never with real seed phrases or private keys. Review access controls on the daemon socket/serial interface and on the nvs_flash.bin file. If a production build of Jade firmware is derived from this code, verify that the libjade-specific RPCs and NVS/camera stubs are not compiled into real hardware releases. No code fix is required for the stated use case, but documentation warnings should be heeded.
Security signals we found
New RPC endpoints allow external read/write of emulated NVS storage
New RPC endpoints allow external screen capture and synthetic input injection
New RPC endpoints allow external camera frame injection
README explicitly states the emulated device is not secure from external applications and secret data can be read
Build option to compile without GUI support is removed; GUI code is always included
Example GUI now includes an interactive Python console with eval/exec of arbitrary commands
NVS storage is persisted to a local file (nvs_flash.bin) and loaded/saved via RPC
Evidence from the diff
The patch refactors libjade so the GUI/camera/input/NVS code is always built, replacing the previous CONFIG_LIBJADE_NO_GUI stub path. New libjade-specific RPC handlers in libjade/libjade.c expose send_input, get_display_bytes, get_display_size, set_camera_bytes, get_nvs and set_nvs. The example GUI (libjade/gui.py) is expanded to use these RPCs, including an interactive Python console and camera frame injection. NVS is serialized to a file (nvs_flash.bin) and can be loaded/saved over RPC, meaning secrets persisted in emulated flash are accessible to any process that can talk to libjade. The README documents this as an intentional, insecure-by-design testing feature. No CVE, advisory, or independent researcher attribution is present in the commit materials.
Changed components
libjade (desktop emulator library)libjade/libjade.c (RPC request handling)libjade/nvs_flash.c (NVS emulation and serialization)libjade/esp_camera.c (camera emulation)libjade/gui.py (example GUI / console)libjade/daemon.c (daemon mode)main/gui.c (GUI task lifecycle)main/camera.c (camera task stop support)Inspect captured patch +1806 / −758
diff --git a/.gitignore b/.gitignore
index d5ca9f1..40f79da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ managed_components/
dist/
jade_client.egg-info/
docs/_build/
+**/nvs_flash.bin
diff --git a/gitlab/test_libjade.yml b/gitlab/test_libjade.yml
index b639d20..0b4c163 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 --gui
+ - ./libjade/make_libjade.sh Debug
- 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 --gui
+ - ./libjade/make_libjade.sh Sanitize
- 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
@@ -44,7 +44,7 @@ test_libjade_coverage:
stage: test
when: manual
script:
- - ./libjade/make_libjade.sh Debug --gui --coverage
+ - ./libjade/make_libjade.sh Debug --coverage
- ./libjade/coverage.sh clean
- export LD_LIBRARY_PATH=$PWD/build_linux/libjade
- python ./test_jade.py --log CRITICAL --libjade
diff --git a/jadepy/jade_sw.py b/jadepy/jade_sw.py
index 95b90e7..13e7015 100644
--- a/jadepy/jade_sw.py
+++ b/jadepy/jade_sw.py
@@ -78,7 +78,7 @@ class JadeSoftwareImpl:
return bytes()
self.msg = bytes([buff[i] for i in range(bytes_len.value)])
self.libjade.libjade_release(buff)
- if logger.isEnabledFor(logging.DEBUG):
+ if False and logger.isEnabledFor(logging.DEBUG):
logger.debug(f'Received message {self.msg.hex()}\n')
# Return as much of the message as the caller asked for
diff --git a/libjade/CMakeLists.txt b/libjade/CMakeLists.txt
index c0d61a7..fffa9e4 100644
--- a/libjade/CMakeLists.txt
+++ b/libjade/CMakeLists.txt
@@ -60,17 +60,17 @@ if (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()
+option(CAMERA "Enable libjade camera support" OFF)
+if (CAMERA)
+ add_compile_options(-DCONFIG_LIBJADE_CAMERA)
+endif()
+
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN YES)
diff --git a/libjade/README.md b/libjade/README.md
index 9107891..fe757a4 100644
--- a/libjade/README.md
+++ b/libjade/README.md
@@ -5,6 +5,12 @@ libjade provides the Jade firmware in a native library.
It can be thought of as an emulated or virtual Jade device that runs using
native code in the address space of the application linked to it.
+libjade exposes several RPC calls that allow an external program to read
+and write storage, fetch the screen contents, and push input events and
+camera images to/from the emulated device. These RPC calls mean that a libjade
+instance is NOT SECURE FROM EXTERNAL APPLICATIONS, and that SECRET DATA CAN
+BE READ FROM THE RUNNING FIRMWARE.
+
This initial implementation is HIGHLY EXPERIMENTAL AND INCOMPLETE, and
should UNDER NO CIRCUMSTANCES BE USED BEYOND DEVELOPMENT AND TESTING.
@@ -23,9 +29,24 @@ To build, run:
The above command builds the files `libjade.so`, `libjade_static.a` and
`libjade_daemon` in the directory `build_linux/libjade/`.
-See `libjade/libjade.h` for the exposed programmatic interface.
+Use `--help` to print other command line options.
+
+See `libjade/libjade.h` for the exposed programmatic interface, and
+the function `process_libjade_request()` in `libjade/libjade.c` to
+see the available libjade-specific RPC calls available.
+
+The standard build above can be used with `test_jade.py` to run the jade
+tests from the root directory of this repo:
+
+```
+LD_LIBRARY_PATH=./build_linux/libjade:$LD_LIBRARY_PATH python test_jade.py --libjade
+```
+
+Note that the standard build uses CI-mode, where the firmware auto-selects a
+default option (OK/continue) instead of waiting for user input. To provide
+your own user input programmatically, build with `--no-ci` (see `--help`).
-### Python
+### From Python
When the `libjade.so` shared library is available in LD_LIBRARY_PATH, the
JadeAPI.create_libjade() function can be used to run libjade in-process.
@@ -33,28 +54,40 @@ JadeAPI.create_libjade() function can be used to run libjade in-process.
The separate daemon process `libjade_daemon` can be run to expose a serial,
socket or tcp connection that the existing JadeAPI can connect to.
+### Desktop Testing
+
+An example GUI application is available in `libjade/gui.py`, allowing
+desktop interaction with a libjade instance for development and testing.
+GUI, NVS storage and camera support are available, in addition to a
+python console that allows the jade to be programmatically manipulated.
+
+To build and run the example, use:
+
+```
+./libjade/run_libjade_gui.sh [--daemon] [Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize]
+```
+
+Use `--help` to print other command line options.
+
## Status
- All message handlers are implemented. OTA is untested.
-- The library currently always runs in CI mode (automatically chooses the
- default option for a given activity). In the future the ability to provide
- input to the firmware may be added.
-- No GUI is currently exposed.
-- GUI activities are currently leaked.
-- No screen or emulation is available.
+- GUI activities arising from the dashboard process are currently leaked.
- Some operations that are expected to be constant-time are currently not.
- Sensitive stack clearing is not implemented.
- Memory is not locked from paging.
- No safety or security analysis has been performed on any code under the
`libjade/` directory.
-- The programmatic interface is not stable and may change at any time,
+- The programmatic interface is not stable and may change in future releases,
including in incompatible ways.
## Implementation
-Portions of the expected runtime environment are implemented using native
-code stubs. Some portions of the firmware itself (e.g. the gui) are also
-reimplemented as stubs.
+Portions of the expected runtime/hardware environment are implemented using
+native code stubs. Some portions of the firmware itself are also
+re-implemented as stubs.
On startup, the firmware code runs in a separate thread and processes
messages using the standard CBOR interface and the standard firmware code.
+Threads for the GUI, events etc are handled in the same way as the native
+firmware, using the host O/S threading support via pthreads.
diff --git a/libjade/daemon.c b/libjade/daemon.c
index e21a328..4ced11b 100644
--- a/libjade/daemon.c
+++ b/libjade/daemon.c
@@ -8,6 +8,7 @@
#include <fcntl.h>
#include <limits.h>
#include <netinet/in.h>
+#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -361,7 +362,10 @@ static void socket_bridge(const char* socket_path)
static int usage(const char* cmd, const char* error)
{
fprintf(stderr, "Error: %s.\n", error);
- fprintf(stderr, "Usage: %s [--serialport [SYMLINK_PATH] | --tcp PORT | --socketfile PATH]\n", cmd);
+ fprintf(stderr,
+ "Usage: %s [--serialport [SYMLINK_PATH] | --tcp PORT | --socketfile PATH]"
+ " [--log-level none|error|warn|info|debug|verbose]\n",
+ cmd);
return EXIT_FAILURE;
}
@@ -373,6 +377,7 @@ int main(int argc, char* argv[])
int tcp_port = 0;
int socket_mode = 0;
char* socket_path = NULL;
+ int log_level = ESP_LOG_NONE;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--serialport") == 0) {
@@ -402,6 +407,26 @@ int main(int argc, char* argv[])
} else {
return usage(argv[0], "--socketfile requires a PATH argument");
}
+ } else if (strcmp(argv[i], "--log-level") == 0) {
+ if (i + 1 >= argc) {
+ return usage(argv[0], "--log-level requires an argument");
+ }
+ ++i;
+ if (strcmp(argv[i], "none") == 0)
+ log_level = ESP_LOG_NONE;
+ else if (strcmp(argv[i], "error") == 0)
+ log_level = ESP_LOG_ERROR;
+ else if (strcmp(argv[i], "warn") == 0)
+ log_level = ESP_LOG_WARN;
+ else if (strcmp(argv[i], "info") == 0)
+ log_level = ESP_LOG_INFO;
+ else if (strcmp(argv[i], "debug") == 0)
+ log_level = ESP_LOG_DEBUG;
+ else if (strcmp(argv[i], "verbose") == 0)
+ log_level = ESP_LOG_VERBOSE;
+ else {
+ return usage(argv[0], "--log-level must be one of: none error warn info debug verbose");
+ }
} else {
return usage(argv[0], "Unknown option");
}
@@ -412,8 +437,7 @@ int main(int argc, char* argv[])
}
libjade_start();
- // FIXME: Add log-level cmdline parameter
- libjade_set_log_level(ESP_LOG_NONE);
+ libjade_set_log_level(log_level);
if (serial_mode) {
serial_init(serial_link_path);
diff --git a/libjade/esp_camera.c b/libjade/esp_camera.c
new file mode 100644
index 0000000..0233cb8
--- /dev/null
+++ b/libjade/esp_camera.c
@@ -0,0 +1,126 @@
+#include "esp_camera.h"
+#include "camera.h"
+#include "jade_assert.h"
+#include "jade_log.h"
+#include "sdkconfig.h"
+#include <string.h>
+#include <time.h>
+
+#ifdef CONFIG_LIBJADE_CAMERA
+
+#include <errno.h>
+#include <pthread.h>
+#include <stdbool.h>
+
+static uint8_t _cam_frame_buffer[CAMERA_IMAGE_WIDTH * CAMERA_IMAGE_HEIGHT];
+static uint64_t _cam_frame_count = 0;
+static bool _cam_stopped = false;
+static pthread_mutex_t _cam_mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t _cam_cond = PTHREAD_COND_INITIALIZER;
+
+// Called by the host to push a grayscale camera frame.
+// data must be CAMERA_IMAGE_WIDTH * CAMERA_IMAGE_HEIGHT bytes of 8-bit grayscale.
+bool libjade_push_camera_frame(const uint8_t* data, const size_t len)
+{
+ if (!data || len != sizeof(_cam_frame_buffer)) {
+ return false;
+ }
+ pthread_mutex_lock(&_cam_mutex);
+ if (!_cam_stopped) {
+ memcpy(_cam_frame_buffer, data, len);
+ _cam_frame_count++;
+ pthread_cond_signal(&_cam_cond);
+ }
+ pthread_mutex_unlock(&_cam_mutex);
+ return true;
+}
+
+esp_err_t esp_camera_init(const camera_config_t* config)
+{
+ JADE_ASSERT(config);
+ JADE_ASSERT(config->pixel_format == PIXFORMAT_GRAYSCALE);
+ JADE_ASSERT(config->frame_size == FRAMESIZE_QVGA);
+ pthread_mutex_lock(&_cam_mutex);
+ _cam_frame_count = 0;
+ _cam_stopped = false;
+ pthread_mutex_unlock(&_cam_mutex);
+ return ESP_OK;
+}
+
+esp_err_t esp_camera_deinit(void)
+{
+ pthread_mutex_lock(&_cam_mutex);
+ _cam_stopped = true;
+ pthread_cond_broadcast(&_cam_cond);
+ pthread_mutex_unlock(&_cam_mutex);
+ return ESP_OK;
+}
+
+camera_fb_t* esp_camera_fb_get(void)
+{
+ static camera_fb_t fb = {
+ .buf = _cam_frame_buffer,
+ .len = sizeof(_cam_frame_buffer),
+ .width = CAMERA_IMAGE_WIDTH,
+ .height = CAMERA_IMAGE_HEIGHT,
+ .format = PIXFORMAT_GRAYSCALE,
+ };
+ gettimeofday(&fb.timestamp, NULL);
+ pthread_mutex_lock(&_cam_mutex);
+ if (_cam_stopped) {
+ pthread_mutex_unlock(&_cam_mutex);
+ return NULL;
+ }
+ const uint64_t count_before = _cam_frame_count;
+ struct timespec deadline;
+ clock_gettime(CLOCK_REALTIME, &deadline);
+ deadline.tv_nsec += 100 * 1000000; // 100 ms
+ if (deadline.tv_nsec >= 1000000000L) {
+ deadline.tv_sec += 1;
+ deadline.tv_nsec -= 1000000000L;
+ }
+ while (_cam_frame_count == count_before && !_cam_stopped) {
+ const int rc = pthread_cond_timedwait(&_cam_cond, &_cam_mutex, &deadline);
+ if (rc == ETIMEDOUT) {
+ break;
+ }
+ }
+ pthread_mutex_unlock(&_cam_mutex);
+ // Always return the buffer (possibly stale/zero if no frame arrived yet),
+ // matching the original v4l2 behaviour of never returning NULL on timeout.
+ return &fb;
+}
+
+void esp_camera_fb_return(camera_fb_t* fb) { /* static buffer, nothing to free */ }
+
+#else
+
+esp_err_t esp_camera_init(const camera_config_t* config) { return ESP_FAIL; }
+esp_err_t esp_camera_deinit() { return ESP_FAIL; }
+camera_fb_t* esp_camera_fb_get(void) { return NULL; }
+void esp_camera_fb_return(camera_fb_t* fb) {}
+bool libjade_push_camera_frame(const uint8_t* data, const size_t len) { return false; }
+
+static const uint8_t* debug_image_data = NULL;
+void camera_set_debug_image(const uint8_t* data, const size_t len)
+{
+ JADE_ASSERT(!data == !len);
+ JADE_ASSERT(!len || len == CAMERA_IMAGE_WIDTH * CAMERA_IMAGE_HEIGHT);
+ debug_image_data = data;
+}
+
+void jade_camera_process_images(camera_process_fn_t fn, void* ctx, const bool show_ui, const char* text_label,
+ const bool show_click_button, const qr_guide_type_t qr_guide_type, const char* help_url,
+ progress_bar_t* progress_bar)
+{
+ if (debug_image_data) {
+ if (!fn(CAMERA_IMAGE_WIDTH, CAMERA_IMAGE_HEIGHT, debug_image_data, CAMERA_IMAGE_WIDTH * CAMERA_IMAGE_HEIGHT,
+ ctx)) {
+ JADE_LOGW("User callback returned false for fixed debug image - exiting camera regardless");
+ }
+ }
+}
+
+void camera_stop(void) {}
+
+#endif // CONFIG_LIBJADE_CAMERA
diff --git a/libjade/esp_event.c b/libjade/esp_event.c
index d8facfe..31ef195 100644
--- a/libjade/esp_event.c
+++ b/libjade/esp_event.c
@@ -10,8 +10,6 @@
#include <wally_crypto.h>
#include <wally_map.h>
-#ifdef CONFIG_LIBJADE_GUI
-
typedef struct {
esp_event_base_t event_base;
int32_t event_id;
@@ -40,12 +38,13 @@ 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 volatile bool _default_event_loop_running = false;
+static pthread_t _default_event_loop_task = 0;
static uint32_t next_entry_id = 0;
void* _default_event_loop(void* params)
{
- while (true) {
+ while (_default_event_loop_running) {
// get next event from queue
if (pthread_mutex_lock(&_queue_mutex)) {
JADE_ABORT();
@@ -89,6 +88,7 @@ void* _default_event_loop(void* params)
// free item
free(item);
}
+ pthread_exit(NULL);
return NULL;
}
@@ -109,21 +109,40 @@ esp_err_t esp_event_loop_create_default(void)
JADE_ASSERT(!_queue_tail);
// init thread
esp_err_t result = ESP_FAIL;
+ _default_event_loop_running = true;
if (pthread_create(&_default_event_loop_task, NULL, _default_event_loop, NULL)) {
goto cleanup;
}
+ const char* task_name = "event_loop";
+ if (pthread_setname_np(_default_event_loop_task, task_name)) {
+ JADE_LOGW("pthread_setname_np failed for task %s", task_name);
+ }
// 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;
+ esp_event_loop_delete_default();
}
}
return result;
}
+esp_err_t esp_event_loop_delete_default(void)
+{
+ if (!_default_event_loop_task) {
+ JADE_LOGE("Default event loop not created");
+ return ESP_ERR_INVALID_STATE;
+ }
+ // kill thread
+ _default_event_loop_running = false;
+ pthread_join(_default_event_loop_task, NULL);
+ _default_event_loop_task = 0;
+ // free event handlers map
+ wally_map_clear(&_event_handlers);
+ 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)
{
@@ -215,32 +234,3 @@ esp_err_t esp_event_handler_instance_unregister(
}
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.c b/libjade/gui.c
deleted file mode 100644
index e8feda4..0000000
--- a/libjade/gui.c
+++ /dev/null
@@ -1,297 +0,0 @@
-#include <stdarg.h>
-#include <string.h>
-
-#include "gui.h"
-#include "jade_assert.h"
-#include "utils/event.h"
-#include "utils/malloc_ext.h"
-
-// display.c constants
-const color_t TFT_BLACK = 0x0000;
-const color_t TFT_NAVY = 0x0F00;
-const color_t TFT_DARKGREEN = 0xE003;
-const color_t TFT_DARKCYAN = 0xEF03;
-const color_t TFT_MAROON = 0x0078;
-const color_t TFT_PURPLE = 0x0F78;
-const color_t TFT_OLIVE = 0xE07B;
-const color_t TFT_LIGHTGREY = 0x18C6;
-const color_t TFT_DARKGREY = 0xEF7B;
-const color_t TFT_BLUE = 0x1F00;
-const color_t TFT_GREEN = 0xE007;
-const color_t TFT_CYAN = 0xFF07;
-const color_t TFT_RED = 0x00F8;
-const color_t TFT_MAGENTA = 0x1FF8;
-const color_t TFT_YELLOW = 0xE0FF;
-const color_t TFT_WHITE = 0xFFFF;
-const color_t TFT_ORANGE = 0x20FD;
-const color_t TFT_GREENYELLOW = 0xE5AF;
-const color_t TFT_PINK = 0x19FE;
-// end display.c constants
-
-ESP_EVENT_DEFINE_BASE(GUI_BUTTON_EVENT);
-ESP_EVENT_DEFINE_BASE(GUI_EVENT);
-
-const color_t GUI_BLOCKSTREAM_JADE_GREEN = 0x4C04;
-const color_t GUI_BLOCKSTREAM_BUTTONBORDER_GREY = 0x0421;
-
-const color_t GUI_BLOCKSTREAM_HIGHTLIGHT_DEFAULT = GUI_BLOCKSTREAM_JADE_GREEN;
-const color_t GUI_BLOCKSTREAM_HIGHTLIGHT_ORANGE = 0xE0D3;
-const color_t GUI_BLOCKSTREAM_HIGHTLIGHT_BLUE = 0xD318;
-const color_t GUI_BLOCKSTREAM_HIGHTLIGHT_DARKGREY = 0xA210;
-const color_t GUI_BLOCKSTREAM_HIGHTLIGHT_LIGHTGREY = 0xB294;
-const color_t GUI_BLOCKSTREAM_UNHIGHTLIGHTED_DEFAULT = 0x494A;
-
-typedef struct _activity_holder_t activity_holder_t;
-struct _activity_holder_t {
- gui_activity_t activity;
- activity_holder_t* next;
-};
-
-typedef struct {
- gui_view_node_t* node_to_repaint;
- gui_activity_t* new_activity;
- activity_holder_t* to_free;
-} gui_task_job_t;
-
-// current activity being drawn on screen
-static gui_activity_t* current_activity = NULL;
-// stack of activities that currently exist
-static activity_holder_t* existing_activities = NULL;
-
-// Click/select event (ie. which button counts as 'click'/select)
-// and which gui highlight colour is in use
-static gui_event_t gui_click_event = GUI_FRONT_CLICK_EVENT;
-
-// status bar
-struct {
- bool unused;
-} status_bar;
-
-gui_event_t gui_get_click_event(void) { return gui_click_event; }
-
-void gui_set_click_event(const bool use_wheel_click)
-{
- gui_click_event = use_wheel_click ? GUI_WHEEL_CLICK_EVENT : GUI_FRONT_CLICK_EVENT;
-}
-
-color_t gui_get_highlight_color(void) { return GUI_BLOCKSTREAM_HIGHTLIGHT_DEFAULT; }
-
-void gui_set_highlight_color(const uint8_t theme) {}
-
-bool gui_get_flipped_orientation(void) { return false; }
-
-bool gui_set_flipped_orientation(const bool flipped_orientation) { return false; }
-
-void gui_init(TaskHandle_t* gui_h, const bool create_event_loop)
-{
- // create a blank activity
- current_activity = gui_make_activity();
-}
-
-bool gui_initialized(void) { return true; } // gui task started
-
-void gui_set_active(gui_view_node_t* node, bool value) {}
-
-void gui_make_activity_ex(gui_activity_t** ppact, const bool has_status_bar, const char* title, const bool managed)
-{
- JADE_INIT_OUT_PPTR(ppact);
- JADE_ASSERT(!title || has_status_bar);
-
- if (managed) {
- // Managed activity - add to activities list
- activity_holder_t* holder = JADE_CALLOC(1, sizeof(activity_holder_t));
-
- // Add to the stack of existing activities
- holder->next = existing_activities;
- existing_activities = holder;
-
- // Return the activity from within this holder
- *ppact = &holder->activity;
- } else {
- // Unmanaged - just create the activity to return
- *ppact = JADE_CALLOC(1, sizeof(gui_activity_t));
- JADE_LOGW("Created unmanaged gui activity at %p", *ppact);
- }
-}
-
-gui_activity_t* gui_make_activity(void)
-{
- gui_activity_t* activity = NULL;
- gui_make_activity_ex(&activity, false, NULL, true);
- JADE_ASSERT(activity);
- activity->selectables_wrap = true;
- return activity;
-}
-
-int32_t gui_activity_wait_button(gui_activity_t* activity, const int32_t default_event_id)
-{
- int32_t ev_id = default_event_id;
-#ifndef CONFIG_DEBUG_UNATTENDED_CI
- if (!gui_activity_wait_event(activity, GUI_BUTTON_EVENT, ESP_EVENT_ANY_ID, NULL, &ev_id, NULL, 0)) {
- ev_id = BTN_EVENT_TIMEOUT;
- }
-#else
- gui_activity_wait_event(activity, GUI_BUTTON_EVENT, ESP_EVENT_ANY_ID, NULL, NULL, NULL,
- CONFIG_DEBUG_UNATTENDED_CI_TIMEOUT_MS / portTICK_PERIOD_MS);
-#endif
- return ev_id;
-}
-
-void gui_chain_activities(const link_activity_t* link_act, linked_activities_info_t* pActInfo) {}
-
-void gui_set_parent(gui_view_node_t* child, gui_view_node_t* parent) {}
-
-void gui_make_hsplit(gui_view_node_t** ptr, enum gui_split_type kind, uint8_t parts, ...) { *ptr = NULL; }
-
-void gui_make_vsplit(gui_view_node_t** ptr, enum gui_split_type kind, uint8_t parts, ...) { *ptr = NULL; }
-
-void gui_make_button(
- gui_view_node_t** ptr, const color_t color, const color_t selected_color, const uint32_t event_id, void* args)
-{
- *ptr = NULL;
-}
-
-void gui_make_fill(gui_view_node_t** ptr, color_t color, enum fill_node_kind fill_type, gui_view_node_t* parent)
-{
- *ptr = NULL;
-}
-
-void gui_make_text(gui_view_node_t** ptr, const char* text, color_t color) { *ptr = NULL; }
-
-void gui_make_text_font(gui_view_node_t** ptr, const char* text, color_t color, uint32_t font) { *ptr = NULL; }
-
-void gui_make_icon(gui_view_node_t** ptr, const Icon* icon, color_t color, const color_t* bg_color) { *ptr = NULL; }
-
-void gui_set_icon_animation(gui_view_node_t* node, Icon* icons, const size_t num_icons, const size_t frames_per_icon) {}
-
-void gui_set_icon_to_qr(gui_view_node_t* node) {}
-
-void gui_next_qrcode_color(void) {}
-
-void gui_make_picture(gui_view_node_t** ptr, const Picture* picture) { *ptr = NULL; }
-
-void gui_make_qrguide(gui_view_node_t** ptr, color_t color) { *ptr = NULL; }
-
-void gui_set_margins(gui_view_node_t* node, uint32_t sides, ...) {}
-
-void gui_set_padding(gui_view_node_t* node, uint32_t sides, ...) {}
-
-void gui_set_borders(gui_view_node_t* node, const color_t color, const uint16_t thickness, const uint8_t borders) {}
-
-void gui_set_borders_selected_color(gui_view_node_t* node, color_t selected_color) {}
-
-void gui_set_borders_inactive_color(gui_view_node_t* node, color_t inactive_color) {}
-
-void gui_set_colors(gui_view_node_t* node, color_t color, color_t selected_color) {}
-
-void gui_set_color(gui_view_node_t* node, color_t color) {}
-
-void gui_set_align(gui_view_node_t* node, enum gui_horizontal_align halign, enum gui_vertical_align valign) {}
-
-void gui_set_text_scroll(gui_view_node_t* node, color_t background_color) {}
-
-void gui_set_text_scroll_selected(
- gui_view_node_t* node, bool only_when_selected, color_t background_color, color_t selected_background_color)
-{
-}
-
-void gui_set_text_noise(gui_view_node_t* node, color_t background_color) {}
-
-void gui_set_text_font(gui_view_node_t* node, uint32_t font) {}
-
-void gui_set_text_default_font(gui_view_node_t* node) {}
-
-void gui_update_text(gui_view_node_t* node, const char* text) {}
-
-void gui_update_icon(gui_view_node_t* node, const Icon icon, const bool repaint_parent) {}
-
-void gui_update_picture(gui_view_node_t* node, const Picture* picture, const bool repaint_parent) {}
-
-void gui_wheel_click(void) {}
-
-void gui_front_click(void) {}
-
-void gui_next(void) {}
-
-void gui_prev(void) {}
-
-void gui_set_activity_initial_selection(gui_view_node_t* node) {}
-
-void gui_activity_set_active_selection(
- gui_activity_t* activity, gui_view_node_t** nodes, size_t num_nodes, const bool* active, gui_view_node_t* selected)
-{
-}
-
-void gui_repaint(gui_view_node_t* node) {}
-
-// Call to initiate a change of current activity - optionally freeing other managed activities.
-// Can also pass a 'retain' activity which is not made current, but is retained and not freed.
-void gui_set_current_activity_ex(gui_activity_t* new_current, const bool free_managed_activities)
-{
- JADE_ASSERT(new_current);
-
- // We will post the gui task the new activity, and the list of activities it can free
- gui_task_job_t switch_info = { .node_to_repaint = NULL, .new_activity = new_current, .to_free = NULL };
-
- // If freeing others, partition existing activities into those to keep (new current and the
- // passed 'retain' activity) and those to free (all others).
- if (free_managed_activities) {
- activity_holder_t* holder = existing_activities;
- existing_activities = NULL;
-
- while (holder) {
- activity_holder_t* const next = holder->next;
-
- if (&holder->activity == new_current) {
- // Retain this activity
- holder->next = existing_activities;
- existing_activities = holder;
- } else {
- // Discard this activity
- holder->next = switch_info.to_free;
- switch_info.to_free = holder;
- }
- holder = next;
- }
-
- // Sanity check
- // 'existing_activities' should be the new current activity only, or be completely empty
- // (if current activity is an "unmanaged" activity)
- JADE_ASSERT(
- !existing_activities || ((&existing_activities->activity == new_current) && !existing_activities->next));
- }
-}
-
-// Initiate change of 'current' activity
-void gui_set_current_activity(gui_activity_t* new_current)
-{
- // Set a new activity without freeing any other activities
- gui_set_current_activity_ex(new_current, false);
-}
-
-struct wait_event_data_t {
- bool unused;
-};
-static wait_event_data_t fake_wait_event_data;
-
-wait_event_data_t* gui_activity_make_wait_event_data(gui_activity_t* activity) { return &fake_wait_event_data; }
-
-void gui_activity_register_event(
- gui_activity_t* activity, const char* event_base, uint32_t event_id, esp_event_handler_t handler, void* args)
-{
-}
-
-bool gui_activity_wait_event(gui_activity_t* activity, const char* event_base, uint32_t event_id,
- esp_event_base_t* trigger_event_base, int32_t* trigger_event_id, void** trigger_event_data, TickType_t max_wait)
-{
- if (trigger_event_id) {
- *trigger_event_id = ESP_NO_EVENT;
- }
- return ESP_OK;
-}
-
-void gui_set_activity_title(gui_activity_t* activity, const char* title) {}
-
-gui_activity_t* gui_current_activity(void) { return current_activity; }
-
-gui_activity_t* gui_display_splash(void) { return gui_make_activity(); }
diff --git a/libjade/gui.py b/libjade/gui.py
index 00f12f7..aea66ef 100644
--- a/libjade/gui.py
+++ b/libjade/gui.py
@@ -1,100 +1,872 @@
-from ctypes import POINTER, c_ubyte, c_size_t, byref
+import argparse
+import builtins
+import io
import logging
+import os
+import re
+import sys
+import threading
import tkinter as tk
+from tkinter import font as tkfont
+import types
from jadepy.jade import JadeAPI, JadeError
# Enable jade logging
jadehandler = logging.StreamHandler()
logger = logging.getLogger('jadepy.jade')
-logger.setLevel(logging.INFO)
+logger.setLevel(logging.WARNING)
logger.addHandler(jadehandler)
-# set global logging level to info
-logging.basicConfig(level=logging.INFO)
+# set global logging level (overridden in __main__ via --log-level)
+logging.basicConfig(level=logging.WARNING)
+
+# mappings from --log-level string to python logging level and ESP log level integer
+_PY_LOG_LEVELS = {
+ 'none': logging.CRITICAL,
+ 'error': logging.ERROR,
+ 'warn': logging.WARNING,
+ 'info': logging.INFO,
+ 'debug': logging.DEBUG,
+ 'verbose': logging.DEBUG,
+}
+_ESP_LOG_LEVELS = {
+ 'none': 0, 'error': 1, 'warn': 2, 'info': 3, 'debug': 4, 'verbose': 5
+}
# Set when we connect to the software implementation
-libjade = None
+_libjade_mutex = threading.Lock()
+_last_frame_data = None
+_display_width = 0
+_display_height = 0
+_camera = None
+
+
+def locked_jadeRpc(self, method, params=None, inputid=None, http_request_fn=None, long_timeout=False):
+ with _libjade_mutex:
+ return self.unlocked_jadeRpc(method, params, inputid, http_request_fn, long_timeout)
+
+
+def rpc_monkey_patch(jade):
+ jade.unlocked_jadeRpc = jade._jadeRpc
+ jade._jadeRpc = types.MethodType(locked_jadeRpc, jade)
+
+
+class CameraManager:
+ """Manages the camera capture thread and static-frame injection."""
+
+ FRAME_W = 320 # CAMERA_IMAGE_WIDTH
+ FRAME_H = 240 # CAMERA_IMAGE_HEIGHT
+
+ def __init__(self, jade):
+ self.jade = jade
+ self._thread_stop = threading.Event()
+ self._thread = None
+ self._static_frame = None
+ self._enabled = False
+
+ def use_live_camera(self):
+ self._static_frame = None
+ self._enabled = True
+ self._restart_thread()
+
+ def set_static_frame(self, frame_bytes):
+ self._static_frame = frame_bytes
+ self._enabled = True
+ self._restart_thread()
+
+ def stop(self):
+ self._enabled = False
+ self._thread_stop.set()
+
+ def shutdown(self):
+ self._thread_stop.set()
+ if self._thread and self._thread.is_alive():
+ self._thread.join(timeout=3.0)
+
+ def _restart_thread(self):
+ self._thread_stop.set()
+ if self._thread and self._thread.is_alive():
+ self._thread.join(timeout=2.0)
+ self._thread_stop.clear()
+ self._thread = threading.Thread(target=self._capture_loop, daemon=True)
+ self._thread.start()
+
+ def _send_frame(self, frame_data):
+ params = {'request': 'set_camera_bytes', 'bytes': frame_data}
+ self.jade._jadeRpc('libjade_request', params)
+
+ def _capture_loop(self):
+ """Serve static frame (once) or live webcam frames via push_fn at ~5 fps."""
+ static_frame = self._static_frame
+ if static_frame:
+ logger.info('Camera capture thread: pushing static frame')
+ if not self._thread_stop.is_set():
+ self._send_frame(static_frame)
+ logger.info('Static camera thread stopped')
+ return
+
+ try:
+ import cv2
+ except ImportError:
+ logger.warning('cv2 not available, camera frames will not be pushed')
+ return
+
+ cap = cv2.VideoCapture(0)
+ if not cap.isOpened():
+ logger.warning('Failed to open host webcam, camera frames will not be pushed')
+ return
+
+ logger.info('Camera capture thread started')
+ frames_per_second = 5
+ try:
+ while not self._thread_stop.is_set():
+ ret, frame = cap.read()
+ if not ret:
+ logger.warning('Camera read failed, stopping camera thread')
+ break
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+ resized = cv2.resize(gray, (self.FRAME_W, self.FRAME_H))
+ self._send_frame(resized.tobytes())
+ self._thread_stop.wait(1.0 / frames_per_second)
+ finally:
+ cap.release()
+ logger.info('Camera capture thread stopped')
-# 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
+TEST_MNEMONIC = 'fish inner face ginger orchard permit useful method fence \
+kidney chuckle party favorite sunset draw limb science crane oval letter \
+slot invite sadness banana'
+
+
+class ConsoleManager:
+ """Manages the interactive Python console: history, execution, and UI widgets."""
+
+ HISTORY_FILE = os.path.expanduser('~/.jade_console_history')
+ HISTORY_MAX = 1000
+
+ def __init__(self):
+ self._history = self._load_history()
+ self._history_idx = len(self._history)
+ self.locals = {} # persistent execution namespace
+ self._output = None # tk.Text widget, set by build_ui
+ self._entry = None # tk.Entry widget, set by build_ui
+ self.body = None # tk.Frame widget, set by build_ui
+
+ #
+ # History persistence
+ #
+
+ def _load_history(self):
+ try:
+ with open(self.HISTORY_FILE, encoding='utf-8') as f:
+ lines = [line.rstrip('\n') for line in f if line.strip()]
+ if len(lines) > self.HISTORY_MAX:
+ lines = lines[-self.HISTORY_MAX:]
+ with open(self.HISTORY_FILE, 'w', encoding='utf-8') as f:
+ f.write('\n'.join(lines) + '\n')
+ return lines
+ except FileNotFoundError:
+ return []
+
+ def _append_history(self, cmd):
+ try:
+ with open(self.HISTORY_FILE, 'a', encoding='utf-8') as f:
+ f.write(cmd + '\n')
+ except OSError as e:
+ logger.warning('Could not write console history: %s', e)
+
+ #
+ # Thread-safe output
+ #
+
+ def write(self, text, tag='output'):
+ """Write text to the console output widget (thread-safe)."""
+ def _do():
+ self._output.config(state=tk.NORMAL)
+ self._output.insert(tk.END, text, tag)
+ self._output.see(tk.END)
+ self._output.config(state=tk.DISABLED)
+ if threading.current_thread() is threading.main_thread():
+ _do()
+ else:
+ _root.after(0, _do)
+
+ def set_busy(self, busy):
+ """Disable/enable the entry widget while a command is running (thread-safe)."""
+ def _do():
+ self._entry.config(state=tk.DISABLED if busy else tk.NORMAL)
+ if not busy:
+ self._entry.focus_set()
+ if threading.current_thread() is threading.main_thread():
+ _do()
+ else:
+ _root.after(0, _do)
+
+ #
+ # Command execution
+ #
+
+ def _run_command(self, cmd):
+ stdout_cap, stderr_cap = io.StringIO(), io.StringIO()
+ old_stdout, old_stderr = sys.stdout, sys.stderr
+ sys.stdout, sys.stderr = stdout_cap, stderr_cap
+ try:
+ try:
+ result = eval(cmd, self.locals) # noqa: S307
+ if result is not None:
+ print(repr(result))
+ except SyntaxError:
+ exec(cmd, self.locals) # noqa: S102
+ except Exception as e:
+ print(f'{type(e).__name__}: {e}', file=sys.stderr)
+ finally:
+ sys.stdout, sys.stderr = old_stdout, old_stderr
+ out = stdout_cap.getvalue()
+ err = stderr_cap.getvalue()
+ if out:
+ self.write(out, 'output')
+ if err:
+ self.write(err, 'error')
+ self.set_busy(False)
+
+ #
+ # Key event handlers
+ #
+
+ def on_execute(self, event=None):
+ """Bound to <Return>: dispatch the typed command to a background thread."""
+ if self._entry['state'] == tk.DISABLED:
+ return 'break' # already running
+ cmd = self._entry.get().strip()
+ self._entry.delete(0, tk.END)
+ if not cmd:
+ return 'break'
+ self._history.append(cmd)
+ self._history_idx = len(self._history)
+ self._append_history(cmd)
+ self.write(f'>>> {cmd}\n', 'input')
+ self.set_busy(True)
+ threading.Thread(target=self._run_command, args=(cmd,), daemon=True).start()
+ return 'break'
+
+ def on_history_prev(self, event):
+ """Bound to <Up>: navigate to the previous history entry."""
+ if not self._history:
+ return 'break'
+ self._history_idx = max(0, self._history_idx - 1)
+ self._entry.delete(0, tk.END)
+ self._entry.insert(0, self._history[self._history_idx])
+ return 'break'
+
+ def on_history_next(self, event):
+ """Bound to <Down>: navigate to the next history entry."""
+ self._history_idx = min(len(self._history), self._history_idx + 1)
+ self._entry.delete(0, tk.END)
+ if self._history_idx < len(self._history):
+ self._entry.insert(0, self._history[self._history_idx])
+ return 'break'
+
+ def on_autocomplete(self, event=None):
+ """Bound to <Control-space>: complete the token at the cursor.
+
+ - 'obj.prefix' -> attribute completion via dir(obj)
+ - 'prefix' -> name completion from console locals + builtins
+ Single match: complete inline. Multiple matches: list in output.
+ """
+ text = self._entry.get()
+ cursor = self._entry.index(tk.INSERT)
+ before = text[:cursor]
+ m = re.search(r'[\w.]+$', before)
+ if not m:
+ return 'break'
+ token = m.group()
+ if '.' in token:
+ obj_expr, attr_prefix = token.rsplit('.', 1)
+ try:
+ obj = eval(obj_expr, self.locals) # noqa: S307
+ candidates = sorted(a for a in dir(obj)
+ if a.startswith(attr_prefix) and not a.startswith('__'))
+ except Exception:
+ return 'break'
+ else:
+ attr_prefix = token
+ all_names = list(self.locals.keys()) + dir(builtins)
+ candidates = sorted(set(n for n in all_names if n.startswith(attr_prefix)))
+ if not candidates:
+ return 'break'
+ if len(candidates) == 1:
+ self._entry.insert(cursor, candidates[0][len(attr_prefix):])
+ else:
+ common = os.path.commonprefix(candidates)
+ extra = common[len(attr_prefix):]
+ if extra:
+ self._entry.insert(cursor, extra)
+ self.write(' '.join(candidates) + '\n', 'output')
+ return 'break'
+
+ #
+ # UI construction
+ #
+
+ def build_ui(self, root, font, on_entry_focus=None):
+ """Create console widgets as children of *root*. Sets self.body and self._entry."""
+ self.body = tk.Frame(root, bg='#1e1e1e')
+
+ scrollbar = tk.Scrollbar(self.body)
+ scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
+
+ self._output = tk.Text(
+ self.body, height=10, bg='#1e1e1e', fg='#d4d4d4',
+ font=font, state=tk.DISABLED, wrap=tk.WORD,
+ yscrollcommand=scrollbar.set)
+ self._output.pack(fill=tk.BOTH, expand=True)
+ self._output.tag_config('input', foreground='#569cd6')
+ self._output.tag_config('output', foreground='#d4d4d4')
+ self._output.tag_config('error', foreground='#f44747')
+ scrollbar.config(command=self._output.yview)
+
+ entry_frame = tk.Frame(self.body, bg='#1e1e1e')
+ entry_frame.pack(fill=tk.X)
+ tk.Label(entry_frame, text='>>>', bg='#1e1e1e', fg='#569cd6', font=font).pack(
+ side=tk.LEFT, padx=(4, 0))
+ self._entry = tk.Entry(
+ entry_frame, bg='#252526', fg='#d4d4d4', insertbackground='white', font=font)
+ self._entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(2, 4), pady=2)
+ self._entry.bind('<Return>', self.on_execute)
+ self._entry.bind('<Control-space>', self.on_autocomplete)
+ self._entry.bind('<Up>', self.on_history_prev)
+ self._entry.bind('<Down>', self.on_history_next)
+ if on_entry_focus:
+ self._entry.bind('<FocusIn>', lambda e: on_entry_focus())
+
+
+_console = ConsoleManager()
+
+
+def hex_to_bin(hex_str):
+ """Convert a hex string (with or without 0x prefix / spaces) to bytes."""
+ return bytes.fromhex(hex_str.replace('0x', '').replace(' ', ''))
+
+
+def bin_to_hex(data):
+ """Convert bytes (or any iterable of ints) to a lowercase hex string."""
+ return bytes(data).hex()
+
def _window_close():
global _root
+ _camera.shutdown()
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
+
+def _render_display_frame(data, width, height):
+ """Convert an RGB565 frame to a tkinter PhotoImage and update the display."""
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]
+ rgb565 = data[i*2] << 8 | data[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
+ _label.image = img
if _root.winfo_width() < width or _root.winfo_height() < height:
- _root.geometry(f'{width}x{height}')
+ _root.geometry(f'{width}x{height + 28}') # +28 for console toggle bar
_label.pack()
- pass
- _root.after(10, _get_display_buffer)
-def tk_basic_gui():
+def jade_send_input(jade, event):
+ """Send a button click event to the connected libjade"""
+ if event.keysym in ('Left', 'Up'):
+ ev = 'left'
+ elif event.keysym in ('Right', 'Down'):
+ ev = 'right'
+ elif event.keysym in ('Return', 'space'):
+ ev = 'click'
+ jade._jadeRpc('libjade_request', {'request': 'send_input', 'event': ev})
+
+
+def jade_update_display(jade):
+ """Fetch the libjade screen contents and display them"""
+ global _last_frame_data, _display_width, _display_height
+ if not _display_width:
+ # First time through: fetch the jade display properties
+ display_size = jade._jadeRpc('libjade_request', {'request': 'get_display_size'})
+ _display_width = display_size['width']
+ _display_height = display_size['height']
+ # Fetch the current display contents
+ display_bytes = jade._jadeRpc('libjade_request', {'request': 'get_display_bytes'})
+ if display_bytes != _last_frame_data:
+ # Contents have changed: re-render the display
+ _last_frame_data = display_bytes
+ _render_display_frame(display_bytes, _display_width, _display_height)
+ # Queue another update 200ms from now
+ _root.after(200, lambda: jade_update_display(jade))
+
+
+def _open_camera_dialog(on_static_confirmed_cb, on_live_camera_cb):
+ """Open a modal dialog to either use the live webcam or load/crop a static image.
+
+ Requires Pillow (pip install Pillow) for the static-image path.
+ """
+ try:
+ from PIL import Image, ImageTk
+ except ImportError:
+ import tkinter.messagebox
+ tkinter.messagebox.showerror(
+ 'Missing dependency',
+ 'Pillow is required for the static image feature.\nInstall with: pip install Pillow')
+ # Still open the dialog so the live-camera option is accessible
+ Image = None
+ ImageTk = None
+
+ PREVIEW_MAX_W = 480
+ PREVIEW_MAX_H = 360
+ TARGET_W = CameraManager.FRAME_W
+ TARGET_H = CameraManager.FRAME_H
+ HANDLE_R = 6
+
+ dlg = tk.Toplevel(_root)
+ dlg.title('Camera Source')
+ dlg.configure(bg='#1e1e1e')
+ dlg.resizable(True, True)
+ dlg.grab_set()
+
+ state = {
+ 'pil_img': None,
+ 'scale': 1.0,
+ 'crop': [0.0, 0.0, 0.0, 0.0],
+ 'drag': None,
+ }
+
+ #
+ # Load / source buttons
+ #
+ load_frame = tk.Frame(dlg, bg='#252526')
+ load_frame.pack(fill=tk.X, padx=8, pady=(8, 4))
+
+ _btn_style = dict(bg='#3c3c3c', fg='white', activebackground='#505050',
+ activeforeground='white', relief=tk.FLAT, padx=8, pady=4)
+
+ def _use_live_camera():
+ on_live_camera_cb()
+ dlg.destroy()
+
+ tk.Button(load_frame, text='\U0001f3a5 Use Live Camera',
+ command=_use_live_camera, **_btn_style).pack(side=tk.LEFT, padx=(0, 8))
+
+ def _paste_from_clipboard():
+ import tkinter.messagebox
+ try:
+ from PIL import ImageGrab
+ img = ImageGrab.grabclipboard()
+ except Exception as exc:
+ tkinter.messagebox.showerror('Clipboard error', str(exc), parent=dlg)
+ return
+ if not isinstance(img, Image.Image):
+ tkinter.messagebox.showinfo('Clipboard', 'No image found in clipboard.', parent=dlg)
+ return
+ _set_image(img)
+
+ def _choose_file():
+ import tkinter.messagebox
+ from tkinter import filedialog
+ path = filedialog.askopenfilename(
+ parent=dlg, title='Choose image',
+ filetypes=[('Images', '*.png *.jpg *.jpeg *.bmp *.gif *.tiff *.webp'),
+ ('All files', '*.*')])
+ if not path:
+ return
+ try:
+ _set_image(Image.open(path))
+ except Exception as exc:
+ tkinter.messagebox.showerror('Error opening image', str(exc), parent=dlg)
+
+ if Image is not None:
+ tk.Button(load_frame, text='Paste from Clipboard',
+ command=_paste_from_clipboard, **_btn_style).pack(side=tk.LEFT, padx=(0, 8))
+ tk.Button(load_frame, text='Choose File\u2026',
+ command=_choose_file, **_btn_style).pack(side=tk.LEFT)
+
+ #
+ # Preview canvas (only when Pillow is available)
+ #
+ confirm_btn = None
+ if Image is not None:
+ canvas = tk.Canvas(dlg, width=PREVIEW_MAX_W, height=PREVIEW_MAX_H,
+ bg='#333333', cursor='crosshair',
+ highlightthickness=1, highlightbackground='#555555')
+ canvas.pack(padx=8, pady=4)
+ ph_ref = [None] # keeps PhotoImage alive
+
+ #
+ # Coordinate helpers
+ #
+ def _img_to_canvas(x, y):
+ s = state['scale']
+ return x * s, y * s
+
+ def _canvas_to_img(cx, cy):
+ s = state['scale']
+ img = state['pil_img']
+ if img is None:
+ return 0.0, 0.0
+ return (max(0.0, min(float(img.width), cx / s)),
+ max(0.0, min(float(img.height), cy / s)))
+
+ #
+ # Crop overlay drawing
+ #
+ def _draw_crop():
+ canvas.delete('crop')
+ img = state['pil_img']
+ c = state['crop']
+ if img is None or c[2] <= c[0] or c[3] <= c[1]:
+ return
+ cx1, cy1 = _img_to_canvas(c[0], c[1])
+ cx2, cy2 = _img_to_canvas(c[2], c[3])
+ iw_c = img.width * state['scale']
+ ih_c = img.height * state['scale']
+ # Dim everything outside the crop rectangle
+ for x0, y0, x1, y1 in [
+ (0, 0, iw_c, cy1),
+ (0, cy1, cx1, cy2),
+ (cx2, cy1, iw_c, cy2),
+ (0, cy2, iw_c, ih_c),
+ ]:
+ if x1 > x0 and y1 > y0:
+ canvas.create_rectangle(x0, y0, x1, y1,
+ fill='black', stipple='gray50', outline='', tags='crop')
+ canvas.create_rectangle(cx1, cy1, cx2, cy2,
+ outline='#00aaff', width=2, tags='crop')
+ for hx, hy in [(cx1, cy1), (cx2, cy1), (cx1, cy2), (cx2, cy2)]:
+ canvas.create_oval(hx - HANDLE_R, hy - HANDLE_R,
+ hx + HANDLE_R, hy + HANDLE_R,
+ fill='#00aaff', outline='', tags='crop')
+ w_crop, h_crop = int(c[2] - c[0]), int(c[3] - c[1])
+ info_var.set(
+ f'Crop: ({int(c[0])}, {int(c[1])}) {w_crop}x{h_crop}'
+ f' -> {TARGET_W}x{TARGET_H} (grayscale)')
+
+ def _set_image(img):
+ state['pil_img'] = img.convert('RGB')
+ iw, ih = img.width, img.height
+ s = min(PREVIEW_MAX_W / iw, PREVIEW_MAX_H / ih)
+ state['scale'] = s
+ pw, ph = max(1, int(iw * s)), max(1, int(ih * s))
+ canvas.config(width=pw, height=ph)
+ ph_ref[0] = ImageTk.PhotoImage(state['pil_img'].resize((pw, ph), Image.LANCZOS))
+ canvas.delete('all')
+ canvas.create_image(0, 0, anchor=tk.NW, image=ph_ref[0], tags='img')
+ # Default crop: centred rectangle matching TARGET aspect ratio
+ aspect = TARGET_W / TARGET_H
+ if iw / ih > aspect:
+ cw = int(ih * aspect)
+ ch = ih
+ cx0, cy0 = (iw - cw) // 2, 0
+ else:
+ cw = iw
+ ch = int(iw / aspect)
+ cx0, cy0 = 0, (ih - ch) // 2
+ state['crop'] = [float(cx0), float(cy0), float(cx0 + cw), float(cy0 + ch)]
+ _draw_crop()
+ confirm_btn.config(state=tk.NORMAL)
+
+ #
+ # Mouse drag for crop rect
+ #
+ _CURSORS = {
+ None: 'crosshair', 'move': 'fleur',
+ 'nw': 'top_left_corner', 'ne': 'top_right_corner',
+ 'sw': 'bottom_left_corner', 'se': 'bottom_right_corner',
+ 'n': 'top_side', 's': 'bottom_side', 'w': 'left_side', 'e': 'right_side',
+ }
+
+ def _hit_test(cx, cy):
+ c = state['crop']
+ if state['pil_img'] is None:
+ return None
+ x1, y1 = _img_to_canvas(c[0], c[1])
+ x2, y2 = _img_to_canvas(c[2], c[3])
+ H = HANDLE_R + 2
+ for name, hx, hy in [('nw', x1, y1), ('ne', x2, y1),
+ ('sw', x1, y2), ('se', x2, y2)]:
+ if abs(cx - hx) <= H and abs(cy - hy) <= H:
+ return name
+ if abs(cx - x1) <= 4 and y1 <= cy <= y2:
+ return 'w'
+ if abs(cx - x2) <= 4 and y1 <= cy <= y2:
+ return 'e'
+ if abs(cy - y1) <= 4 and x1 <= cx <= x2:
+ return 'n'
+ if abs(cy - y2) <= 4 and x1 <= cx <= x2:
+ return 's'
+ if x1 < cx < x2 and y1 < cy < y2:
+ return 'move'
+ return None
+
+ def _on_press(e):
+ mode = _hit_test(e.x, e.y)
+ if mode:
+ state['drag'] = (mode, e.x, e.y, list(state['crop']))
+ else:
+ ix, iy = _canvas_to_img(e.x, e.y)
+ state['crop'] = [ix, iy, ix, iy]
+ state['drag'] = ('se', e.x, e.y, list(state['crop']))
+ canvas.config(cursor=_CURSORS.get(state['drag'][0], 'crosshair'))
+
+ def _on_drag(e):
+ if not state['drag']:
+ return
+ mode, x0, y0, orig = state['drag']
+ s = state['scale']
+ dx, dy = (e.x - x0) / s, (e.y - y0) / s
+ img = state['pil_img']
+ iw, ih = float(img.width), float(img.height)
+ c = list(orig)
+ if mode == 'move':
+ cw, ch = c[2] - c[0], c[3] - c[1]
+ nx = max(0.0, min(iw - cw, c[0] + dx))
+ ny = max(0.0, min(ih - ch, c[1] + dy))
+ c = [nx, ny, nx + cw, ny + ch]
+ else:
+ if 'w' in mode:
+ c[0] = max(0.0, min(c[2] - 1, c[0] + dx))
+ if 'e' in mode:
+ c[2] = max(c[0] + 1, min(iw, c[2] + dx))
+ if 'n' in mode:
+ c[1] = max(0.0, min(c[3] - 1, c[1] + dy))
+ if 's' in mode:
+ c[3] = max(c[1] + 1, min(ih, c[3] + dy))
+ state['crop'] = c
+ _draw_crop()
+
+ def _on_release(e):
+ state['drag'] = None
+ c = state['crop']
+ state['crop'] = [min(c[0], c[2]), min(c[1], c[3]),
+ max(c[0], c[2]), max(c[1], c[3])]
+ _draw_crop()
+ canvas.config(cursor='crosshair')
+
+ def _on_motion(e):
+ if state['drag']:
+ return
+ canvas.config(cursor=_CURSORS.get(_hit_test(e.x, e.y), 'crosshair'))
+
+ canvas.bind('<ButtonPress-1>', _on_press)
+ canvas.bind('<B1-Motion>', _on_drag)
+ canvas.bind('<ButtonRelease-1>', _on_release)
+ canvas.bind('<Motion>', _on_motion)
+
+ #
+ # Info bar
+ #
+ info_var = tk.StringVar(value='Load an image above to begin')
+ info_frame = tk.Frame(dlg, bg='#252526')
+ info_frame.pack(fill=tk.X, padx=8, pady=(0, 4))
+ tk.Label(info_frame, textvariable=info_var, bg='#252526', fg='#a0a0a0',
+ font=('Monospace', 8), anchor=tk.W).pack(side=tk.LEFT, fill=tk.X, expand=True)
+ tk.Button(info_frame, text='Reset Crop',
+ command=lambda: _set_image(state['pil_img']) if state['pil_img'] else None,
+ bg='#3c3c3c', fg='white', activebackground='#505050', activeforeground='white',
+ relief=tk.FLAT, padx=6, pady=2).pack(side=tk.RIGHT)
+
+ #
+ # Bottom buttons
+ #
+ btn_frame = tk.Frame(dlg, bg='#1e1e1e')
+ btn_frame.pack(fill=tk.X, padx=8, pady=(4, 10))
+ tk.Button(btn_frame, text='Cancel', command=dlg.destroy,
+ bg='#3c3c3c', fg='white', activebackground='#505050', activeforeground='white',
+ relief=tk.FLAT, padx=12, pady=5).pack(side=tk.LEFT)
+
+ if Image is not None:
+ def _confirm():
+ img = state['pil_img']
+ if img is None:
+ return
+ c = state['crop']
+ cropped = img.crop((int(c[0]), int(c[1]), int(c[2]), int(c[3])))
+ frame = cropped.convert('L').resize((TARGET_W, TARGET_H), Image.LANCZOS)
+ on_static_confirmed_cb(frame.tobytes())
+ dlg.destroy()
+
+ confirm_btn = tk.Button(btn_frame, text='Feed to Camera', command=_confirm,
+ bg='#1c6b38', fg='white', activebackground='#238a4a',
+ activeforeground='white', relief=tk.FLAT,
+ padx=12, pady=5, state=tk.DISABLED)
+ confirm_btn.pack(side=tk.RIGHT)
+ dlg.focus_set()
+
+
+def tk_basic_gui(jade, args):
+ """Launch the Tkinter GUI"""
global _root, _label
+
+ _console.locals.update({'jade': jade, 'JadeAPI': JadeAPI, 'JadeError': JadeError,
+ 'hex_to_bin': hex_to_bin, 'bin_to_hex': bin_to_hex,
+ 'TEST_MNEMONIC': TEST_MNEMONIC})
+
_root = tk.Tk()
_root.title('libjade GUI')
- _root.geometry('200x100')
- _label = tk.Label(_root, text='Waiting for framebuffer updates...')
+ _root.resizable(True, True)
+
+ mono = tkfont.Font(family='Monospace', size=9)
+
+ # display area
+ display_frame = tk.Frame(_root, bg='black', highlightthickness=2, highlightbackground='#444', highlightcolor='#00aaff')
+ display_frame.pack(fill=tk.X)
+ _label = tk.Label(display_frame, text='Waiting for framebuffer...', bg='black', fg='white', takefocus=True)
_label.pack()
+
+ # bind navigation keys to the display label
+ for key in ('<Left>', '<Right>', '<Up>', '<Down>', '<Return>', '<space>'):
+ _label.bind(key, lambda event: jade_send_input(jade, event))
+ _label.bind('<Button-1>', lambda e: _label.focus_set())
+ display_frame.bind('<Button-1>', lambda e: _label.focus_set())
+ # highlight the display frame border to show which area is active
+ _label.bind('<FocusIn>', lambda e: display_frame.config(highlightbackground='#00aaff'))
+ _label.bind('<FocusOut>', lambda e: display_frame.config(highlightbackground='#444'))
+ _label.focus_set()
+
+ # console toggle bar
+ _console_visible = False
+ _console_welcomed = False
+
+ toggle_bar = tk.Frame(_root, bg='#252526', cursor='hand2')
+ toggle_bar.pack(fill=tk.X)
+ toggle_lbl = tk.Label(toggle_bar, text='>>>', bg='#252526', fg='#569cd6',
+ font=mono, cursor='hand2', padx=6, pady=3)
+ toggle_lbl.pack(side=tk.LEFT)
+
+ _console.build_ui(_root, mono,
+ on_entry_focus=lambda: display_frame.config(highlightbackground='#444'))
+
+ def _toggle_console():
+ nonlocal _console_visible, _console_welcomed
+ if not _console_visible:
+ # Open: reveal console
+ _console.body.pack(fill=tk.BOTH, expand=True)
+ if not _console_welcomed:
+ _console.write(
+ "Python console - 'jade', 'hex_to_bin()', 'bin_to_hex()', "
+ "'TEST_MNEMONIC' are in scope\n", 'output')
+ _console_welcomed = True
+ _console._entry.focus_set()
+ toggle_lbl.config(text='▼▼▼')
+ _console_visible = True
+ else:
+ # Close: hide console
+ _console.body.pack_forget()
+ toggle_lbl.config(text='>>>')
+ _console_visible = False
+ _label.focus_set()
+ # let Tkinter shrink/grow the window to exactly fit visible widgets
+ _root.update_idletasks()
+ _root.geometry('')
+
+ toggle_bar.bind('<Button-1>', lambda e: _toggle_console())
+ toggle_lbl.bind('<Button-1>', lambda e: _toggle_console())
+
+ # Camera button
+ def _stop_camera():
+ _camera.stop()
+ camera_btn.config(
+ text='\U0001f4f7',
+ bg='#3c3c3c',
+ command=lambda: _open_camera_dialog(_on_static_confirmed, _on_live_camera))
+
+ def _on_static_confirmed(frame_bytes):
+ _camera.set_static_frame(frame_bytes)
+ camera_btn.config(text='\u23f9 \U0001f4f7', bg='#8b2020', command=_stop_camera)
+
+ def _on_live_camera():
+ _camera.use_live_camera()
+ camera_btn.config(text='\u23f9 \U0001f4f7', bg='#8b2020', command=_stop_camera)
+
+ camera_btn = tk.Button(
+ toggle_bar, text='\U0001f4f7', font=mono,
+ command=lambda: _open_camera_dialog(_on_static_confirmed, _on_live_camera),
+ bg='#3c3c3c', fg='white', activebackground='#505050', activeforeground='white',
+ relief=tk.FLAT, padx=6, pady=3)
+ camera_btn.pack(side=tk.RIGHT, padx=(0, 4))
+
_root.protocol("WM_DELETE_WINDOW", _window_close)
- _root.bind('<Key>', _key_press)
- _root.after(10, _get_display_buffer)
+ _root.after(500, lambda: jade_update_display(jade))
_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()
+ parser = argparse.ArgumentParser(description='Jade libjade GUI')
+ grp = parser.add_mutually_exclusive_group()
+ grp.add_argument('--device', metavar='DEVICE',
+ help='Connect to daemon for CBOR via this device '
+ '(e.g. tcp:/tmp/jade.sock or tcp:localhost:30121). '
+ 'Passed directly to JadeAPI.create_serial().')
+ parser.add_argument('--log-level', metavar='LEVEL',
+ choices=['none', 'error', 'warn', 'info', 'debug', 'verbose'],
+ default='none',
+ help='Log verbosity level (default: none)')
+ parser.add_argument('--nvs-file', metavar='PATH',
+ default='nvs_flash.bin',
+ help='NVS flash storage filename (default: "nvs_flash.bin", use "none" to avoid storing')
+ args = parser.parse_args()
+
+ # set python logging level
+ py_level = _PY_LOG_LEVELS[args.log_level]
+ logging.getLogger().setLevel(py_level)
+ logger.setLevel(py_level)
+
+ if args.device:
+ # daemon mode
+ jade = JadeAPI.create_serial(args.device, timeout=120)
+ jade.connect()
+ else:
+ # in-process mode
+ jade = JadeAPI.create_libjade(timeout=120)
+ jade.connect()
+ libjade = jade.jade.impl.libjade
+ libjade.libjade_set_log_level(_ESP_LOG_LEVELS[args.log_level])
+
+ _camera = CameraManager(jade)
+
+ # Monkey patch our connected libjade to mutex RPC requests:
+ # This ensures that RPC calls don't interfere with GUI calls.
+ rpc_monkey_patch(jade)
+ if args.nvs_file != 'none':
+ # Load NVS storage into the libjade instance
+ try:
+ with open(args.nvs_file, 'rb') as f:
+ jade._jadeRpc('libjade_request', {'request': 'set_nvs', 'bytes': f.read()})
+ except FileNotFoundError:
+ # Ignore failure to load, so we can initialize a new file
+ pass
+
+ # Start GUI
+ tk_basic_gui(jade, args)
logger.debug('gui closed')
- jade.disconnect()
+
+ _camera.shutdown()
+
+ if args.nvs_file != 'none':
+ # Save NVS storage to the given file
+ data = jade._jadeRpc('libjade_request', {'request': 'get_nvs'})
+ with open(args.nvs_file, 'wb') as f:
+ f.write(data)
+
+ try:
+ jade.disconnect()
+ except Exception:
+ pass
logger.debug('jade disconnected')
- exit(0)
diff --git a/libjade/include/esp_camera.h b/libjade/include/esp_camera.h
index e69de29..dc9d03c 100644
--- a/libjade/include/esp_camera.h
+++ b/libjade/include/esp_camera.h
@@ -0,0 +1,92 @@
+#ifndef _LIBJADE_ESP_CAMERA_H_
+#define _LIBJADE_ESP_CAMERA_H_ 1
+
+#include "esp_err.h"
+#include <stddef.h>
+#include <stdint.h>
+#include <sys/time.h>
+
+typedef enum {
+ FRAMESIZE_96X96, // 96x96
+ FRAMESIZE_QQVGA, // 160x120
+ FRAMESIZE_128X128, // 128x128
+ FRAMESIZE_QCIF, // 176x144
+ FRAMESIZE_HQVGA, // 240x176
+ FRAMESIZE_240X240, // 240x240
+ FRAMESIZE_QVGA, // 320x240
+ FRAMESIZE_320X320, // 320x320
+ FRAMESIZE_CIF, // 400x296
+ FRAMESIZE_HVGA, // 480x320
+ FRAMESIZE_VGA, // 640x480
+ FRAMESIZE_SVGA, // 800x600
+ FRAMESIZE_XGA, // 1024x768
+ FRAMESIZE_HD, // 1280x720
+ FRAMESIZE_SXGA, // 1280x1024
+ FRAMESIZE_UXGA, // 1600x1200
+ // 3MP Sensors
+ FRAMESIZE_FHD, // 1920x1080
+ FRAMESIZE_P_HD, // 720x1280
+ FRAMESIZE_P_3MP, // 864x1536
+ FRAMESIZE_QXGA, // 2048x1536
+ // 5MP Sensors
+ FRAMESIZE_QHD, // 2560x1440
+ FRAMESIZE_WQXGA, // 2560x1600
+ FRAMESIZE_P_FHD, // 1080x1920
+ FRAMESIZE_QSXGA, // 2560x1920
+ FRAMESIZE_5MP, // 2592x1944
+ FRAMESIZE_INVALID
+} framesize_t;
+
+typedef enum {
+ PIXFORMAT_RGB565, // 2BPP/RGB565
+ PIXFORMAT_YUV422, // 2BPP/YUV422
+ PIXFORMAT_YUV420, // 1.5BPP/YUV420
+ PIXFORMAT_GRAYSCALE, // 1BPP/GRAYSCALE
+ PIXFORMAT_JPEG, // JPEG/COMPRESSED
+ PIXFORMAT_RGB888, // 3BPP/RGB888
+ PIXFORMAT_RAW, // RAW
+ PIXFORMAT_RGB444, // 3BP2P/RGB444
+ PIXFORMAT_RGB555, // 3BP2P/RGB555
+} pixformat_t;
+
+/**
+ * @brief Configuration structure for camera initialization
+ */
+typedef struct {
+ pixformat_t pixel_format; /*!< Format of the pixel data: PIXFORMAT_ + YUV422|GRAYSCALE|RGB565|JPEG */
+ framesize_t frame_size; /*!< Size of the output image: FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA */
+
+ int jpeg_quality; /*!< Quality of JPEG output. 0-63 lower means higher quality */
+} camera_config_t;
+
+/**
+ * @brief Data structure of camera frame buffer
+ */
+typedef struct {
+ uint8_t* buf; /*!< Pointer to the pixel data */
+ size_t len; /*!< Length of the buffer in bytes */
+ size_t width; /*!< Width of the buffer in pixels */
+ size_t height; /*!< Height of the buffer in pixels */
+ pixformat_t format; /*!< Format of the pixel data */
+ struct timeval timestamp; /*!< Timestamp since boot of the first DMA buffer of the frame */
+} camera_fb_t;
+
+esp_err_t esp_camera_init(const camera_config_t* config);
+
+esp_err_t esp_camera_deinit();
+
+/**
+ * @brief Obtain pointer to a frame buffer.
+ *
+ * @return pointer to the frame buffer
+ */
+camera_fb_t* esp_camera_fb_get(void);
+
+/**
+ * @brief Return the frame buffer to be reused again.
+ *
+ * @param fb Pointer to the frame buffer
+ */
+void esp_camera_fb_return(camera_fb_t* fb);
+
+#endif // _LIBJADE_ESP_CAMERA_H_
diff --git a/libjade/include/esp_event.h b/libjade/include/esp_event.h
index 5a36ab1..0d175c0 100644
--- a/libjade/include/esp_event.h
+++ b/libjade/include/esp_event.h
@@ -19,6 +19,8 @@ typedef void* esp_event_loop_handle_t;
esp_err_t esp_event_loop_create_default(void);
+esp_err_t esp_event_loop_delete_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);
diff --git a/libjade/include/nvs_flash.h b/libjade/include/nvs_flash.h
index 30695a5..402894a 100644
--- a/libjade/include/nvs_flash.h
+++ b/libjade/include/nvs_flash.h
@@ -41,7 +41,7 @@ esp_err_t nvs_open(const char* ns, nvs_open_mode_t open_mode, nvs_handle_t* out_
static inline void nvs_close(nvs_handle_t handle) {}
-static inline esp_err_t nvs_commit(nvs_handle_t handle) { return ESP_OK; }
+esp_err_t nvs_commit(nvs_handle_t handle);
esp_err_t nvs_erase_key(nvs_handle_t handle, const char* key);
diff --git a/libjade/include/sdkconfig.h b/libjade/include/sdkconfig.h
index aa1298e..95abd70 100644
--- a/libjade/include/sdkconfig.h
+++ b/libjade/include/sdkconfig.h
@@ -12,11 +12,6 @@
// 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)
#ifndef CONFIG_LIBJADE_NO_SPIRAM
@@ -32,7 +27,7 @@
#define CONFIG_DISPLAY_FULL_FRAME_BUFFER 1
#define CONFIG_DISPLAY_FULL_FRAME_BUFFER_DOUBLE 1
-// libjade has no camera, but supports the debug scan_qr message
+// libjade may have no camera, but supports the debug scan_qr message
#define CONFIG_HAS_CAMERA 1
#define CONFIG_IDF_FIRMWARE_CHIP_ID 0 // Needed to build
diff --git a/libjade/libjade.c b/libjade/libjade.c
index 267ad94..4cb988e 100644
--- a/libjade/libjade.c
+++ b/libjade/libjade.c
@@ -33,6 +33,7 @@
#include <errno.h>
#include <pthread.h>
+#include <stdio.h>
#include <stdlib.h>
#include <sys/random.h>
#include <sys/time.h> // Must be included before we redefine settimeofday()
@@ -51,7 +52,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/event functions
+// Include the emulation of the o/s task/event/camera functions
+#include "esp_camera.c"
#include "esp_event.c"
#include "task.c"
// Include the esp32_deflate component
@@ -82,24 +84,21 @@ static void __real_abort(void) { abort(); }
static int settimeofday_no_op(const void* yv, const void* tz) { return 0; }
#define settimeofday settimeofday_no_op
-#ifndef CONFIG_LIBJADE_NO_GUI
+#include "main/camera.h"
#include "main/display.h"
#include "main/gui.h"
typedef void* locale_multilang_string_t;
const locale_multilang_string_t* locale_get(const char* key) { return NULL; }
const char* locale_lang_with_fallback(const locale_multilang_string_t* str, jlocale_t lang) { return NULL; }
-#endif // CONFIG_LIBJADE_NO_GUI
// Include the core Jade firmware core, including wally/secp.
#define AMALGAMATED_BUILD
#include "main/amalgamated.c"
#undef settimeofday
-#ifdef CONFIG_LIBJADE_NO_GUI
-// GUI: Include our fake GUI
-#include "gui.c"
-#endif // CONFIG_LIBJADE_NO_GUI
+// Include the NVS emulation code
+#include "nvs_flash.c"
//
// Stubs for code that does not apply to libjade or is not yet implemented
@@ -138,43 +137,12 @@ esp_err_t esp_efuse_mac_get_default(uint8_t* out)
void esp_deep_sleep_start(void) { abort(); }
-// UI
-#ifdef CONFIG_LIBJADE_NO_GUI
-uint8_t GUI_DEFAULT_FONT = 0;
-uint8_t GUI_TITLE_FONT = 1;
-
-// Display
-void display_init(TaskHandle_t* task) {}
-
-Icon* get_icon(const uint8_t* const start, const uint8_t* const end) { return NULL; }
-#endif
-
+// No physical buttons
void input_init(void) {}
// Serial
bool serial_init(TaskHandle_t* task) { return true; }
-// Camera
-static const uint8_t* debug_image_data = NULL;
-void camera_set_debug_image(const uint8_t* data, const size_t len)
-{
- JADE_ASSERT(!data == !len);
- JADE_ASSERT(!len || len == CAMERA_IMAGE_WIDTH * CAMERA_IMAGE_HEIGHT);
- debug_image_data = data;
-}
-
-void jade_camera_process_images(camera_process_fn_t fn, void* ctx, const bool show_ui, const char* text_label,
- const bool show_click_button, const qr_guide_type_t qr_guide_type, const char* help_url,
- progress_bar_t* progress_bar)
-{
- if (debug_image_data) {
- if (!fn(CAMERA_IMAGE_WIDTH, CAMERA_IMAGE_HEIGHT, debug_image_data, CAMERA_IMAGE_WIDTH * CAMERA_IMAGE_HEIGHT,
- ctx)) {
- JADE_LOGW("User callback returned false for fixed debug image - exiting camera regardless");
- }
- }
-}
-
// main/ui/keyboard.c
void make_keyboard_entry_activity(keyboard_entry_t* kb_entry, const char* title) {}
@@ -187,26 +155,6 @@ const uint8_t _binary_pinserver_public_key_pub_start[33]
// Events
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) {}
-
-esp_err_t sync_wait_event(wait_event_data_t* wait_event_data, esp_event_base_t* trigger_event_base,
- int32_t* trigger_event_id, void** trigger_event_data, TickType_t max_wait)
-{
- if (_libjade_stop_requested) {
- // User requested the firmware to exit
- pthread_exit(NULL);
- }
- return ESP_NO_EVENT;
-}
-
-esp_err_t sync_await_single_event(esp_event_base_t event_base, int32_t event_id, esp_event_base_t* trigger_event_base,
- int32_t* trigger_event_id, void** trigger_event_data, TickType_t max_wait)
-{
- return ESP_OK;
-}
-#endif // CONFIG_LIBJADE_NO_GUI
-
// HW: Task API
bool run_on_temporary_stack(size_t stack_size, temporary_stack_function_t fn, void* ctx) { return fn(ctx); }
@@ -290,154 +238,6 @@ int random_mbedtls_cb(void* ctx, uint8_t* buf, const size_t len)
return 0;
}
-// HW: NVS storage
-static struct wally_map nvs_storage[5]; // Map of field name to contents
-
-esp_err_t nvs_flash_init(void) { return ESP_OK; }
-
-static struct wally_map* get_nvs_ns(const char* ns)
-{
- if (!strcmp(ns, DEFAULT_NAMESPACE)) {
- return &nvs_storage[0];
- }
- if (!strcmp(ns, MULTISIG_NAMESPACE)) {
- return &nvs_storage[1];
- }
- if (!strcmp(ns, DESCRIPTOR_NAMESPACE)) {
- return &nvs_storage[2];
- }
- if (!strcmp(ns, OTP_NAMESPACE)) {
- return &nvs_storage[3];
- }
- if (!strcmp(ns, HOTP_COUNTERS_NAMESPACE)) {
- return &nvs_storage[4];
- }
- return NULL;
-}
-
-esp_err_t nvs_open(const char* ns, nvs_open_mode_t open_mode, nvs_handle_t* out_handle)
-{
- *out_handle = get_nvs_ns(ns);
- return *out_handle ? ESP_OK : ESP_ERR_NVS_NOT_FOUND;
-}
-
-esp_err_t nvs_set_blob(nvs_handle_t handle, const char* key, const void* value, size_t length)
-{
- int ret = wally_map_replace(handle, (const unsigned char*)key, strlen(key), value, length);
- return ret == WALLY_OK ? ESP_OK : ESP_FAIL;
-}
-
-esp_err_t nvs_get_blob(nvs_handle_t handle, const char* key, void* out_value, size_t* length)
-{
- const struct wally_map_item* item = wally_map_get(handle, (const unsigned char*)key, strlen(key));
- if (!item || item->value_len > *length) {
- return ESP_ERR_NVS_NOT_FOUND;
- }
- memcpy(out_value, item->value, item->value_len);
- *length = item->value_len;
- return ESP_OK;
-}
-
-esp_err_t nvs_set_str(nvs_handle_t handle, const char* key, const char* value)
-{
- return nvs_set_blob(handle, key, value, strlen(value) + 1); // Include NUL terminator
-}
-
-esp_err_t nvs_get_str(nvs_handle_t handle, const char* key, char* out_value, size_t* length)
-{
- return nvs_get_blob(handle, key, out_value, length);
-}
-
-esp_err_t nvs_set_u32(nvs_handle_t handle, const char* key, uint32_t value)
-{
- // FIXME: endianess, if we will allow loading/saving flash
- return nvs_set_blob(handle, key, (void*)&value, sizeof(value));
-}
-
-esp_err_t nvs_get_u32(nvs_handle_t handle, const char* key, uint32_t* out_value)
-{
- // FIXME: endianess, if we will allow loading/saving flash
- size_t length = sizeof(out_value);
- return nvs_get_blob(handle, key, (void*)out_value, &length);
-}
-
-esp_err_t nvs_erase_key(nvs_handle_t handle, const char* key)
-{
- if (wally_map_remove(handle, (const unsigned char*)key, strlen(key)) != WALLY_OK) {
- return ESP_ERR_NVS_NOT_FOUND;
- }
- return ESP_OK;
-}
-
-esp_err_t nvs_entry_find(const char* part_name, const char* ns, nvs_type_t type, nvs_iterator_t* output_iterator)
-{
- *output_iterator = malloc(sizeof(**output_iterator));
- if (!*output_iterator) {
- return ESP_FAIL;
- }
- if (!((*output_iterator)->m = get_nvs_ns(ns)) || !(*output_iterator)->m->num_items) {
- goto fail;
- }
- // FIXME: Ignores type, pretty sure we only store the same type in each map?
- (*output_iterator)->idx = 0;
- return ESP_OK;
-fail:
- free(*output_iterator);
- *output_iterator = NULL;
- return ESP_ERR_NVS_NOT_FOUND;
-}
-
-esp_err_t nvs_entry_next(nvs_iterator_t* iterator)
-{
- if ((*iterator)->idx >= (*iterator)->m->num_items) {
- nvs_release_iterator(*iterator);
- *iterator = NULL;
- return ESP_ERR_NVS_NOT_FOUND;
- }
- ++(*iterator)->idx;
- return ESP_OK;
-}
-
-esp_err_t nvs_entry_info(const nvs_iterator_t iterator, nvs_entry_info_t* out_info)
-{
- // FIXME: Only sets key, as thats all we ever read
- if (!iterator || iterator->idx >= iterator->m->num_items) {
- return ESP_ERR_INVALID_ARG;
- }
- const struct wally_map_item* item = iterator->m->items + iterator->idx;
- if (item->key_len >= NVS_KEY_NAME_MAX_SIZE) {
- abort();
- }
- memcpy(out_info->key, item->key, item->key_len);
- out_info->key[item->key_len] = '\0';
- return ESP_OK;
-}
-
-void nvs_release_iterator(nvs_iterator_t iterator)
-{
- if (iterator) {
- free(iterator);
- }
-}
-
-esp_err_t nvs_flash_erase(void)
-{
- for (size_t i = 0; i < sizeof(nvs_storage) / sizeof(nvs_storage[0]); ++i) {
- wally_map_clear(&nvs_storage[i]);
- }
- return ESP_OK;
-}
-
-esp_err_t nvs_get_stats(const char* part_name, nvs_stats_t* nvs_stats)
-{
- nvs_stats->used_entries = 0;
- for (size_t i = 0; i < sizeof(nvs_storage) / sizeof(nvs_storage[0]); ++i) {
- nvs_stats->used_entries += nvs_storage[i].num_items;
- }
- nvs_stats->free_entries = ESP_NVS_TOTAL_ENTRIES - nvs_stats->used_entries;
- return ESP_OK;
-}
-
static void* jade_fw_thread_fn(void* arg)
{
start_dashboard();
@@ -445,7 +245,7 @@ static void* jade_fw_thread_fn(void* arg)
}
// External API:
-static pthread_t _libjade_thread_id; // Thread ID of the FW thread
+static pthread_t _libjade_thread_id = 0; // Thread ID of the FW thread
void libjade_start(void)
{
@@ -455,14 +255,25 @@ void libjade_start(void)
boot_process();
sensitive_assert_empty();
pthread_create(&_libjade_thread_id, NULL, &jade_fw_thread_fn, NULL);
+ pthread_setname_np(_libjade_thread_id, "libjade_fw");
}
void libjade_stop(void)
{
+ // stop camera task (if running)
+ camera_stop();
// Request the firmware to stop
+ // the main firmware thread branches down various code paths depending on what activity the user
+ // is doing, and in some of those code paths it may be waiting for an event with no timeout.
_libjade_stop_requested = true;
+ _trigger_last_wait_handle();
pthread_join(_libjade_thread_id, NULL);
+ _libjade_thread_id = 0;
_libjade_stop_requested = false;
+ // stop the gui task
+ gui_stop();
+ // clean up remaining resources
+ esp_event_loop_delete_default();
vRingbufferDelete(shared_in);
shared_in = NULL;
vRingbufferDelete(serial_out);
@@ -524,38 +335,86 @@ 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)
+static void build_display_size_reply(const void* ctx, CborEncoder* container)
{
-#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
+ JADE_ASSERT(ctx && container);
+ CborEncoder map_encoder;
+ JADE_ASSERT(cbor_encoder_create_map(container, &map_encoder, 2) == CborNoError);
+ add_uint_to_map(&map_encoder, "width", CONFIG_DISPLAY_WIDTH);
+ add_uint_to_map(&map_encoder, "height", CONFIG_DISPLAY_HEIGHT);
+ JADE_ASSERT(cbor_encoder_close_container(container, &map_encoder) == CborNoError);
}
-void libjade_handle_gui_event(int event_type)
+// libjade RPC handlers
+#define CONST_STRNCMP(str, str_len, cmp_to) str_len == sizeof(cmp_to) - 1 && !strncmp(str, cmp_to, str_len)
+#define IS_JADE_REQUEST(name) CONST_STRNCMP(request, request_len, name)
+
+void process_libjade_request(const cbor_msg_t* const ctx)
{
-#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;
+ CborValue params;
+ if (!rpc_get_map("params", &ctx->value, ¶ms)) {
+ goto cleanup;
}
-#endif
+
+ const char* request;
+ size_t request_len = 0;
+ rpc_get_string_ptr("request", ¶ms, &request, &request_len);
+ JADE_ASSERT(request_len != 0);
+
+ if (IS_JADE_REQUEST("send_input")) {
+ const char* event;
+ size_t event_len = 0;
+ rpc_get_string_ptr("event", ¶ms, &event, &event_len);
+ if (CONST_STRNCMP(event, event_len, "left")) {
+ jade_process_reply_to_message_ok_ex(ctx);
+ gui_prev();
+ } else if (CONST_STRNCMP(event, event_len, "right")) {
+ jade_process_reply_to_message_ok_ex(ctx);
+ gui_next();
+ } else if (CONST_STRNCMP(event, event_len, "click")) {
+ jade_process_reply_to_message_ok_ex(ctx);
+ gui_front_click();
+ } else {
+ goto cleanup;
+ }
+ return;
+ } else if (IS_JADE_REQUEST("get_display_bytes")) {
+ const uint8_t* output = (uint8_t*)display_hw_get_buffer();
+ const size_t output_len = CONFIG_DISPLAY_WIDTH * CONFIG_DISPLAY_HEIGHT * sizeof(color_t);
+ jade_process_reply_to_message_bytes(ctx, output, output_len);
+ return;
+ } else if (IS_JADE_REQUEST("get_display_size")) {
+ uint8_t buf[128]; // sufficient
+ jade_process_reply_to_message_result(ctx, buf, sizeof(buf), &ctx->source, build_display_size_reply);
+ return;
+ } else if (IS_JADE_REQUEST("set_camera_bytes")) {
+ const uint8_t* bytes = NULL;
+ size_t bytes_len = 0;
+ rpc_get_bytes_ptr("bytes", ¶ms, &bytes, &bytes_len);
+ if (libjade_push_camera_frame(bytes, bytes_len)) {
+ jade_process_reply_to_message_ok_ex(ctx);
+ return;
+ }
+ } else if (IS_JADE_REQUEST("get_nvs")) {
+ uint8_t* output;
+ size_t output_len;
+ if (libjade_save_nvs(&output, &output_len) == ESP_OK) {
+ jade_process_reply_to_message_bytes(ctx, output, output_len);
+ JADE_WALLY_VERIFY(wally_bzero(output, output_len));
+ free(output);
+ return;
+ }
+ } else if (IS_JADE_REQUEST("set_nvs")) {
+ const uint8_t* bytes = NULL;
+ size_t bytes_len = 0;
+ rpc_get_bytes_ptr("bytes", ¶ms, &bytes, &bytes_len);
+ if (bytes_len && libjade_load_nvs(bytes, bytes_len) == ESP_OK) {
+ jade_process_reply_to_message_ok_ex(ctx);
+ return;
+ }
+ }
+
+cleanup:
+ uint8_t buf[JADE_MSG_REPLY_LEN];
+ jade_process_reject_message_ex(ctx, CBOR_RPC_BAD_PARAMETERS, "Unhandled error", NULL, 0, buf, sizeof(buf));
}
diff --git a/libjade/libjade.h b/libjade/libjade.h
index 0462a9c..d257f20 100644
--- a/libjade/libjade.h
+++ b/libjade/libjade.h
@@ -53,16 +53,4 @@ 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 6c3cdd0..9dfbb2e 100755
--- a/libjade/make_libjade.sh
+++ b/libjade/make_libjade.sh
@@ -2,18 +2,26 @@
#
# Build the Jade firmware into a shared library for in-process debugging
#
-# ./libjade/make_libjade.sh [Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize] [--log] [--gui] [--no-ci] [--coverage]
+# ./libjade/make_libjade.sh [Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize] [--log] [--camera] [--no-ci] [--coverage]
#
set -e
BUILD_TYPE="Debug"
LOG="0"
-GUI="0"
CI="CI"
+CAMERA="0"
+
+usage() {
+ echo "Usage: $0 [Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize] [--log] [--camera] [--no-ci] [--coverage]"
+ exit 1
+}
# iterate through optional arguments and set variables accordingly
for arg in "$@"; do
case $arg in
+ --help)
+ usage
+ ;;
Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize)
BUILD_TYPE="$arg"
shift
@@ -26,18 +34,17 @@ for arg in "$@"; do
LOG="LOG"
shift
;;
- --gui)
- GUI="GUI"
- shift
- ;;
--no-ci)
CI="0"
shift
;;
+ --camera)
+ CAMERA="CAMERA"
+ shift
+ ;;
*)
echo "Unknown argument: $arg"
- echo "Usage: $0 [Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize] [--log] [--gui] [--no-ci] [--coverage]"
- exit 1
+ usage
;;
esac
done
@@ -48,7 +55,7 @@ EXTRA_ARGS=''
if [ "${BUILD_TYPE}" == "Sanitize" ]; then
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} ..
+cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${EXTRA_ARGS} -DLOG=${LOG} -DCOVERAGE=${COVERAGE} -DCAMERA=${CAMERA} -DCI=${CI} ..
make -j8
cd ..
diff --git a/libjade/nvs_flash.c b/libjade/nvs_flash.c
new file mode 100644
index 0000000..4841735
--- /dev/null
+++ b/libjade/nvs_flash.c
@@ -0,0 +1,275 @@
+#include "nvs_flash.h"
+#include <endian.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wally_core.h>
+#include <wally_map.h>
+
+// HW: NVS storage
+static struct wally_map nvs_storage[5]; // Map of field name to contents
+
+// Binary NVS format: magic header (8 bytes) followed by entries.
+// Each entry:
+// ns_len(uint8), ns(ns_len bytes),
+// key_len(uint8), key(key_len bytes),
+// value_len(uint32_t LE), value(value_len bytes).
+static const char NVS_FILE_MAGIC[8] = { 'J', 'A', 'D', 'E', '_', 'N', 'V', 'S' };
+
+static struct wally_map* get_nvs_ns(const char* ns)
+{
+ if (!strcmp(ns, DEFAULT_NAMESPACE)) {
+ return &nvs_storage[0];
+ }
+ if (!strcmp(ns, MULTISIG_NAMESPACE)) {
+ return &nvs_storage[1];
+ }
+ if (!strcmp(ns, DESCRIPTOR_NAMESPACE)) {
+ return &nvs_storage[2];
+ }
+ if (!strcmp(ns, OTP_NAMESPACE)) {
+ return &nvs_storage[3];
+ }
+ if (!strcmp(ns, HOTP_COUNTERS_NAMESPACE)) {
+ return &nvs_storage[4];
+ }
+ return NULL;
+}
+
+#define ensure_n(n) \
+ do { \
+ if (p + n > end) \
+ goto error; \
+ } while (0)
+
+esp_err_t libjade_load_nvs(const uint8_t* bytes, const size_t bytes_len)
+{
+ const uint8_t *p = bytes, *end = bytes + bytes_len;
+ ensure_n(sizeof(NVS_FILE_MAGIC));
+ if (memcmp(p, NVS_FILE_MAGIC, sizeof(NVS_FILE_MAGIC))) {
+ goto error;
+ }
+ p += sizeof(NVS_FILE_MAGIC);
+ while (p < end) {
+ // ns_len(1), ns(ns_len)
+ char ns[NVS_NS_NAME_MAX_SIZE] = { 0 };
+ ensure_n(1 + p[0]);
+ if (p[0] >= sizeof(ns)) {
+ goto error;
+ }
+ memcpy(ns, p + 1, p[0]);
+ p += 1 + p[0];
+
+ // key_len(1), key(key_len), value_len(4), value(value_len)
+ uint8_t key[NVS_KEY_NAME_MAX_SIZE] = { 0 };
+ ensure_n(1);
+ size_t key_len = p[0];
+ ensure_n(1 + key_len);
+ if (key_len >= sizeof(key)) {
+ goto error;
+ }
+ memcpy(key, p + 1, key_len);
+ p += 1 + p[0];
+
+ // value_len(4), value(value_len)
+ uint32_t value_len;
+ ensure_n(sizeof(value_len));
+ memcpy(&value_len, p, sizeof(value_len));
+ value_len = le32toh(value_len);
+ p += sizeof(value_len);
+ ensure_n(value_len);
+
+ struct wally_map* m = get_nvs_ns(ns);
+ if (m) {
+ wally_map_replace(m, key, key_len, p, value_len);
+ }
+ p += value_len;
+ }
+ return ESP_OK;
+error:
+ // TODO: Wipe NVS on failure?
+ return ESP_ERR_INVALID_ARG;
+}
+
+esp_err_t libjade_save_nvs(uint8_t** output, size_t* output_len)
+{
+ const char* const ns_names[] = {
+ DEFAULT_NAMESPACE,
+ MULTISIG_NAMESPACE,
+ DESCRIPTOR_NAMESPACE,
+ OTP_NAMESPACE,
+ HOTP_COUNTERS_NAMESPACE,
+ };
+ JADE_INIT_OUT_PPTR(output);
+ JADE_INIT_OUT_SIZE(output_len);
+
+ size_t required_len = sizeof(NVS_FILE_MAGIC);
+ for (size_t i = 0; i < sizeof(ns_names) / sizeof(ns_names[0]); ++i) {
+ const size_t ns_len = strlen(ns_names[i]);
+ const struct wally_map* m = get_nvs_ns(ns_names[i]);
+ JADE_ASSERT(m);
+ for (size_t j = 0; j < m->num_items; ++j) {
+ const struct wally_map_item* item = &m->items[j];
+ required_len += 1 + ns_len + 1 + item->key_len + sizeof(uint32_t) + item->value_len;
+ }
+ }
+ if (!required_len || !(*output = malloc(required_len))) {
+ return ESP_FAIL;
+ }
+ uint8_t* p = *output;
+ memcpy(p, NVS_FILE_MAGIC, sizeof(NVS_FILE_MAGIC));
+ p += sizeof(NVS_FILE_MAGIC);
+ for (size_t i = 0; i < sizeof(ns_names) / sizeof(ns_names[0]); ++i) {
+ const size_t ns_len = strlen(ns_names[i]);
+ const struct wally_map* m = get_nvs_ns(ns_names[i]);
+ for (size_t j = 0; j < m->num_items; ++j) {
+ const struct wally_map_item* item = &m->items[j];
+ *p++ = ns_len;
+ memcpy(p, ns_names[i], ns_len);
+ p += ns_len;
+ *p++ = (uint8_t)item->key_len;
+ memcpy(p, item->key, item->key_len);
+ p += item->key_len;
+ const uint32_t value_len = htole32(item->value_len);
+ memcpy(p, &value_len, sizeof(value_len));
+ p += sizeof(value_len);
+ memcpy(p, item->value, item->value_len);
+ p += value_len;
+ }
+ }
+ JADE_ASSERT(p - *output == required_len);
+ *output_len = required_len;
+ return ESP_OK;
+}
+
+esp_err_t nvs_flash_init(void) { return ESP_OK; }
+
+esp_err_t nvs_open(const char* ns, nvs_open_mode_t open_mode, nvs_handle_t* out_handle)
+{
+ *out_handle = get_nvs_ns(ns);
+ return *out_handle ? ESP_OK : ESP_ERR_NVS_NOT_FOUND;
+}
+
+esp_err_t nvs_set_blob(nvs_handle_t handle, const char* key, const void* value, size_t length)
+{
+ int ret = wally_map_replace(handle, (const unsigned char*)key, strlen(key), value, length);
+ return ret == WALLY_OK ? ESP_OK : ESP_FAIL;
+}
+
+esp_err_t nvs_get_blob(nvs_handle_t handle, const char* key, void* out_value, size_t* length)
+{
+ const struct wally_map_item* item = wally_map_get(handle, (const unsigned char*)key, strlen(key));
+ if (!item || item->value_len > *length) {
+ return ESP_ERR_NVS_NOT_FOUND;
+ }
+ memcpy(out_value, item->value, item->value_len);
+ *length = item->value_len;
+ return ESP_OK;
+}
+
+esp_err_t nvs_set_str(nvs_handle_t handle, const char* key, const char* value)
+{
+ return nvs_set_blob(handle, key, value, strlen(value) + 1); // Include NUL terminator
+}
+
+esp_err_t nvs_get_str(nvs_handle_t handle, const char* key, char* out_value, size_t* length)
+{
+ return nvs_get_blob(handle, key, out_value, length);
+}
+
+esp_err_t nvs_set_u32(nvs_handle_t handle, const char* key, uint32_t value)
+{
+ const uint32_t le_value = htole32(value);
+ return nvs_set_blob(handle, key, &le_value, sizeof(le_value));
+}
+
+esp_err_t nvs_get_u32(nvs_handle_t handle, const char* key, uint32_t* out_value)
+{
+ uint32_t le_value;
+ size_t length = sizeof(le_value);
+ const esp_err_t ret = nvs_get_blob(handle, key, &le_value, &length);
+ if (ret == ESP_OK) {
+ *out_value = le32toh(le_value);
+ }
+ return ret;
+}
+
+esp_err_t nvs_erase_key(nvs_handle_t handle, const char* key)
+{
+ if (wally_map_remove(handle, (const unsigned char*)key, strlen(key)) != WALLY_OK) {
+ return ESP_ERR_NVS_NOT_FOUND;
+ }
+ return ESP_OK;
+}
+
+esp_err_t nvs_entry_find(const char* part_name, const char* ns, nvs_type_t type, nvs_iterator_t* output_iterator)
+{
+ *output_iterator = malloc(sizeof(**output_iterator));
+ if (!*output_iterator) {
+ return ESP_FAIL;
+ }
+ if (!((*output_iterator)->m = get_nvs_ns(ns)) || !(*output_iterator)->m->num_items) {
+ goto fail;
+ }
+ // FIXME: Ignores type, pretty sure we only store the same type in each map?
+ (*output_iterator)->idx = 0;
+ return ESP_OK;
+fail:
+ free(*output_iterator);
+ *output_iterator = NULL;
+ return ESP_ERR_NVS_NOT_FOUND;
+}
+
+esp_err_t nvs_entry_next(nvs_iterator_t* iterator)
+{
+ ++(*iterator)->idx;
+ if ((*iterator)->idx >= (*iterator)->m->num_items) {
+ nvs_release_iterator(*iterator);
+ *iterator = NULL;
+ return ESP_ERR_NVS_NOT_FOUND;
+ }
+ return ESP_OK;
+}
+
+esp_err_t nvs_entry_info(const nvs_iterator_t iterator, nvs_entry_info_t* out_info)
+{
+ // FIXME: Only sets key, as thats all we ever read
+ if (!iterator || iterator->idx >= iterator->m->num_items) {
+ return ESP_ERR_INVALID_ARG;
+ }
+ const struct wally_map_item* item = iterator->m->items + iterator->idx;
+ if (item->key_len >= NVS_KEY_NAME_MAX_SIZE) {
+ abort();
+ }
+ memcpy(out_info->key, item->key, item->key_len);
+ out_info->key[item->key_len] = '\0';
+ return ESP_OK;
+}
+
+void nvs_release_iterator(nvs_iterator_t iterator)
+{
+ if (iterator) {
+ free(iterator);
+ }
+}
+
+esp_err_t nvs_flash_erase(void)
+{
+ for (size_t i = 0; i < sizeof(nvs_storage) / sizeof(nvs_storage[0]); ++i) {
+ wally_map_clear(&nvs_storage[i]);
+ }
+ return ESP_OK;
+}
+
+esp_err_t nvs_commit(nvs_handle_t handle) { return ESP_OK; }
+
+esp_err_t nvs_get_stats(const char* part_name, nvs_stats_t* nvs_stats)
+{
+ nvs_stats->used_entries = 0;
+ for (size_t i = 0; i < sizeof(nvs_storage) / sizeof(nvs_storage[0]); ++i) {
+ nvs_stats->used_entries += nvs_storage[i].num_items;
+ }
+ nvs_stats->free_entries = ESP_NVS_TOTAL_ENTRIES - nvs_stats->used_entries;
+ return ESP_OK;
+}
diff --git a/libjade/run_libjade_gui.sh b/libjade/run_libjade_gui.sh
index b6e4449..741bf32 100755
--- a/libjade/run_libjade_gui.sh
+++ b/libjade/run_libjade_gui.sh
@@ -2,7 +2,41 @@
set -e
-BUILD_TYPE="${1:-Debug}"
+usage() {
+ echo "Usage: $0 [--inprocess | --daemon] [--nvs-file PATH] [--log-level none|error|warn|info|debug|verbose] [Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize]"
+ echo " --inprocess Load libjade.so directly in the GUI process (default)"
+ echo " --daemon Run libjade as a separate daemon process"
+ echo " --nvs-file NVS flash storage file (default: nvs_flash.bin)"
+ echo " --log-level Set log verbosity (default: info)"
+ exit 1
+}
+
+MODE="inprocess"
+BUILD_TYPE="Debug"
+NVS_FILE="nvs_flash.bin"
+LOG_LEVEL="info"
+CBOR_SOCKET="/tmp/jade_cbor.sock"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --help) usage ;;
+ --inprocess) MODE="inprocess"; shift ;;
+ --daemon) MODE="daemon"; shift ;;
+ --nvs-file)
+ shift
+ NVS_FILE="$1"
+ shift ;;
+ --log-level)
+ shift
+ case "$1" in
+ none|error|warn|info|debug|verbose) LOG_LEVEL="$1" ;;
+ *) echo "Invalid log level: $1"; usage ;;
+ esac
+ shift ;;
+ Debug|Release|RelWithDebInfo|MinSizeRel|Sanitize) BUILD_TYPE="$1"; shift ;;
+ *) echo "Unknown argument: $1"; usage ;;
+ esac
+done
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
JADE_PATH=$(realpath $SCRIPT_DIR/..)
@@ -12,24 +46,55 @@ if [ ! -d "$JADE_PATH" ]; then
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 "Building libjade ($MODE mode, $BUILD_TYPE)..."
echo "--------------------------------"
+$JADE_PATH/libjade/make_libjade.sh $BUILD_TYPE --log --camera --no-ci
+
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"
+fi
+
+if [ "$MODE" == "daemon" ]; then
+ DAEMON_BIN=$JADE_PATH/build_linux/libjade/libjade_daemon
+ echo "--------------------------------"
+ echo "Starting libjade daemon..."
+ echo " CBOR socket : $CBOR_SOCKET"
+ echo "--------------------------------"
+
+ "$DAEMON_BIN" --socketfile "$CBOR_SOCKET" --log-level "$LOG_LEVEL" &
+ DAEMON_PID=$!
+
+ cleanup() {
+ echo "Stopping libjade daemon (pid $DAEMON_PID)..."
+ kill "$DAEMON_PID" 2>/dev/null || true
+ rm -f "$CBOR_SOCKET"
+ }
+ trap cleanup EXIT INT TERM
+
+ # wait for daemon to create its socket file (up to 10s)
+ echo "Waiting for daemon sockets..."
+ for i in $(seq 1 100); do
+ if [ -S "$CBOR_SOCKET" ]; then
+ echo "Daemon socket ready."
+ break
+ fi
+ if [ "$i" -eq 100 ]; then
+ echo "Error: daemon socket did not appear after 10s" >&2
+ exit 1
+ fi
+ sleep 0.1
+ done
+
+ echo "--------------------------------"
+ echo "Running Jade GUI (daemon mode)..."
+ echo "--------------------------------"
+ python $JADE_PATH/libjade/gui.py --device "tcp:$CBOR_SOCKET" --nvs-file "$NVS_FILE" --log-level "$LOG_LEVEL"
+else
+ export LD_LIBRARY_PATH=$JADE_PATH/build_linux/libjade:$LD_LIBRARY_PATH
+ echo "--------------------------------"
+ echo "Running Jade GUI (in-process mode)..."
echo "--------------------------------"
+ python $JADE_PATH/libjade/gui.py --nvs-file "$NVS_FILE" --log-level "$LOG_LEVEL"
fi
-python $JADE_PATH/libjade/gui.py
diff --git a/libjade/task.c b/libjade/task.c
index d13778c..0cb1523 100644
--- a/libjade/task.c
+++ b/libjade/task.c
@@ -5,15 +5,42 @@
#include <pthread.h>
#include <signal.h>
#include <stdlib.h>
+#include <string.h>
#include <sys/time.h>
#include <time.h>
-#ifndef CONFIG_LIBJADE_NO_GUI
-// variables to help implement vTaskDelete
-// TODO: move to thread local storage so less chance of interference between threads
-static pthread_mutex_t _task_delay_mutex = PTHREAD_MUTEX_INITIALIZER;
-static pthread_cond_t _task_delay_cond = PTHREAD_COND_INITIALIZER;
-#endif
+// Per-thread condition variable for portMAX_DELAY waits, stored in TLS.
+
+static void _task_cond_free(void* p)
+{
+ pthread_cond_destroy((pthread_cond_t*)p);
+ free(p);
+}
+
+static pthread_key_t _task_cond_key;
+static pthread_once_t _task_cond_key_once = PTHREAD_ONCE_INIT;
+static void _task_cond_key_init(void) { pthread_key_create(&_task_cond_key, _task_cond_free); }
+
+static pthread_cond_t* _get_task_cond(void)
+{
+ pthread_once(&_task_cond_key_once, _task_cond_key_init);
+ pthread_cond_t* c = pthread_getspecific(_task_cond_key);
+ if (!c) {
+ c = malloc(sizeof(pthread_cond_t));
+ JADE_ASSERT(c);
+ pthread_cond_init(c, NULL);
+ pthread_setspecific(_task_cond_key, c);
+ }
+ return c;
+}
+
+#define MAX_WAITERS 32
+static pthread_mutex_t _waiter_mutex = PTHREAD_MUTEX_INITIALIZER;
+static struct {
+ pthread_t tid;
+ pthread_cond_t* cond;
+} _waiters[MAX_WAITERS];
+static size_t _waiter_count = 0;
// HW: TLS/Sensitive
static void* _tls_ptrs[3];
@@ -64,10 +91,6 @@ void* pthread_shim_func(void* arg)
BaseType_t xTaskCreatePinnedToCore(TaskFunction_t func, const char* name, uint32_t stack_size, void* params,
uint32_t ux_prio, TaskHandle_t* output, uint32_t xCoreID)
{
-#ifdef CONFIG_LIBJADE_NO_GUI
- func(params);
- return pdTRUE;
-#else
BaseType_t result = pdTRUE;
pthread_attr_t attr = { 0 };
pthread_t thread_id = 0;
@@ -79,6 +102,10 @@ BaseType_t xTaskCreatePinnedToCore(TaskFunction_t func, const char* name, uint32
if (stack_size < PTHREAD_STACK_MIN) {
stack_size = PTHREAD_STACK_MIN;
}
+ if (strcmp(name, "jade_camera") == 0) {
+ // give the camera thread more stack
+ stack_size *= 2;
+ }
if (pthread_attr_setstacksize(&attr, stack_size) != 0) {
JADE_LOGE("pthread_attr_setstacksize failed for task %s", name);
result = pdFALSE;
@@ -111,7 +138,6 @@ cleanup:
*output = NULL;
}
return result;
-#endif
}
BaseType_t xTaskCreatePinnedToCoreWithCaps(TaskFunction_t func, const char* const name, uint32_t stack_size,
@@ -132,15 +158,24 @@ unsigned int xPortGetFreeHeapSize(void) { return 0xffffff; }
void vTaskDelay(TickType_t delay)
{
-#ifdef CONFIG_LIBJADE_NO_GUI
- // Don't delay, since we don't have multiple threads running
- // in the firmware to wait on.
-#else
// if portMAX_DELAY we will make the thread listen for a signal to exit instead of sleeping,
if (delay == portMAX_DELAY) {
- pthread_mutex_lock(&_task_delay_mutex);
- pthread_cond_wait(&_task_delay_cond, &_task_delay_mutex);
- pthread_mutex_unlock(&_task_delay_mutex);
+ pthread_cond_t* cond = _get_task_cond();
+ const pthread_t self = pthread_self();
+ pthread_mutex_lock(&_waiter_mutex);
+ JADE_ASSERT(_waiter_count < MAX_WAITERS);
+ _waiters[_waiter_count].tid = self;
+ _waiters[_waiter_count].cond = cond;
+ _waiter_count++;
+ pthread_cond_wait(cond, &_waiter_mutex);
+ // remove self from registry
+ for (size_t i = 0; i < _waiter_count; i++) {
+ if (pthread_equal(_waiters[i].tid, self)) {
+ _waiters[i] = _waiters[--_waiter_count];
+ break;
+ }
+ }
+ pthread_mutex_unlock(&_waiter_mutex);
// jade often uses vTaskDelay(portMAX_DELAY) in a loop so we need to exit the thread here
pthread_exit(NULL);
return;
@@ -148,35 +183,35 @@ void vTaskDelay(TickType_t delay)
// otherwise sleep as normal
struct timespec ts = timespec_from_ticktype(delay);
nanosleep(&ts, NULL);
-#endif
}
void vTaskDelayUntil(TickType_t* prev_wake_time, const TickType_t delay)
{
-#ifndef CONFIG_LIBJADE_NO_GUI
// Only used by the GUI main loop
TickType_t current_time = xTaskGetTickCount();
if (*prev_wake_time + delay > current_time) {
vTaskDelay(*prev_wake_time + delay - current_time);
}
*prev_wake_time += delay;
-#endif
}
void vTaskDelete(void* task)
{
-#ifdef CONFIG_LIBJADE_NO_GUI
- // Don't delete, since we didn't create any tasks
-#else
+ // if deleting the current task we can just use pthread_exit.
+ // pthread_exit() cannot be used to terminate another thread (like vTaskDelete can),
+ // so if task != current we have to use cooperation with the other thread to signal it to exit.
if (task == NULL) {
pthread_exit(NULL);
} else {
- // use pthread_cond_signal
- pthread_mutex_lock(&_task_delay_mutex);
- pthread_cond_signal(&_task_delay_cond);
- pthread_mutex_unlock(&_task_delay_mutex);
+ pthread_mutex_lock(&_waiter_mutex);
+ for (size_t i = 0; i < _waiter_count; i++) {
+ if (pthread_equal(_waiters[i].tid, (pthread_t)task)) {
+ pthread_cond_signal(_waiters[i].cond);
+ break;
+ }
+ }
+ pthread_mutex_unlock(&_waiter_mutex);
}
-#endif
}
void vTaskDeleteWithCaps(void* task) { vTaskDelete(task); }
diff --git a/main/amalgamated.c b/main/amalgamated.c
index 3e04d95..6013649 100644
--- a/main/amalgamated.c
+++ b/main/amalgamated.c
@@ -31,14 +31,12 @@ void __wrap_abort(void);
#ifdef CONFIG_BT_ENABLED
#include "./ble/ble.c"
#endif // CONFIG_BT_ENABLED
-#ifndef CONFIG_LIBJADE
+#if !defined(CONFIG_LIBJADE) || defined(CONFIG_LIBJADE_CAMERA)
#include "./camera.c"
#endif
#include "./descriptor.c"
-#ifndef CONFIG_LIBJADE_NO_GUI
#include "./display.c"
#include "./display_hw.c"
-#endif // CONFIG_LIBJADE_NO_GUI
#include "./fonts/BigFont.c"
#include "./fonts/DefaultFont.c"
#include "./fonts/DejaVuSans18.c"
@@ -58,9 +56,7 @@ void __wrap_abort(void);
#include "./fonts/minya24.c"
#include "./fonts/tooney32.c"
#include "./fonts/various_symbols.c"
-#ifndef CONFIG_LIBJADE_NO_GUI
#include "./gui.c"
-#endif // CONFIG_LIBJADE_NO_GUI
#include "./identity.c"
#ifndef CONFIG_LIBJADE
#include "./idletimer.c"
@@ -166,9 +162,7 @@ void __wrap_abort(void);
#endif // CONFIG_IDF_TARGET_ESP32S3 && CONFIG_HAS_BATTERY
#include "./utils/address.c"
#include "./utils/cbor_rpc.c"
-#ifndef CONFIG_LIBJADE_NO_GUI
#include "./utils/event.c"
-#endif // CONFIG_LIBJADE_NO_GUI
#include "./utils/network.c"
#include "./utils/psbt.c"
#include "./utils/shake256.c"
diff --git a/main/camera.c b/main/camera.c
index 6a19be2..5835ddd 100644
--- a/main/camera.c
+++ b/main/camera.c
@@ -37,6 +37,11 @@ void camera_set_debug_image(const uint8_t* data, const size_t len)
// as we don't want the unit to shut down because of apparent inactivity.
#define CAMERA_MIN_TIMEOUT_SECS 300
+// Flag set to false to request the camera task exits its loop cleanly.
+static volatile bool camera_task_should_run = false;
+// Set once the camera task has finished and returned.
+static volatile bool camera_task_running = false;
+
// Size of the image as provided by the camera
#define CAMERA_IMAGE_RESOLUTION FRAMESIZE_QVGA
#if (CAMERA_IMAGE_WIDTH != 320) || (CAMERA_IMAGE_HEIGHT != 240)
@@ -227,7 +232,6 @@ static void camera_post_exit_event_and_await_death(void)
static void jade_camera_init(void)
{
-#if !defined(CONFIG_ETH_USE_OPENETH) && defined(ESP_PLATFORM)
JADE_LOGI("CAMERA_IMAGE_WIDTH: %u", CAMERA_IMAGE_WIDTH);
JADE_LOGI("CAMERA_IMAGE_HEIGHT: %u", CAMERA_IMAGE_HEIGHT);
JADE_LOGI("UI_CAMERA_IMAGE_WIDTH: %u", UI_CAMERA_IMAGE_WIDTH);
@@ -245,7 +249,9 @@ static void jade_camera_init(void)
JADE_LOGE("Failed to inititialise/power camera on: %u", ret);
}
- const camera_config_t camera_config = { .pin_d0 = CONFIG_CAMERA_D0,
+ const camera_config_t camera_config = {
+#if !defined(CONFIG_ETH_USE_OPENETH) && defined(ESP_PLATFORM)
+ .pin_d0 = CONFIG_CAMERA_D0,
.pin_d1 = CONFIG_CAMERA_D1,
.pin_d2 = CONFIG_CAMERA_D2,
.pin_d3 = CONFIG_CAMERA_D3,
@@ -266,14 +272,15 @@ static void jade_camera_init(void)
.ledc_timer = LEDC_TIMER_0,
.xclk_freq_hz = CONFIG_CAMERA_XCLK_FREQ,
- .pixel_format = PIXFORMAT_GRAYSCALE,
- .frame_size = CAMERA_IMAGE_RESOLUTION,
-
.fb_count = 2,
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_LATEST,
+#endif
+ .pixel_format = PIXFORMAT_GRAYSCALE,
+ .frame_size = CAMERA_IMAGE_RESOLUTION,
- .jpeg_quality = 0 };
+ .jpeg_quality = 0
+ };
const esp_err_t err = esp_camera_init(&camera_config);
JADE_LOGI("Camera init done");
if (err != ESP_OK) {
@@ -281,6 +288,7 @@ static void jade_camera_init(void)
camera_post_exit_event_and_await_death();
}
+#if !defined(CONFIG_ETH_USE_OPENETH) && defined(ESP_PLATFORM)
sensor_t* camera_sensor = esp_camera_sensor_get();
JADE_ASSERT(camera_sensor);
@@ -311,12 +319,11 @@ static void jade_camera_init(void)
JADE_LOGE("Failed to set camera vflip, returned: %d", vret);
}
}
-
+#endif // !defined(CONFIG_ETH_USE_OPENETH) && defined(ESP_PLATFORM)
#if defined(CONFIG_DISPLAY_TOUCHSCREEN)
touchscreen_deinit();
touchscreen_init();
#endif
-#endif // !defined(CONFIG_ETH_USE_OPENETH) && defined(ESP_PLATFORM)
}
// Stop the camera
@@ -330,6 +337,15 @@ static void jade_camera_stop(void)
#endif
}
+// Signal the camera task to exit and wait until it has cleared all GUI resources
+void camera_stop(void)
+{
+ camera_task_should_run = false;
+ while (camera_task_running) {
+ vTaskDelay(10 / portTICK_PERIOD_MS);
+ }
+}
+
static inline bool invoke_user_cb_fn(const camera_task_config_t* camera_config, const camera_fb_t* fb)
{
#ifdef CONFIG_DEBUG_MODE
@@ -428,7 +444,9 @@ static void jade_camera_task(void* data)
// Loop periodically refreshes screen image from camera, and waits for button event
bool done = false;
uint32_t num_captures = 0;
- while (!done) {
+ camera_task_should_run = true;
+ camera_task_running = true;
+ while (!done && camera_task_should_run) {
// Capture camera output
camera_fb_t* const fb = esp_camera_fb_get();
if (!fb) {
@@ -501,6 +519,7 @@ static void jade_camera_task(void* data)
// with our stack-allocated 'pic' after this task's stack is freed.
gui_clear_picture(image_node);
}
+ camera_task_running = false;
camera_post_exit_event_and_await_death();
}
@@ -575,5 +594,7 @@ void jade_camera_process_images(camera_process_fn_t fn, void* ctx, const bool sh
await_error("No camera detected");
}
+void camera_stop(void) {}
+
#endif // CONFIG_HAS_CAMERA
#endif // AMALGAMATED_BUILD
diff --git a/main/camera.h b/main/camera.h
index 19c91d2..85beb3e 100644
--- a/main/camera.h
+++ b/main/camera.h
@@ -40,4 +40,8 @@ void camera_set_debug_image(const uint8_t* data, size_t len);
void jade_camera_process_images(camera_process_fn_t fn, void* ctx, bool show_ui, const char* text_label,
bool show_click_button, qr_guide_type_t qr_guide_type, const char* help_url, progress_bar_t* progress_bar);
+// Signal the camera task to exit its loop and wait until it has finished
+// Safe to call when no camera task is running (it will be a no-op).
+void camera_stop(void);
+
#endif /* CAMERA_H_ */
diff --git a/main/gui.c b/main/gui.c
index 5dd2f35..4c6b3ef 100644
--- a/main/gui.c
+++ b/main/gui.c
@@ -70,9 +70,16 @@ static gui_activity_t* current_activity = NULL;
static activity_holder_t* existing_activities = NULL;
// handle to the task running to update the gui
-static TaskHandle_t* gui_task_handle = NULL;
+static TaskHandle_t gui_task_handle = NULL;
// queue for gui task to receive items to process (eg. repaint node, switch activities, etc.)
static RingbufHandle_t gui_input_queue = NULL;
+// flag to indicate whether the gui task should stop. This is set
+// to true when the gui task starts, and is only ever set to false
+// by libjade (on shutdown).
+static volatile bool gui_task_should_run = false;
+// flag indicating the gui task is running. As above, this is only
+// set to false when libjade has exited the gui task.
+static volatile bool gui_task_running = false;
// Click/select event (ie. which button counts as 'click'/select)
// and which gui highlight colour is in use
@@ -262,11 +269,6 @@ bool gui_set_flipped_orientation(const bool flipped_orientation)
void gui_init(TaskHandle_t* gui_h, const bool create_event_loop)
{
-#ifdef CONFIG_LIBJADE
- if (gui_mutex) {
- return; // Already initialized
- }
-#endif
// Create mutex semaphore
gui_mutex = xSemaphoreCreateMutex();
JADE_ASSERT(gui_mutex);
@@ -297,17 +299,27 @@ void gui_init(TaskHandle_t* gui_h, const bool create_event_loop)
// Create (high priority) gui task
BaseType_t retval
= xTaskCreatePinnedToCore(gui_task, "gui", 3 * 1024 + 256, NULL, JADE_TASK_PRIO_GUI, gui_h, JADE_CORE_GUI);
- gui_task_handle = gui_h;
JADE_ASSERT_MSG(retval == pdPASS, "Failed to create GUI task, xTaskCreatePinnedToCore() returned %d", retval);
}
-#if defined(CONFIG_LIBJADE) && !defined(CONFIG_LIBJADE_GUI)
+void gui_stop(void)
+{
+#ifdef CONFIG_LIBJADE
+ // Only used by libjade
+ JADE_ASSERT(gui_task_handle);
+ JADE_ASSERT(gui_input_queue);
+ JADE_ASSERT(gui_mutex);
+
+ // Stop the gui task and wait for it to fully exit.
+ gui_task_should_run = false;
+ while (gui_task_running) {
+ vTaskDelay(10 / portTICK_PERIOD_MS);
+ }
+#endif // CONFIG_LIBJADE
+}
+
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
+static bool gui_is_gui_task(void) { return gui_task_handle && xTaskGetCurrentTaskHandle() == gui_task_handle; }
// Is this kind of node selectable?
static inline bool is_kind_selectable(enum view_node_kind kind) { return kind == BUTTON; }
@@ -2461,6 +2473,9 @@ static bool update_updateables(void)
// gui task, for managing display/activities
static void gui_task(void* args)
{
+ // Set the global handle for this task
+ gui_task_handle = xTaskGetCurrentTaskHandle();
+
// Flush/clear display as soon as we're able
JADE_SEMAPHORE_TAKE(gui_mutex);
display_flush();
@@ -2470,7 +2485,9 @@ static void gui_task(void* args)
const TickType_t period = 1000 / GUI_TARGET_FRAMERATE / portTICK_PERIOD_MS;
TickType_t last_wake = xTaskGetTickCount();
- for (;;) {
+ gui_task_should_run = true;
+ gui_task_running = true;
+ while (gui_task_should_run) {
// Wait for the next frame
// Note: this task is never suspended, so no need to re-fetch the tick-
@@ -2504,7 +2521,30 @@ static void gui_task(void* args)
JADE_SEMAPHORE_GIVE(gui_mutex);
}
+#ifdef CONFIG_LIBJADE
+ // gui task is exiting - only happens for libjade.
+ // Free all activities
+ current_activity = NULL;
+ free_activities(existing_activities);
+ existing_activities = NULL;
+
+ // Delete the main input queue
+ if (gui_input_queue) {
+ vRingbufferDelete(gui_input_queue);
+ gui_input_queue = NULL;
+ }
+
+ // Delete the mutex semaphore
+ if (gui_mutex) {
+ vSemaphoreDelete(gui_mutex);
+ gui_mutex = NULL;
+ }
+
+ // Clear handle and running flag.
+ gui_task_handle = NULL;
+ gui_task_running = false;
vTaskDelete(NULL);
+#endif // CONFIG_LIBJADE
}
// TODO: different functions for different types of click
diff --git a/main/gui.h b/main/gui.h
index bfa40de..96e4f66 100644
--- a/main/gui.h
+++ b/main/gui.h
@@ -423,6 +423,7 @@ bool gui_get_flipped_orientation(void);
bool gui_set_flipped_orientation(bool flipped_orientation);
void gui_init(TaskHandle_t* gui_h, bool create_event_loop);
+void gui_stop(void);
bool gui_initialized(void);
void gui_make_activity_ex(gui_activity_t** ppact, const bool has_status_bar, const char* title, const bool managed);
diff --git a/main/process.c b/main/process.c
index e4aae1c..49c2dfb 100644
--- a/main/process.c
+++ b/main/process.c
@@ -544,13 +544,15 @@ void jade_process_reply_to_message_result(
jade_process_reply_to_message_result_with_id(id, output, output_size, ctx->source, cbctx, cb);
}
-void jade_process_reply_to_message_ok(jade_process_t* process)
+void jade_process_reply_to_message_ok_ex(const cbor_msg_t* const ctx)
{
uint8_t buf[64];
const bool ok = true;
- jade_process_reply_to_message_result(&process->ctx, buf, sizeof(buf), &ok, cbor_result_boolean_cb);
+ jade_process_reply_to_message_result(ctx, buf, sizeof(buf), &ok, cbor_result_boolean_cb);
}
+void jade_process_reply_to_message_ok(jade_process_t* process) { jade_process_reply_to_message_ok_ex(&process->ctx); }
+
void jade_process_reply_to_message_fail(jade_process_t* process)
{
uint8_t buf[64];
diff --git a/main/process.h b/main/process.h
index da5c0c6..1c1ffc3 100644
--- a/main/process.h
+++ b/main/process.h
@@ -93,6 +93,7 @@ void jade_process_reply_to_message_result_with_id(const char* id, uint8_t* outpu
jade_msg_source_t source, const void* cbctx, cbor_encoder_fn_t cb);
void jade_process_reply_to_message_result(
const cbor_msg_t* const ctx, uint8_t* output, size_t output_size, const void* cbctx, cbor_encoder_fn_t cb);
+void jade_process_reply_to_message_ok_ex(const cbor_msg_t* const ctx);
void jade_process_reply_to_message_ok(jade_process_t* process);
void jade_process_reply_to_message_fail(jade_process_t* process);
void jade_process_reply_to_message_ex(jade_msg_source_t source, const uint8_t* reply_payload, size_t payload_len);
diff --git a/main/process/dashboard.c b/main/process/dashboard.c
index 9d227d3..67d191d 100644
--- a/main/process/dashboard.c
+++ b/main/process/dashboard.c
@@ -1499,10 +1499,8 @@ static bool display_totp_screen(otpauth_ctx_t* otp_ctx, uint64_t epoch_value, ch
progress_bar_t time_left = {};
gui_activity_t* const act
= make_show_totp_code_activity(otp_ctx->name, timestr, token, confirm_only, &time_left, &txt_ts, &txt_code);
-#ifndef CONFIG_LIBJADE_NO_GUI
JADE_ASSERT(txt_ts);
JADE_ASSERT(txt_code);
-#endif
gui_set_current_activity(act);
vTaskDelay(100 / portTICK_PERIOD_MS);
@@ -2716,8 +2714,6 @@ void dashboard_process(void* process_ptr)
gui_view_node_t* label = NULL;
gui_activity_t* const act_home = make_home_screen_activity(device_name, running_app_info.version,
&home_screen_selected_entry, &home_screen_next_entry, &status_light, &status_text, &label);
-#ifndef CONFIG_LIBJADE_NO_GUI
- // If no GUI is enabled, we do not expect these elements to be set
JADE_ASSERT(home_screen_selected_entry.symbol);
JADE_ASSERT(home_screen_selected_entry.text);
JADE_ASSERT(home_screen_next_entry.symbol);
@@ -2725,7 +2721,6 @@ void dashboard_process(void* process_ptr)
JADE_ASSERT(status_light);
JADE_ASSERT(status_text);
JADE_ASSERT(label);
-#endif // CONFIG_LIBJADE_NO_GUI
// We may as well associate the long-lived event data with this activity also
wait_event_data_t* const event_data = gui_activity_make_wait_event_data(act_home);
diff --git a/main/ui/dialogs.c b/main/ui/dialogs.c
index 17147e1..ee225aa 100644
--- a/main/ui/dialogs.c
+++ b/main/ui/dialogs.c
@@ -727,7 +727,6 @@ gui_activity_t* make_progress_bar_activity(const char* title, const char* messag
void update_progress_bar(progress_bar_t* progress_bar, const size_t total, const size_t current)
{
-#ifndef CONFIG_LIBJADE_NO_GUI
JADE_ASSERT(progress_bar);
JADE_ASSERT(progress_bar->progress_bar);
// progress_bar->pcnt_txt is optional
@@ -763,6 +762,5 @@ void update_progress_bar(progress_bar_t* progress_bar, const size_t total, const
}
progress_bar->percent_last_value = pcnt;
-#endif
}
#endif // AMALGAMATED_BUILD
diff --git a/main/ui/otpauth.c b/main/ui/otpauth.c
index 9e296e3..1eba689 100644
--- a/main/ui/otpauth.c
+++ b/main/ui/otpauth.c
@@ -325,10 +325,8 @@ gui_activity_t* make_show_totp_code_activity(const char* name, const char* times
gui_set_parent(*txt_ts, node);
gui_set_align(*txt_ts, GUI_ALIGN_CENTER, GUI_ALIGN_MIDDLE);
-#ifndef CONFIG_LIBJADE_NO_GUI
// Display 'progress' bar (time remaining)
make_progress_bar(vsplit, progress_bar);
-#endif
// Display the OTP code large/central
gui_make_fill(&node, TFT_BLACK, FILL_PLAIN, vsplit);
diff --git a/main/utils/event.c b/main/utils/event.c
index 137a3ff..4ce6f34 100644
--- a/main/utils/event.c
+++ b/main/utils/event.c
@@ -61,6 +61,14 @@ void sync_wait_event_handler(void* handler_arg, esp_event_base_t base, int32_t i
#ifdef CONFIG_LIBJADE
extern volatile bool _libjade_stop_requested;
+// Handle of the semaphore currently being waited on, so libjade_stop() can unblock the wait.
+static volatile SemaphoreHandle_t _last_wait_handle = NULL;
+void _trigger_last_wait_handle(void)
+{
+ if (_last_wait_handle) {
+ xSemaphoreGive(_last_wait_handle);
+ }
+}
#endif
// This function waits for a previously registered event to be triggered.
@@ -75,9 +83,9 @@ esp_err_t sync_wait_event(wait_event_data_t* wait_event_data, esp_event_base_t*
#ifdef CONFIG_LIBJADE
if (_libjade_stop_requested) {
- // User requested the firmware to exit
pthread_exit(NULL);
}
+ _last_wait_handle = wait_event_data->triggered;
#endif
JADE_LOGD("Awaiting event %p (timeout = %lu)", wait_event_data, max_wait);
@@ -87,11 +95,21 @@ esp_err_t sync_wait_event(wait_event_data_t* wait_event_data, esp_event_base_t*
}
} else {
if (xSemaphoreTake(wait_event_data->triggered, max_wait) != pdTRUE) {
+#ifdef CONFIG_LIBJADE
+ _last_wait_handle = NULL;
+#endif
JADE_LOGD("Event %p timed-out", wait_event_data);
return ESP_NO_EVENT;
}
}
+#ifdef CONFIG_LIBJADE
+ _last_wait_handle = NULL;
+ if (_libjade_stop_requested) {
+ pthread_exit(NULL);
+ }
+#endif
+
// ESP_OK means the event was fired, so copy the ids into the output params
JADE_LOGD("Event %p received in waiting task", wait_event_data);
if (trigger_event_base) {
diff --git a/main/wire.c b/main/wire.c
index 8ae49b7..27c0ba2 100644
--- a/main/wire.c
+++ b/main/wire.c
@@ -57,6 +57,9 @@ static void reject_data(const cbor_msg_t* const ctx, const char* msg, size_t rej
// Some messages we handle immediately in this task
static const char PING[] = { 'p', 'i', 'n', 'g' };
static const char VERINFO[] = { 'g', 'e', 't', '_', 'v', 'e', 'r', 's', 'i', 'o', 'n', '_', 'i', 'n', 'f', 'o' };
+#if defined(CONFIG_DEBUG_MODE) && defined(CONFIG_LIBJADE)
+void process_libjade_request(const cbor_msg_t* const ctx);
+#endif // CONFIG_LIBJADE
static bool handle_immediate_message(const cbor_msg_t* const ctx)
{
@@ -87,6 +90,12 @@ static bool handle_immediate_message(const cbor_msg_t* const ctx)
return true;
}
}
+#if defined(CONFIG_DEBUG_MODE) && defined(CONFIG_LIBJADE)
+ else if (method_len == strlen("libjade_request") && !strncmp(method, "libjade_request", method_len)) {
+ process_libjade_request(ctx);
+ return true;
+ }
+#endif
}
return false;
}
Why this scored 40/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.