From a2894a02132808d33566b9e3bd86ffd7423b83eb Mon Sep 17 00:00:00 2001 From: gustavoschaedler Date: Mon, 27 Apr 2026 22:49:40 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20(capture=5Fentropy.c,=20encoder?= =?UTF-8?q?.c,=20scanner.c):=20add=20support=20for=20webcam=20capture=20on?= =?UTF-8?q?=20macOS=20and=20Linux=20to=20enhance=20QR=20code=20scanning=20?= =?UTF-8?q?capabilities=20=F0=9F=93=9D=20(README.md):=20update=20documenta?= =?UTF-8?q?tion=20for=20building=20and=20running=20the=20simulator=20with?= =?UTF-8?q?=20webcam=20support=20on=20macOS=20and=20Linux=20=E2=99=BB?= =?UTF-8?q?=EF=B8=8F=20(CMakeLists.txt):=20refactor=20CMake=20configuratio?= =?UTF-8?q?n=20to=20include=20platform-specific=20webcam=20capture=20logic?= =?UTF-8?q?=20and=20dependencies=20=F0=9F=90=9B=20(bsp=5Fsim.c):=20fix=20m?= =?UTF-8?q?utex=20locking=20mechanism=20for=20better=20thread=20safety=20i?= =?UTF-8?q?n=20the=20simulator=20=F0=9F=92=A1=20(v4l2=5Fcapture.h,=20v4l2?= =?UTF-8?q?=5Fcapture=5Fmacos.mm):=20add=20comments=20and=20improve=20code?= =?UTF-8?q?=20readability=20for=20webcam=20capture=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/pages/capture_entropy.c | 2 + main/qr/encoder.c | 4 + main/qr/scanner.c | 2 + simulator/CMakeLists.txt | 79 ++++- simulator/README.md | 74 ++++- simulator/platform/bsp_sim/bsp_sim.c | 29 +- .../esp_idf_stubs/include/mbedtls_compat.h | 6 + .../platform/video_sim/include/v4l2_capture.h | 8 + .../platform/video_sim/v4l2_capture_macos.mm | 307 ++++++++++++++++++ simulator/src/main_sim.c | 4 + 10 files changed, 485 insertions(+), 30 deletions(-) create mode 100644 simulator/platform/video_sim/v4l2_capture_macos.mm diff --git a/main/pages/capture_entropy.c b/main/pages/capture_entropy.c index 4ecdd0b..4f94596 100644 --- a/main/pages/capture_entropy.c +++ b/main/pages/capture_entropy.c @@ -230,7 +230,9 @@ static void camera_frame_cb(uint8_t *camera_buf, uint8_t camera_buf_index, current_display_buffer = back_buffer; img_dsc.data = back_buffer; lv_img_set_src(camera_img, &img_dsc); +#ifndef SIMULATOR lv_refr_now(NULL); +#endif } bsp_display_unlock(); } diff --git a/main/qr/encoder.c b/main/qr/encoder.c index f2d2d6a..001dacf 100644 --- a/main/qr/encoder.c +++ b/main/qr/encoder.c @@ -1,5 +1,9 @@ #include "encoder.h" +#ifdef SIMULATOR +#include +#else #include "../managed_components/lvgl__lvgl/src/libs/qrcode/qrcodegen.h" +#endif #include #include #include diff --git a/main/qr/scanner.c b/main/qr/scanner.c index 849fae1..4b5551c 100644 --- a/main/qr/scanner.c +++ b/main/qr/scanner.c @@ -885,7 +885,9 @@ static void camera_video_frame_operation(uint8_t *camera_buf, current_display_buffer = back_buffer; img_refresh_dsc.data = display_src; lv_img_set_src(camera_img, &img_refresh_dsc); +#ifndef SIMULATOR lv_refr_now(NULL); +#endif } buffer_swap_needed = false; bsp_display_unlock(); diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 0228a6d..3cf3936 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -29,10 +29,23 @@ find_package(Threads REQUIRED) find_library(MBEDTLS_LIB mbedtls) find_library(MBEDCRYPTO_LIB mbedcrypto) find_library(MBEDX509_LIB mbedx509) +find_path(MBEDTLS_INCLUDE_DIR NAMES mbedtls/pkcs5.h mbedtls/private/pkcs5.h) if(NOT MBEDTLS_LIB OR NOT MBEDCRYPTO_LIB) message(FATAL_ERROR "mbedTLS not found. Install with: sudo apt install libmbedtls-dev") endif() +if(NOT MBEDTLS_INCLUDE_DIR AND MBEDTLS_LIB) + get_filename_component(_mbedtls_lib_dir "${MBEDTLS_LIB}" DIRECTORY) + get_filename_component(_mbedtls_prefix "${_mbedtls_lib_dir}" DIRECTORY) + set(MBEDTLS_INCLUDE_DIR "${_mbedtls_prefix}/include") +endif() + +set(MBEDTLS_COMPAT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../components/mbedtls_compat) +set(MBEDTLS_INCLUDE_DIRS ${MBEDTLS_INCLUDE_DIR}) +if(EXISTS "${MBEDTLS_INCLUDE_DIR}/mbedtls/private/pkcs5.h" + AND NOT EXISTS "${MBEDTLS_INCLUDE_DIR}/mbedtls/pkcs5.h") + list(PREPEND MBEDTLS_INCLUDE_DIRS ${MBEDTLS_COMPAT_DIR}) +endif() # --- libwally-core (native desktop build) --- set(WALLY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../components/libwally-core) @@ -93,7 +106,20 @@ target_link_libraries(wally PUBLIC ) # --- LVGL --- +include(FetchContent) set(LVGL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../managed_components/lvgl__lvgl) +if(NOT EXISTS "${LVGL_DIR}/src/lvgl.h") + FetchContent_Declare( + lvgl_upstream + GIT_REPOSITORY https://github.com/lvgl/lvgl.git + GIT_TAG v9.3.0 + ) + FetchContent_GetProperties(lvgl_upstream) + if(NOT lvgl_upstream_POPULATED) + FetchContent_Populate(lvgl_upstream) + endif() + set(LVGL_DIR ${lvgl_upstream_SOURCE_DIR}) +endif() file(GLOB_RECURSE LVGL_SOURCES ${LVGL_DIR}/src/*.c) set(APP_UI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../main/ui) @@ -164,7 +190,7 @@ set(PIN_PAGE_SOURCES ) # --- Platform simulators (BSP, SD card, video/camera) --- -option(SIM_WEBCAM "Enable webcam capture via V4L2" OFF) +option(SIM_WEBCAM "Enable webcam capture" OFF) set(PLATFORM_SIM_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/platform/bsp_sim/bsp_sim.c @@ -177,9 +203,26 @@ set(PLATFORM_SIM_SOURCES if(SIM_WEBCAM) # v4l2_capture uses real kernel V4L2 headers (not the ESP-IDF stubs), # so it is compiled as a separate library without the stubs include path. - add_library(v4l2_capture STATIC - ${CMAKE_CURRENT_SOURCE_DIR}/platform/video_sim/v4l2_capture.c - ) + if(APPLE) + enable_language(OBJCXX) + add_library(v4l2_capture STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/platform/video_sim/v4l2_capture_macos.mm + ) + find_library(AVFOUNDATION_FRAMEWORK AVFoundation) + find_library(COREMEDIA_FRAMEWORK CoreMedia) + find_library(COREVIDEO_FRAMEWORK CoreVideo) + find_library(FOUNDATION_FRAMEWORK Foundation) + target_link_libraries(v4l2_capture PRIVATE + ${AVFOUNDATION_FRAMEWORK} + ${COREMEDIA_FRAMEWORK} + ${COREVIDEO_FRAMEWORK} + ${FOUNDATION_FRAMEWORK} + ) + else() + add_library(v4l2_capture STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/platform/video_sim/v4l2_capture.c + ) + endif() target_include_directories(v4l2_capture PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/platform/video_sim/include ${CMAKE_CURRENT_SOURCE_DIR}/platform/video_sim @@ -271,6 +314,7 @@ target_include_directories(kern_simulator PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/platform/sd_card_sim/include ${CMAKE_CURRENT_SOURCE_DIR}/platform # sdkconfig.h ${CMAKE_CURRENT_SOURCE_DIR} # lv_conf.h + ${MBEDTLS_INCLUDE_DIRS} # Application source tree ${CMAKE_CURRENT_SOURCE_DIR}/../main # ui/theme.h, core/, utils/, pages/ # Component headers (for kef.c -> bbqr/miniz, cUR, k_quirc, video) @@ -316,18 +360,21 @@ target_compile_options(kern_simulator PRIVATE ) set_target_properties(kern_simulator PROPERTIES POSITION_INDEPENDENT_CODE ON) -target_link_options(kern_simulator PRIVATE - # Redirect lv_refr_now() to a no-op wrapper in bsp_sim.c. - # Background threads (camera stream) call lv_refr_now() which clears - # LVGL's dirty state without rendering (SDL2 requires the main thread). - # The wrapper keeps widgets dirty so lv_timer_handler() renders them. - -Wl,--wrap=lv_refr_now - # Hardening linker flags - -pie - -Wl,-z,relro - -Wl,-z,now - -Wl,-z,noexecstack -) +if(APPLE) +else() + target_link_options(kern_simulator PRIVATE + # Redirect lv_refr_now() to a no-op wrapper in bsp_sim.c. + # Background threads (camera stream) call lv_refr_now() which clears + # LVGL's dirty state without rendering (SDL2 requires the main thread). + # The wrapper keeps widgets dirty so lv_timer_handler() renders them. + -Wl,--wrap=lv_refr_now + # Hardening linker flags + -pie + -Wl,-z,relro + -Wl,-z,now + -Wl,-z,noexecstack + ) +endif() target_link_libraries(kern_simulator PRIVATE SDL2 diff --git a/simulator/README.md b/simulator/README.md index 72926bf..da27d3b 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -15,18 +15,37 @@ your main workstation. ## Prerequisites +### Linux (Debian/Ubuntu) + ```bash sudo apt install build-essential cmake libsdl2-dev libmbedtls-dev ``` +### macOS + +```bash +brew install cmake sdl2 mbedtls +``` + ## Build +### Linux + ```bash cd simulator \ && cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug \ && cmake --build build -- -j$(nproc) ``` +### macOS + +```bash +cd simulator \ + && cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_PREFIX_PATH="$(brew --prefix mbedtls);$(brew --prefix sdl2)" \ + && cmake --build build -- -j"$(sysctl -n hw.ncpu)" +``` + Or with just (from the repo root): ```bash @@ -35,6 +54,16 @@ just sim-build ## Run +### Linux + +```bash +./simulator/build/kern_simulator +# or, from the repo root: +just sim +``` + +### macOS + ```bash ./simulator/build/kern_simulator # or, from the repo root: @@ -43,16 +72,16 @@ just sim ## CLI Options -| Option | Description | -|---------------------|--------------------------------------------| -| `--qr-image ` | Load a single QR image for camera sim | -| `--qr-dir ` | Load QR images from dir (cycled through) | -| `--data-dir ` | Base data directory (default: `sim_data/`) | -| `--width ` | Display width in pixels (default: 720) | -| `--height ` | Display height in pixels (default: 720) | +| Option | Description | +| ------------------- | --------------------------------------------------------------------- | +| `--qr-image ` | Load a single QR image for camera sim | +| `--qr-dir ` | Load QR images from dir (cycled through) | +| `--data-dir ` | Base data directory (default: `sim_data/`) | +| `--width ` | Display width in pixels (default: 720) | +| `--height ` | Display height in pixels (default: 720) | | `--webcam [device]` | Use webcam (default: `/dev/video0`). Requires `-DSIM_WEBCAM=ON` build | -| `--verbose` | Enable DEBUG-level logging | -| `--help` | Show usage and exit | +| `--verbose` | Enable DEBUG-level logging | +| `--help` | Show usage and exit | ## Examples @@ -98,8 +127,10 @@ factory state. ## Webcam Support (Optional) -Build with V4L2 webcam capture to scan real QR codes and generate -real entropy: +Build with real webcam capture to scan QR codes and generate +real entropy (V4L2 on Linux, AVFoundation on macOS): + +### Linux ```bash cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -DSIM_WEBCAM=ON @@ -115,6 +146,23 @@ Your user must be in the `video` group to access the webcam device: sudo usermod -aG video $USER # then log out/in ``` +### macOS + +```bash +cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -DSIM_WEBCAM=ON \ + -DCMAKE_PREFIX_PATH="$(brew --prefix mbedtls);$(brew --prefix sdl2)" +cmake --build build -- -j"$(sysctl -n hw.ncpu)" +./build/kern_simulator --webcam +# Or specify a device by index: +./build/kern_simulator --webcam 0 +``` + +On macOS, the first run will prompt for Camera permission. If the +permission dialog does not appear, enable camera access for your +terminal in: + +- System Settings → Privacy & Security → Camera + When `--webcam` is passed but the device cannot be opened, the simulator falls back to blank-frame mode. @@ -138,7 +186,7 @@ Forcing the software renderer works around this. ## Known Limitations - Camera simulation is file-based by default (build with - `-DSIM_WEBCAM=ON` for real webcam support via V4L2) + `-DSIM_WEBCAM=ON` for real webcam support) - eFuse HMAC uses a hardcoded test key (anti-phishing words differ from real device, and PIN-derived keys are trivially recoverable from `sim_data/`) @@ -150,4 +198,4 @@ Forcing the software renderer works around this. hashes, or any other cryptographic output produced by the simulator will round-trip on a physical Kern device. - PPA rotation may not match hardware exactly -- Linux only (SDL2 backend) +- Webcam support differs by OS (V4L2 on Linux, AVFoundation on macOS) diff --git a/simulator/platform/bsp_sim/bsp_sim.c b/simulator/platform/bsp_sim/bsp_sim.c index 062cea4..0594dc0 100644 --- a/simulator/platform/bsp_sim/bsp_sim.c +++ b/simulator/platform/bsp_sim/bsp_sim.c @@ -9,6 +9,7 @@ #include "src/drivers/sdl/lv_sdl_window.h" #include "src/drivers/sdl/lv_sdl_mouse.h" +#include #include #include @@ -19,6 +20,32 @@ static pthread_once_t s_lvgl_mutex_once = PTHREAD_ONCE_INIT; static pthread_t s_main_thread; static volatile bool s_main_thread_set = false; +static int kern_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abs_timeout) { +#if defined(__APPLE__) + for (;;) { + int ret = pthread_mutex_trylock(mutex); + if (ret == 0) { + return 0; + } + if (ret != EBUSY) { + return ret; + } + + struct timespec now; + clock_gettime(CLOCK_REALTIME, &now); + if (now.tv_sec > abs_timeout->tv_sec || + (now.tv_sec == abs_timeout->tv_sec && now.tv_nsec >= abs_timeout->tv_nsec)) { + return ETIMEDOUT; + } + + struct timespec sleep_ts = {0, 1000000L}; + nanosleep(&sleep_ts, NULL); + } +#else + return pthread_mutex_timedlock(mutex, abs_timeout); +#endif +} + static void init_lvgl_mutex(void) { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); @@ -64,7 +91,7 @@ bool lvgl_port_lock(uint32_t timeout_ms) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; } - return pthread_mutex_timedlock(&s_lvgl_mutex, &ts) == 0; + return kern_mutex_timedlock(&s_lvgl_mutex, &ts) == 0; } void lvgl_port_unlock(void) { diff --git a/simulator/platform/esp_idf_stubs/include/mbedtls_compat.h b/simulator/platform/esp_idf_stubs/include/mbedtls_compat.h index 9e39a66..674021e 100644 --- a/simulator/platform/esp_idf_stubs/include/mbedtls_compat.h +++ b/simulator/platform/esp_idf_stubs/include/mbedtls_compat.h @@ -11,6 +11,12 @@ #include #include +#if defined(__has_include) +#if __has_include() +#define MBEDTLS_ALLOW_PRIVATE_ACCESS +#endif +#endif + #include #include #include diff --git a/simulator/platform/video_sim/include/v4l2_capture.h b/simulator/platform/video_sim/include/v4l2_capture.h index 4a2e508..77802ec 100644 --- a/simulator/platform/video_sim/include/v4l2_capture.h +++ b/simulator/platform/video_sim/include/v4l2_capture.h @@ -4,6 +4,10 @@ #include #include +#ifdef __cplusplus +extern "C" { +#endif + typedef struct v4l2_capture v4l2_capture_t; /** @@ -34,3 +38,7 @@ size_t v4l2_capture_read_rgb565(v4l2_capture_t *cap, * Stop streaming, unmap buffers, close device, free handle. */ void v4l2_capture_close(v4l2_capture_t *cap); + +#ifdef __cplusplus +} +#endif diff --git a/simulator/platform/video_sim/v4l2_capture_macos.mm b/simulator/platform/video_sim/v4l2_capture_macos.mm new file mode 100644 index 0000000..97be1b2 --- /dev/null +++ b/simulator/platform/video_sim/v4l2_capture_macos.mm @@ -0,0 +1,307 @@ +#include "v4l2_capture.h" + +#import +#import +#import +#import + +#include +#include +#include +#include +#include +#include + +struct v4l2_capture { + uint32_t width; + uint32_t height; + bool configured; + + pthread_mutex_t mutex; + pthread_cond_t cond; + bool has_frame; + bool closed; + + uint8_t *rgb565; + size_t rgb565_size; + + AVCaptureSession *session; + AVCaptureVideoDataOutput *output; + dispatch_queue_t queue; + id delegate; +}; + +static uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) { + return (uint16_t)(((r & 0xF8u) << 8) | ((g & 0xFCu) << 3) | (b >> 3)); +} + +@interface KernVideoDelegate : NSObject +@property(nonatomic, assign) v4l2_capture_t *cap; +@end + +@implementation KernVideoDelegate +- (void)captureOutput:(AVCaptureOutput *)output + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection { + (void)output; + (void)connection; + v4l2_capture_t *cap = self.cap; + if (!cap) + return; + + CVPixelBufferRef pb = CMSampleBufferGetImageBuffer(sampleBuffer); + if (!pb) + return; + + CVPixelBufferLockBaseAddress(pb, kCVPixelBufferLock_ReadOnly); + + const size_t width = CVPixelBufferGetWidth(pb); + const size_t height = CVPixelBufferGetHeight(pb); + const size_t bytes_per_row = CVPixelBufferGetBytesPerRow(pb); + const uint8_t *base = (const uint8_t *)CVPixelBufferGetBaseAddress(pb); + + if (!base || width == 0 || height == 0) { + CVPixelBufferUnlockBaseAddress(pb, kCVPixelBufferLock_ReadOnly); + return; + } + + pthread_mutex_lock(&cap->mutex); + if (cap->closed) { + pthread_mutex_unlock(&cap->mutex); + CVPixelBufferUnlockBaseAddress(pb, kCVPixelBufferLock_ReadOnly); + return; + } + + if (!cap->configured) { + cap->width = (uint32_t)width; + cap->height = (uint32_t)height; + } + if (cap->width != (uint32_t)width || cap->height != (uint32_t)height) { + pthread_mutex_unlock(&cap->mutex); + CVPixelBufferUnlockBaseAddress(pb, kCVPixelBufferLock_ReadOnly); + return; + } + const size_t needed = width * height * 2; + if (!cap->configured || cap->rgb565_size != needed) { + free(cap->rgb565); + cap->rgb565 = (uint8_t *)malloc(needed); + cap->rgb565_size = cap->rgb565 ? needed : 0; + } + + if (cap->rgb565) { + uint16_t *dst = (uint16_t *)cap->rgb565; + for (size_t y = 0; y < height; y++) { + const uint8_t *row = base + y * bytes_per_row; + for (size_t x = 0; x < width; x++) { + const uint8_t b = row[x * 4 + 0]; + const uint8_t g = row[x * 4 + 1]; + const uint8_t r = row[x * 4 + 2]; + dst[y * width + x] = rgb888_to_rgb565(r, g, b); + } + } + cap->has_frame = true; + cap->configured = true; + pthread_cond_broadcast(&cap->cond); + } + + pthread_mutex_unlock(&cap->mutex); + CVPixelBufferUnlockBaseAddress(pb, kCVPixelBufferLock_ReadOnly); +} +@end + +static bool parse_device_index(const char *device, int *out_index) { + if (!device || !out_index) + return false; + if (strncmp(device, "/dev/video", 9) == 0) { + device += 9; + } + char *end = NULL; + long idx = strtol(device, &end, 10); + if (end == device || *end != '\0' || idx < 0 || idx > 1000) + return false; + *out_index = (int)idx; + return true; +} + +static AVCaptureDevice *select_device(const char *device) { + NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; + if (devices.count == 0) + return nil; + + if (!device || device[0] == '\0') + return devices.firstObject; + + int idx = 0; + if (parse_device_index(device, &idx)) { + if (idx >= 0 && idx < (int)devices.count) { + return devices[(NSUInteger)idx]; + } + return devices.firstObject; + } + + NSString *needle = [NSString stringWithUTF8String:device]; + for (AVCaptureDevice *d in devices) { + if ([d.localizedName rangeOfString:needle options:NSCaseInsensitiveSearch].location != NSNotFound) { + return d; + } + if ([d.uniqueID rangeOfString:needle options:NSCaseInsensitiveSearch].location != NSNotFound) { + return d; + } + } + return devices.firstObject; +} + +extern "C" { + +v4l2_capture_t *v4l2_capture_open(const char *device, uint32_t desired_width, + uint32_t desired_height) { + @autoreleasepool { + v4l2_capture_t *cap = (v4l2_capture_t *)calloc(1, sizeof(*cap)); + if (!cap) + return NULL; + + pthread_mutex_init(&cap->mutex, NULL); + pthread_cond_init(&cap->cond, NULL); + cap->queue = dispatch_queue_create("kern.sim.webcam", DISPATCH_QUEUE_SERIAL); + + AVCaptureDevice *dev = select_device(device); + if (!dev) { + v4l2_capture_close(cap); + return NULL; + } + + NSError *err = nil; + AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:dev error:&err]; + if (!input || err) { + v4l2_capture_close(cap); + return NULL; + } + + cap->session = [[AVCaptureSession alloc] init]; + + if (desired_width >= 1280 || desired_height >= 720) { + if ([cap->session canSetSessionPreset:AVCaptureSessionPreset1280x720]) { + cap->session.sessionPreset = AVCaptureSessionPreset1280x720; + } + } else if ([cap->session canSetSessionPreset:AVCaptureSessionPreset640x480]) { + cap->session.sessionPreset = AVCaptureSessionPreset640x480; + } + + if (![cap->session canAddInput:input]) { + v4l2_capture_close(cap); + return NULL; + } + [cap->session addInput:input]; + + cap->output = [[AVCaptureVideoDataOutput alloc] init]; + cap->output.videoSettings = @{ + (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), + }; + cap->output.alwaysDiscardsLateVideoFrames = YES; + + KernVideoDelegate *delegate = [[KernVideoDelegate alloc] init]; + delegate.cap = cap; + cap->delegate = delegate; + [cap->output setSampleBufferDelegate:delegate queue:cap->queue]; + + if (![cap->session canAddOutput:cap->output]) { + v4l2_capture_close(cap); + return NULL; + } + [cap->session addOutput:cap->output]; + + [cap->session startRunning]; + + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += 2; + pthread_mutex_lock(&cap->mutex); + while (!cap->configured && !cap->closed) { + int ret = pthread_cond_timedwait(&cap->cond, &cap->mutex, &ts); + if (ret != 0) { + break; + } + } + pthread_mutex_unlock(&cap->mutex); + + if (!cap->configured) { + v4l2_capture_close(cap); + return NULL; + } + + return cap; + } +} + +void v4l2_capture_get_resolution(const v4l2_capture_t *cap, uint32_t *width, + uint32_t *height) { + if (!cap) + return; + if (width) + *width = cap->width; + if (height) + *height = cap->height; +} + +size_t v4l2_capture_read_rgb565(v4l2_capture_t *cap, uint8_t *rgb565_buf, + size_t buf_size) { + if (!cap || !rgb565_buf) + return 0; + + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += 1; + + pthread_mutex_lock(&cap->mutex); + while (!cap->has_frame && !cap->closed) { + int ret = pthread_cond_timedwait(&cap->cond, &cap->mutex, &ts); + if (ret != 0) + break; + } + + if (!cap->has_frame || cap->closed || !cap->rgb565) { + pthread_mutex_unlock(&cap->mutex); + return 0; + } + + const size_t needed = cap->rgb565_size; + if (buf_size < needed) { + pthread_mutex_unlock(&cap->mutex); + return 0; + } + + memcpy(rgb565_buf, cap->rgb565, needed); + cap->has_frame = false; + pthread_mutex_unlock(&cap->mutex); + return needed; +} + +void v4l2_capture_close(v4l2_capture_t *cap) { + if (!cap) + return; + + @autoreleasepool { + pthread_mutex_lock(&cap->mutex); + cap->closed = true; + pthread_cond_broadcast(&cap->cond); + pthread_mutex_unlock(&cap->mutex); + + if (cap->session) { + [cap->session stopRunning]; + cap->session = nil; + } + cap->output = nil; + cap->delegate = nil; + + free(cap->rgb565); + cap->rgb565 = NULL; + cap->rgb565_size = 0; + + pthread_cond_destroy(&cap->cond); + pthread_mutex_destroy(&cap->mutex); + + free(cap); + } +} + +} // extern "C" diff --git a/simulator/src/main_sim.c b/simulator/src/main_sim.c index c33372b..33cab6f 100644 --- a/simulator/src/main_sim.c +++ b/simulator/src/main_sim.c @@ -30,7 +30,9 @@ #include #include #include +#if defined(__linux__) #include +#endif #include #include "esp_log.h" @@ -123,7 +125,9 @@ int main(int argc, char *argv[]) { * accidentally typed into the simulator does not hit disk. Both calls * are non-fatal — typical desktops cap RLIMIT_MEMLOCK low. */ (void)mlockall(MCL_CURRENT | MCL_FUTURE); +#if defined(__linux__) (void)prctl(PR_SET_DUMPABLE, 0, 0, 0, 0); +#endif fprintf(stderr, "\n" From c7be4d092d676a0ff0ebd67651066ac1edcfefe5 Mon Sep 17 00:00:00 2001 From: gustavoschaedler Date: Tue, 28 Apr 2026 00:16:44 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(capture=5Fentropy.c,?= =?UTF-8?q?=20encoder.c,=20scanner.c,=20bsp=5Fsim.c):=20refactor=20camera?= =?UTF-8?q?=20frame=20handling=20to=20remove=20simulator-specific=20code?= =?UTF-8?q?=20and=20improve=20clarity=20=E2=9C=A8=20(CMakeLists.txt):=20ad?= =?UTF-8?q?d=20lvgl=20library=20to=20simulator=20build=20for=20better=20mo?= =?UTF-8?q?dularity=20and=20maintainability=20=F0=9F=92=A1=20(bsp=5Fsim.c)?= =?UTF-8?q?:=20update=20comments=20to=20clarify=20the=20purpose=20of=20the?= =?UTF-8?q?=20lv=5Frefr=5Fnow=20wrapper=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/pages/capture_entropy.c | 15 ++------------- main/qr/encoder.c | 6 +----- main/qr/scanner.c | 27 ++++----------------------- simulator/CMakeLists.txt | 27 +++++++++++++++++++++------ simulator/platform/bsp_sim/bsp_sim.c | 22 +++++++--------------- 5 files changed, 35 insertions(+), 62 deletions(-) diff --git a/main/pages/capture_entropy.c b/main/pages/capture_entropy.c index 4f94596..f28fcb7 100644 --- a/main/pages/capture_entropy.c +++ b/main/pages/capture_entropy.c @@ -183,21 +183,12 @@ static void camera_frame_cb(uint8_t *camera_buf, uint8_t camera_buf_index, ? display_buffer_b : display_buffer_a; -#ifdef SIMULATOR - uint32_t in_w = camera_buf_hes; - uint32_t in_h = camera_buf_ves; + uint32_t in_w = camera_buf_hes ? camera_buf_hes : CAMERA_INPUT_WIDTH; + uint32_t in_h = camera_buf_ves ? camera_buf_ves : CAMERA_INPUT_HEIGHT; uint32_t crop = (in_w < in_h) ? in_w : in_h; uint32_t crop_ox = (in_w - crop) / 2; uint32_t crop_oy = (in_h - crop) / 2; float scale = (float)CAMERA_WIDTH / (float)crop; -#else - uint32_t in_w = CAMERA_INPUT_WIDTH; - uint32_t in_h = CAMERA_INPUT_HEIGHT; - uint32_t crop = CAMERA_INPUT_CROP; - uint32_t crop_ox = CAMERA_INPUT_CROP_OFFSET_X; - uint32_t crop_oy = CAMERA_INPUT_CROP_OFFSET_Y; - float scale = CAMERA_PPA_SCALE; -#endif ppa_srm_oper_config_t srm = { .in.buffer = camera_buf, @@ -230,9 +221,7 @@ static void camera_frame_cb(uint8_t *camera_buf, uint8_t camera_buf_index, current_display_buffer = back_buffer; img_dsc.data = back_buffer; lv_img_set_src(camera_img, &img_dsc); -#ifndef SIMULATOR lv_refr_now(NULL); -#endif } bsp_display_unlock(); } diff --git a/main/qr/encoder.c b/main/qr/encoder.c index 001dacf..bdbde17 100644 --- a/main/qr/encoder.c +++ b/main/qr/encoder.c @@ -1,9 +1,5 @@ #include "encoder.h" -#ifdef SIMULATOR -#include -#else -#include "../managed_components/lvgl__lvgl/src/libs/qrcode/qrcodegen.h" -#endif +#include "src/libs/qrcode/qrcodegen.h" #include #include #include diff --git a/main/qr/scanner.c b/main/qr/scanner.c index 4b5551c..05b5742 100644 --- a/main/qr/scanner.c +++ b/main/qr/scanner.c @@ -807,18 +807,10 @@ static void camera_video_frame_operation(uint8_t *camera_buf, return; } -#ifndef SIMULATOR - if (camera_buf_hes != CAMERA_INPUT_WIDTH || - camera_buf_ves != CAMERA_INPUT_HEIGHT) { - ESP_LOGE(TAG, - "Unexpected camera resolution %" PRIu32 "x%" PRIu32 - ", expected %dx%d", - camera_buf_hes, camera_buf_ves, CAMERA_INPUT_WIDTH, - CAMERA_INPUT_HEIGHT); + if (camera_buf_hes == 0 || camera_buf_ves == 0) { __atomic_sub_fetch(&active_frame_operations, 1, __ATOMIC_SEQ_CST); return; } -#endif uint8_t *back_buffer = (current_display_buffer == display_buffer_a) ? display_buffer_b @@ -828,24 +820,15 @@ static void camera_video_frame_operation(uint8_t *camera_buf, // counter-rotation uint8_t *display_src = back_buffer; if (cam_ppa_client && !closing) { -#ifdef SIMULATOR - // Simulator webcam can be any resolution; derive crop from actual frame uint32_t in_w = camera_buf_hes; uint32_t in_h = camera_buf_ves; uint32_t crop = (in_w < in_h) ? in_w : in_h; - if (crop > (uint32_t)(CAMERA_SCREEN_SIZE * 2)) - crop = CAMERA_SCREEN_SIZE * 2; + if (crop > CAMERA_INPUT_CROP) { + crop = CAMERA_INPUT_CROP; + } uint32_t crop_ox = (in_w - crop) / 2; uint32_t crop_oy = (in_h - crop) / 2; float sim_scale = (float)CAMERA_SCREEN_WIDTH / (float)crop; -#else - uint32_t in_w = CAMERA_INPUT_WIDTH; - uint32_t in_h = CAMERA_INPUT_HEIGHT; - uint32_t crop = CAMERA_INPUT_CROP; - uint32_t crop_ox = CAMERA_INPUT_CROP_OFFSET_X; - uint32_t crop_oy = CAMERA_INPUT_CROP_OFFSET_Y; - float sim_scale = CAMERA_PPA_SCALE; -#endif ppa_srm_oper_config_t srm = { .in.buffer = camera_buf, .in.pic_w = in_w, @@ -885,9 +868,7 @@ static void camera_video_frame_operation(uint8_t *camera_buf, current_display_buffer = back_buffer; img_refresh_dsc.data = display_src; lv_img_set_src(camera_img, &img_refresh_dsc); -#ifndef SIMULATOR lv_refr_now(NULL); -#endif } buffer_swap_needed = false; bsp_display_unlock(); diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 3cf3936..5d8da38 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -290,6 +290,26 @@ set(QR_SOURCES file(GLOB KQUIRC_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../components/k_quirc/src/*.c) file(GLOB_RECURSE CUR_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../components/cUR/src/*.c) +add_library(lvgl STATIC + ${LVGL_SOURCES} +) +target_include_directories(lvgl PUBLIC + ${LVGL_DIR} + ${LVGL_DIR}/src +) +target_include_directories(lvgl PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) +target_compile_definitions(lvgl PRIVATE + LV_CONF_INCLUDE_SIMPLE + SIM_LCD_H_RES=${SIM_LCD_H_RES} + SIM_LCD_V_RES=${SIM_LCD_V_RES} + lv_refr_now=kern_lv_refr_now_real +) +target_link_libraries(lvgl PUBLIC + SDL2 +) + add_executable(kern_simulator ${SIM_SOURCES} ${STUB_SOURCES} @@ -302,7 +322,6 @@ add_executable(kern_simulator ${PLATFORM_SIM_SOURCES} ${KQUIRC_SOURCES} ${CUR_SOURCES} - ${LVGL_SOURCES} ) # Include paths — order matters: simulator headers shadow ESP-IDF @@ -363,11 +382,6 @@ set_target_properties(kern_simulator PROPERTIES POSITION_INDEPENDENT_CODE ON) if(APPLE) else() target_link_options(kern_simulator PRIVATE - # Redirect lv_refr_now() to a no-op wrapper in bsp_sim.c. - # Background threads (camera stream) call lv_refr_now() which clears - # LVGL's dirty state without rendering (SDL2 requires the main thread). - # The wrapper keeps widgets dirty so lv_timer_handler() renders them. - -Wl,--wrap=lv_refr_now # Hardening linker flags -pie -Wl,-z,relro @@ -377,6 +391,7 @@ else() endif() target_link_libraries(kern_simulator PRIVATE + lvgl SDL2 Threads::Threads m diff --git a/simulator/platform/bsp_sim/bsp_sim.c b/simulator/platform/bsp_sim/bsp_sim.c index 0594dc0..1feb4be 100644 --- a/simulator/platform/bsp_sim/bsp_sim.c +++ b/simulator/platform/bsp_sim/bsp_sim.c @@ -60,6 +60,7 @@ static void init_lvgl_mutex(void) { esp_err_t lvgl_port_init(const lvgl_port_cfg_t *cfg) { (void)cfg; + pthread_once(&s_lvgl_mutex_once, init_lvgl_mutex); return ESP_OK; } @@ -102,21 +103,12 @@ void lvgl_port_flush_ready(lv_display_t *disp) { (void)disp; } -/** - * Wrapper for lv_refr_now() — linked via --wrap=lv_refr_now. - * - * Camera frame callbacks call lv_refr_now() from background threads to force - * an immediate display update. On the real device this works because the - * display driver writes directly to the LCD. With SDL2, rendering must happen - * on the main thread; calling from a background thread silently fails but - * clears LVGL's dirty state, preventing the main thread from ever rendering - * the update. - * - * Making this a no-op lets lv_img_set_src() mark the widget dirty, and the - * main loop's lv_timer_handler() picks it up on the next iteration (~30 fps). - */ -void __wrap_lv_refr_now(lv_display_t *disp) { - (void)disp; +void kern_lv_refr_now_real(lv_display_t *disp); + +void lv_refr_now(lv_display_t *disp) { + if (s_main_thread_set && pthread_equal(pthread_self(), s_main_thread)) { + kern_lv_refr_now_real(disp); + } } /* ---------- BSP display stubs ---------- */